Make add task a component

Add add-task to home
Change add task to component for lists
Made welcome update on time.. because.
This commit is contained in:
sytone 2021-05-26 13:33:12 -07:00
parent 9c799ab161
commit 1200f0b416
5 changed files with 400 additions and 232 deletions

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = tab
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
[*.vue]
indent_style = tab

View File

@ -19,21 +19,25 @@ If you find any security-related issues you don't want to disclose publicly, ple
There is a [docker image available](https://hub.docker.com/r/vikunja/api) with support for http/2 and aggressive caching enabled. There is a [docker image available](https://hub.docker.com/r/vikunja/api) with support for http/2 and aggressive caching enabled.
## Project setup ## Project setup
```
```shell
yarn install yarn install
``` ```
### Compiles and hot-reloads for development ### Compiles and hot-reloads for development
```
```shell
yarn run serve yarn run serve
``` ```
### Compiles and minifies for production ### Compiles and minifies for production
```
```shell
yarn run build yarn run build
``` ```
### Lints and fixes files ### Lints and fixes files
```
```shell
yarn run lint yarn run lint
``` ```

View File

@ -0,0 +1,215 @@
<template>
<div class="field is-grouped">
<p
:class="{ 'is-loading': taskService.loading }"
class="control has-icons-left is-expanded"
>
<input
:class="{ disabled: taskService.loading }"
@keyup.enter="addTask()"
class="input"
placeholder="Add a new task..."
type="text"
v-focus
v-model="newTaskText"
ref="newTaskInput"
/>
<span class="icon is-small is-left">
<icon icon="tasks" />
</span>
</p>
<p class="control">
<x-button
:disabled="newTaskText.length === 0"
@click="addTask()"
icon="plus"
>
Add
</x-button>
</p>
</div>
</template>
<script>
import LabelTask from "../../models/labelTask";
import LabelModel from "../../models/label";
import { HAS_TASKS } from "@/store/mutation-types";
// import Nothing from "@/components/misc/nothing";
import ListService from "../../services/list";
import TaskService from "../../services/task";
import TaskModel from "../../models/task";
import LabelService from "../../services/label";
import LabelTaskService from "../../services/labelTask";
export default {
name: "add-task",
data() {
return {
newTaskText: "",
listService: ListService,
taskService: TaskService,
labelService: LabelService,
labelTaskService: LabelTaskService
};
},
components: {},
props: {
listId: {
type: Number,
required: false
}
},
created() {
this.listService = new ListService();
this.taskService = new TaskService();
this.labelService = new LabelService();
this.labelTaskService = new LabelTaskService();
},
methods: {
addTask() {
if (this.newTaskText === "") {
this.showError = true;
return;
}
this.showError = false;
let task = new TaskModel({
title: this.newTaskText,
listId: this.listId
});
if (this.listId === undefined) {
// TODO: Have a default list in settings.
task.listId = 1;
}
this.taskService
.create(task)
.then(task => {
// this.tasks.push(task);
// this.sortTasks();
this.newTaskText = "";
// Check if the task has words starting with ~ in the title and make them to labels
const parts = task.title.split(" ~");
// The first element will always contain the title, even if there is no occurrence of ~
if (parts.length > 1) {
// First, create an unresolved promise for each entry in the array to wait
// until all labels are added to update the task title once again
let labelAddings = [];
let labelAddsToWaitFor = [];
parts.forEach((p, index) => {
if (index < 1) {
return;
}
labelAddsToWaitFor.push(
new Promise((resolve, reject) => {
labelAddings.push({ resolve: resolve, reject: reject });
})
);
});
// Then do everything that is involved in finding, creating and adding the label to the task
parts.forEach((p, index) => {
if (index < 1) {
return;
}
// The part up until the next space
const labelTitle = p.split(" ")[0];
// Don't create an empty label
if (labelTitle === "") {
return;
}
// Check if the label exists
this.labelService
.getAll({}, { s: labelTitle })
.then(res => {
// Label found, use it
if (res.length > 0 && res[0].title === labelTitle) {
const labelTask = new LabelTask({
taskId: task.id,
labelId: res[0].id
});
this.labelTaskService
.create(labelTask)
.then(result => {
task.labels.push(res[0]);
// Remove the label text from the task title
task.title = task.title.replace(` ~${labelTitle}`, "");
// Make the promise done (the one with the index 0 does not exist)
labelAddings[index - 1].resolve(result);
})
.catch(e => {
this.error(e, this);
});
} else {
// label not found, create it
const label = new LabelModel({ title: labelTitle });
this.labelService
.create(label)
.then(res => {
const labelTask = new LabelTask({
taskId: task.id,
labelId: res.id
});
this.labelTaskService
.create(labelTask)
.then(result => {
task.labels.push(res);
// Remove the label text from the task title
task.title = task.title.replace(
` ~${labelTitle}`,
""
);
// Make the promise done (the one with the index 0 does not exist)
labelAddings[index - 1].resolve(result);
})
.catch(e => {
this.error(e, this);
});
})
.catch(e => {
this.error(e, this);
});
}
})
.catch(e => {
this.error(e, this);
});
});
// This waits to update the task until all labels have been added and the title has
// been modified to remove each label text
Promise.all(labelAddsToWaitFor).then(() => {
this.taskService
.update(task)
// .then(updatedTask => {
// this.updateTasks(updatedTask);
// this.$store.commit(HAS_TASKS, true);
// })
.then(() => {
this.$store.commit(HAS_TASKS, true);
})
.catch(e => {
this.error(e, this);
});
});
}
this.$emit("taskAdded", task);
})
.catch(e => {
this.error(e, this);
});
}
}
};
</script>

View File

@ -1,12 +1,18 @@
<template> <template>
<div class="content has-text-centered"> <div class="content has-text-centered">
<h2> <h2>
Hi {{ userInfo.name !== '' ? userInfo.name : userInfo.username }}! {{ welcomePrefix }}
{{ userInfo.name !== "" ? userInfo.name : userInfo.username }}!
</h2> </h2>
<add-task
:list="defaultList"
@taskAdded="updateTaskList"
class="is-max-width-desktop"
/>
<template v-if="!hasTasks"> <template v-if="!hasTasks">
<p>You can create a new list for your new tasks:</p> <p>You can create a new list for your new tasks:</p>
<x-button <x-button
:to="{name: 'list.create', params: { id: defaultNamespaceId }}" :to="{ name: 'list.create', params: { id: defaultNamespaceId } }"
:shadow="false" :shadow="false"
class="ml-2" class="ml-2"
v-if="defaultNamespaceId > 0" v-if="defaultNamespaceId > 0"
@ -19,49 +25,89 @@
<x-button <x-button
v-if="migratorsEnabled" v-if="migratorsEnabled"
:to="{ name: 'migrate.start' }" :to="{ name: 'migrate.start' }"
:shadow="false"> :shadow="false"
>
Import your data into Vikunja Import your data into Vikunja
</x-button> </x-button>
</template> </template>
<ShowTasks :show-all="true" v-if="hasLists"/> <ShowTasks :show-all="true" v-if="hasLists" :key="showTasksKey" />
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from "vuex";
import ShowTasks from './tasks/ShowTasks' import ShowTasks from "./tasks/ShowTasks";
import AddTask from "../components/tasks/add-task";
import ListModel from "../models/list";
export default { export default {
name: 'Home', name: "Home",
components: { components: {
ShowTasks, ShowTasks,
AddTask
}, },
data() { data() {
return { return {
loading: false, loading: false,
currentDate: new Date(), currentDate: new Date(),
tasks: [], tasks: [],
} defaultList: ListModel,
updateWelcomeInterval: 1000,
welcomePrefix: "Hi",
showTasksKey: 0
};
},
created() {
this.defaultList = new ListModel();
this.defaultList.id = 1;
},
mounted() {
const timer = window.setTimeout(
this.updateWelcome,
this.updateWelcomeInterval
);
this.$on("hook:destroyed", () => window.clearTimeout(timer));
}, },
computed: mapState({ computed: mapState({
migratorsEnabled: state => state.config.availableMigrators !== null && state.config.availableMigrators.length > 0, migratorsEnabled: state =>
state.config.availableMigrators !== null &&
state.config.availableMigrators.length > 0,
authenticated: state => state.auth.authenticated, authenticated: state => state.auth.authenticated,
userInfo: state => state.auth.info, userInfo: state => state.auth.info,
hasTasks: state => state.hasTasks, hasTasks: state => state.hasTasks,
defaultNamespaceId: state => { defaultNamespaceId: state => {
if (state.namespaces.namespaces.length === 0) { if (state.namespaces.namespaces.length === 0) {
return 0 return 0;
} }
return state.namespaces.namespaces[0].id return state.namespaces.namespaces[0].id;
}, },
hasLists: state => { hasLists: state => {
if (state.namespaces.namespaces.length === 0) { if (state.namespaces.namespaces.length === 0) {
return false return false;
} }
return state.namespaces.namespaces[0].lists.length > 0 return state.namespaces.namespaces[0].lists.length > 0;
}, }
}), }),
} methods: {
updateTaskList() {
this.showTasksKey += 1;
},
updateWelcome() {
this.currentDate = new Date();
if (this.currentDate.getHours() < 12) {
this.welcomePrefix = "Good Morning";
} else if (this.currentDate.getHours() < 17) {
this.welcomePrefix = "Good Afternoon";
} else {
this.welcomePrefix = "Good Evening";
}
this.$options.timer = window.setTimeout(
this.updateDateTime,
this.updateWelcomeInterval
);
}
}
};
</script> </script>

View File

@ -1,11 +1,15 @@
<template> <template>
<div <div
:class="{ 'is-loading': taskCollectionService.loading}" :class="{ 'is-loading': taskCollectionService.loading }"
class="loader-container is-max-width-desktop list-view"> class="loader-container is-max-width-desktop list-view"
<div class="filter-container" v-if="list.isSavedFilter && !list.isSavedFilter()"> >
<div
class="filter-container"
v-if="list.isSavedFilter && !list.isSavedFilter()"
>
<div class="items"> <div class="items">
<div class="search"> <div class="search">
<div :class="{ 'hidden': !showTaskSearch }" class="field has-addons"> <div :class="{ hidden: !showTaskSearch }" class="field has-addons">
<div class="control has-icons-left has-icons-right"> <div class="control has-icons-left has-icons-right">
<input <input
@blur="hideSearchBar()" @blur="hideSearchBar()"
@ -14,9 +18,10 @@
placeholder="Search" placeholder="Search"
type="text" type="text"
v-focus v-focus
v-model="searchTerm"/> v-model="searchTerm"
/>
<span class="icon is-left"> <span class="icon is-left">
<icon icon="search"/> <icon icon="search" />
</span> </span>
</div> </div>
<div class="control"> <div class="control">
@ -52,45 +57,34 @@
</div> </div>
<card :padding="false" :has-content="false" class="has-overflow"> <card :padding="false" :has-content="false" class="has-overflow">
<div class="field task-add" v-if="!list.isArchived && canWrite && list.id > 0"> <div
<div class="field is-grouped"> class="field task-add"
<p :class="{ 'is-loading': taskService.loading}" class="control has-icons-left is-expanded"> v-if="!list.isArchived && canWrite && list.id > 0"
<input >
:class="{ 'disabled': taskService.loading}" <add-task
@keyup.enter="addTask()" :listId="Number($route.params.listId)"
class="input" @taskAdded="updateTaskList"
placeholder="Add a new task..." />
type="text"
v-focus
v-model="newTaskText"
ref="newTaskInput"
/>
<span class="icon is-small is-left">
<icon icon="tasks"/>
</span>
</p>
<p class="control">
<x-button
:disabled="newTaskText.length === 0"
@click="addTask()"
icon="plus"
>
Add
</x-button>
</p>
</div>
<p class="help is-danger" v-if="showError && newTaskText === ''"> <p class="help is-danger" v-if="showError && newTaskText === ''">
Please specify a list title. Please specify a list title.
</p> </p>
</div> </div>
<nothing v-if="ctaVisible && tasks.length === 0 && !taskCollectionService.loading"> <nothing
v-if="
ctaVisible && tasks.length === 0 && !taskCollectionService.loading
"
>
This list is currently empty. This list is currently empty.
<a @click="$refs.newTaskInput.focus()">Create a new task.</a> <a @click="$refs.newTaskInput.focus()">Create a new task.</a>
</nothing> </nothing>
<div class="tasks-container"> <div class="tasks-container">
<div :class="{'short': isTaskEdit}" class="tasks mt-0" v-if="tasks && tasks.length > 0"> <div
:class="{ short: isTaskEdit }"
class="tasks mt-0"
v-if="tasks && tasks.length > 0"
>
<single-task-in-list <single-task-in-list
:show-list-color="false" :show-list-color="false"
:disabled="!canWrite" :disabled="!canWrite"
@ -100,16 +94,24 @@
task-detail-route="task.detail" task-detail-route="task.detail"
v-for="t in tasks" v-for="t in tasks"
> >
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived && canWrite"> <div
<icon icon="pencil-alt"/> @click="editTask(t.id)"
class="icon settings"
v-if="!list.isArchived && canWrite"
>
<icon icon="pencil-alt" />
</div> </div>
</single-task-in-list> </single-task-in-list>
</div> </div>
<card <card
v-if="isTaskEdit" v-if="isTaskEdit"
class="taskedit mt-0" title="Edit Task" :has-close="true" @close="() => isTaskEdit = false" class="taskedit mt-0"
:shadow="false"> title="Edit Task"
<edit-task :task="taskEditTask"/> :has-close="true"
@close="() => (isTaskEdit = false)"
:shadow="false"
>
<edit-task :task="taskEditTask" />
</card> </card>
</div> </div>
@ -117,30 +119,36 @@
aria-label="pagination" aria-label="pagination"
class="pagination is-centered p-4" class="pagination is-centered p-4"
role="navigation" role="navigation"
v-if="taskCollectionService.totalPages > 1"> v-if="taskCollectionService.totalPages > 1"
>
<router-link <router-link
:disabled="currentPage === 1" :disabled="currentPage === 1"
:to="getRouteForPagination(currentPage - 1)" :to="getRouteForPagination(currentPage - 1)"
class="pagination-previous" class="pagination-previous"
tag="button"> tag="button"
>
Previous Previous
</router-link> </router-link>
<router-link <router-link
:disabled="currentPage === taskCollectionService.totalPages" :disabled="currentPage === taskCollectionService.totalPages"
:to="getRouteForPagination(currentPage + 1)" :to="getRouteForPagination(currentPage + 1)"
class="pagination-next" class="pagination-next"
tag="button"> tag="button"
>
Next page Next page
</router-link> </router-link>
<ul class="pagination-list"> <ul class="pagination-list">
<template v-for="(p, i) in pages"> <template v-for="(p, i) in pages">
<li :key="'page'+i" v-if="p.isEllipsis"><span class="pagination-ellipsis">&hellip;</span></li> <li :key="'page' + i" v-if="p.isEllipsis">
<li :key="'page'+i" v-else> <span class="pagination-ellipsis">&hellip;</span>
</li>
<li :key="'page' + i" v-else>
<router-link <router-link
:aria-label="'Goto page ' + p.number" :aria-label="'Goto page ' + p.number"
:class="{'is-current': p.number === currentPage}" :class="{ 'is-current': p.number === currentPage }"
:to="getRouteForPagination(p.number)" :to="getRouteForPagination(p.number)"
class="pagination-link"> class="pagination-link"
>
{{ p.number }} {{ p.number }}
</router-link> </router-link>
</li> </li>
@ -151,222 +159,103 @@
<!-- This router view is used to show the task popup while keeping the kanban board itself --> <!-- This router view is used to show the task popup while keeping the kanban board itself -->
<transition name="modal"> <transition name="modal">
<router-view/> <router-view />
</transition> </transition>
</div> </div>
</template> </template>
<script> <script>
import TaskService from '../../../services/task' import TaskService from "../../../services/task";
import TaskModel from '../../../models/task' import TaskModel from "../../../models/task";
import LabelTaskService from '../../../services/labelTask' import LabelTaskService from "../../../services/labelTask";
import LabelService from '../../../services/label' import LabelService from "../../../services/label";
import LabelTask from '../../../models/labelTask'
import LabelModel from '../../../models/label'
import EditTask from '../../../components/tasks/edit-task' import EditTask from "../../../components/tasks/edit-task";
import SingleTaskInList from '../../../components/tasks/partials/singleTaskInList' import AddTask from "../../../components/tasks/add-task";
import taskList from '../../../components/tasks/mixins/taskList' import SingleTaskInList from "../../../components/tasks/partials/singleTaskInList";
import {saveListView} from '@/helpers/saveListView' import taskList from "../../../components/tasks/mixins/taskList";
import Rights from '../../../models/rights.json' import { saveListView } from "@/helpers/saveListView";
import {mapState} from 'vuex' import Rights from "../../../models/rights.json";
import FilterPopup from '@/components/list/partials/filter-popup' import { mapState } from "vuex";
import {HAS_TASKS} from '@/store/mutation-types' import FilterPopup from "@/components/list/partials/filter-popup";
import Nothing from '@/components/misc/nothing' import { HAS_TASKS } from "@/store/mutation-types";
import Nothing from "@/components/misc/nothing";
export default { export default {
name: 'List', name: "List",
data() { data() {
return { return {
taskService: TaskService, taskService: TaskService,
isTaskEdit: false, isTaskEdit: false,
taskEditTask: TaskModel, taskEditTask: TaskModel,
newTaskText: '', newTaskText: "",
showError: false, showError: false,
labelTaskService: LabelTaskService, labelTaskService: LabelTaskService,
labelService: LabelService, labelService: LabelService,
ctaVisible: false, ctaVisible: false
} };
}, },
mixins: [ mixins: [taskList],
taskList,
],
components: { components: {
Nothing, Nothing,
FilterPopup, FilterPopup,
SingleTaskInList, SingleTaskInList,
EditTask, EditTask,
AddTask
}, },
created() { created() {
this.taskService = new TaskService() this.taskService = new TaskService();
this.labelService = new LabelService() this.labelService = new LabelService();
this.labelTaskService = new LabelTaskService() this.labelTaskService = new LabelTaskService();
// Save the current list view to local storage // Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads. // We use local storage and not vuex here to make it persistent across reloads.
saveListView(this.$route.params.listId, this.$route.name) saveListView(this.$route.params.listId, this.$route.name);
}, },
computed: mapState({ computed: mapState({
canWrite: state => state.currentList.maxRight > Rights.READ, canWrite: state => state.currentList.maxRight > Rights.READ,
list: state => state.currentList, list: state => state.currentList
}), }),
mounted() { mounted() {
this.$nextTick(() => this.ctaVisible = true) this.$nextTick(() => (this.ctaVisible = true));
}, },
methods: { methods: {
// This function initializes the tasks page and loads the first page of tasks // This function initializes the tasks page and loads the first page of tasks
initTasks(page, search = '') { initTasks(page, search = "") {
this.taskEditTask = null this.taskEditTask = null;
this.isTaskEdit = false this.isTaskEdit = false;
this.loadTasks(page, search) this.loadTasks(page, search);
}, },
addTask() { updateTaskList(task) {
if (this.newTaskText === '') { this.tasks.push(task);
this.showError = true this.sortTasks();
return this.$store.commit(HAS_TASKS, true);
}
this.showError = false
let task = new TaskModel({title: this.newTaskText, listId: this.$route.params.listId})
this.taskService.create(task)
.then(task => {
this.tasks.push(task)
this.sortTasks()
this.newTaskText = ''
// Check if the task has words starting with ~ in the title and make them to labels
const parts = task.title.split(' ~')
// The first element will always contain the title, even if there is no occurrence of ~
if (parts.length > 1) {
// First, create an unresolved promise for each entry in the array to wait
// until all labels are added to update the task title once again
let labelAddings = []
let labelAddsToWaitFor = []
parts.forEach((p, index) => {
if (index < 1) {
return
}
labelAddsToWaitFor.push(new Promise((resolve, reject) => {
labelAddings.push({resolve: resolve, reject: reject})
}))
})
// Then do everything that is involved in finding, creating and adding the label to the task
parts.forEach((p, index) => {
if (index < 1) {
return
}
// The part up until the next space
const labelTitle = p.split(' ')[0]
// Don't create an empty label
if (labelTitle === '') {
return
}
// Check if the label exists
this.labelService.getAll({}, {s: labelTitle})
.then(res => {
// Label found, use it
if (res.length > 0 && res[0].title === labelTitle) {
const labelTask = new LabelTask({
taskId: task.id,
labelId: res[0].id,
})
this.labelTaskService.create(labelTask)
.then(result => {
task.labels.push(res[0])
// Remove the label text from the task title
task.title = task.title.replace(` ~${labelTitle}`, '')
// Make the promise done (the one with the index 0 does not exist)
labelAddings[index - 1].resolve(result)
})
.catch(e => {
this.error(e, this)
})
} else {
// label not found, create it
const label = new LabelModel({title: labelTitle})
this.labelService.create(label)
.then(res => {
const labelTask = new LabelTask({
taskId: task.id,
labelId: res.id,
})
this.labelTaskService.create(labelTask)
.then(result => {
task.labels.push(res)
// Remove the label text from the task title
task.title = task.title.replace(` ~${labelTitle}`, '')
// Make the promise done (the one with the index 0 does not exist)
labelAddings[index - 1].resolve(result)
})
.catch(e => {
this.error(e, this)
})
})
.catch(e => {
this.error(e, this)
})
}
})
.catch(e => {
this.error(e, this)
})
})
// This waits to update the task until all labels have been added and the title has
// been modified to remove each label text
Promise.all(labelAddsToWaitFor)
.then(() => {
this.taskService.update(task)
.then(updatedTask => {
this.updateTasks(updatedTask)
this.$store.commit(HAS_TASKS, true)
})
.catch(e => {
this.error(e, this)
})
})
}
})
.catch(e => {
this.error(e, this)
})
}, },
editTask(id) { editTask(id) {
// Find the selected task and set it to the current object // Find the selected task and set it to the current object
let theTask = this.getTaskById(id) // Somehow this does not work if we directly assign this to this.taskEditTask let theTask = this.getTaskById(id); // Somehow this does not work if we directly assign this to this.taskEditTask
this.taskEditTask = theTask this.taskEditTask = theTask;
this.isTaskEdit = true this.isTaskEdit = true;
}, },
getTaskById(id) { getTaskById(id) {
for (const t in this.tasks) { for (const t in this.tasks) {
if (this.tasks[t].id === parseInt(id)) { if (this.tasks[t].id === parseInt(id)) {
return this.tasks[t] return this.tasks[t];
} }
} }
return {} // FIXME: This should probably throw something to make it clear to the user noting was found return {}; // FIXME: This should probably throw something to make it clear to the user noting was found
}, },
updateTasks(updatedTask) { updateTasks(updatedTask) {
for (const t in this.tasks) { for (const t in this.tasks) {
if (this.tasks[t].id === updatedTask.id) { if (this.tasks[t].id === updatedTask.id) {
this.$set(this.tasks, t, updatedTask) this.$set(this.tasks, t, updatedTask);
break break;
} }
} }
this.sortTasks() this.sortTasks();
}, }
}, }
} };
</script> </script>