From 1fdda07f650702b7e3943e0afc7532367ee20100 Mon Sep 17 00:00:00 2001 From: Dominik Pschenitschni Date: Fri, 23 Sep 2022 11:45:52 +0200 Subject: [PATCH 001/146] feat: move tasks to stores --- src/store/index.ts | 2 -- src/{store/modules => stores}/tasks.ts | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) rename src/{store/modules => stores}/tasks.ts (99%) diff --git a/src/store/index.ts b/src/store/index.ts index 6c530b53..40690179 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -14,7 +14,6 @@ import { QUICK_ACTIONS_ACTIVE, } from './mutation-types' import kanban from './modules/kanban' -import tasks from './modules/tasks' import ListModel from '@/models/list' @@ -35,7 +34,6 @@ export const store = createStore({ strict: import.meta.env.DEV, modules: { kanban, - tasks, }, state: () => ({ loading: false, diff --git a/src/store/modules/tasks.ts b/src/stores/tasks.ts similarity index 99% rename from src/store/modules/tasks.ts rename to src/stores/tasks.ts index 9f7248aa..5ef453a7 100644 --- a/src/store/modules/tasks.ts +++ b/src/stores/tasks.ts @@ -7,8 +7,8 @@ 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' +import {HAS_TASKS} from '../store/mutation-types' +import {setLoading} from '../store/helper' import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode' import {parseTaskText} from '@/modules/parseTaskText' From 9d2990a23b60d139f8774b41d7f89f146c7cf114 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 8 Sep 2022 00:25:10 +0200 Subject: [PATCH 002/146] feat: allow saving a default reminder amount --- src/helpers/defaultReminder.ts | 42 ++++++++++++++++++++++ src/i18n/lang/en.json | 6 +++- src/modelTypes/IUserSettings.ts | 2 ++ src/models/userSettings.ts | 2 ++ src/styles/theme/form.scss | 4 +++ src/views/user/settings/General.vue | 54 +++++++++++++++++++++++++++++ 6 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/helpers/defaultReminder.ts diff --git a/src/helpers/defaultReminder.ts b/src/helpers/defaultReminder.ts new file mode 100644 index 00000000..53b389b1 --- /dev/null +++ b/src/helpers/defaultReminder.ts @@ -0,0 +1,42 @@ +const DEFAULT_REMINDER_KEY = 'defaultReminder' + +interface DefaultReminderSettings { + enabled: boolean, + amount: number, +} + +function calculateDefaultReminderSeconds(type: string, amount: number): number { + switch (type) { + case 'minutes': + return amount * 60 + case 'hours': + return amount * 60 * 60 + case 'days': + return amount * 60 * 60 * 24 + case 'months': + return amount * 60 * 60 * 24 * 30 + } + + return 0 +} + +export function saveDefaultReminder(enabled: boolean, type: string, amount: number) { + const defaultReminderSeconds = calculateDefaultReminderSeconds(type, amount) + localStorage.setItem(DEFAULT_REMINDER_KEY, JSON.stringify({ + enabled, + amount: defaultReminderSeconds, + })) +} + +export function getDefaultReminderAmount(): number | null { + const s: string | null = localStorage.getItem(DEFAULT_REMINDER_KEY) + if (s === null) { + return null + } + + const settings: DefaultReminderSettings = JSON.parse(s) + + return settings.enabled + ? settings.amount + : null +} diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json index 4674f6e8..4b1fc8f2 100644 --- a/src/i18n/lang/en.json +++ b/src/i18n/lang/en.json @@ -87,7 +87,11 @@ "language": "Language", "defaultList": "Default List", "timezone": "Time Zone", - "overdueTasksRemindersTime": "Overdue tasks reminder email time" + "overdueTasksRemindersTime": "Overdue tasks reminder email time", + "defaultReminder": "Set a default task reminder", + "defaultReminderHint": "If enabled, Vikunja will automatically create a reminder for a task if you set a due date and the task does not have any reminders yet.", + "defaultReminderAmount": "Default task reminder amount", + "defaultReminderAmountHint": "The time difference the reminder will be set to before the due date." }, "totp": { "title": "Two Factor Authentication", diff --git a/src/modelTypes/IUserSettings.ts b/src/modelTypes/IUserSettings.ts index 31e921e0..9ba54a55 100644 --- a/src/modelTypes/IUserSettings.ts +++ b/src/modelTypes/IUserSettings.ts @@ -12,4 +12,6 @@ export interface IUserSettings extends IAbstract { weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6 timezone: string language: string + defaultReminder: boolean + defaultReminderAmount: number // The amount of seconds a reminder should be set before a given due date } \ No newline at end of file diff --git a/src/models/userSettings.ts b/src/models/userSettings.ts index ddb6f029..fbe02b73 100644 --- a/src/models/userSettings.ts +++ b/src/models/userSettings.ts @@ -13,6 +13,8 @@ export default class UserSettingsModel extends AbstractModel impl weekStart = 0 as IUserSettings['weekStart'] timezone = '' language = getCurrentLanguage() + defaultReminder = false + defaultReminderAmount = 0 constructor(data: Partial = {}) { super() diff --git a/src/styles/theme/form.scss b/src/styles/theme/form.scss index 86c78f2f..7ac5aa23 100644 --- a/src/styles/theme/form.scss +++ b/src/styles/theme/form.scss @@ -56,6 +56,10 @@ } } +.field.has-addons .select select { + height: 100%; +} + .field.has-addons .control .select select { height: 100%; } diff --git a/src/views/user/settings/General.vue b/src/views/user/settings/General.vue index 2664fe01..3ab31ccf 100644 --- a/src/views/user/settings/General.vue +++ b/src/views/user/settings/General.vue @@ -18,6 +18,55 @@ +
+ +

+ {{ $t('user.settings.general.defaultReminderHint') }} +

+
+
+ +

+ {{ $t('user.settings.general.defaultReminderAmountHint') }} +

+
+ +
+ +
+
+ +
+
+
diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json index cf5b4550..0dd088db 100644 --- a/src/i18n/lang/en.json +++ b/src/i18n/lang/en.json @@ -551,19 +551,16 @@ "fromto": "{from} to {to}", "ranges": { "today": "Today", - "thisWeek": "This Week", "restOfThisWeek": "The Rest of This Week", "nextWeek": "Next Week", "next7Days": "Next 7 Days", "lastWeek": "Last Week", - "thisMonth": "This Month", "restOfThisMonth": "The Rest of This Month", "nextMonth": "Next Month", "next30Days": "Next 30 Days", "lastMonth": "Last Month", - "thisYear": "This Year", "restOfThisYear": "The Rest of This Year" } @@ -580,15 +577,6 @@ "roundDay": "Round down to the nearest day", "supportedUnits": "Supported time units are:", "someExamples": "Some examples of time expressions:", - "units": { - "seconds": "Seconds", - "minutes": "Minutes", - "hours": "Hours", - "days": "Days", - "weeks": "Weeks", - "months": "Months", - "years": "Years" - }, "examples": { "now": "Right now", "in24h": "In 24h", @@ -600,6 +588,22 @@ } } }, + "time": { + "seconds": "Seconds", + "second": "Second", + "minutes": "Minutes", + "minute": "Minute", + "hours": "Hours", + "hour": "Hour", + "days": "Days", + "day": "Day", + "weeks": "Weeks", + "week": "Week", + "months": "Months", + "month": "Month", + "years": "Years", + "year": "Year" + }, "task": { "task": "Task", "new": "Create a new task", diff --git a/src/views/user/settings/General.vue b/src/views/user/settings/General.vue index 34f0adb9..ef86589b 100644 --- a/src/views/user/settings/General.vue +++ b/src/views/user/settings/General.vue @@ -43,21 +43,17 @@
From 80cc58a45dc6bd125288294cba3bfc42e1cde45d Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 8 Sep 2022 16:38:12 +0200 Subject: [PATCH 005/146] feat: automatically add a reminder to a task with due date but no reminders --- src/services/task.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/services/task.ts b/src/services/task.ts index bb41b685..f0f098ed 100644 --- a/src/services/task.ts +++ b/src/services/task.ts @@ -6,6 +6,7 @@ import LabelService from './label' import {formatISO} from 'date-fns' import {colorFromHex} from '@/helpers/color/colorFromHex' +import {getDefaultReminderAmount} from '@/helpers/defaultReminder' const parseDate = date => { if (date) { @@ -39,7 +40,7 @@ export default class TaskService extends AbstractService { } processModel(updatedModel) { - const model = { ...updatedModel } + const model = {...updatedModel} model.title = model.title?.trim() @@ -68,6 +69,15 @@ export default class TaskService extends AbstractService { }) } + if (model.dueDate !== null && model.reminderDates.length === 0) { + const defaultReminder = getDefaultReminderAmount() + if (defaultReminder !== null) { + const dueDate = +new Date(model.dueDate) + const reminderDate = new Date(dueDate - (defaultReminder * 1000)) + model.reminderDates.push(formatISO(reminderDate)) + } + } + // Make the repeating amount to seconds let repeatAfterSeconds = 0 if (model.repeatAfter !== null && (model.repeatAfter.amount !== null || model.repeatAfter.amount !== 0)) { From 8baafab4561c2a2b7688ae52c2903a627081d47f Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 8 Sep 2022 16:40:46 +0200 Subject: [PATCH 006/146] fix: show reminder field when changing a due date and a reminder was set --- src/views/tasks/TaskDetailView.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/views/tasks/TaskDetailView.vue b/src/views/tasks/TaskDetailView.vue index ea67956e..bbe26dc5 100644 --- a/src/views/tasks/TaskDetailView.vue +++ b/src/views/tasks/TaskDetailView.vue @@ -697,6 +697,9 @@ export default defineComponent({ } this.task = await this.$store.dispatch('tasks/update', task) + + // Activate new fields which may have been set from the api + this.$nextTick(() => this.setActiveFields()) if (!showNotification) { return From 28312081ae3bf4a78ba6f18cae95ec444eb67df3 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 8 Sep 2022 16:47:12 +0200 Subject: [PATCH 007/146] feat: re-populate default reminder enabled state when loading settings --- src/helpers/defaultReminder.ts | 18 +++++++++++------- src/views/user/settings/General.vue | 12 ++++++++---- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/helpers/defaultReminder.ts b/src/helpers/defaultReminder.ts index 53b389b1..b24399b0 100644 --- a/src/helpers/defaultReminder.ts +++ b/src/helpers/defaultReminder.ts @@ -16,7 +16,7 @@ function calculateDefaultReminderSeconds(type: string, amount: number): number { case 'months': return amount * 60 * 60 * 24 * 30 } - + return 0 } @@ -29,14 +29,18 @@ export function saveDefaultReminder(enabled: boolean, type: string, amount: numb } export function getDefaultReminderAmount(): number | null { + const settings = getDefaultReminderSettings() + + return settings?.enabled + ? settings.amount + : null +} + +export function getDefaultReminderSettings(): DefaultReminderSettings | null { const s: string | null = localStorage.getItem(DEFAULT_REMINDER_KEY) if (s === null) { return null } - - const settings: DefaultReminderSettings = JSON.parse(s) - - return settings.enabled - ? settings.amount - : null + + return JSON.parse(s) } diff --git a/src/views/user/settings/General.vue b/src/views/user/settings/General.vue index ef86589b..4a16c06f 100644 --- a/src/views/user/settings/General.vue +++ b/src/views/user/settings/General.vue @@ -20,14 +20,14 @@

{{ $t('user.settings.general.defaultReminderHint') }}

-
+
@@ -219,7 +219,7 @@ import {AuthenticatedHTTPFactory} from '@/http-common' import {useColorScheme} from '@/composables/useColorScheme' import {useTitle} from '@/composables/useTitle' import {objectIsEmpty} from '@/helpers/objectIsEmpty' -import {saveDefaultReminder} from '@/helpers/defaultReminder' +import {getDefaultReminderSettings, saveDefaultReminder} from '@/helpers/defaultReminder' const {t} = useI18n({useScope: 'global'}) useTitle(() => `${t('user.settings.general.title')} - ${t('user.settings.title')}`) @@ -311,13 +311,17 @@ watch( async function updateSettings() { localStorage.setItem(playSoundWhenDoneKey, playSoundWhenDone.value ? 'true' : 'false') setQuickAddMagicMode(quickAddMagicMode.value) - saveDefaultReminder(settings.value.defaultReminder, defaultReminderAmountType.value, defaultReminderAmount.value) + saveDefaultReminder(defaultReminderEnabled.value, defaultReminderAmountType.value, defaultReminderAmount.value) await authStore.saveUserSettings({ settings: {...settings.value}, }) } +const reminderSettings = getDefaultReminderSettings() + +const defaultReminderEnabled = ref(reminderSettings?.enabled || false) +// TODO: re-populate amount and type const defaultReminderAmount = ref(1) const defaultReminderAmountType = ref('days') From 5aafbd9a72cd73079dffdad0e41ccb78f42b1923 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 13 Sep 2022 23:23:34 +0200 Subject: [PATCH 008/146] feat: re-populate default reminder from saved settings --- src/helpers/defaultReminder.test.ts | 62 +++++++++++++++++++++++++++++ src/helpers/defaultReminder.ts | 45 ++++++++++++++++++++- src/views/user/settings/General.vue | 8 ++-- vite.config.ts | 1 + 4 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 src/helpers/defaultReminder.test.ts diff --git a/src/helpers/defaultReminder.test.ts b/src/helpers/defaultReminder.test.ts new file mode 100644 index 00000000..6fbc117b --- /dev/null +++ b/src/helpers/defaultReminder.test.ts @@ -0,0 +1,62 @@ +import {describe, it, expect, vi, afterEach, beforeEach} from 'vitest' +import { + getDefaultReminderSettings, + getSavedReminderSettings, + parseSavedReminderAmount, + saveDefaultReminder, +} from '@/helpers/defaultReminder' +import * as exports from '@/helpers/defaultReminder' + +describe('Default Reminder Save', () => { + it('Should save a default reminder with minutes', () => { + const spy = vi.spyOn(window.localStorage, 'setItem') + saveDefaultReminder(true, 'minutes', 5) + + expect(spy).toHaveBeenCalledWith('defaultReminder', '{"enabled":true,"amount":300}') + }) + it('Should save a default reminder with hours', () => { + const spy = vi.spyOn(window.localStorage, 'setItem') + saveDefaultReminder(true, 'hours', 5) + + expect(spy).toHaveBeenCalledWith('defaultReminder', '{"enabled":true,"amount":18000}') + }) + it('Should save a default reminder with days', () => { + const spy = vi.spyOn(window.localStorage, 'setItem') + saveDefaultReminder(true, 'days', 5) + + expect(spy).toHaveBeenCalledWith('defaultReminder', '{"enabled":true,"amount":432000}') + }) + it('Should save a default reminder with months', () => { + const spy = vi.spyOn(window.localStorage, 'setItem') + saveDefaultReminder(true, 'months', 5) + + expect(spy).toHaveBeenCalledWith('defaultReminder', '{"enabled":true,"amount":12960000}') + }) +}) + +describe('Default Reminder Load', () => { + it('Should parse minutes', () => { + const settings = parseSavedReminderAmount(5 * 60) + + expect(settings.amount).toBe(5) + expect(settings.type).toBe('minutes') + }) + it('Should parse hours', () => { + const settings = parseSavedReminderAmount(5 * 60 * 60) + + expect(settings.amount).toBe(5) + expect(settings.type).toBe('hours') + }) + it('Should parse days', () => { + const settings = parseSavedReminderAmount(5 * 60 * 60 * 24) + + expect(settings.amount).toBe(5) + expect(settings.type).toBe('days') + }) + it('Should parse months', () => { + const settings = parseSavedReminderAmount(5 * 60 * 60 * 24 * 30) + + expect(settings.amount).toBe(5) + expect(settings.type).toBe('months') + }) +}) diff --git a/src/helpers/defaultReminder.ts b/src/helpers/defaultReminder.ts index b24399b0..fb567d7b 100644 --- a/src/helpers/defaultReminder.ts +++ b/src/helpers/defaultReminder.ts @@ -5,6 +5,12 @@ interface DefaultReminderSettings { amount: number, } +interface SavedReminderSettings { + enabled: boolean, + amount?: number, + type?: 'minutes' | 'hours' | 'days' | 'months', +} + function calculateDefaultReminderSeconds(type: string, amount: number): number { switch (type) { case 'minutes': @@ -37,10 +43,47 @@ export function getDefaultReminderAmount(): number | null { } export function getDefaultReminderSettings(): DefaultReminderSettings | null { - const s: string | null = localStorage.getItem(DEFAULT_REMINDER_KEY) + const s: string | null = window.localStorage.getItem(DEFAULT_REMINDER_KEY) if (s === null) { return null } return JSON.parse(s) } + +export function parseSavedReminderAmount(amountSeconds: number): SavedReminderSettings { + const amountMinutes = amountSeconds / 60 + const settings: SavedReminderSettings = { + enabled: true, // We're assuming the caller to have checked this properly + amount: amountMinutes, + type: 'minutes', + } + + if ((amountMinutes / 60 / 24) % 30 === 0) { + settings.amount = amountMinutes / 60 / 24 / 30 + settings.type = 'months' + } else if ((amountMinutes / 60) % 24 === 0) { + settings.amount = amountMinutes / 60 / 24 + settings.type = 'days' + } else if (amountMinutes % 60 === 0) { + settings.amount = amountMinutes / 60 + settings.type = 'hours' + } + + return settings +} + +export function getSavedReminderSettings(): SavedReminderSettings | null { + const s = getDefaultReminderSettings() + if (s === null) { + return null + } + + if (!s.enabled) { + return { + enabled: false, + } + } + + return parseSavedReminderAmount(s.amount) +} diff --git a/src/views/user/settings/General.vue b/src/views/user/settings/General.vue index 4a16c06f..28b84b8c 100644 --- a/src/views/user/settings/General.vue +++ b/src/views/user/settings/General.vue @@ -219,7 +219,7 @@ import {AuthenticatedHTTPFactory} from '@/http-common' import {useColorScheme} from '@/composables/useColorScheme' import {useTitle} from '@/composables/useTitle' import {objectIsEmpty} from '@/helpers/objectIsEmpty' -import {getDefaultReminderSettings, saveDefaultReminder} from '@/helpers/defaultReminder' +import {getDefaultReminderSettings, getSavedReminderSettings, saveDefaultReminder} from '@/helpers/defaultReminder' const {t} = useI18n({useScope: 'global'}) useTitle(() => `${t('user.settings.general.title')} - ${t('user.settings.title')}`) @@ -318,10 +318,10 @@ async function updateSettings() { }) } -const reminderSettings = getDefaultReminderSettings() +const reminderSettings = getSavedReminderSettings() const defaultReminderEnabled = ref(reminderSettings?.enabled || false) // TODO: re-populate amount and type -const defaultReminderAmount = ref(1) -const defaultReminderAmountType = ref('days') +const defaultReminderAmount = ref(reminderSettings?.amount || 1) +const defaultReminderAmountType = ref(reminderSettings?.type || 'days') diff --git a/vite.config.ts b/vite.config.ts index 2ddc30dd..39daacd2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -32,6 +32,7 @@ export default defineConfig({ // https://vitest.dev/config/ test: { environment: 'happy-dom', + mockReset: true, }, css: { preprocessorOptions: { From e65c2867304a001750660d7062dbcb2ce3660bb6 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 15 Sep 2022 13:35:19 +0200 Subject: [PATCH 009/146] fix: lint --- src/views/user/settings/General.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/user/settings/General.vue b/src/views/user/settings/General.vue index 28b84b8c..c6305a81 100644 --- a/src/views/user/settings/General.vue +++ b/src/views/user/settings/General.vue @@ -219,7 +219,7 @@ import {AuthenticatedHTTPFactory} from '@/http-common' import {useColorScheme} from '@/composables/useColorScheme' import {useTitle} from '@/composables/useTitle' import {objectIsEmpty} from '@/helpers/objectIsEmpty' -import {getDefaultReminderSettings, getSavedReminderSettings, saveDefaultReminder} from '@/helpers/defaultReminder' +import {getSavedReminderSettings, saveDefaultReminder} from '@/helpers/defaultReminder' const {t} = useI18n({useScope: 'global'}) useTitle(() => `${t('user.settings.general.title')} - ${t('user.settings.title')}`) From 7725de7483ada3f7f90713e5d60250c3eba7b870 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 29 Sep 2022 18:20:43 +0200 Subject: [PATCH 010/146] feat: move amount second calculation to mapping const --- src/helpers/defaultReminder.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/helpers/defaultReminder.ts b/src/helpers/defaultReminder.ts index fb567d7b..66e604ae 100644 --- a/src/helpers/defaultReminder.ts +++ b/src/helpers/defaultReminder.ts @@ -1,5 +1,12 @@ const DEFAULT_REMINDER_KEY = 'defaultReminder' +const AMOUNTS_IN_SECONDS: { [key: string]: number } = { + minutes: 60, + hours: 60 * 60, + days: 60 * 60 * 24, + months: 60 * 60 * 24 * 30, +} + interface DefaultReminderSettings { enabled: boolean, amount: number, @@ -12,18 +19,7 @@ interface SavedReminderSettings { } function calculateDefaultReminderSeconds(type: string, amount: number): number { - switch (type) { - case 'minutes': - return amount * 60 - case 'hours': - return amount * 60 * 60 - case 'days': - return amount * 60 * 60 * 24 - case 'months': - return amount * 60 * 60 * 24 * 30 - } - - return 0 + return amount * (AMOUNTS_IN_SECONDS[type] || 0) } export function saveDefaultReminder(enabled: boolean, type: string, amount: number) { @@ -84,6 +80,6 @@ export function getSavedReminderSettings(): SavedReminderSettings | null { enabled: false, } } - + return parseSavedReminderAmount(s.amount) } From 429b8a1ec48bff67d7dec0988e7b8ab5e325f3b0 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 29 Sep 2022 18:23:07 +0200 Subject: [PATCH 011/146] chore: use amount const in tests --- src/helpers/defaultReminder.test.ts | 9 +++++---- src/helpers/defaultReminder.ts | 7 ++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/helpers/defaultReminder.test.ts b/src/helpers/defaultReminder.test.ts index 6fbc117b..d6f4793e 100644 --- a/src/helpers/defaultReminder.test.ts +++ b/src/helpers/defaultReminder.test.ts @@ -1,5 +1,6 @@ import {describe, it, expect, vi, afterEach, beforeEach} from 'vitest' import { + AMOUNTS_IN_SECONDS, getDefaultReminderSettings, getSavedReminderSettings, parseSavedReminderAmount, @@ -36,25 +37,25 @@ describe('Default Reminder Save', () => { describe('Default Reminder Load', () => { it('Should parse minutes', () => { - const settings = parseSavedReminderAmount(5 * 60) + const settings = parseSavedReminderAmount(5 * AMOUNTS_IN_SECONDS.minutes) expect(settings.amount).toBe(5) expect(settings.type).toBe('minutes') }) it('Should parse hours', () => { - const settings = parseSavedReminderAmount(5 * 60 * 60) + const settings = parseSavedReminderAmount(5 * AMOUNTS_IN_SECONDS.hours) expect(settings.amount).toBe(5) expect(settings.type).toBe('hours') }) it('Should parse days', () => { - const settings = parseSavedReminderAmount(5 * 60 * 60 * 24) + const settings = parseSavedReminderAmount(5 * AMOUNTS_IN_SECONDS.days) expect(settings.amount).toBe(5) expect(settings.type).toBe('days') }) it('Should parse months', () => { - const settings = parseSavedReminderAmount(5 * 60 * 60 * 24 * 30) + const settings = parseSavedReminderAmount(5 * AMOUNTS_IN_SECONDS.months) expect(settings.amount).toBe(5) expect(settings.type).toBe('months') diff --git a/src/helpers/defaultReminder.ts b/src/helpers/defaultReminder.ts index 66e604ae..98a2de84 100644 --- a/src/helpers/defaultReminder.ts +++ b/src/helpers/defaultReminder.ts @@ -1,6 +1,11 @@ const DEFAULT_REMINDER_KEY = 'defaultReminder' -const AMOUNTS_IN_SECONDS: { [key: string]: number } = { +export const AMOUNTS_IN_SECONDS: { + minutes: number, + hours: number, + days: number, + months: number, +} = { minutes: 60, hours: 60 * 60, days: 60 * 60 * 24, From 2aee048f6109ee85587017243fcc2ee33a931851 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 29 Sep 2022 18:30:40 +0200 Subject: [PATCH 012/146] fix: use vue-i18n pluralization --- src/components/date/datemathHelp.vue | 16 +++++++------- src/components/tasks/partials/repeatAfter.vue | 10 ++++----- src/i18n/lang/en.json | 21 +++++++------------ src/views/user/settings/General.vue | 8 +++---- 4 files changed, 24 insertions(+), 31 deletions(-) diff --git a/src/components/date/datemathHelp.vue b/src/components/date/datemathHelp.vue index eeda712f..89e20a06 100644 --- a/src/components/date/datemathHelp.vue +++ b/src/components/date/datemathHelp.vue @@ -36,35 +36,35 @@ s - {{ $t('time.seconds') }} + {{ $tc('time.seconds', 2) }} m - {{ $t('time.minutes') }} + {{ $tc('time.minutes', 2) }} h - {{ $t('time.hours') }} + {{ $tc('time.hours', 2) }} H - {{ $t('time.hours') }} + {{ $tc('time.hours', 2) }} d - {{ $t('time.days') }} + {{ $tc('time.days', 2) }} w - {{ $t('time.weeks') }} + {{ $tc('time.weeks', 2) }} M - {{ $t('time.months') }} + {{ $tc('time.months', 2) }} y - {{ $t('time.years') }} + {{ $tc('time.years', 2) }} diff --git a/src/components/tasks/partials/repeatAfter.vue b/src/components/tasks/partials/repeatAfter.vue index e0e7b200..14744155 100644 --- a/src/components/tasks/partials/repeatAfter.vue +++ b/src/components/tasks/partials/repeatAfter.vue @@ -48,11 +48,11 @@ @change="updateData" :disabled="disabled || undefined" > - - - - - + + + + +
diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json index 0dd088db..43206c59 100644 --- a/src/i18n/lang/en.json +++ b/src/i18n/lang/en.json @@ -589,20 +589,13 @@ } }, "time": { - "seconds": "Seconds", - "second": "Second", - "minutes": "Minutes", - "minute": "Minute", - "hours": "Hours", - "hour": "Hour", - "days": "Days", - "day": "Day", - "weeks": "Weeks", - "week": "Week", - "months": "Months", - "month": "Month", - "years": "Years", - "year": "Year" + "seconds": "Second | Seconds", + "minutes": "Minute | Minutes", + "hours": "Hour | Hours", + "days": "Day | Days", + "weeks": "Week | Weeks", + "months": "Month | Months", + "years": "Year | Years" }, "task": { "task": "Task", diff --git a/src/views/user/settings/General.vue b/src/views/user/settings/General.vue index c6305a81..bc3cffb8 100644 --- a/src/views/user/settings/General.vue +++ b/src/views/user/settings/General.vue @@ -44,16 +44,16 @@
From 5c6864389236cfa73110eae842d8d6a93380cc61 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 29 Sep 2022 18:31:56 +0200 Subject: [PATCH 013/146] fix: directly populate user settings with default reminder amount --- src/models/userSettings.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/userSettings.ts b/src/models/userSettings.ts index fbe02b73..50dbb3be 100644 --- a/src/models/userSettings.ts +++ b/src/models/userSettings.ts @@ -2,6 +2,7 @@ import AbstractModel from './abstractModel' import type {IUserSettings} from '@/modelTypes/IUserSettings' import {getCurrentLanguage} from '@/i18n' +import {getDefaultReminderAmount} from '@/helpers/defaultReminder' export default class UserSettingsModel extends AbstractModel implements IUserSettings { name = '' @@ -14,7 +15,7 @@ export default class UserSettingsModel extends AbstractModel impl timezone = '' language = getCurrentLanguage() defaultReminder = false - defaultReminderAmount = 0 + defaultReminderAmount = getDefaultReminderAmount() || 0 constructor(data: Partial = {}) { super() From a341dbd5d238f4480a626ca2e70ab4e30b8fa52d Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 29 Sep 2022 18:32:51 +0200 Subject: [PATCH 014/146] fix: combine related css classes --- src/styles/theme/form.scss | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/styles/theme/form.scss b/src/styles/theme/form.scss index 7ac5aa23..e790d552 100644 --- a/src/styles/theme/form.scss +++ b/src/styles/theme/form.scss @@ -56,10 +56,7 @@ } } -.field.has-addons .select select { - height: 100%; -} - +.field.has-addons .select select, .field.has-addons .control .select select { height: 100%; } From 64cf1c8ccf1bf4950560cd3fede723b3144eb525 Mon Sep 17 00:00:00 2001 From: renovate Date: Thu, 29 Sep 2022 17:03:26 +0000 Subject: [PATCH 015/146] chore(deps): update dependency esbuild to v0.15.10 --- package.json | 2 +- pnpm-lock.yaml | 142 ++++++++++++++++++++++++------------------------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/package.json b/package.json index a487c0c9..536cde16 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "browserslist": "4.21.4", "caniuse-lite": "1.0.30001412", "cypress": "10.9.0", - "esbuild": "0.15.9", + "esbuild": "0.15.10", "eslint": "8.24.0", "eslint-plugin-vue": "9.5.1", "express": "4.18.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f965343a..64a064d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,7 +40,7 @@ specifiers: date-fns: 2.29.3 dompurify: 2.4.0 easymde: 2.18.0 - esbuild: 0.15.9 + esbuild: 0.15.10 eslint: 8.24.0 eslint-plugin-vue: 9.5.1 express: 4.18.1 @@ -148,7 +148,7 @@ devDependencies: browserslist: 4.21.4 caniuse-lite: 1.0.30001412 cypress: 10.9.0 - esbuild: 0.15.9 + esbuild: 0.15.10 eslint: 8.24.0 eslint-plugin-vue: 9.5.1_eslint@8.24.0 express: 4.18.1 @@ -1611,8 +1611,8 @@ packages: kuler: 2.0.0 dev: true - /@esbuild/android-arm/0.15.9: - resolution: {integrity: sha512-VZPy/ETF3fBG5PiinIkA0W/tlsvlEgJccyN2DzWZEl0DlVKRbu91PvY2D6Lxgluj4w9QtYHjOWjAT44C+oQ+EQ==} + /@esbuild/android-arm/0.15.10: + resolution: {integrity: sha512-FNONeQPy/ox+5NBkcSbYJxoXj9GWu8gVGJTVmUyoOCKQFDTrHVKgNSzChdNt0I8Aj/iKcsDf2r9BFwv+FSNUXg==} engines: {node: '>=12'} cpu: [arm] os: [android] @@ -1620,8 +1620,8 @@ packages: dev: true optional: true - /@esbuild/linux-loong64/0.15.9: - resolution: {integrity: sha512-O+NfmkfRrb3uSsTa4jE3WApidSe3N5++fyOVGP1SmMZi4A3BZELkhUUvj5hwmMuNdlpzAZ8iAPz2vmcR7DCFQA==} + /@esbuild/linux-loong64/0.15.10: + resolution: {integrity: sha512-w0Ou3Z83LOYEkwaui2M8VwIp+nLi/NA60lBLMvaJ+vXVMcsARYdEzLNE7RSm4+lSg4zq4d7fAVuzk7PNQ5JFgg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] @@ -5859,8 +5859,8 @@ packages: resolution: {integrity: sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg==} dev: true - /esbuild-android-64/0.15.9: - resolution: {integrity: sha512-HQCX7FJn9T4kxZQkhPjNZC7tBWZqJvhlLHPU2SFzrQB/7nDXjmTIFpFTjt7Bd1uFpeXmuwf5h5fZm+x/hLnhbw==} + /esbuild-android-64/0.15.10: + resolution: {integrity: sha512-UI7krF8OYO1N7JYTgLT9ML5j4+45ra3amLZKx7LO3lmLt1Ibn8t3aZbX5Pu4BjWiqDuJ3m/hsvhPhK/5Y/YpnA==} engines: {node: '>=12'} cpu: [x64] os: [android] @@ -5868,8 +5868,8 @@ packages: dev: true optional: true - /esbuild-android-arm64/0.15.9: - resolution: {integrity: sha512-E6zbLfqbFVCNEKircSHnPiSTsm3fCRxeIMPfrkS33tFjIAoXtwegQfVZqMGR0FlsvVxp2NEDOUz+WW48COCjSg==} + /esbuild-android-arm64/0.15.10: + resolution: {integrity: sha512-EOt55D6xBk5O05AK8brXUbZmoFj4chM8u3riGflLa6ziEoVvNjRdD7Cnp82NHQGfSHgYR06XsPI8/sMuA/cUwg==} engines: {node: '>=12'} cpu: [arm64] os: [android] @@ -5877,8 +5877,8 @@ packages: dev: true optional: true - /esbuild-darwin-64/0.15.9: - resolution: {integrity: sha512-gI7dClcDN/HHVacZhTmGjl0/TWZcGuKJ0I7/xDGJwRQQn7aafZGtvagOFNmuOq+OBFPhlPv1T6JElOXb0unkSQ==} + /esbuild-darwin-64/0.15.10: + resolution: {integrity: sha512-hbDJugTicqIm+WKZgp208d7FcXcaK8j2c0l+fqSJ3d2AzQAfjEYDRM3Z2oMeqSJ9uFxyj/muSACLdix7oTstRA==} engines: {node: '>=12'} cpu: [x64] os: [darwin] @@ -5886,8 +5886,8 @@ packages: dev: true optional: true - /esbuild-darwin-arm64/0.15.9: - resolution: {integrity: sha512-VZIMlcRN29yg/sv7DsDwN+OeufCcoTNaTl3Vnav7dL/nvsApD7uvhVRbgyMzv0zU/PP0xRhhIpTyc7lxEzHGSw==} + /esbuild-darwin-arm64/0.15.10: + resolution: {integrity: sha512-M1t5+Kj4IgSbYmunf2BB6EKLkWUq+XlqaFRiGOk8bmBapu9bCDrxjf4kUnWn59Dka3I27EiuHBKd1rSO4osLFQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] @@ -5895,8 +5895,8 @@ packages: dev: true optional: true - /esbuild-freebsd-64/0.15.9: - resolution: {integrity: sha512-uM4z5bTvuAXqPxrI204txhlsPIolQPWRMLenvGuCPZTnnGlCMF2QLs0Plcm26gcskhxewYo9LkkmYSS5Czrb5A==} + /esbuild-freebsd-64/0.15.10: + resolution: {integrity: sha512-KMBFMa7C8oc97nqDdoZwtDBX7gfpolkk6Bcmj6YFMrtCMVgoU/x2DI1p74DmYl7CSS6Ppa3xgemrLrr5IjIn0w==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] @@ -5904,8 +5904,8 @@ packages: dev: true optional: true - /esbuild-freebsd-arm64/0.15.9: - resolution: {integrity: sha512-HHDjT3O5gWzicGdgJ5yokZVN9K9KG05SnERwl9nBYZaCjcCgj/sX8Ps1jvoFSfNCO04JSsHSOWo4qvxFuj8FoA==} + /esbuild-freebsd-arm64/0.15.10: + resolution: {integrity: sha512-m2KNbuCX13yQqLlbSojFMHpewbn8wW5uDS6DxRpmaZKzyq8Dbsku6hHvh2U+BcLwWY4mpgXzFUoENEf7IcioGg==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] @@ -5913,8 +5913,8 @@ packages: dev: true optional: true - /esbuild-linux-32/0.15.9: - resolution: {integrity: sha512-AQIdE8FugGt1DkcekKi5ycI46QZpGJ/wqcMr7w6YUmOmp2ohQ8eO4sKUsOxNOvYL7hGEVwkndSyszR6HpVHLFg==} + /esbuild-linux-32/0.15.10: + resolution: {integrity: sha512-guXrwSYFAvNkuQ39FNeV4sNkNms1bLlA5vF1H0cazZBOLdLFIny6BhT+TUbK/hdByMQhtWQ5jI9VAmPKbVPu1w==} engines: {node: '>=12'} cpu: [ia32] os: [linux] @@ -5922,8 +5922,8 @@ packages: dev: true optional: true - /esbuild-linux-64/0.15.9: - resolution: {integrity: sha512-4RXjae7g6Qs7StZyiYyXTZXBlfODhb1aBVAjd+ANuPmMhWthQilWo7rFHwJwL7DQu1Fjej2sODAVwLbcIVsAYQ==} + /esbuild-linux-64/0.15.10: + resolution: {integrity: sha512-jd8XfaSJeucMpD63YNMO1JCrdJhckHWcMv6O233bL4l6ogQKQOxBYSRP/XLWP+6kVTu0obXovuckJDcA0DKtQA==} engines: {node: '>=12'} cpu: [x64] os: [linux] @@ -5931,8 +5931,8 @@ packages: dev: true optional: true - /esbuild-linux-arm/0.15.9: - resolution: {integrity: sha512-3Zf2GVGUOI7XwChH3qrnTOSqfV1V4CAc/7zLVm4lO6JT6wbJrTgEYCCiNSzziSju+J9Jhf9YGWk/26quWPC6yQ==} + /esbuild-linux-arm/0.15.10: + resolution: {integrity: sha512-6N8vThLL/Lysy9y4Ex8XoLQAlbZKUyExCWyayGi2KgTBelKpPgj6RZnUaKri0dHNPGgReJriKVU6+KDGQwn10A==} engines: {node: '>=12'} cpu: [arm] os: [linux] @@ -5940,8 +5940,8 @@ packages: dev: true optional: true - /esbuild-linux-arm64/0.15.9: - resolution: {integrity: sha512-a+bTtxJmYmk9d+s2W4/R1SYKDDAldOKmWjWP0BnrWtDbvUBNOm++du0ysPju4mZVoEFgS1yLNW+VXnG/4FNwdQ==} + /esbuild-linux-arm64/0.15.10: + resolution: {integrity: sha512-GByBi4fgkvZFTHFDYNftu1DQ1GzR23jws0oWyCfhnI7eMOe+wgwWrc78dbNk709Ivdr/evefm2PJiUBMiusS1A==} engines: {node: '>=12'} cpu: [arm64] os: [linux] @@ -5949,8 +5949,8 @@ packages: dev: true optional: true - /esbuild-linux-mips64le/0.15.9: - resolution: {integrity: sha512-Zn9HSylDp89y+TRREMDoGrc3Z4Hs5u56ozZLQCiZAUx2+HdbbXbWdjmw3FdTJ/i7t5Cew6/Q+6kfO3KCcFGlyw==} + /esbuild-linux-mips64le/0.15.10: + resolution: {integrity: sha512-BxP+LbaGVGIdQNJUNF7qpYjEGWb0YyHVSKqYKrn+pTwH/SiHUxFyJYSP3pqkku61olQiSBnSmWZ+YUpj78Tw7Q==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] @@ -5958,8 +5958,8 @@ packages: dev: true optional: true - /esbuild-linux-ppc64le/0.15.9: - resolution: {integrity: sha512-OEiOxNAMH9ENFYqRsWUj3CWyN3V8P3ZXyfNAtX5rlCEC/ERXrCEFCJji/1F6POzsXAzxvUJrTSTCy7G6BhA6Fw==} + /esbuild-linux-ppc64le/0.15.10: + resolution: {integrity: sha512-LoSQCd6498PmninNgqd/BR7z3Bsk/mabImBWuQ4wQgmQEeanzWd5BQU2aNi9mBURCLgyheuZS6Xhrw5luw3OkQ==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] @@ -5967,8 +5967,8 @@ packages: dev: true optional: true - /esbuild-linux-riscv64/0.15.9: - resolution: {integrity: sha512-ukm4KsC3QRausEFjzTsOZ/qqazw0YvJsKmfoZZm9QW27OHjk2XKSQGGvx8gIEswft/Sadp03/VZvAaqv5AIwNA==} + /esbuild-linux-riscv64/0.15.10: + resolution: {integrity: sha512-Lrl9Cr2YROvPV4wmZ1/g48httE8z/5SCiXIyebiB5N8VT7pX3t6meI7TQVHw/wQpqP/AF4SksDuFImPTM7Z32Q==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] @@ -5976,8 +5976,8 @@ packages: dev: true optional: true - /esbuild-linux-s390x/0.15.9: - resolution: {integrity: sha512-uDOQEH55wQ6ahcIKzQr3VyjGc6Po/xblLGLoUk3fVL1qjlZAibtQr6XRfy5wPJLu/M2o0vQKLq4lyJ2r1tWKcw==} + /esbuild-linux-s390x/0.15.10: + resolution: {integrity: sha512-ReP+6q3eLVVP2lpRrvl5EodKX7EZ1bS1/z5j6hsluAlZP5aHhk6ghT6Cq3IANvvDdscMMCB4QEbI+AjtvoOFpA==} engines: {node: '>=12'} cpu: [s390x] os: [linux] @@ -5985,8 +5985,8 @@ packages: dev: true optional: true - /esbuild-netbsd-64/0.15.9: - resolution: {integrity: sha512-yWgxaYTQz+TqX80wXRq6xAtb7GSBAp6gqLKfOdANg9qEmAI1Bxn04IrQr0Mzm4AhxvGKoHzjHjMgXbCCSSDxcw==} + /esbuild-netbsd-64/0.15.10: + resolution: {integrity: sha512-iGDYtJCMCqldMskQ4eIV+QSS/CuT7xyy9i2/FjpKvxAuCzrESZXiA1L64YNj6/afuzfBe9i8m/uDkFHy257hTw==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] @@ -5994,8 +5994,8 @@ packages: dev: true optional: true - /esbuild-openbsd-64/0.15.9: - resolution: {integrity: sha512-JmS18acQl4iSAjrEha1MfEmUMN4FcnnrtTaJ7Qg0tDCOcgpPPQRLGsZqhes0vmx8VA6IqRyScqXvaL7+Q0Uf3A==} + /esbuild-openbsd-64/0.15.10: + resolution: {integrity: sha512-ftMMIwHWrnrYnvuJQRJs/Smlcb28F9ICGde/P3FUTCgDDM0N7WA0o9uOR38f5Xe2/OhNCgkjNeb7QeaE3cyWkQ==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] @@ -6003,8 +6003,8 @@ packages: dev: true optional: true - /esbuild-sunos-64/0.15.9: - resolution: {integrity: sha512-UKynGSWpzkPmXW3D2UMOD9BZPIuRaSqphxSCwScfEE05Be3KAmvjsBhht1fLzKpiFVJb0BYMd4jEbWMyJ/z1hQ==} + /esbuild-sunos-64/0.15.10: + resolution: {integrity: sha512-mf7hBL9Uo2gcy2r3rUFMjVpTaGpFJJE5QTDDqUFf1632FxteYANffDZmKbqX0PfeQ2XjUDE604IcE7OJeoHiyg==} engines: {node: '>=12'} cpu: [x64] os: [sunos] @@ -6012,8 +6012,8 @@ packages: dev: true optional: true - /esbuild-windows-32/0.15.9: - resolution: {integrity: sha512-aqXvu4/W9XyTVqO/hw3rNxKE1TcZiEYHPsXM9LwYmKSX9/hjvfIJzXwQBlPcJ/QOxedfoMVH0YnhhQ9Ffb0RGA==} + /esbuild-windows-32/0.15.10: + resolution: {integrity: sha512-ttFVo+Cg8b5+qHmZHbEc8Vl17kCleHhLzgT8X04y8zudEApo0PxPg9Mz8Z2cKH1bCYlve1XL8LkyXGFjtUYeGg==} engines: {node: '>=12'} cpu: [ia32] os: [win32] @@ -6021,8 +6021,8 @@ packages: dev: true optional: true - /esbuild-windows-64/0.15.9: - resolution: {integrity: sha512-zm7h91WUmlS4idMtjvCrEeNhlH7+TNOmqw5dJPJZrgFaxoFyqYG6CKDpdFCQXdyKpD5yvzaQBOMVTCBVKGZDEg==} + /esbuild-windows-64/0.15.10: + resolution: {integrity: sha512-2H0gdsyHi5x+8lbng3hLbxDWR7mKHWh5BXZGKVG830KUmXOOWFE2YKJ4tHRkejRduOGDrBvHBriYsGtmTv3ntA==} engines: {node: '>=12'} cpu: [x64] os: [win32] @@ -6030,8 +6030,8 @@ packages: dev: true optional: true - /esbuild-windows-arm64/0.15.9: - resolution: {integrity: sha512-yQEVIv27oauAtvtuhJVfSNMztJJX47ismRS6Sv2QMVV9RM+6xjbMWuuwM2nxr5A2/gj/mu2z9YlQxiwoFRCfZA==} + /esbuild-windows-arm64/0.15.10: + resolution: {integrity: sha512-S+th4F+F8VLsHLR0zrUcG+Et4hx0RKgK1eyHc08kztmLOES8BWwMiaGdoW9hiXuzznXQ0I/Fg904MNbr11Nktw==} engines: {node: '>=12'} cpu: [arm64] os: [win32] @@ -6039,34 +6039,34 @@ packages: dev: true optional: true - /esbuild/0.15.9: - resolution: {integrity: sha512-OnYr1rkMVxtmMHIAKZLMcEUlJmqcbxBz9QoBU8G9v455na0fuzlT/GLu6l+SRghrk0Mm2fSSciMmzV43Q8e0Gg==} + /esbuild/0.15.10: + resolution: {integrity: sha512-N7wBhfJ/E5fzn/SpNgX+oW2RLRjwaL8Y0ezqNqhjD6w0H2p0rDuEz2FKZqpqLnO8DCaWumKe8dsC/ljvVSSxng==} engines: {node: '>=12'} hasBin: true requiresBuild: true optionalDependencies: - '@esbuild/android-arm': 0.15.9 - '@esbuild/linux-loong64': 0.15.9 - esbuild-android-64: 0.15.9 - esbuild-android-arm64: 0.15.9 - esbuild-darwin-64: 0.15.9 - esbuild-darwin-arm64: 0.15.9 - esbuild-freebsd-64: 0.15.9 - esbuild-freebsd-arm64: 0.15.9 - esbuild-linux-32: 0.15.9 - esbuild-linux-64: 0.15.9 - esbuild-linux-arm: 0.15.9 - esbuild-linux-arm64: 0.15.9 - esbuild-linux-mips64le: 0.15.9 - esbuild-linux-ppc64le: 0.15.9 - esbuild-linux-riscv64: 0.15.9 - esbuild-linux-s390x: 0.15.9 - esbuild-netbsd-64: 0.15.9 - esbuild-openbsd-64: 0.15.9 - esbuild-sunos-64: 0.15.9 - esbuild-windows-32: 0.15.9 - esbuild-windows-64: 0.15.9 - esbuild-windows-arm64: 0.15.9 + '@esbuild/android-arm': 0.15.10 + '@esbuild/linux-loong64': 0.15.10 + esbuild-android-64: 0.15.10 + esbuild-android-arm64: 0.15.10 + esbuild-darwin-64: 0.15.10 + esbuild-darwin-arm64: 0.15.10 + esbuild-freebsd-64: 0.15.10 + esbuild-freebsd-arm64: 0.15.10 + esbuild-linux-32: 0.15.10 + esbuild-linux-64: 0.15.10 + esbuild-linux-arm: 0.15.10 + esbuild-linux-arm64: 0.15.10 + esbuild-linux-mips64le: 0.15.10 + esbuild-linux-ppc64le: 0.15.10 + esbuild-linux-riscv64: 0.15.10 + esbuild-linux-s390x: 0.15.10 + esbuild-netbsd-64: 0.15.10 + esbuild-openbsd-64: 0.15.10 + esbuild-sunos-64: 0.15.10 + esbuild-windows-32: 0.15.10 + esbuild-windows-64: 0.15.10 + esbuild-windows-arm64: 0.15.10 dev: true /escalade/3.1.1: @@ -12596,7 +12596,7 @@ packages: terser: optional: true dependencies: - esbuild: 0.15.9 + esbuild: 0.15.10 postcss: 8.4.16 resolve: 1.22.1 rollup: 2.78.0 From 34ffd1d5729341bdede217387a4a4c490d7d60d8 Mon Sep 17 00:00:00 2001 From: Dominik Pschenitschni Date: Fri, 23 Sep 2022 12:55:53 +0200 Subject: [PATCH 016/146] feat: port tasks store to pinia --- .../quick-actions/quick-actions.vue | 4 +- src/components/tasks/add-task.vue | 9 +- src/components/tasks/partials/description.vue | 11 +- .../tasks/partials/editAssignees.vue | 11 +- src/components/tasks/partials/editLabels.vue | 20 +-- src/components/tasks/partials/heading.vue | 8 +- src/components/tasks/partials/kanban-card.vue | 3 +- .../tasks/partials/relatedTasks.vue | 6 +- .../tasks/partials/singleTaskInList.vue | 3 +- src/helpers/attachments.ts | 4 +- src/main.ts | 5 +- src/pinia.ts | 5 + src/store/index.ts | 3 +- src/store/types.ts | 4 +- src/stores/labels.ts | 2 +- src/stores/tasks.ts | 158 ++++++++++-------- src/views/list/ListKanban.vue | 8 +- src/views/list/ListList.vue | 11 +- src/views/tasks/ShowTasks.vue | 4 +- src/views/tasks/TaskDetailView.vue | 6 +- 20 files changed, 157 insertions(+), 128 deletions(-) create mode 100644 src/pinia.ts diff --git a/src/components/quick-actions/quick-actions.vue b/src/components/quick-actions/quick-actions.vue index a71d7b38..a90467a7 100644 --- a/src/components/quick-actions/quick-actions.vue +++ b/src/components/quick-actions/quick-actions.vue @@ -73,6 +73,7 @@ import {PREFIXES} from '@/modules/parseTaskText' import {useListStore} from '@/stores/lists' import {useNamespaceStore} from '@/stores/namespaces' import {useLabelStore} from '@/stores/labels' +import {useTaskStore} from '@/stores/tasks' const TYPE_LIST = 'list' const TYPE_TASK = 'task' @@ -412,7 +413,8 @@ export default defineComponent({ return } - const task = await this.$store.dispatch('tasks/createNewTask', { + const taskStore = useTaskStore() + const task = await taskStore.createNewTask({ title: this.query, listId: this.currentList.id, }) diff --git a/src/components/tasks/add-task.vue b/src/components/tasks/add-task.vue index d6873a16..2a295386 100644 --- a/src/components/tasks/add-task.vue +++ b/src/components/tasks/add-task.vue @@ -43,12 +43,11 @@ diff --git a/src/views/tasks/ShowTasks.vue b/src/views/tasks/ShowTasks.vue index ab80110c..de2acce3 100644 --- a/src/views/tasks/ShowTasks.vue +++ b/src/views/tasks/ShowTasks.vue @@ -60,9 +60,11 @@ import {LOADING, LOADING_MODULE} from '@/store/mutation-types' import LlamaCool from '@/assets/llama-cool.svg?component' import type {ITask} from '@/modelTypes/ITask' import {useAuthStore} from '@/stores/auth' +import {useTaskStore} from '@/stores/tasks' const store = useStore() const authStore = useAuthStore() +const taskStore = useTaskStore() const route = useRoute() const router = useRouter() const {t} = useI18n({useScope: 'global'}) @@ -180,7 +182,7 @@ async function loadPendingTasks(from: string, to: string) { } } - tasks.value = await store.dispatch('tasks/loadTasks', params) + tasks.value = await taskStore.loadTasks(params) } // FIXME: this modification should happen in the store diff --git a/src/views/tasks/TaskDetailView.vue b/src/views/tasks/TaskDetailView.vue index ea67956e..ab61a077 100644 --- a/src/views/tasks/TaskDetailView.vue +++ b/src/views/tasks/TaskDetailView.vue @@ -464,6 +464,7 @@ import type {IList} from '@/modelTypes/IList' import {colorIsDark} from '@/helpers/color/colorIsDark' import {useNamespaceStore} from '@/stores/namespaces' import {useAttachmentStore} from '@/stores/attachments' +import {useTaskStore} from '@/stores/tasks' function scrollIntoView(el) { if (!el) { @@ -696,7 +697,8 @@ export default defineComponent({ task.endDate = task.dueDate } - this.task = await this.$store.dispatch('tasks/update', task) + + this.task = await useTaskStore().update(task) if (!showNotification) { return @@ -728,7 +730,7 @@ export default defineComponent({ }, async deleteTask() { - await this.$store.dispatch('tasks/delete', this.task) + await useTaskStore().delete(this.task) this.$message.success({message: this.$t('task.detail.deleteSuccess')}) this.$router.push({name: 'list.index', params: {listId: this.task.listId}}) }, From 9f26ae1ee6241b2ef529f01d3511380c9d7a4576 Mon Sep 17 00:00:00 2001 From: Dominik Pschenitschni Date: Fri, 23 Sep 2022 17:04:43 +0200 Subject: [PATCH 017/146] feat: move kanban to stores --- src/{store/modules => stores}/kanban.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{store/modules => stores}/kanban.ts (100%) diff --git a/src/store/modules/kanban.ts b/src/stores/kanban.ts similarity index 100% rename from src/store/modules/kanban.ts rename to src/stores/kanban.ts From c35810f28fc5aacefabad7526b0ac4e982d53cc7 Mon Sep 17 00:00:00 2001 From: Dominik Pschenitschni Date: Sat, 24 Sep 2022 12:35:41 +0200 Subject: [PATCH 018/146] feat: port kanban store to pinia --- src/store/index.ts | 4 - src/store/types.ts | 5 +- src/stores/kanban.ts | 442 +++++++++++++++-------------- src/stores/tasks.ts | 30 +- src/views/list/ListKanban.vue | 50 ++-- src/views/list/ListWrapper.vue | 4 +- src/views/tasks/TaskDetailView.vue | 3 +- 7 files changed, 277 insertions(+), 261 deletions(-) diff --git a/src/store/index.ts b/src/store/index.ts index 73b4a698..cd001a87 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -13,7 +13,6 @@ import { MENU_ACTIVE, QUICK_ACTIONS_ACTIVE, } from './mutation-types' -import kanban from './modules/kanban' import ListModel from '@/models/list' @@ -33,9 +32,6 @@ export function useStore () { export const store = createStore({ strict: import.meta.env.DEV, - modules: { - kanban, - }, state: () => ({ loading: false, loadingModule: null, diff --git a/src/store/types.ts b/src/store/types.ts index 76696a05..f7ea2ce8 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -68,13 +68,16 @@ export interface ConfigState { export interface KanbanState { buckets: IBucket[], listId: IList['id'], - bucketLoading: {}, + bucketLoading: { + [id: IBucket['id']]: boolean + }, taskPagesPerBucket: { [id: IBucket['id']]: number }, allTasksLoadedForBucket: { [id: IBucket['id']]: boolean }, + isLoading: boolean, } export interface LabelState { diff --git a/src/stores/kanban.ts b/src/stores/kanban.ts index df09d46c..f5a19119 100644 --- a/src/stores/kanban.ts +++ b/src/stores/kanban.ts @@ -1,14 +1,14 @@ -import type { Module } from 'vuex' +import {defineStore, acceptHMRUpdate} from 'pinia' import cloneDeep from 'lodash.clonedeep' import {findById, findIndexById} from '@/helpers/utils' import {i18n} from '@/i18n' import {success} from '@/message' -import BucketService from '../../services/bucket' -import {setLoading} from '../helper' +import BucketService from '../services/bucket' +import {setLoadingPinia} from '@/store/helper' import TaskCollectionService from '@/services/taskCollection' -import type { RootStoreState, KanbanState } from '@/store/types' +import type { KanbanState } from '@/store/types' import type { ITask } from '@/modelTypes/ITask' import type { IList } from '@/modelTypes/IList' import type { IBucket } from '@/modelTypes/IBucket' @@ -41,188 +41,16 @@ const addTaskToBucketAndSort = (state: KanbanState, task: ITask) => { * This store is intended to hold the currently active kanban view. * It should hold only the current buckets. */ -const kanbanStore : Module = { - namespaced: true, - - state: () => ({ +export const useKanbanStore = defineStore('kanban', { + state: () : KanbanState => ({ buckets: [], listId: 0, bucketLoading: {}, taskPagesPerBucket: {}, allTasksLoadedForBucket: {}, + isLoading: false, }), - mutations: { - setListId(state, listId: IList['id']) { - state.listId = parseInt(listId) - }, - - setBuckets(state, buckets: IBucket[]) { - state.buckets = buckets - buckets.forEach(b => { - state.taskPagesPerBucket[b.id] = 1 - state.allTasksLoadedForBucket[b.id] = false - }) - }, - - addBucket(state, bucket: IBucket) { - state.buckets.push(bucket) - }, - - removeBucket(state, bucket: IBucket) { - const bucketIndex = findIndexById(state.buckets, bucket.id) - state.buckets.splice(bucketIndex, 1) - }, - - setBucketById(state, bucket: IBucket) { - const bucketIndex = findIndexById(state.buckets, bucket.id) - state.buckets[bucketIndex] = bucket - }, - - setBucketByIndex(state, { - bucketIndex, - bucket, - } : { - bucketIndex: number, - bucket: IBucket - }) { - state.buckets[bucketIndex] = bucket - }, - - setTaskInBucketByIndex(state, { - bucketIndex, - taskIndex, - task, - } : { - bucketIndex: number, - taskIndex: number, - task: ITask - }) { - const bucket = state.buckets[bucketIndex] - bucket.tasks[taskIndex] = task - state.buckets[bucketIndex] = bucket - }, - - setTasksInBucketByBucketId(state, { - bucketId, - tasks, - } : { - bucketId: IBucket['id'], - tasks: ITask[], - }) { - const bucketIndex = findIndexById(state.buckets, bucketId) - state.buckets[bucketIndex] = { - ...state.buckets[bucketIndex], - tasks, - } - }, - - setTaskInBucket(state, task: ITask) { - // If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task - if (state.buckets.length === 0) { - return - } - - let found = false - - const findAndUpdate = b => { - for (const t in state.buckets[b].tasks) { - if (state.buckets[b].tasks[t].id === task.id) { - const bucket = state.buckets[b] - bucket.tasks[t] = task - - if (bucket.id !== task.bucketId) { - bucket.tasks.splice(t, 1) - addTaskToBucketAndSort(state, task) - } - - state.buckets[b] = bucket - - found = true - return - } - } - } - - for (const b in state.buckets) { - if (state.buckets[b].id === task.bucketId) { - findAndUpdate(b) - if (found) { - return - } - } - } - - for (const b in state.buckets) { - findAndUpdate(b) - if (found) { - return - } - } - }, - - addTaskToBucket(state, task: ITask) { - const bucketIndex = findIndexById(state.buckets, task.bucketId) - const oldBucket = state.buckets[bucketIndex] - const newBucket = { - ...oldBucket, - tasks: [ - ...oldBucket.tasks, - task, - ], - } - state.buckets[bucketIndex] = newBucket - }, - - addTasksToBucket(state, {tasks, bucketId}: { - tasks: ITask[]; - bucketId: IBucket['id']; - }) { - const bucketIndex = findIndexById(state.buckets, bucketId) - const oldBucket = state.buckets[bucketIndex] - const newBucket = { - ...oldBucket, - tasks: [ - ...oldBucket.tasks, - ...tasks, - ], - } - state.buckets[bucketIndex] = newBucket - }, - - removeTaskInBucket(state, task: ITask) { - // If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task - if (state.buckets.length === 0) { - return - } - - const { bucketIndex, taskIndex } = getTaskIndicesById(state, task.id) - - if ( - !bucketIndex || - state.buckets[bucketIndex]?.id !== task.bucketId || - !taskIndex || - (state.buckets[bucketIndex]?.tasks[taskIndex]?.id !== task.id) - ) { - return - } - - state.buckets[bucketIndex].tasks.splice(taskIndex, 1) - }, - - setBucketLoading(state, {bucketId, loading}) { - state.bucketLoading[bucketId] = loading - }, - - setTasksLoadedForBucketPage(state: KanbanState, {bucketId, page}) { - state.taskPagesPerBucket[bucketId] = page - }, - - setAllTasksLoadedForBucket(state: KanbanState, bucketId) { - state.allTasksLoadedForBucket[bucketId] = true - }, - }, - getters: { getBucketById(state) { return (bucketId: IBucket['id']) => findById(state.buckets, bucketId) @@ -243,40 +71,216 @@ const kanbanStore : Module = { }, actions: { - async loadBucketsForList(ctx, {listId, params}) { - const cancel = setLoading(ctx, 'kanban') + setIsLoading(isLoading: boolean) { + this.isLoading = isLoading + }, + + setListId(listId: IList['id']) { + this.listId = Number(listId) + }, + + setBuckets(buckets: IBucket[]) { + this.buckets = buckets + buckets.forEach(b => { + this.taskPagesPerBucket[b.id] = 1 + this.allTasksLoadedForBucket[b.id] = false + }) + }, + + addBucket(bucket: IBucket) { + this.buckets.push(bucket) + }, + + removeBucket(bucket: IBucket) { + const bucketIndex = findIndexById(this.buckets, bucket.id) + this.buckets.splice(bucketIndex, 1) + }, + + setBucketById(bucket: IBucket) { + const bucketIndex = findIndexById(this.buckets, bucket.id) + this.buckets[bucketIndex] = bucket + }, + + setBucketByIndex({ + bucketIndex, + bucket, + } : { + bucketIndex: number, + bucket: IBucket + }) { + this.buckets[bucketIndex] = bucket + }, + + setTaskInBucketByIndex({ + bucketIndex, + taskIndex, + task, + } : { + bucketIndex: number, + taskIndex: number, + task: ITask + }) { + const bucket = this.buckets[bucketIndex] + bucket.tasks[taskIndex] = task + this.buckets[bucketIndex] = bucket + }, + + setTasksInBucketByBucketId({ + bucketId, + tasks, + } : { + bucketId: IBucket['id'], + tasks: ITask[], + }) { + const bucketIndex = findIndexById(this.buckets, bucketId) + this.buckets[bucketIndex] = { + ...this.buckets[bucketIndex], + tasks, + } + }, + + setTaskInBucket(task: ITask) { + // If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task + if (this.buckets.length === 0) { + return + } + + let found = false + + const findAndUpdate = b => { + for (const t in this.buckets[b].tasks) { + if (this.buckets[b].tasks[t].id === task.id) { + const bucket = this.buckets[b] + bucket.tasks[t] = task + + if (bucket.id !== task.bucketId) { + bucket.tasks.splice(t, 1) + addTaskToBucketAndSort(this, task) + } + + this.buckets[b] = bucket + + found = true + return + } + } + } + + for (const b in this.buckets) { + if (this.buckets[b].id === task.bucketId) { + findAndUpdate(b) + if (found) { + return + } + } + } + + for (const b in this.buckets) { + findAndUpdate(b) + if (found) { + return + } + } + }, + + addTaskToBucket(task: ITask) { + const bucketIndex = findIndexById(this.buckets, task.bucketId) + const oldBucket = this.buckets[bucketIndex] + const newBucket = { + ...oldBucket, + tasks: [ + ...oldBucket.tasks, + task, + ], + } + this.buckets[bucketIndex] = newBucket + }, + + addTasksToBucket({tasks, bucketId}: { + tasks: ITask[]; + bucketId: IBucket['id']; + }) { + const bucketIndex = findIndexById(this.buckets, bucketId) + const oldBucket = this.buckets[bucketIndex] + const newBucket = { + ...oldBucket, + tasks: [ + ...oldBucket.tasks, + ...tasks, + ], + } + this.buckets[bucketIndex] = newBucket + }, + + removeTaskInBucket(task: ITask) { + // If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task + if (this.buckets.length === 0) { + return + } + + const { bucketIndex, taskIndex } = getTaskIndicesById(this, task.id) + + if ( + !bucketIndex || + this.buckets[bucketIndex]?.id !== task.bucketId || + !taskIndex || + (this.buckets[bucketIndex]?.tasks[taskIndex]?.id !== task.id) + ) { + return + } + + this.buckets[bucketIndex].tasks.splice(taskIndex, 1) + }, + + setBucketLoading({bucketId, loading}: {bucketId: IBucket['id'], loading: boolean}) { + this.bucketLoading[bucketId] = loading + }, + + setTasksLoadedForBucketPage({bucketId, page}: {bucketId: IBucket['id'], page: number}) { + this.taskPagesPerBucket[bucketId] = page + }, + + setAllTasksLoadedForBucket(bucketId: IBucket['id']) { + this.allTasksLoadedForBucket[bucketId] = true + }, + + async loadBucketsForList({listId, params}: {listId: IList['id'], params}) { + const cancel = setLoadingPinia(this) // Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments - ctx.commit('setBuckets', []) + this.setBuckets([]) params.per_page = TASKS_PER_BUCKET const bucketService = new BucketService() try { - const response = await bucketService.getAll({listId}, params) - ctx.commit('setBuckets', response) - ctx.commit('setListId', listId) - return response + const buckets = await bucketService.getAll({listId}, params) + this.setBuckets(buckets) + this.setListId(listId) + return buckets } finally { cancel() } }, - async loadNextTasksForBucket(ctx, {listId, ps = {}, bucketId}) { - const isLoading = ctx.state.bucketLoading[bucketId] ?? false + async loadNextTasksForBucket( + {listId, ps = {}, bucketId} : + {listId: IList['id'], ps, bucketId: IBucket['id']}, + ) { + const isLoading = this.bucketLoading[bucketId] ?? false if (isLoading) { return } - const page = (ctx.state.taskPagesPerBucket[bucketId] ?? 1) + 1 + const page = (this.taskPagesPerBucket[bucketId] ?? 1) + 1 - const alreadyLoaded = ctx.state.allTasksLoadedForBucket[bucketId] ?? false + const alreadyLoaded = this.allTasksLoadedForBucket[bucketId] ?? false if (alreadyLoaded) { return } - const cancel = setLoading(ctx, 'kanban') - ctx.commit('setBucketLoading', {bucketId: bucketId, loading: true}) + const cancel = setLoadingPinia(this) + this.setBucketLoading({bucketId: bucketId, loading: true}) const params = JSON.parse(JSON.stringify(ps)) @@ -305,67 +309,67 @@ const kanbanStore : Module = { const taskService = new TaskCollectionService() try { const tasks = await taskService.getAll({listId}, params, page) - ctx.commit('addTasksToBucket', {tasks, bucketId: bucketId}) - ctx.commit('setTasksLoadedForBucketPage', {bucketId, page}) + this.addTasksToBucket({tasks, bucketId: bucketId}) + this.setTasksLoadedForBucketPage({bucketId, page}) if (taskService.totalPages <= page) { - ctx.commit('setAllTasksLoadedForBucket', bucketId) + this.setAllTasksLoadedForBucket(bucketId) } return tasks } finally { cancel() - ctx.commit('setBucketLoading', {bucketId, loading: false}) + this.setBucketLoading({bucketId, loading: false}) } }, - async createBucket(ctx, bucket: IBucket) { - const cancel = setLoading(ctx, 'kanban') + async createBucket(bucket: IBucket) { + const cancel = setLoadingPinia(this) const bucketService = new BucketService() try { const createdBucket = await bucketService.create(bucket) - ctx.commit('addBucket', createdBucket) + this.addBucket(createdBucket) return createdBucket } finally { cancel() } }, - async deleteBucket(ctx, {bucket, params}) { - const cancel = setLoading(ctx, 'kanban') + async deleteBucket({bucket, params}: {bucket: IBucket, params}) { + const cancel = setLoadingPinia(this) const bucketService = new BucketService() try { const response = await bucketService.delete(bucket) - ctx.commit('removeBucket', bucket) + this.removeBucket(bucket) // We reload all buckets because tasks are being moved from the deleted bucket - ctx.dispatch('loadBucketsForList', {listId: bucket.listId, params}) + this.loadBucketsForList({listId: bucket.listId, params}) return response } finally { cancel() } }, - async updateBucket(ctx, updatedBucketData) { - const cancel = setLoading(ctx, 'kanban') + async updateBucket(updatedBucketData: IBucket) { + const cancel = setLoadingPinia(this) - const bucketIndex = findIndexById(ctx.state.buckets, updatedBucketData.id) - const oldBucket = cloneDeep(ctx.state.buckets[bucketIndex]) + const bucketIndex = findIndexById(this.buckets, updatedBucketData.id) + const oldBucket = cloneDeep(this.buckets[bucketIndex]) const updatedBucket = { ...oldBucket, ...updatedBucketData, } - ctx.commit('setBucketByIndex', {bucketIndex, bucket: updatedBucket}) + this.setBucketByIndex({bucketIndex, bucket: updatedBucket}) const bucketService = new BucketService() try { const returnedBucket = await bucketService.update(updatedBucket) - ctx.commit('setBucketByIndex', {bucketIndex, bucket: returnedBucket}) + this.setBucketByIndex({bucketIndex, bucket: returnedBucket}) return returnedBucket } catch(e) { // restore original state - ctx.commit('setBucketByIndex', {bucketIndex, bucket: oldBucket}) + this.setBucketByIndex({bucketIndex, bucket: oldBucket}) throw e } finally { @@ -373,23 +377,21 @@ const kanbanStore : Module = { } }, - async updateBucketTitle(ctx, { id, title }) { - const bucket = findById(ctx.state.buckets, id) + async updateBucketTitle({ id, title }: { id: IBucket['id'], title: IBucket['title'] }) { + const bucket = findById(this.buckets, id) if (bucket?.title === title) { // bucket title has not changed return } - const updatedBucketData = { - id, - title, - } - - await ctx.dispatch('updateBucket', updatedBucketData) + await this.updateBucket({ id, title }) success({message: i18n.global.t('list.kanban.bucketTitleSavedSuccess')}) }, }, -} +}) -export default kanbanStore \ No newline at end of file +// support hot reloading +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useKanbanStore, import.meta.hot)) +} \ No newline at end of file diff --git a/src/stores/tasks.ts b/src/stores/tasks.ts index a69b1f59..8e3575e1 100644 --- a/src/stores/tasks.ts +++ b/src/stores/tasks.ts @@ -28,6 +28,7 @@ import type {TaskState} from '@/store/types' import {useLabelStore} from '@/stores/labels' import {useListStore} from '@/stores/lists' import {useAttachmentStore} from '@/stores/attachments' +import {useKanbanStore} from '@/stores/kanban' import {playPop} from '@/helpers/playPop' import {store} from '@/store' @@ -99,7 +100,7 @@ export const useTaskStore = defineStore('task', { const taskService = new TaskService() try { const updatedTask = await taskService.update(task) - store.commit('kanban/setTaskInBucket', updatedTask) + useKanbanStore().setTaskInBucket(updatedTask) if (task.done) { playPop() } @@ -112,7 +113,7 @@ export const useTaskStore = defineStore('task', { async delete(task: ITask) { const taskService = new TaskService() const response = await taskService.delete(task) - store.commit('kanban/removeTaskInBucket', task) + useKanbanStore().removeTaskInBucket(task) return response }, @@ -125,7 +126,8 @@ export const useTaskStore = defineStore('task', { taskId: ITask['id'] attachment: IAttachment }) { - const t = store.getters['kanban/getTaskById'](taskId) + const kanbanStore = useKanbanStore() + const t = kanbanStore.getTaskById(taskId) if (t.task !== null) { const attachments = [ ...t.task.attachments, @@ -139,7 +141,7 @@ export const useTaskStore = defineStore('task', { attachments, }, } - store.commit('kanban/setTaskInBucketByIndex', newTask) + kanbanStore.setTaskInBucketByIndex(newTask) } const attachmentStore = useAttachmentStore() attachmentStore.add(attachment) @@ -152,12 +154,13 @@ export const useTaskStore = defineStore('task', { user: IUser, taskId: ITask['id'] }) { + const kanbanStore = useKanbanStore() const taskAssigneeService = new TaskAssigneeService() const r = await taskAssigneeService.create(new TaskAssigneeModel({ userId: user.id, taskId: taskId, })) - const t = store.getters['kanban/getTaskById'](taskId) + const t = kanbanStore.getTaskById(taskId) if (t.task === null) { // Don't try further adding a label if the task is not in kanban // Usually this means the kanban board hasn't been accessed until now. @@ -166,7 +169,7 @@ export const useTaskStore = defineStore('task', { return r } - store.commit('kanban/setTaskInBucketByIndex', { + kanbanStore.setTaskInBucketByIndex({ ...t, task: { ...t.task, @@ -186,12 +189,13 @@ export const useTaskStore = defineStore('task', { user: IUser, taskId: ITask['id'] }) { + const kanbanStore = useKanbanStore() const taskAssigneeService = new TaskAssigneeService() const response = await taskAssigneeService.delete(new TaskAssigneeModel({ userId: user.id, taskId: taskId, })) - const t = store.getters['kanban/getTaskById'](taskId) + const t = kanbanStore.getTaskById(taskId) if (t.task === null) { // Don't try further adding a label if the task is not in kanban // Usually this means the kanban board hasn't been accessed until now. @@ -202,7 +206,7 @@ export const useTaskStore = defineStore('task', { const assignees = t.task.assignees.filter(({ id }) => id !== user.id) - store.commit('kanban/setTaskInBucketByIndex', { + kanbanStore.setTaskInBucketByIndex({ ...t, task: { ...t.task, @@ -220,12 +224,13 @@ export const useTaskStore = defineStore('task', { label: ILabel, taskId: ITask['id'] }) { + const kanbanStore = useKanbanStore() const labelTaskService = new LabelTaskService() const r = await labelTaskService.create(new LabelTaskModel({ taskId, labelId: label.id, })) - const t = store.getters['kanban/getTaskById'](taskId) + const t = kanbanStore.getTaskById(taskId) if (t.task === null) { // Don't try further adding a label if the task is not in kanban // Usually this means the kanban board hasn't been accessed until now. @@ -234,7 +239,7 @@ export const useTaskStore = defineStore('task', { return r } - store.commit('kanban/setTaskInBucketByIndex', { + kanbanStore.setTaskInBucketByIndex({ ...t, task: { ...t.task, @@ -252,12 +257,13 @@ export const useTaskStore = defineStore('task', { {label, taskId}: {label: ILabel, taskId: ITask['id']}, ) { + const kanbanStore = useKanbanStore() const labelTaskService = new LabelTaskService() const response = await labelTaskService.delete(new LabelTaskModel({ taskId, labelId: label.id, })) - const t = store.getters['kanban/getTaskById'](taskId) + const t = kanbanStore.getTaskById(taskId) if (t.task === null) { // Don't try further adding a label if the task is not in kanban // Usually this means the kanban board hasn't been accessed until now. @@ -269,7 +275,7 @@ export const useTaskStore = defineStore('task', { // Remove the label from the list const labels = t.task.labels.filter(({ id }) => id !== label.id) - store.commit('kanban/setTaskInBucketByIndex', { + kanbanStore.setTaskInBucketByIndex({ ...t, task: { ...t.task, diff --git a/src/views/list/ListKanban.vue b/src/views/list/ListKanban.vue index c950e008..90a5d9a4 100644 --- a/src/views/list/ListKanban.vue +++ b/src/views/list/ListKanban.vue @@ -229,9 +229,9 @@ import draggable from 'zhyswan-vuedraggable' import cloneDeep from 'lodash.clonedeep' import BucketModel from '../../models/bucket' -import {mapState} from 'vuex' +import {mapState as mapStateVuex} from 'vuex' +import {mapState} from 'pinia' import {RIGHTS as Rights} from '@/constants/rights' -import {LOADING, LOADING_MODULE} from '@/store/mutation-types' import ListWrapper from './ListWrapper.vue' import FilterPopup from '@/components/list/partials/filter-popup.vue' import Dropdown from '@/components/misc/dropdown.vue' @@ -241,6 +241,7 @@ import KanbanCard from '@/components/tasks/partials/kanban-card.vue' import DropdownItem from '@/components/misc/dropdown-item.vue' import {isSavedFilter} from '@/helpers/savedFilter' import {useTaskStore} from '@/stores/tasks' +import {useKanbanStore} from '@/stores/kanban' const DRAG_OPTIONS = { // sortable options @@ -342,16 +343,18 @@ export default defineComponent({ ], } }, - buckets() { - return this.$store.state.kanban.buckets - }, - ...mapState({ - loadedListId: state => state.kanban.listId, - loading: state => state[LOADING] && state[LOADING_MODULE] === 'kanban', - taskLoading: state => state[LOADING] && state[LOADING_MODULE] === 'tasks', + ...mapStateVuex({ canWrite: state => state.currentList.maxRight > Rights.READ, list: state => state.currentList, }), + ...mapState(useKanbanStore, { + buckets: state => state.buckets, + loadedListId: state => state.listId, + loading: state => state.isLoading, + }), + ...mapState(useTaskStore, { + taskLoading: state => state.isLoading, + }), }, methods: { @@ -364,7 +367,7 @@ export default defineComponent({ console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $attrs = ${this.$attrs} $route.params =`, this.$route.params) - this.$store.dispatch('kanban/loadBucketsForList', {listId, params}) + useKanbanStore().loadBucketsForList({listId, params}) }, setTaskContainerRef(id, el) { @@ -382,7 +385,7 @@ export default defineComponent({ return } - this.$store.dispatch('kanban/loadNextTasksForBucket', { + useKanbanStore().loadNextTasksForBucket({ listId: listId, params: this.params, bucketId: id, @@ -390,12 +393,13 @@ export default defineComponent({ }, updateTasks(bucketId, tasks) { + const kanbanStore = useKanbanStore() const newBucket = { - ...this.$store.getters['kanban/getBucketById'](bucketId), + ...kanbanStore.getBucketById(bucketId), tasks, } - this.$store.commit('kanban/setBucketById', newBucket) + kanbanStore.setBucketById(newBucket) }, async updateTaskPosition(e) { @@ -472,7 +476,7 @@ export default defineComponent({ listId: this.listId, }) this.newTaskText = '' - this.$store.commit('kanban/addTaskToBucket', task) + useKanbanStore().addTaskToBucket(task) this.scrollTaskContainerToBottom(bucketId) }, @@ -494,7 +498,7 @@ export default defineComponent({ listId: this.listId, }) - await this.$store.dispatch('kanban/createBucket', newBucket) + await useKanbanStore().createBucket(newBucket) this.newBucketTitle = '' this.showNewBucketInput = false }, @@ -515,7 +519,7 @@ export default defineComponent({ }) try { - await this.$store.dispatch('kanban/deleteBucket', { + await useKanbanStore().deleteBucket({ bucket, params: this.params, }) @@ -537,13 +541,13 @@ export default defineComponent({ title: bucketTitle, } - await this.$store.dispatch('kanban/updateBucketTitle', updatedBucketData) + await useKanbanStore().updateBucketTitle(updatedBucketData) this.bucketTitleEditable = false }, updateBuckets(value) { // (1) buckets get updated in store and tasks positions get invalidated - this.$store.commit('kanban/setBuckets', value) + useKanbanStore().setBuckets(value) }, updateBucketPosition(e) { @@ -562,7 +566,7 @@ export default defineComponent({ ), } - this.$store.dispatch('kanban/updateBucket', updatedData) + useKanbanStore().updateBucket(updatedData) }, async setBucketLimit(bucketId, limit) { @@ -570,12 +574,14 @@ export default defineComponent({ return } + const kanbanStore = useKanbanStore() + const newBucket = { - ...this.$store.getters['kanban/getBucketById'](bucketId), + ...kanbanStore.getBucketById(bucketId), limit, } - await this.$store.dispatch('kanban/updateBucket', newBucket) + await kanbanStore.updateBucket(newBucket) this.$message.success({message: this.$t('list.kanban.bucketLimitSavedSuccess')}) }, @@ -600,7 +606,7 @@ export default defineComponent({ ...bucket, isDoneBucket: !bucket.isDoneBucket, } - await this.$store.dispatch('kanban/updateBucket', newBucket) + await useKanbanStore().updateBucket(newBucket) this.$message.success({message: this.$t('list.kanban.doneBucketSavedSuccess')}) }, diff --git a/src/views/list/ListWrapper.vue b/src/views/list/ListWrapper.vue index 6509324c..6f008e88 100644 --- a/src/views/list/ListWrapper.vue +++ b/src/views/list/ListWrapper.vue @@ -62,6 +62,7 @@ import {saveListToHistory} from '@/modules/listHistory' import {useTitle} from '@/composables/useTitle' import {useStore} from '@/store' import {useListStore} from '@/stores/lists' +import {useKanbanStore} from '@/stores/kanban' const props = defineProps({ listId: { @@ -77,6 +78,7 @@ const props = defineProps({ const route = useRoute() const store = useStore() +const kanbanStore = useKanbanStore() const listStore = useListStore() const listService = ref(new ListService()) const loadedListId = ref(0) @@ -116,7 +118,7 @@ async function loadList(listIdToLoad: number) { props.viewName === 'list.list' || props.viewName === 'list.gantt' ) { - store.commit('kanban/setListId', 0) + kanbanStore.setListId(0) } // Don't load the list if we either already loaded it or aren't dealing with a list at all currently and diff --git a/src/views/tasks/TaskDetailView.vue b/src/views/tasks/TaskDetailView.vue index ab61a077..13f26ee9 100644 --- a/src/views/tasks/TaskDetailView.vue +++ b/src/views/tasks/TaskDetailView.vue @@ -465,6 +465,7 @@ import {colorIsDark} from '@/helpers/color/colorIsDark' import {useNamespaceStore} from '@/stores/namespaces' import {useAttachmentStore} from '@/stores/attachments' import {useTaskStore} from '@/stores/tasks' +import {useKanbanStore} from '@/stores/kanban' function scrollIntoView(el) { if (!el) { @@ -748,7 +749,7 @@ export default defineComponent({ }, async changeList(list: IList) { - this.$store.commit('kanban/removeTaskInBucket', this.task) + useKanbanStore().removeTaskInBucket(this.task) await this.saveTask({ task: { ...this.task, From b84da722ca6b08ba6bed7e0a666ba56ea2922460 Mon Sep 17 00:00:00 2001 From: renovate Date: Thu, 29 Sep 2022 21:53:19 +0000 Subject: [PATCH 019/146] chore(deps): update dependency @cypress/vite-dev-server to v3.2.0 (#2448) Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2448 Co-authored-by: renovate Co-committed-by: renovate --- package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 536cde16..70f70d54 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ }, "devDependencies": { "@4tw/cypress-drag-drop": "2.2.1", - "@cypress/vite-dev-server": "3.1.1", + "@cypress/vite-dev-server": "3.2.0", "@cypress/vue": "4.2.0", "@faker-js/faker": "7.5.0", "@types/dompurify": "2.3.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64a064d2..30965c0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2,7 +2,7 @@ lockfileVersion: 5.3 specifiers: '@4tw/cypress-drag-drop': 2.2.1 - '@cypress/vite-dev-server': 3.1.1 + '@cypress/vite-dev-server': 3.2.0 '@cypress/vue': 4.2.0 '@faker-js/faker': 7.5.0 '@fortawesome/fontawesome-svg-core': 6.2.0 @@ -131,7 +131,7 @@ dependencies: devDependencies: '@4tw/cypress-drag-drop': 2.2.1_cypress@10.9.0 - '@cypress/vite-dev-server': 3.1.1 + '@cypress/vite-dev-server': 3.2.0 '@cypress/vue': 4.2.0_cypress@10.9.0+vue@3.2.40 '@faker-js/faker': 7.5.0 '@types/dompurify': 2.3.4 @@ -1567,8 +1567,8 @@ packages: uuid: 8.3.2 dev: true - /@cypress/vite-dev-server/3.1.1: - resolution: {integrity: sha512-bZhDt3D3KEu8RDCxMb4GpFJoAEZOMOdFPB1xebHyjTZ6wmTs6i3oi4zXhN6FgGL8kXCI7JZXwC70LthEmxIZfw==} + /@cypress/vite-dev-server/3.2.0: + resolution: {integrity: sha512-tVOtSQst1yjah972nmhFue67UstxyEm69yuvcaOv9BxtI0e1hd5mM6gfLJINHXCeMmCjsoUxM382iUAr6UGu/Q==} dependencies: debug: 4.3.3 find-up: 6.3.0 From d5bc1cd1d69b0304eaf9868d576e61058ba12323 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 30 Sep 2022 13:22:46 +0200 Subject: [PATCH 020/146] chore: make amounts const --- src/helpers/defaultReminder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/defaultReminder.ts b/src/helpers/defaultReminder.ts index 98a2de84..3df722fb 100644 --- a/src/helpers/defaultReminder.ts +++ b/src/helpers/defaultReminder.ts @@ -10,7 +10,7 @@ export const AMOUNTS_IN_SECONDS: { hours: 60 * 60, days: 60 * 60 * 24, months: 60 * 60 * 24 * 30, -} +} as const interface DefaultReminderSettings { enabled: boolean, From e62395435166b132c75c4c13309b9498f19f1ece Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 30 Sep 2022 13:27:14 +0200 Subject: [PATCH 021/146] chore: better typing --- src/helpers/defaultReminder.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/helpers/defaultReminder.ts b/src/helpers/defaultReminder.ts index 3df722fb..03504611 100644 --- a/src/helpers/defaultReminder.ts +++ b/src/helpers/defaultReminder.ts @@ -1,10 +1,7 @@ const DEFAULT_REMINDER_KEY = 'defaultReminder' export const AMOUNTS_IN_SECONDS: { - minutes: number, - hours: number, - days: number, - months: number, + [type in SavedReminderSettings['type']]: number } = { minutes: 60, hours: 60 * 60, @@ -19,15 +16,15 @@ interface DefaultReminderSettings { interface SavedReminderSettings { enabled: boolean, - amount?: number, - type?: 'minutes' | 'hours' | 'days' | 'months', + amount: number, + type: 'minutes' | 'hours' | 'days' | 'months', } -function calculateDefaultReminderSeconds(type: string, amount: number): number { +function calculateDefaultReminderSeconds(type: SavedReminderSettings['type'], amount: number): number { return amount * (AMOUNTS_IN_SECONDS[type] || 0) } -export function saveDefaultReminder(enabled: boolean, type: string, amount: number) { +export function saveDefaultReminder(enabled: boolean, type: SavedReminderSettings['type'], amount: number) { const defaultReminderSeconds = calculateDefaultReminderSeconds(type, amount) localStorage.setItem(DEFAULT_REMINDER_KEY, JSON.stringify({ enabled, @@ -83,6 +80,8 @@ export function getSavedReminderSettings(): SavedReminderSettings | null { if (!s.enabled) { return { enabled: false, + type: 'minutes', + amount: 0, } } From aacd0a1331e7634a2d66582957e93a6d8b05efc6 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 30 Sep 2022 13:31:04 +0200 Subject: [PATCH 022/146] chore: clarify comment --- src/views/tasks/TaskDetailView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/tasks/TaskDetailView.vue b/src/views/tasks/TaskDetailView.vue index bbe26dc5..35a624c3 100644 --- a/src/views/tasks/TaskDetailView.vue +++ b/src/views/tasks/TaskDetailView.vue @@ -698,7 +698,7 @@ export default defineComponent({ this.task = await this.$store.dispatch('tasks/update', task) - // Activate new fields which may have been set from the api + // Show new fields set from the api or a newly set default reminder this.$nextTick(() => this.setActiveFields()) if (!showNotification) { From ad2644edf8e3c064ba20bdec4ad94aa1675480bb Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 30 Sep 2022 13:32:01 +0200 Subject: [PATCH 023/146] chore: remove unused comment --- src/views/user/settings/General.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/src/views/user/settings/General.vue b/src/views/user/settings/General.vue index bc3cffb8..97323f51 100644 --- a/src/views/user/settings/General.vue +++ b/src/views/user/settings/General.vue @@ -321,7 +321,6 @@ async function updateSettings() { const reminderSettings = getSavedReminderSettings() const defaultReminderEnabled = ref(reminderSettings?.enabled || false) -// TODO: re-populate amount and type const defaultReminderAmount = ref(reminderSettings?.amount || 1) const defaultReminderAmountType = ref(reminderSettings?.type || 'days') From cc378b83fee2b326610cdda1997cc5236f947fbf Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 28 Sep 2022 19:46:12 +0200 Subject: [PATCH 024/146] feat: automatically create subtask relations based on indention --- src/components/tasks/add-task.vue | 51 ++++++-- src/helpers/parseSubtasksViaIndention.test.ts | 109 ++++++++++++++++++ src/helpers/parseSubtasksViaIndention.ts | 42 +++++++ 3 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 src/helpers/parseSubtasksViaIndention.test.ts create mode 100644 src/helpers/parseSubtasksViaIndention.ts diff --git a/src/components/tasks/add-task.vue b/src/components/tasks/add-task.vue index 2a295386..88d1f0cf 100644 --- a/src/components/tasks/add-task.vue +++ b/src/components/tasks/add-task.vue @@ -41,18 +41,19 @@ From 13157e3bba1f81d722be5c61a9b0f709bcf11c12 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 30 Sep 2022 21:06:26 +0200 Subject: [PATCH 033/146] fix(filters): changing filter checkbox values not being emitted to parent components See https://community.vikunja.io/t/saved-filters-option-include-tasks-which-dont-have-a-value-set-is-still-set-after-saving-the-filter-despite-the-option-was-unselected/858 --- src/components/list/partials/filters.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/list/partials/filters.vue b/src/components/list/partials/filters.vue index 838f259d..df5e399e 100644 --- a/src/components/list/partials/filters.vue +++ b/src/components/list/partials/filters.vue @@ -1,7 +1,7 @@