diff --git a/src/components/input/SimpleButton.vue b/src/components/input/SimpleButton.vue new file mode 100644 index 000000000..ffe8c5b56 --- /dev/null +++ b/src/components/input/SimpleButton.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/src/components/input/datepicker.vue b/src/components/input/datepicker.vue index 1e3839307..a937189d6 100644 --- a/src/components/input/datepicker.vue +++ b/src/components/input/datepicker.vue @@ -1,78 +1,15 @@ @@ -23,11 +23,14 @@ const props = defineProps({ }, }) +const emit = defineEmits(['close']) + const open = ref(false) const popup = ref(null) function close() { open.value = false + emit('close') } function toggle() { diff --git a/src/components/tasks/partials/reminder-detail.vue b/src/components/tasks/partials/reminder-detail.vue new file mode 100644 index 000000000..9fd7b0309 --- /dev/null +++ b/src/components/tasks/partials/reminder-detail.vue @@ -0,0 +1,269 @@ + + + + + diff --git a/src/components/tasks/partials/reminder-period.vue b/src/components/tasks/partials/reminder-period.vue new file mode 100644 index 000000000..8e456b08d --- /dev/null +++ b/src/components/tasks/partials/reminder-period.vue @@ -0,0 +1,122 @@ + + + + + \ No newline at end of file diff --git a/src/components/tasks/partials/reminders.story.vue b/src/components/tasks/partials/reminders.story.vue new file mode 100644 index 000000000..2fb40a1cb --- /dev/null +++ b/src/components/tasks/partials/reminders.story.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/components/tasks/partials/reminders.vue b/src/components/tasks/partials/reminders.vue index 47c842954..b259fce5d 100644 --- a/src/components/tasks/partials/reminders.vue +++ b/src/components/tasks/partials/reminders.vue @@ -3,104 +3,70 @@
- - - + v-model="reminders[index]" + @update:model-value="updateData"/> + +
-
- -
+ + \ No newline at end of file diff --git a/src/helpers/time/period.ts b/src/helpers/time/period.ts new file mode 100644 index 000000000..cea6ac74a --- /dev/null +++ b/src/helpers/time/period.ts @@ -0,0 +1,50 @@ +import { + SECONDS_A_DAY, + SECONDS_A_HOUR, + SECONDS_A_MINUTE, + SECONDS_A_MONTH, + SECONDS_A_WEEK, + SECONDS_A_YEAR, +} from '@/constants/date' + +export type PeriodUnit = 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months' | 'years' + +/** + * Convert time period given as seconds to days, hour, minutes, seconds + */ +export function secondsToPeriod(seconds: number): { unit: PeriodUnit, amount: number } { + if (seconds % SECONDS_A_DAY === 0) { + if (seconds % SECONDS_A_WEEK === 0) { + return {unit: 'weeks', amount: seconds / SECONDS_A_WEEK} + } else if (seconds % SECONDS_A_MONTH === 0) { + return {unit: 'months', amount: seconds / SECONDS_A_MONTH} + } else if (seconds % SECONDS_A_YEAR === 0) { + return {unit: 'years', amount: seconds / SECONDS_A_YEAR} + } else { + return {unit: 'days', amount: seconds / SECONDS_A_DAY} + } + } + + return { + unit: 'hours', + amount: seconds / SECONDS_A_HOUR, + } +} + +/** + * Convert time period of days, hour, minutes, seconds to duration in seconds + */ +export function periodToSeconds(period: number, unit: PeriodUnit): number { + switch (unit) { + case 'minutes': + return period * SECONDS_A_MINUTE + case 'hours': + return period * SECONDS_A_HOUR + case 'days': + return period * SECONDS_A_DAY + case 'weeks': + return period * SECONDS_A_WEEK + } + + return 0 +} diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json index 344b67292..66b15159a 100644 --- a/src/i18n/lang/en.json +++ b/src/i18n/lang/en.json @@ -720,6 +720,17 @@ "copiedto": "Copied To | Copied To" } }, + "reminder": { + "before": "{amount} {unit} before {type}", + "after": "{amount} {unit} after {type}", + "beforeShort": "before", + "afterShort": "after", + "onDueDate": "On the due date", + "onStartDate": "On the start date", + "onEndDate": "On the end date", + "custom": "Custom", + "dateAndTime": "Date and time" + }, "repeat": { "everyDay": "Every Day", "everyWeek": "Every Week", @@ -983,5 +994,16 @@ "title": "About", "frontendVersion": "Frontend Version: {version}", "apiVersion": "API Version: {version}" + }, + "time": { + "units": { + "seconds": "second|seconds", + "minutes": "minute|minutes", + "hours": "hour|hours", + "days": "day|days", + "weeks": "week|weeks", + "months": "month|months", + "years": "year|years" + } } } \ No newline at end of file diff --git a/src/modelTypes/ITask.ts b/src/modelTypes/ITask.ts index e66d447ef..b4cfc4582 100644 --- a/src/modelTypes/ITask.ts +++ b/src/modelTypes/ITask.ts @@ -13,6 +13,7 @@ import type {IRepeatAfter} from '@/types/IRepeatAfter' import type {IRepeatMode} from '@/types/IRepeatMode' import type {PartialWithId} from '@/types/PartialWithId' +import type {ITaskReminder} from '@/modelTypes/ITaskReminder' export interface ITask extends IAbstract { id: number @@ -30,7 +31,7 @@ export interface ITask extends IAbstract { repeatAfter: number | IRepeatAfter repeatFromCurrentDate: boolean repeatMode: IRepeatMode - reminderDates: Date[] + reminders: ITaskReminder[] parentTaskId: ITask['id'] hexColor: string percentDone: number diff --git a/src/modelTypes/ITaskReminder.ts b/src/modelTypes/ITaskReminder.ts new file mode 100644 index 000000000..a02e0ffc6 --- /dev/null +++ b/src/modelTypes/ITaskReminder.ts @@ -0,0 +1,8 @@ +import type { IAbstract } from './IAbstract' +import type { IReminderPeriodRelativeTo } from '@/types/IReminderPeriodRelativeTo' + +export interface ITaskReminder extends IAbstract { + reminder: Date | null + relativePeriod: number + relativeTo: IReminderPeriodRelativeTo | null +} \ No newline at end of file diff --git a/src/models/task.ts b/src/models/task.ts index 9974f41b9..7a7d69c60 100644 --- a/src/models/task.ts +++ b/src/models/task.ts @@ -1,5 +1,4 @@ import {PRIORITIES, type Priority} from '@/constants/priorities' -import {SECONDS_A_DAY, SECONDS_A_HOUR, SECONDS_A_MONTH, SECONDS_A_WEEK, SECONDS_A_YEAR} from '@/constants/date' import type {ITask} from '@/modelTypes/ITask' import type {ILabel} from '@/modelTypes/ILabel' @@ -20,6 +19,9 @@ import LabelModel from './label' import UserModel from './user' import AttachmentModel from './attachment' import SubscriptionModel from './subscription' +import type {ITaskReminder} from '@/modelTypes/ITaskReminder' +import TaskReminderModel from '@/models/taskReminder' +import {secondsToPeriod} from '@/helpers/time/period' export const TASK_DEFAULT_COLOR = '#1973ff' @@ -35,21 +37,13 @@ export function getHexColor(hexColor: string): string { * Parses `repeatAfterSeconds` into a usable js object. */ export function parseRepeatAfter(repeatAfterSeconds: number): IRepeatAfter { - let repeatAfter: IRepeatAfter = {type: 'hours', amount: repeatAfterSeconds / SECONDS_A_HOUR} - - // if its dividable by 24, its something with days, otherwise hours - if (repeatAfterSeconds % SECONDS_A_DAY === 0) { - if (repeatAfterSeconds % SECONDS_A_WEEK === 0) { - repeatAfter = {type: 'weeks', amount: repeatAfterSeconds / SECONDS_A_WEEK} - } else if (repeatAfterSeconds % SECONDS_A_MONTH === 0) { - repeatAfter = {type:'months', amount: repeatAfterSeconds / SECONDS_A_MONTH} - } else if (repeatAfterSeconds % SECONDS_A_YEAR === 0) { - repeatAfter = {type: 'years', amount: repeatAfterSeconds / SECONDS_A_YEAR} - } else { - repeatAfter = {type: 'days', amount: repeatAfterSeconds / SECONDS_A_DAY} - } + + const period = secondsToPeriod(repeatAfterSeconds) + + return { + type: period.unit, + amount: period.amount, } - return repeatAfter } export default class TaskModel extends AbstractModel implements ITask { @@ -68,7 +62,13 @@ export default class TaskModel extends AbstractModel implements ITask { repeatAfter: number | IRepeatAfter = 0 repeatFromCurrentDate = false repeatMode: IRepeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT - reminderDates: Date[] = [] + /* Make sure to not return reminderDates to the server. + The server currently supports both reminderDates (old API) and reminder (new API) and assumes the old logic + if it still receives reminderDates. + This line and reminderDates attributes will be removed after https://kolaente.dev/vikunja/api/pulls/1448 was merged. + */ + reminderDates = null + reminders: ITaskReminder[] = [] parentTaskId: ITask['id'] = 0 hexColor = '' percentDone = 0 @@ -115,7 +115,7 @@ export default class TaskModel extends AbstractModel implements ITask { // Parse the repeat after into something usable this.repeatAfter = parseRepeatAfter(this.repeatAfter as number) - this.reminderDates = this.reminderDates.map(d => new Date(d)) + this.reminders = this.reminders.map(r => new TaskReminderModel(r)) if (this.hexColor !== '' && this.hexColor.substring(0, 1) !== '#') { this.hexColor = '#' + this.hexColor diff --git a/src/models/taskReminder.ts b/src/models/taskReminder.ts new file mode 100644 index 000000000..e14e071ac --- /dev/null +++ b/src/models/taskReminder.ts @@ -0,0 +1,20 @@ +import AbstractModel from './abstractModel' +import type {ITaskReminder} from '@/modelTypes/ITaskReminder' +import {parseDateOrNull} from '@/helpers/parseDateOrNull' +import type {IReminderPeriodRelativeTo} from '@/types/IReminderPeriodRelativeTo' + +export default class TaskReminderModel extends AbstractModel implements ITaskReminder { + reminder: Date | null + relativePeriod = 0 + relativeTo: IReminderPeriodRelativeTo | null = null + + constructor(data: Partial = {}) { + super() + this.assignData(data) + this.reminder = parseDateOrNull(data.reminder) + if (this.relativeTo === '') { + this.relativeTo = null + } + } + +} \ No newline at end of file diff --git a/src/services/task.ts b/src/services/task.ts index f40642b0c..a05bdacaf 100644 --- a/src/services/task.ts +++ b/src/services/task.ts @@ -54,17 +54,17 @@ export default class TaskService extends AbstractService { model.created = new Date(model.created).toISOString() model.updated = new Date(model.updated).toISOString() + model.reminderDates = null // remove all nulls, these would create empty reminders - for (const index in model.reminderDates) { - if (model.reminderDates[index] === null) { - model.reminderDates.splice(index, 1) + for (const index in model.reminders) { + if (model.reminders[index] === null) { + model.reminders.splice(index, 1) } } - // Make normal timestamps from js dates - if (model.reminderDates.length > 0) { - model.reminderDates = model.reminderDates.map(r => { - return new Date(r).toISOString() + if (model.reminders.length > 0) { + model.reminders.forEach(r => { + r.reminder = new Date(r.reminder).toISOString() }) } diff --git a/src/types/IReminderPeriodRelativeTo.ts b/src/types/IReminderPeriodRelativeTo.ts new file mode 100644 index 000000000..4254ebf7b --- /dev/null +++ b/src/types/IReminderPeriodRelativeTo.ts @@ -0,0 +1,8 @@ +export const REMINDER_PERIOD_RELATIVE_TO_TYPES = { + DUEDATE: 'due_date', + STARTDATE: 'start_date', + ENDDATE: 'end_date', +} as const + +export type IReminderPeriodRelativeTo = typeof REMINDER_PERIOD_RELATIVE_TO_TYPES[keyof typeof REMINDER_PERIOD_RELATIVE_TO_TYPES] + diff --git a/src/types/IRepeatAfter.ts b/src/types/IRepeatAfter.ts index 0b132a2f7..b86e1d559 100644 --- a/src/types/IRepeatAfter.ts +++ b/src/types/IRepeatAfter.ts @@ -1,4 +1,6 @@ export const REPEAT_TYPES = { + Seconds: 'seconds', + Minutes: 'minutes', Hours: 'hours', Days: 'days', Weeks: 'weeks', diff --git a/src/views/tasks/TaskDetailView.vue b/src/views/tasks/TaskDetailView.vue index e891869d9..7bb41375e 100644 --- a/src/views/tasks/TaskDetailView.vue +++ b/src/views/tasks/TaskDetailView.vue @@ -160,7 +160,7 @@ @@ -639,7 +639,7 @@ function setActiveFields() { activeFields.percentDone = task.percentDone > 0 activeFields.priority = task.priority !== PRIORITIES.UNSET activeFields.relatedTasks = Object.keys(task.relatedTasks).length > 0 - activeFields.reminders = task.reminderDates.length > 0 + activeFields.reminders = task.reminders.length > 0 activeFields.repeatAfter = task.repeatAfter.amount > 0 activeFields.startDate = task.startDate !== null }