feat(frontend): add basic task view

This commit is contained in:
DCsunset 2020-07-14 01:57:01 -07:00
parent 1589762009
commit fbab994c10
5 changed files with 334 additions and 33 deletions

View file

@ -1,15 +1,87 @@
<template> <template>
<v-dialog v-model="showDialog" max-width="600px" persistent> <v-dialog
v-model="showDialog"
max-width="600px"
persistent
@keydown.esc="closeDialog"
>
<v-card> <v-card>
<v-card-title> <v-card-title>
{{ task ? 'Edit Task' : 'New Task' }} {{ task ? 'Edit Task' : 'New Task' }}
</v-card-title> </v-card-title>
<v-card-text>
<v-form ref="formRef" lazy-validation>
<v-text-field
autofocus
v-model="formData.description"
label="Description*"
:rules="requiredRules"
required
/>
<v-combobox
v-model="formData.project"
:items="projects"
hide-selected
label="Project"
/>
<v-text-field
v-model="formData.due"
:label="recur ? 'Due*' : 'Due'"
:rules="recur ? requiredRules : []"
:required="recur"
/>
<v-text-field
v-model="formData.wait"
label="Wait"
/>
<v-text-field
v-model="formData.until"
label="Until"
/>
<v-combobox
v-model="formData.tags"
:items="tags"
hide-selected
small-chips
multiple
label="Tags"
hint="Press tab or enter to add new tags"
/>
<v-radio-group v-model="formData.priority" row hide-details class="align-center">
<template v-slot:prepend>
<span class="mr-3 subtitle-1">
Priority
</span>
</template>
<v-radio
v-for="p in priorities"
:key="p.text"
:label="p.text"
:value="p.value"
/>
</v-radio-group>
<v-row class="px-3">
<v-checkbox v-model="recur" class="mr-3" label="Recur" />
<v-text-field
label="period*"
v-model="formData.recur"
:rules="recur ? requiredRules : []"
:required="recur"
:disabled="!recur"
/>
</v-row>
</v-form>
</v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn text @click="cloesDialog"> <v-btn text @click="closeDialog" width="80px">
Cancel Cancel
</v-btn> </v-btn>
<v-btn color="primary" @click="submit"> <v-btn @click="reset" width="80px">
Reset
</v-btn>
<v-btn color="primary" @click="submit" width="80px">
Submit Submit
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
@ -18,7 +90,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, computed, reactive, ref } from '@vue/composition-api'; import { defineComponent, watch, computed, ref } from '@vue/composition-api';
import { Task } from 'taskwarrior-lib'; import { Task } from 'taskwarrior-lib';
interface Props { interface Props {
@ -39,20 +111,94 @@ export default defineComponent({
} }
}, },
setup(props: Props, context) { setup(props: Props, context) {
const projects = computed(() => context.root.$store.getters.projects);
const tags = computed(() => context.root.$store.getters.tags);
const showDialog = computed({ const showDialog = computed({
get: () => props.value, get: () => props.value,
set: val => context.emit('input', val) set: val => context.emit('input', val)
}); });
const cloesDialog = () => {
showDialog.value = false; const requiredRules = [
}; (str: string) => Boolean(str) || 'Required'
const submit = () => { ];
// TODO: submit
cloesDialog(); const recur = ref(Boolean(props.task?.recur));
const formData = ref({
description: '',
project: '',
due: '',
until: '',
wait: '',
tags: [] as string[],
priority: 'N',
recur: '',
...props.task
});
const reset = () => {
formData.value = {
description: '',
project: '',
due: '',
until: '',
wait: '',
tags: [] as string[],
priority: 'N',
recur: '',
...props.task
};
recur.value = Boolean(props.task?.recur);
(formRef.value as any).resetValidation();
}; };
watch(() => props.task, () => {
reset();
});
const formRef = ref(null);
const closeDialog = () => {
showDialog.value = false;
reset();
};
const submit = async () => {
const valid = (formRef.value as any).validate();
if (valid) {
await context.root.$store.dispatch('updateTasks', [{
...formData.value,
project: formData.value.project || undefined,
due: formData.value.due || undefined,
until: formData.value.until || undefined,
wait: formData.value.wait || undefined,
priority: formData.value.priority === 'N' ? undefined : formData.value.priority,
recur: recur.value ? formData.value.recur : undefined
}]);
context.root.$store.commit('setNotification', {
color: 'success',
text: `Successfully ${props.task ? 'update' : 'create'} the task`
});
closeDialog();
}
};
const priorities = [
{ text: 'None', value: 'N' },
{ text: 'Low', value: 'L' },
{ text: 'Medium', value: 'M' },
{ text: 'High', value: 'H' }
];
return { return {
cloesDialog, requiredRules,
formRef,
tags,
projects,
priorities,
recur,
formData,
closeDialog,
reset,
submit, submit,
showDialog showDialog
}; };

View file

@ -2,7 +2,7 @@
<div> <div>
<TaskDialog v-model="showTaskDialog" :task="currentTask" /> <TaskDialog v-model="showTaskDialog" :task="currentTask" />
<v-btn-toggle v-model="status" mandatory> <v-btn-toggle v-model="status" mandatory background-color="rgba(0, 0, 0, 0)">
<v-row class="pa-3"> <v-row class="pa-3">
<v-btn <v-btn
v-for="st in allStatus" v-for="st in allStatus"
@ -20,9 +20,9 @@
</v-icon> </v-icon>
{{ st }} {{ st }}
<v-badge <v-badge
v-if="st === 'pending'" v-if="st === 'pending' && classifiedTasks[st].length"
:content="classifiedTasks[st].length" :content="classifiedTasks[st].length"
:color="st === status ? 'primary' : 'grey darken-1'" :color="st === status ? 'primary' : 'grey'"
inline inline
/> />
</v-btn> </v-btn>
@ -33,6 +33,7 @@
:items="classifiedTasks[status]" :items="classifiedTasks[status]"
:headers="headers" :headers="headers"
show-select show-select
item-key="uuid"
v-model="selected" v-model="selected"
class="elevation-1" class="elevation-1"
> >
@ -47,9 +48,22 @@
small small
dark dark
title="Done" title="Done"
@click="completeTasks(selected)"
> >
<v-icon>mdi-check</v-icon> <v-icon>mdi-check</v-icon>
</v-btn> </v-btn>
<v-btn
v-show="status === 'completed' || status === 'deleted'"
class="ma-1"
color="primary"
fab
dark
small
title="Restore"
@click="restoreTasks(selected)"
>
<v-icon>mdi-restore</v-icon>
</v-btn>
<v-btn <v-btn
v-show="status !== 'deleted'" v-show="status !== 'deleted'"
class="ma-1 red" class="ma-1 red"
@ -89,17 +103,59 @@
</v-row> </v-row>
</template> </template>
<template v-if="status === 'waiting'" v-slot:item.wait="{ item }">
{{ displayDate(item.wait) }}
</template>
<template v-slot:item.due="{ item }">
{{ displayDate(item.due) }}
</template>
<template v-slot:item.until="{ item }">
{{ displayDate(item.until) }}
</template>
<template v-slot:item.tags="{ item }">
<v-chip
v-for="tag in item.tags"
:key="tag"
small
>
{{ tag }}
</v-chip>
</template>
<template v-slot:item.actions="{ item }"> <template v-slot:item.actions="{ item }">
<v-icon <v-icon
small v-show="status === 'pending'"
class="mr-2" size="20px"
class="ml-2"
@click="completeTasks([item])"
title="Done"
>
mdi-check
</v-icon>
<v-icon
v-show="status === 'completed' || status === 'deleted'"
size="20px"
class="ml-2"
@click="restoreTasks([item])"
title="Restore"
>
mdi-restore
</v-icon>
<v-icon
class="ml-2"
size="20px"
@click="editTask(item)" @click="editTask(item)"
title="Edit"
> >
mdi-pencil mdi-pencil
</v-icon> </v-icon>
<v-icon <v-icon
small v-show="status !== 'deleted'"
class="ml-2"
size="20px"
@click="deleteTasks([item])" @click="deleteTasks([item])"
title="Delete"
> >
mdi-delete mdi-delete
</v-icon> </v-icon>
@ -113,6 +169,11 @@ import { defineComponent, computed, reactive, ref, ComputedRef, Ref } from '@vue
import { Task } from 'taskwarrior-lib'; import { Task } from 'taskwarrior-lib';
import _ from 'lodash'; import _ from 'lodash';
import TaskDialog from '../components/TaskDialog.vue'; import TaskDialog from '../components/TaskDialog.vue';
import moment from 'moment';
function displayDate(str?: string) {
return str && moment(str).format('YYYY-MM-DD');
}
interface Props { interface Props {
[key: string]: unknown, [key: string]: unknown,
@ -127,15 +188,6 @@ export default defineComponent({
}, },
setup(props: Props, context) { setup(props: Props, context) {
const headers = [
{ text: 'Project', value: 'project' },
{ text: 'Description', value: 'description' },
{ text: 'Priority', value: 'priority' },
{ text: 'Due', value: 'due' },
{ text: 'Tags', value: 'tags' },
{ text: 'Actions', value: 'actions', sortable: false }
];
const selected = ref([] as Task[]); const selected = ref([] as Task[]);
const status = ref('pending'); const status = ref('pending');
@ -147,31 +199,76 @@ export default defineComponent({
deleted: 'mdi-delete', deleted: 'mdi-delete',
recurring: 'mdi-restart' recurring: 'mdi-restart'
}; };
const headers = computed(() => [
{ text: 'Project', value: 'project' },
{ text: 'Description', value: 'description' },
{ text: 'Priority', value: 'priority' },
...(status.value !== 'waiting'
? [{ text: 'Due', value: 'due' }]
: [{ text: 'Wait', value: 'wait' }]),
{ text: 'Until', value: 'until' },
{ text: 'Tags', value: 'tags' },
{ text: 'Actions', value: 'actions', sortable: false }
]);
const tempTasks: { [key: string]: ComputedRef<Task[]> } = {}; const tempTasks: { [key: string]: ComputedRef<Task[]> } = {};
for (const status of allStatus) { for (const status of allStatus) {
tempTasks[status] = computed((): Task[] => props.tasks?.filter(task => task.status === status)); tempTasks[status] = computed((): Task[] => props.tasks?.filter(task => task.status === status));
} }
const classifiedTasks = reactive(tempTasks); const classifiedTasks = reactive(tempTasks);
console.log(tempTasks);
const refresh = () => { const refresh = () => {
context.root.$store.dispatch('fetchTasks'); context.root.$store.dispatch('fetchTasks');
}; };
const showTaskDialog = ref(false); const showTaskDialog = ref(false);
const currentTask: Ref<Task> = ref(null); const currentTask: Ref<Task | null> = ref(null);
const newTask = () => { const newTask = () => {
showTaskDialog.value = true; showTaskDialog.value = true;
currentTask.value = null; currentTask.value = null;
}; };
const editTask = (_task: Task) => { const editTask = (task: Task) => {
// TODO showTaskDialog.value = true;
currentTask.value = _.cloneDeep(task);
}; };
const deleteTasks = async (tasks: Task[]) => { const deleteTasks = async (tasks: Task[]) => {
await context.root.$store.dispatch('deleteTasks', tasks); await context.root.$store.dispatch('deleteTasks', tasks);
_.remove(selected.value, task => tasks.findIndex(t => t.uuid === task.uuid) !== -1); selected.value = selected.value.filter(task => tasks.findIndex(t => t.uuid === task.uuid) === -1);
context.root.$store.commit('setNotification', {
color: 'success',
text: 'Successfully delete the task(s)'
});
};
const completeTasks = async (tasks: Task[]) => {
await context.root.$store.dispatch('updateTasks', tasks.map(task => {
return {
...task,
status: 'completed'
};
}));
selected.value = selected.value.filter(task => tasks.findIndex(t => t.uuid === task.uuid) === -1);
context.root.$store.commit('setNotification', {
color: 'success',
text: 'Successfully complete the task(s)'
});
};
const restoreTasks = async (tasks: Task[]) => {
await context.root.$store.dispatch('updateTasks', tasks.map(task => {
return {
...task,
status: 'pending'
};
}));
selected.value = selected.value.filter(task => tasks.findIndex(t => t.uuid === task.uuid) === -1);
context.root.$store.commit('setNotification', {
color: 'success',
text: 'Successfully restore the task(s)'
});
}; };
return { return {
@ -186,8 +283,11 @@ export default defineComponent({
currentTask, currentTask,
editTask, editTask,
deleteTasks, deleteTasks,
completeTasks,
restoreTasks,
showTaskDialog, showTaskDialog,
TaskDialog TaskDialog,
displayDate
}; };
} }
}); });

View file

@ -1,5 +1,24 @@
<template> <template>
<v-app> <v-app>
<v-snackbar
v-model="snackbar"
:color="notification.color"
:timeout="4000"
>
{{ notification.text }}
<template v-slot:action="{ attrs }">
<v-btn
dark
text
v-bind="attrs"
@click="snackbar = false"
>
Close
</v-btn>
</template>
</v-snackbar>
<v-app-bar height="54px" fixed app> <v-app-bar height="54px" fixed app>
<v-toolbar-title> <v-toolbar-title>
Taskwarrior WebUI Taskwarrior WebUI
@ -19,7 +38,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, computed } from '@vue/composition-api'; import { defineComponent, computed, onErrorCaptured } from '@vue/composition-api';
export default defineComponent({ export default defineComponent({
setup(_props, context) { setup(_props, context) {
@ -29,8 +48,38 @@ export default defineComponent({
context.root.$vuetify.theme.dark = val; context.root.$vuetify.theme.dark = val;
} }
}); });
const notification = computed(() => context.root.$store.state.notification);
const snackbar = computed({
get: () => context.root.$store.state.snackbar,
set: val => context.root.$store.commit('setSnackbar', val)
});
onErrorCaptured((err: any) => {
// axios error
let notification: any;
if (err?.response) {
const { status, data } = err.response!;
notification = {
color: 'error',
text: `Error ${status}: ${data}`
};
}
else {
const { name, message } = err as Error;
notification = {
color: 'error',
text: `Error ${name}: ${message}`
};
}
context.root.$store.commit('setNotification', notification);
return false;
});
return { return {
dark dark,
snackbar,
notification
}; };
} }
}); });

View file

@ -7788,6 +7788,11 @@
"minimist": "^1.2.5" "minimist": "^1.2.5"
} }
}, },
"moment": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
},
"move-concurrently": { "move-concurrently": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npm.taobao.org/move-concurrently/download/move-concurrently-1.0.1.tgz", "resolved": "https://registry.npm.taobao.org/move-concurrently/download/move-concurrently-1.0.1.tgz",

View file

@ -17,6 +17,7 @@
"@nuxtjs/pwa": "^3.0.0-beta.20", "@nuxtjs/pwa": "^3.0.0-beta.20",
"@vue/composition-api": "^1.0.0-beta.1", "@vue/composition-api": "^1.0.0-beta.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"moment": "^2.27.0",
"nuxt": "^2.13.0", "nuxt": "^2.13.0",
"nuxt-typed-vuex": "^0.1.19" "nuxt-typed-vuex": "^0.1.19"
}, },