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.
## Project setup
```
```shell
yarn install
```
### Compiles and hot-reloads for development
```
```shell
yarn run serve
```
### Compiles and minifies for production
```
```shell
yarn run build
```
### Lints and fixes files
```
```shell
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>
<div class="content has-text-centered">
<h2>
Hi {{ userInfo.name !== '' ? userInfo.name : userInfo.username }}!
{{ welcomePrefix }}
{{ userInfo.name !== "" ? userInfo.name : userInfo.username }}!
</h2>
<add-task
:list="defaultList"
@taskAdded="updateTaskList"
class="is-max-width-desktop"
/>
<template v-if="!hasTasks">
<p>You can create a new list for your new tasks:</p>
<x-button
:to="{name: 'list.create', params: { id: defaultNamespaceId }}"
:to="{ name: 'list.create', params: { id: defaultNamespaceId } }"
:shadow="false"
class="ml-2"
v-if="defaultNamespaceId > 0"
@ -19,49 +25,89 @@
<x-button
v-if="migratorsEnabled"
:to="{ name: 'migrate.start' }"
:shadow="false">
:shadow="false"
>
Import your data into Vikunja
</x-button>
</template>
<ShowTasks :show-all="true" v-if="hasLists"/>
<ShowTasks :show-all="true" v-if="hasLists" :key="showTasksKey" />
</div>
</template>
<script>
import { mapState } from 'vuex'
import ShowTasks from './tasks/ShowTasks'
import { mapState } from "vuex";
import ShowTasks from "./tasks/ShowTasks";
import AddTask from "../components/tasks/add-task";
import ListModel from "../models/list";
export default {
name: 'Home',
name: "Home",
components: {
ShowTasks,
AddTask
},
data() {
return {
loading: false,
currentDate: new Date(),
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({
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,
userInfo: state => state.auth.info,
hasTasks: state => state.hasTasks,
defaultNamespaceId: state => {
if (state.namespaces.namespaces.length === 0) {
return 0
return 0;
}
return state.namespaces.namespaces[0].id
return state.namespaces.namespaces[0].id;
},
hasLists: state => {
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>

View File

@ -1,11 +1,15 @@
<template>
<div
:class="{ 'is-loading': taskCollectionService.loading}"
class="loader-container is-max-width-desktop list-view">
<div class="filter-container" v-if="list.isSavedFilter && !list.isSavedFilter()">
:class="{ 'is-loading': taskCollectionService.loading }"
class="loader-container is-max-width-desktop list-view"
>
<div
class="filter-container"
v-if="list.isSavedFilter && !list.isSavedFilter()"
>
<div class="items">
<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">
<input
@blur="hideSearchBar()"
@ -14,9 +18,10 @@
placeholder="Search"
type="text"
v-focus
v-model="searchTerm"/>
v-model="searchTerm"
/>
<span class="icon is-left">
<icon icon="search"/>
<icon icon="search" />
</span>
</div>
<div class="control">
@ -52,45 +57,34 @@
</div>
<card :padding="false" :has-content="false" class="has-overflow">
<div class="field task-add" v-if="!list.isArchived && canWrite && list.id > 0">
<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>
<div
class="field task-add"
v-if="!list.isArchived && canWrite && list.id > 0"
>
<add-task
:listId="Number($route.params.listId)"
@taskAdded="updateTaskList"
/>
<p class="help is-danger" v-if="showError && newTaskText === ''">
Please specify a list title.
</p>
</div>
<nothing v-if="ctaVisible && tasks.length === 0 && !taskCollectionService.loading">
<nothing
v-if="
ctaVisible && tasks.length === 0 && !taskCollectionService.loading
"
>
This list is currently empty.
<a @click="$refs.newTaskInput.focus()">Create a new task.</a>
</nothing>
<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
:show-list-color="false"
:disabled="!canWrite"
@ -100,16 +94,24 @@
task-detail-route="task.detail"
v-for="t in tasks"
>
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived && canWrite">
<icon icon="pencil-alt"/>
<div
@click="editTask(t.id)"
class="icon settings"
v-if="!list.isArchived && canWrite"
>
<icon icon="pencil-alt" />
</div>
</single-task-in-list>
</div>
<card
v-if="isTaskEdit"
class="taskedit mt-0" title="Edit Task" :has-close="true" @close="() => isTaskEdit = false"
:shadow="false">
<edit-task :task="taskEditTask"/>
class="taskedit mt-0"
title="Edit Task"
:has-close="true"
@close="() => (isTaskEdit = false)"
:shadow="false"
>
<edit-task :task="taskEditTask" />
</card>
</div>
@ -117,30 +119,36 @@
aria-label="pagination"
class="pagination is-centered p-4"
role="navigation"
v-if="taskCollectionService.totalPages > 1">
v-if="taskCollectionService.totalPages > 1"
>
<router-link
:disabled="currentPage === 1"
:to="getRouteForPagination(currentPage - 1)"
class="pagination-previous"
tag="button">
tag="button"
>
Previous
</router-link>
<router-link
:disabled="currentPage === taskCollectionService.totalPages"
:to="getRouteForPagination(currentPage + 1)"
class="pagination-next"
tag="button">
tag="button"
>
Next page
</router-link>
<ul class="pagination-list">
<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-else>
<li :key="'page' + i" v-if="p.isEllipsis">
<span class="pagination-ellipsis">&hellip;</span>
</li>
<li :key="'page' + i" v-else>
<router-link
:aria-label="'Goto page ' + p.number"
:class="{'is-current': p.number === currentPage}"
:class="{ 'is-current': p.number === currentPage }"
:to="getRouteForPagination(p.number)"
class="pagination-link">
class="pagination-link"
>
{{ p.number }}
</router-link>
</li>
@ -151,222 +159,103 @@
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
<transition name="modal">
<router-view/>
<router-view />
</transition>
</div>
</template>
<script>
import TaskService from '../../../services/task'
import TaskModel from '../../../models/task'
import LabelTaskService from '../../../services/labelTask'
import LabelService from '../../../services/label'
import LabelTask from '../../../models/labelTask'
import LabelModel from '../../../models/label'
import TaskService from "../../../services/task";
import TaskModel from "../../../models/task";
import LabelTaskService from "../../../services/labelTask";
import LabelService from "../../../services/label";
import EditTask from '../../../components/tasks/edit-task'
import SingleTaskInList from '../../../components/tasks/partials/singleTaskInList'
import taskList from '../../../components/tasks/mixins/taskList'
import {saveListView} from '@/helpers/saveListView'
import Rights from '../../../models/rights.json'
import {mapState} from 'vuex'
import FilterPopup from '@/components/list/partials/filter-popup'
import {HAS_TASKS} from '@/store/mutation-types'
import Nothing from '@/components/misc/nothing'
import EditTask from "../../../components/tasks/edit-task";
import AddTask from "../../../components/tasks/add-task";
import SingleTaskInList from "../../../components/tasks/partials/singleTaskInList";
import taskList from "../../../components/tasks/mixins/taskList";
import { saveListView } from "@/helpers/saveListView";
import Rights from "../../../models/rights.json";
import { mapState } from "vuex";
import FilterPopup from "@/components/list/partials/filter-popup";
import { HAS_TASKS } from "@/store/mutation-types";
import Nothing from "@/components/misc/nothing";
export default {
name: 'List',
name: "List",
data() {
return {
taskService: TaskService,
isTaskEdit: false,
taskEditTask: TaskModel,
newTaskText: '',
newTaskText: "",
showError: false,
labelTaskService: LabelTaskService,
labelService: LabelService,
ctaVisible: false,
}
ctaVisible: false
};
},
mixins: [
taskList,
],
mixins: [taskList],
components: {
Nothing,
FilterPopup,
SingleTaskInList,
EditTask,
AddTask
},
created() {
this.taskService = new TaskService()
this.labelService = new LabelService()
this.labelTaskService = new LabelTaskService()
this.taskService = new TaskService();
this.labelService = new LabelService();
this.labelTaskService = new LabelTaskService();
// Save the current list view to local storage
// 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({
canWrite: state => state.currentList.maxRight > Rights.READ,
list: state => state.currentList,
list: state => state.currentList
}),
mounted() {
this.$nextTick(() => this.ctaVisible = true)
this.$nextTick(() => (this.ctaVisible = true));
},
methods: {
// This function initializes the tasks page and loads the first page of tasks
initTasks(page, search = '') {
this.taskEditTask = null
this.isTaskEdit = false
this.loadTasks(page, search)
initTasks(page, search = "") {
this.taskEditTask = null;
this.isTaskEdit = false;
this.loadTasks(page, search);
},
addTask() {
if (this.newTaskText === '') {
this.showError = true
return
}
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)
})
updateTaskList(task) {
this.tasks.push(task);
this.sortTasks();
this.$store.commit(HAS_TASKS, true);
},
editTask(id) {
// 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
this.taskEditTask = theTask
this.isTaskEdit = true
let theTask = this.getTaskById(id); // Somehow this does not work if we directly assign this to this.taskEditTask
this.taskEditTask = theTask;
this.isTaskEdit = true;
},
getTaskById(id) {
for (const t in this.tasks) {
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) {
for (const t in this.tasks) {
if (this.tasks[t].id === updatedTask.id) {
this.$set(this.tasks, t, updatedTask)
break
this.$set(this.tasks, t, updatedTask);
break;
}
}
this.sortTasks()
},
},
}
</script>
this.sortTasks();
}
}
};
</script>