Compare commits
12 commits
main
...
feature/ta
Author | SHA1 | Date | |
---|---|---|---|
|
e520cd7053 | ||
|
bfb8ab407b | ||
|
8c61d99393 | ||
|
070b629fb6 | ||
|
a712c2b514 | ||
|
a2b9971e67 | ||
|
e492ce40d2 | ||
|
169f95af33 | ||
|
d950aae3a5 | ||
|
bf027397b5 | ||
|
ec01f516a8 | ||
|
4f38f25d11 |
10 changed files with 253 additions and 37 deletions
|
@ -1,8 +1,10 @@
|
|||
<template>
|
||||
<div class="datepicker">
|
||||
<slot :date="date" :openPopup="toggleDatePopup">
|
||||
<BaseButton @click.stop="toggleDatePopup" class="show" :disabled="disabled || undefined">
|
||||
{{ date === null ? chooseDateLabel : formatDateShort(date) }}
|
||||
</BaseButton>
|
||||
</slot>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="show" class="datepicker-popup" ref="datepickerPopup">
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
@close="$router.back()"
|
||||
:loading="loading"
|
||||
>
|
||||
<div class="p-4">
|
||||
<div :class="{'p-4': padding}">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
@ -72,6 +72,10 @@ defineProps({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
padding: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['create', 'primary', 'tertiary'])
|
||||
|
|
|
@ -186,7 +186,7 @@ export default defineComponent({
|
|||
if (this.selectedCmd !== null) {
|
||||
switch (this.selectedCmd.action) {
|
||||
case CMD_NEW_TASK:
|
||||
return this.$t('quickActions.newTask')
|
||||
return this.$t('task.create.title')
|
||||
case CMD_NEW_LIST:
|
||||
return this.$t('quickActions.newList')
|
||||
case CMD_NEW_NAMESPACE:
|
||||
|
|
23
src/helpers/findAssignees.ts
Normal file
23
src/helpers/findAssignees.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import UserService from '@/services/user'
|
||||
import {findPropertyByValue} from '@/helpers/findPropertyByValue'
|
||||
|
||||
// Check if the user exists
|
||||
function validateUsername(users: IUser[], username: IUser['username']) {
|
||||
return findPropertyByValue(users, 'username', username)
|
||||
}
|
||||
|
||||
export async function findAssignees(parsedTaskAssignees: string[]) {
|
||||
if (parsedTaskAssignees.length <= 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const userService = new UserService()
|
||||
const assignees = parsedTaskAssignees.map(async a => {
|
||||
const users = await userService.getAll({}, {s: a})
|
||||
return validateUsername(users, a)
|
||||
})
|
||||
|
||||
const validatedUsers = await Promise.all(assignees)
|
||||
return validatedUsers.filter((item) => Boolean(item))
|
||||
}
|
||||
|
7
src/helpers/findPropertyByValue.ts
Normal file
7
src/helpers/findPropertyByValue.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
// IDEA: maybe use a small fuzzy search here to prevent errors
|
||||
export function findPropertyByValue(object, key, value) {
|
||||
return Object.values(object).find(
|
||||
(l) => l[key]?.toLowerCase() === value.toLowerCase(),
|
||||
)
|
||||
}
|
||||
|
|
@ -616,6 +616,12 @@
|
|||
"select": "Select a date range",
|
||||
"noTasks": "Nothing to do — Have a nice day!"
|
||||
},
|
||||
"create": {
|
||||
"heading": "Create a task in {list}",
|
||||
"title": "Enter the title of the new task…",
|
||||
"descriptionPlaceholder": "Enter your description here…",
|
||||
"description": "Add description…"
|
||||
},
|
||||
"detail": {
|
||||
"chooseDueDate": "Click here to set a due date",
|
||||
"chooseStartDate": "Click here to set a start date",
|
||||
|
@ -933,7 +939,6 @@
|
|||
"lists": "Lists",
|
||||
"teams": "Teams",
|
||||
"newList": "Enter the title of the new list…",
|
||||
"newTask": "Enter the title of the new task…",
|
||||
"newNamespace": "Enter the title of the new namespace…",
|
||||
"newTeam": "Enter the name of the new team…",
|
||||
"createTask": "Create a task in the current list ({title})",
|
||||
|
|
|
@ -23,6 +23,7 @@ import UpcomingTasksComponent from '../views/tasks/ShowTasks.vue'
|
|||
import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth.vue'
|
||||
import ListNamespaces from '../views/namespaces/ListNamespaces.vue'
|
||||
import TaskDetailView from '../views/tasks/TaskDetailView.vue'
|
||||
import CreateTask from '../views/tasks/CreateTask.vue'
|
||||
// Team Handling
|
||||
import ListTeamsComponent from '../views/teams/ListTeams.vue'
|
||||
// Label Handling
|
||||
|
@ -348,6 +349,15 @@ const router = createRouter({
|
|||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/create-task',
|
||||
name: 'task.create',
|
||||
component: CreateTask,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId',
|
||||
name: 'list.index',
|
||||
|
|
|
@ -51,8 +51,8 @@ export default class TaskService extends AbstractService<ITask> {
|
|||
model.startDate = parseDate(model.startDate)
|
||||
model.endDate = parseDate(model.endDate)
|
||||
model.doneAt = parseDate(model.doneAt)
|
||||
model.created = formatISO(new Date(model.created))
|
||||
model.updated = formatISO(new Date(model.updated))
|
||||
model.created = +(model.created) ? parseDate(model.created) : null
|
||||
model.updated = +(model.updated) ? parseDate(model.updated) : null
|
||||
|
||||
// remove all nulls, these would create empty reminders
|
||||
for (const index in model.reminderDates) {
|
||||
|
|
|
@ -5,7 +5,6 @@ import {formatISO} from 'date-fns'
|
|||
import TaskService from '@/services/task'
|
||||
import TaskAssigneeService from '@/services/taskAssignee'
|
||||
import LabelTaskService from '@/services/labelTask'
|
||||
import UserService from '@/services/user'
|
||||
|
||||
import {HAS_TASKS} from '../mutation-types'
|
||||
import {setLoading} from '../helper'
|
||||
|
@ -28,18 +27,8 @@ import type { RootStoreState, TaskState } from '@/store/types'
|
|||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {playPop} from '@/helpers/playPop'
|
||||
|
||||
// IDEA: maybe use a small fuzzy search here to prevent errors
|
||||
function findPropertyByValue(object, key, value) {
|
||||
return Object.values(object).find(
|
||||
(l) => l[key]?.toLowerCase() === value.toLowerCase(),
|
||||
)
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
function validateUsername(users: IUser[], username: IUser['username']) {
|
||||
return findPropertyByValue(users, 'username', username)
|
||||
}
|
||||
import {findPropertyByValue} from '@/helpers/findPropertyByValue'
|
||||
import {findAssignees} from '@/helpers/findAssignees'
|
||||
|
||||
// Check if the label exists
|
||||
function validateLabel(labels: ILabel[], label: ILabel) {
|
||||
|
@ -57,22 +46,6 @@ async function addLabelToTask(task: ITask, label: ILabel) {
|
|||
return response
|
||||
}
|
||||
|
||||
async function findAssignees(parsedTaskAssignees) {
|
||||
if (parsedTaskAssignees.length <= 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const userService = new UserService()
|
||||
const assignees = parsedTaskAssignees.map(async a => {
|
||||
const users = await userService.getAll({}, {s: a})
|
||||
return validateUsername(users, a)
|
||||
})
|
||||
|
||||
const validatedUsers = await Promise.all(assignees)
|
||||
return validatedUsers.filter((item) => Boolean(item))
|
||||
}
|
||||
|
||||
|
||||
const tasksStore : Module<TaskState, RootStoreState>= {
|
||||
namespaced: true,
|
||||
state: () => ({}),
|
||||
|
|
192
src/views/tasks/CreateTask.vue
Normal file
192
src/views/tasks/CreateTask.vue
Normal file
|
@ -0,0 +1,192 @@
|
|||
<template>
|
||||
<CreateEdit
|
||||
:padding="false"
|
||||
:title="heading"
|
||||
@primary="create"
|
||||
:loading="taskService.loading"
|
||||
>
|
||||
<input
|
||||
:placeholder="$t('task.create.title')"
|
||||
class="task-title input"
|
||||
@keyup.enter="create"
|
||||
v-model="newTask.title"
|
||||
v-focus
|
||||
/>
|
||||
<p class="help is-danger ml-4" v-if="errorMessage !== ''">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
<QuickAddMagic class="ml-4" v-else/>
|
||||
<BaseButton
|
||||
v-if="!descriptionFormVisible"
|
||||
@click="() => descriptionFormVisible = true"
|
||||
class="toggle-description-button"
|
||||
>
|
||||
{{ $t('task.create.description') }}
|
||||
</BaseButton>
|
||||
<editor
|
||||
v-if="descriptionFormVisible"
|
||||
v-model="newTask.description"
|
||||
:placeholder="$t('task.create.descriptionPlaceholder')"
|
||||
:preview-is-default="false"
|
||||
class="m-4"
|
||||
/>
|
||||
<div class="px-4 pb-4 task-attributes">
|
||||
<Datepicker
|
||||
v-model="newTask.dueDate"
|
||||
v-slot="{ date, openPopup }"
|
||||
>
|
||||
<XButton variant="secondary" @click.stop="openPopup()" class="datepicker-button">
|
||||
{{ date ? formatDateShort(date) : t('task.attributes.dueDate') }}
|
||||
</XButton>
|
||||
</Datepicker>
|
||||
<XButton variant="secondary" class="ml-2">
|
||||
{{ t('task.detail.actions.label') }}
|
||||
</XButton>
|
||||
<div class="is-flex pl-2">
|
||||
<span
|
||||
v-for="label in realLabels"
|
||||
:style="{'background': label.hexColor, 'color': label.textColor}"
|
||||
class="tag mr-2">
|
||||
<span>{{ label.title }}</span>
|
||||
<BaseButton @click="removeLabel(label)" class="delete is-small"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CreateEdit>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, ref, watch} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
import Editor from '@/components/input/AsyncEditor'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
||||
import XButton from '@/components/input/button.vue'
|
||||
import Datepicker from '@/components/input/datepicker.vue'
|
||||
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import TaskModel from '@/models/task'
|
||||
import TaskService from '@/services/task'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
|
||||
import {parseTaskText} from '@/modules/parseTaskText'
|
||||
import {findAssignees} from '@/helpers/findAssignees'
|
||||
import {formatDateShort} from '@/helpers/time/formatDate'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useStore} from '@/store'
|
||||
import type {ILabel} from '@/modelTypes/ILabel'
|
||||
import LabelModel from '@/models/label'
|
||||
|
||||
const listStore = useListStore()
|
||||
const labelStore = useLabelStore()
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
const {t} = useI18n()
|
||||
const props = defineProps<{
|
||||
listId: number,
|
||||
}>()
|
||||
|
||||
const heading = computed(() => {
|
||||
const listTitle = listStore.getListById(props.listId)?.title || ''
|
||||
return listTitle !== ''
|
||||
? t('task.create.heading', {list: listTitle})
|
||||
: t('task.new')
|
||||
})
|
||||
|
||||
const errorMessage = ref('')
|
||||
const taskService = ref(new TaskService())
|
||||
const descriptionFormVisible = ref(false)
|
||||
const newTask = ref<ITask>(new TaskModel({}))
|
||||
|
||||
const parsedTask = computed(() => parseTaskText(newTask.value.title, getQuickAddMagicMode()))
|
||||
watch(
|
||||
() => parsedTask.value.date,
|
||||
date => newTask.value.dueDate = date,
|
||||
)
|
||||
|
||||
const labels = ref<string[]>([])
|
||||
watch(
|
||||
() => parsedTask.value.labels,
|
||||
labelTitles => labels.value = labelTitles,
|
||||
)
|
||||
const realLabels = computed<ILabel[]>(() => {
|
||||
const existingLabels = labelStore.getLabelsByExactTitles(labels.value)
|
||||
|
||||
const newLabels = labels.value
|
||||
.filter(l => l !== '' && !(existingLabels.map(le => le.title).includes(l)))
|
||||
.map(newLabel => new LabelModel({title: newLabel}))
|
||||
|
||||
return [
|
||||
...existingLabels,
|
||||
...newLabels,
|
||||
]
|
||||
})
|
||||
|
||||
function removeLabel(label: ILabel) {
|
||||
while (true) { // Using a loop to remove all labels, including possible duplicates added via quick add magic
|
||||
const index = labels.value.findIndex(el => el.toLowerCase() === label.title.toLowerCase())
|
||||
if (index === -1) {
|
||||
break
|
||||
}
|
||||
labels.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
async function create() {
|
||||
if (newTask.value.title === '') {
|
||||
errorMessage.value = t('list.create.addTitleRequired')
|
||||
return
|
||||
}
|
||||
errorMessage.value = ''
|
||||
|
||||
const assignees = await findAssignees(parsedTask.value.assignees)
|
||||
|
||||
const finalTask = new TaskModel({
|
||||
...newTask.value,
|
||||
listId: props.listId,
|
||||
title: parsedTask.value.text,
|
||||
dueDate: newTask.value.dueDate !== null ? formatISO(newTask.value.dueDate) : null,
|
||||
priority: parsedTask.value.priority,
|
||||
assignees: parsedTask.value.assignees,
|
||||
})
|
||||
|
||||
const task = await taskService.value.create(finalTask)
|
||||
|
||||
await store.dispatch('tasks/addLabelsToTask', {
|
||||
task,
|
||||
parsedLabels: labels.value,
|
||||
})
|
||||
|
||||
return router.push({name: 'task.detail', params: {id: task.id}})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-title {
|
||||
width: 100%;
|
||||
font-size: 1.5rem;
|
||||
border: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.toggle-description-button {
|
||||
padding: 1rem;
|
||||
color: var(--grey-400);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.datepicker-button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-attributes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
Loading…
Reference in a new issue