From 021d71b90e62237b1faf6d8eac51f0429cf8586f Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 21 Feb 2025 15:42:58 +0000 Subject: [PATCH] fix: remove date-fns (#3039) This removes date-fns and replaces it with the already used dayjs library. It does not make sense to have two libraries for the same purpose, and dayjs seems to be smaller and its translations are already integrated. Since we have to use dayjs because it is used by the gantt chart, this was the obvious way to go (instead of replacing dayjs with date-fns). Resolves https://github.com/go-vikunja/vikunja/issues/391 Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/3039 Co-authored-by: kolaente Co-committed-by: kolaente --- .../e2e/project/project-view-gantt.spec.ts | 10 +++---- frontend/cypress/e2e/task/task.spec.ts | 6 ++--- frontend/package.json | 1 - frontend/pnpm-lock.yaml | 8 ------ .../src/components/input/DatepickerInline.vue | 6 ++--- frontend/src/components/tasks/GanttChart.vue | 5 ++-- frontend/src/constants/date.ts | 2 -- frontend/src/helpers/time/formatDate.ts | 26 ++++++++----------- frontend/src/helpers/time/parseKebabDate.ts | 7 ----- frontend/src/i18n/index.ts | 10 +++++++ frontend/src/i18n/useDayjsLanguageSync.ts | 10 ++++++- frontend/src/types/DateKebab.ts | 4 --- frontend/src/views/tasks/ShowTasks.vue | 4 +-- 13 files changed, 45 insertions(+), 54 deletions(-) delete mode 100644 frontend/src/helpers/time/parseKebabDate.ts delete mode 100644 frontend/src/types/DateKebab.ts diff --git a/frontend/cypress/e2e/project/project-view-gantt.spec.ts b/frontend/cypress/e2e/project/project-view-gantt.spec.ts index f8e3f84f7..16eadab20 100644 --- a/frontend/cypress/e2e/project/project-view-gantt.spec.ts +++ b/frontend/cypress/e2e/project/project-view-gantt.spec.ts @@ -1,4 +1,4 @@ -import {formatISO, format} from 'date-fns' +import dayjs from 'dayjs' import {createFakeUserAndLogin} from '../../support/authenticateUser' @@ -28,8 +28,8 @@ describe('Project View Gantt', () => { cy.visit('/projects/1/2') cy.get('.g-timeunits-container') - .should('contain', format(now, 'MMMM')) - .should('contain', format(nextMonth, 'MMMM')) + .should('contain', dayjs(now).format('MMMM')) + .should('contain', dayjs(nextMonth).format('MMMM')) }) it('Shows tasks with dates', () => { @@ -112,8 +112,8 @@ describe('Project View Gantt', () => { it('Should open a task when double clicked on it', () => { const now = new Date() const tasks = TaskFactory.create(1, { - start_date: formatISO(now), - end_date: formatISO(now.setDate(now.getDate() + 4)), + start_date: dayjs(now).format(), + end_date: dayjs(now.setDate(now.getDate() + 4)).format(), }) cy.visit('/projects/1/2') diff --git a/frontend/cypress/e2e/task/task.spec.ts b/frontend/cypress/e2e/task/task.spec.ts index d84935d68..5040cc49c 100644 --- a/frontend/cypress/e2e/task/task.spec.ts +++ b/frontend/cypress/e2e/task/task.spec.ts @@ -12,7 +12,7 @@ import {BucketFactory} from '../../factories/bucket' import {TaskAttachmentFactory} from '../../factories/task_attachments' import {TaskReminderFactory} from '../../factories/task_reminders' -import {createDefaultViews} from "../project/prepareProjects"; +import {createDefaultViews} from '../project/prepareProjects' import { TaskBucketFactory } from '../../factories/task_buckets' function addLabelToTaskAndVerify(labelTitle: string) { @@ -572,7 +572,7 @@ describe('Task', () => { const day = today.toLocaleString('default', {day: 'numeric'}) const month = today.toLocaleString('default', {month: 'short'}) const year = today.toLocaleString('default', {year: 'numeric'}) - const date = `${day} ${month} ${year}, 12:00:00` + const date = `${month} ${day}, ${year} 12:00 PM` cy.get('.task-view .columns.details .column') .contains('Due Date') .get('.date-input .datepicker-popup') @@ -615,7 +615,7 @@ describe('Task', () => { const day = today.toLocaleString('default', {day: 'numeric'}) const month = today.toLocaleString('default', {month: 'short'}) const year = today.toLocaleString('default', {year: 'numeric'}) - const date = `${day} ${month} ${year}, 12:00:00` + const date = `${month} ${day}, ${year} 12:00 PM` cy.get('.task-view .columns.details .column') .contains('Due Date') .get('.date-input .datepicker-popup') diff --git a/frontend/package.json b/frontend/package.json index cab43bb20..b9655d987 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -84,7 +84,6 @@ "blurhash": "2.0.5", "bulma-css-variables": "0.9.33", "change-case": "5.4.4", - "date-fns": "4.1.0", "dayjs": "1.11.13", "dompurify": "3.2.3", "fast-deep-equal": "3.1.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 05d2d5cd1..9c2938fd8 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -121,9 +121,6 @@ importers: change-case: specifier: 5.4.4 version: 5.4.4 - date-fns: - specifier: 4.1.0 - version: 4.1.0 dayjs: specifier: 1.11.13 version: 1.11.13 @@ -3430,9 +3427,6 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} - date-fns@4.1.0: - resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} - dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} @@ -10224,8 +10218,6 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 - date-fns@4.1.0: {} - dayjs@1.11.13: {} de-indent@1.0.2: {} diff --git a/frontend/src/components/input/DatepickerInline.vue b/frontend/src/components/input/DatepickerInline.vue index bee2f3e85..ecd152943 100644 --- a/frontend/src/components/input/DatepickerInline.vue +++ b/frontend/src/components/input/DatepickerInline.vue @@ -127,7 +127,7 @@ const flatPickrDate = computed({ } if (date.value !== null) { - const oldDate = formatDate(date.value, 'yyy-LL-dd H:mm') + const oldDate = formatDate(date.value, 'YYYY-MM-DD h:m') if (oldDate === newValue) { return } @@ -140,7 +140,7 @@ const flatPickrDate = computed({ return '' } - return formatDate(date.value, 'yyy-LL-dd H:mm') + return formatDate(date.value, 'YYYY-MM-DD h:m') }, }) @@ -208,7 +208,7 @@ function getWeekdayFromStringInterval(dateString: string) { const interval = calculateDayInterval(dateString) const newDate = new Date() newDate.setDate(newDate.getDate() + interval) - return formatDate(newDate, 'E') + return formatDate(newDate, 'ddd') } diff --git a/frontend/src/components/tasks/GanttChart.vue b/frontend/src/components/tasks/GanttChart.vue index cf13335a5..842c0798e 100644 --- a/frontend/src/components/tasks/GanttChart.vue +++ b/frontend/src/components/tasks/GanttChart.vue @@ -52,7 +52,6 @@ import {getHexColor} from '@/models/task' import {colorIsDark} from '@/helpers/color/colorIsDark' import {isoToKebabDate} from '@/helpers/time/isoToKebabDate' -import {parseKebabDate} from '@/helpers/time/parseKebabDate' import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask' import type {DateISO} from '@/types/DateISO' @@ -181,8 +180,8 @@ async function updateGanttTask(e: { }) { emit('update:task', { id: Number(e.bar.ganttBarConfig.id), - startDate: new Date(parseKebabDate(e.bar.startDate).setHours(0,0,0,0)), - endDate: new Date(parseKebabDate(e.bar.endDate).setHours(23,59,0,0)), + startDate: new Date((new Date(e.bar.startDate)).setHours(0,0,0,0)), + endDate: new Date((new Date(e.bar.endDate)).setHours(23,59,0,0)), }) } diff --git a/frontend/src/constants/date.ts b/frontend/src/constants/date.ts index a47b0c391..23820dba2 100644 --- a/frontend/src/constants/date.ts +++ b/frontend/src/constants/date.ts @@ -1,5 +1,3 @@ -export const DATEFNS_DATE_FORMAT_KEBAB = 'yyyy-LL-dd' as const - export const SECONDS_A_MINUTE = 60 export const SECONDS_A_HOUR = SECONDS_A_MINUTE * 60 export const SECONDS_A_DAY = SECONDS_A_HOUR * 24 diff --git a/frontend/src/helpers/time/formatDate.ts b/frontend/src/helpers/time/formatDate.ts index 69408caf4..dbe73de85 100644 --- a/frontend/src/helpers/time/formatDate.ts +++ b/frontend/src/helpers/time/formatDate.ts @@ -1,15 +1,10 @@ import {createDateFromString} from '@/helpers/time/createDateFromString' -import {format, formatDistanceToNow} from 'date-fns' - -// FIXME: support all locales and load dynamically -import {enGB, de, fr, ru} from 'date-fns/locale' +import dayjs from 'dayjs' import {i18n} from '@/i18n' import {createSharedComposable} from '@vueuse/core' import {computed, toValue, type MaybeRefOrGetter} from 'vue' -const locales = {en: enGB, de, ch: de, fr, ru} - export function dateIsValid(date: Date | null) { if (date === null) { return false @@ -18,35 +13,36 @@ export function dateIsValid(date: Date | null) { return date instanceof Date && !isNaN(date) } -export const formatDate = (date, f, locale = i18n.global.t('date.locale')) => { +export const formatDate = (date: Date | string | null, f: string, locale = i18n.global.t('date.locale')) => { if (!dateIsValid(date)) { return '' } date = createDateFromString(date) - return date ? format(date, f, {locale: locales[locale]}) : '' + return date + ? dayjs(date).locale(locale).format(f) + : '' } export function formatDateLong(date) { - return formatDate(date, 'PPPPpppp') + return formatDate(date, 'LLLL') } export function formatDateShort(date) { - return formatDate(date, 'PPpp') + return formatDate(date, 'lll') } -export const formatDateSince = (date) => { +export const formatDateSince = (date: Date | string | null, locale = i18n.global.t('date.locale')) => { if (!dateIsValid(date)) { return '' } date = createDateFromString(date) - return formatDistanceToNow(date, { - locale: locales[i18n.global.t('date.locale')], - addSuffix: true, - }) + return date + ? dayjs(date).locale(locale).fromNow() + : '' } export function formatISO(date) { diff --git a/frontend/src/helpers/time/parseKebabDate.ts b/frontend/src/helpers/time/parseKebabDate.ts deleted file mode 100644 index f1643aa48..000000000 --- a/frontend/src/helpers/time/parseKebabDate.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {parse} from 'date-fns' -import {DATEFNS_DATE_FORMAT_KEBAB} from '@/constants/date' -import type {DateKebab} from '@/types/DateKebab' - -export function parseKebabDate(date: DateKebab): Date { - return parse(date, DATEFNS_DATE_FORMAT_KEBAB, new Date()) -} \ No newline at end of file diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index c0c8f26d5..cf54bee99 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -2,6 +2,14 @@ import {createI18n} from 'vue-i18n' import type {PluralizationRule} from 'vue-i18n' import langEN from './lang/en.json' +import localizedFormat from 'dayjs/plugin/localizedFormat' +import relativeTime from 'dayjs/plugin/relativeTime' +import dayjs from 'dayjs' +import {loadDayJsLocale} from '@/i18n/useDayjsLanguageSync.ts' + +dayjs.extend(localizedFormat) +dayjs.extend(relativeTime) + export const SUPPORTED_LOCALES = { 'en': 'English', 'de-DE': 'Deutsch', @@ -85,6 +93,8 @@ export async function setLanguage(lang: SupportedLocale): Promise import('dayjs/locale/ko'), } as Record Promise> +export async function loadDayJsLocale(language: SupportedLocale) { + if (language === 'en') { + return + } + + await DAYJS_LANGUAGE_IMPORTS[language.toLowerCase()]() +} + export function useDayjsLanguageSync(dayjsGlobal: typeof dayjs) { const dayjsLanguageLoaded = ref(false) @@ -70,7 +78,7 @@ export function useDayjsLanguageSync(dayjsGlobal: typeof dayjs) { if (dayjsLanguageLoaded.value) { return } - await DAYJS_LANGUAGE_IMPORTS[currentLanguage.toLowerCase()]() + await loadDayJsLocale(currentLanguage) dayjsGlobal.locale(dayjsLanguageCode) dayjsLanguageLoaded.value = true }, diff --git a/frontend/src/types/DateKebab.ts b/frontend/src/types/DateKebab.ts deleted file mode 100644 index bdc1808df..000000000 --- a/frontend/src/types/DateKebab.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** -* Date in Format 2022-12-10 -*/ -export type DateKebab = `${string}-${string}-${string}` diff --git a/frontend/src/views/tasks/ShowTasks.vue b/frontend/src/views/tasks/ShowTasks.vue index 4915d965d..6ef71bcb1 100644 --- a/frontend/src/views/tasks/ShowTasks.vue +++ b/frontend/src/views/tasks/ShowTasks.vue @@ -130,8 +130,8 @@ const pageTitle = computed(() => { return showAll.value ? t('task.show.titleCurrent') : t('task.show.fromuntil', { - from: formatDate(props.dateFrom, 'PPP'), - until: formatDate(props.dateTo, 'PPP'), + from: formatDate(props.dateFrom, 'LL'), + until: formatDate(props.dateTo, 'LL'), }) }) const hasTasks = computed(() => tasks.value && tasks.value.length > 0)