feat: add time display with configurable format (12h/24h) to non-relative date formats (#1807)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kolaente <k@knt.li>
This commit is contained in:
Copilot 2025-11-13 08:57:06 +00:00 committed by GitHub
parent b38780e246
commit 650fb94978
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 140 additions and 22 deletions

View File

@ -3,6 +3,7 @@ import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task'
import {login} from '../../support/authenticateUser'
import {DATE_DISPLAY} from '../../../src/constants/dateDisplay'
import {TIME_FORMAT} from '../../../src/constants/timeFormat'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
@ -13,30 +14,85 @@ const now = new Date(Date.UTC(2022, 6, 30, 12))
const expectedFormats = {
[DATE_DISPLAY.RELATIVE]: dayjs(createdDate).from(now),
[DATE_DISPLAY.MM_DD_YYYY]: dayjs(createdDate).format('MM-DD-YYYY'),
[DATE_DISPLAY.DD_MM_YYYY]: dayjs(createdDate).format('DD-MM-YYYY'),
[DATE_DISPLAY.YYYY_MM_DD]: dayjs(createdDate).format('YYYY-MM-DD'),
[DATE_DISPLAY.MM_SLASH_DD_YYYY]: dayjs(createdDate).format('MM/DD/YYYY'),
[DATE_DISPLAY.DD_SLASH_MM_YYYY]: dayjs(createdDate).format('DD/MM/YYYY'),
[DATE_DISPLAY.YYYY_SLASH_MM_DD]: dayjs(createdDate).format('YYYY/MM/DD'),
[DATE_DISPLAY.MM_DD_YYYY]: dayjs(createdDate).format('MM-DD-YYYY hh:mm A'),
[DATE_DISPLAY.DD_MM_YYYY]: dayjs(createdDate).format('DD-MM-YYYY hh:mm A'),
[DATE_DISPLAY.YYYY_MM_DD]: dayjs(createdDate).format('YYYY-MM-DD hh:mm A'),
[DATE_DISPLAY.MM_SLASH_DD_YYYY]: dayjs(createdDate).format('MM/DD/YYYY hh:mm A'),
[DATE_DISPLAY.DD_SLASH_MM_YYYY]: dayjs(createdDate).format('DD/MM/YYYY hh:mm A'),
[DATE_DISPLAY.YYYY_SLASH_MM_DD]: dayjs(createdDate).format('YYYY/MM/DD hh:mm A'),
[DATE_DISPLAY.DAY_MONTH_YEAR]: new Intl.DateTimeFormat('en', {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true,
}).format(createdDate),
[DATE_DISPLAY.WEEKDAY_DAY_MONTH_YEAR]: new Intl.DateTimeFormat('en', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true,
}).format(createdDate),
}
const expectedFormats24h = {
[DATE_DISPLAY.RELATIVE]: dayjs(createdDate).from(now),
[DATE_DISPLAY.MM_DD_YYYY]: dayjs(createdDate).format('MM-DD-YYYY HH:mm'),
[DATE_DISPLAY.DD_MM_YYYY]: dayjs(createdDate).format('DD-MM-YYYY HH:mm'),
[DATE_DISPLAY.YYYY_MM_DD]: dayjs(createdDate).format('YYYY-MM-DD HH:mm'),
[DATE_DISPLAY.MM_SLASH_DD_YYYY]: dayjs(createdDate).format('MM/DD/YYYY HH:mm'),
[DATE_DISPLAY.DD_SLASH_MM_YYYY]: dayjs(createdDate).format('DD/MM/YYYY HH:mm'),
[DATE_DISPLAY.YYYY_SLASH_MM_DD]: dayjs(createdDate).format('YYYY/MM/DD HH:mm'),
[DATE_DISPLAY.DAY_MONTH_YEAR]: new Intl.DateTimeFormat('en', {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: false,
}).format(createdDate),
[DATE_DISPLAY.WEEKDAY_DAY_MONTH_YEAR]: new Intl.DateTimeFormat('en', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: false,
}).format(createdDate),
}
describe('Date display setting', () => {
Object.entries(expectedFormats).forEach(([format, expected]) => {
it(`shows ${format}`, () => {
it(`shows ${format} with 12h time format`, () => {
const user = UserFactory.create(1, {
frontend_settings: JSON.stringify({dateDisplay: format}),
frontend_settings: JSON.stringify({dateDisplay: format, timeFormat: TIME_FORMAT.HOURS_12}),
})[0]
const project = ProjectFactory.create(1, {owner_id: user.id})[0]
TaskFactory.truncate()
const task = TaskFactory.create(1, {
id: 1,
project_id: project.id,
created_by_id: user.id,
created: createdDate.toISOString(),
updated: createdDate.toISOString(),
})[0]
cy.clock(now, ['Date'])
login(user)
cy.visit(`/tasks/${task.id}`)
cy.get('.task-view .created time span').should('contain', expected)
})
})
Object.entries(expectedFormats24h).forEach(([format, expected]) => {
it(`shows ${format} with 24h time format`, () => {
const user = UserFactory.create(1, {
frontend_settings: JSON.stringify({dateDisplay: format, timeFormat: TIME_FORMAT.HOURS_24}),
})[0]
const project = ProjectFactory.create(1, {owner_id: user.id})[0]
TaskFactory.truncate()

View File

@ -0,0 +1,9 @@
import {computed} from 'vue'
import {createSharedComposable} from '@vueuse/core'
import {useAuthStore} from '@/stores/auth'
export const useTimeFormat = createSharedComposable(() => {
const authStore = useAuthStore()
const store = computed(() => authStore.settings.frontendSettings.timeFormat)
return {store}
})

View File

@ -0,0 +1,6 @@
export const TIME_FORMAT = {
HOURS_12: '12h',
HOURS_24: '24h',
} as const
export type TimeFormat = typeof TIME_FORMAT[keyof typeof TIME_FORMAT]

View File

@ -5,7 +5,9 @@ import {i18n} from '@/i18n'
import {createSharedComposable} from '@vueuse/core'
import {computed, toValue, type MaybeRefOrGetter} from 'vue'
import {useDateDisplay} from '@/composables/useDateDisplay'
import {useTimeFormat} from '@/composables/useTimeFormat'
import {DATE_DISPLAY, type DateDisplay} from '@/constants/dateDisplay'
import {TIME_FORMAT, type TimeFormat} from '@/constants/timeFormat'
import {DAYJS_LOCALE_MAPPING} from '@/i18n/useDayjsLanguageSync.ts'
export function dateIsValid(date: Date | null) {
@ -71,13 +73,13 @@ export function useWeekDayFromDate() {
}
export function formatDisplayDate(date: Date | string | null) {
const {store} = useDateDisplay()
const current = store.value
const {store: dateDisplay} = useDateDisplay()
const {store: timeFormat} = useTimeFormat()
return formatDisplayDateFormat(date, current)
return formatDisplayDateFormat(date, dateDisplay.value, timeFormat.value)
}
export function formatDisplayDateFormat(date: Date | string | null, format: DateDisplay) {
export function formatDisplayDateFormat(date: Date | string | null, format: DateDisplay, timeFormat?: TimeFormat) {
if (typeof date === 'string') {
date = createDateFromString(date)
}
@ -86,24 +88,31 @@ export function formatDisplayDateFormat(date: Date | string | null, format: Date
return ''
}
// Determine the time format string to use
// For 24-hour: HH:mm (24-hour format)
// For 12-hour: hh:mm A (explicit 12-hour format with AM/PM, ignoring locale default)
const timeFormatString = timeFormat === TIME_FORMAT.HOURS_24 ? 'HH:mm' : 'hh:mm A'
switch (format) {
case DATE_DISPLAY.MM_DD_YYYY:
return formatDate(date, 'MM-DD-YYYY')
return formatDate(date, `MM-DD-YYYY ${timeFormatString}`)
case DATE_DISPLAY.DD_MM_YYYY:
return formatDate(date, 'DD-MM-YYYY')
return formatDate(date, `DD-MM-YYYY ${timeFormatString}`)
case DATE_DISPLAY.YYYY_MM_DD:
return formatDate(date, 'YYYY-MM-DD')
return formatDate(date, `YYYY-MM-DD ${timeFormatString}`)
case DATE_DISPLAY.MM_SLASH_DD_YYYY:
return formatDate(date, 'MM/DD/YYYY')
return formatDate(date, `MM/DD/YYYY ${timeFormatString}`)
case DATE_DISPLAY.DD_SLASH_MM_YYYY:
return formatDate(date, 'DD/MM/YYYY')
return formatDate(date, `DD/MM/YYYY ${timeFormatString}`)
case DATE_DISPLAY.YYYY_SLASH_MM_DD:
return formatDate(date, 'YYYY/MM/DD')
return formatDate(date, `YYYY/MM/DD ${timeFormatString}`)
case DATE_DISPLAY.DAY_MONTH_YEAR: {
return new Intl.DateTimeFormat(i18n.global.locale.value, {day: 'numeric', month: 'long', year: 'numeric'}).format(date)
const hour12 = timeFormat !== TIME_FORMAT.HOURS_24
return new Intl.DateTimeFormat(i18n.global.locale.value, {day: 'numeric', month: 'long', year: 'numeric', hour: 'numeric', minute: 'numeric', hour12}).format(date)
}
case DATE_DISPLAY.WEEKDAY_DAY_MONTH_YEAR: {
return new Intl.DateTimeFormat(i18n.global.locale.value, {weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'}).format(date)
const hour12 = timeFormat !== TIME_FORMAT.HOURS_24
return new Intl.DateTimeFormat(i18n.global.locale.value, {weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', hour: 'numeric', minute: 'numeric', hour12}).format(date)
}
case DATE_DISPLAY.RELATIVE:
default:

View File

@ -114,6 +114,11 @@
"dd\/mm\/yyyy": "DD\/MM\/YYYY",
"yyyy\/mm\/dd": "YYYY\/MM\/DD"
},
"timeFormat": "Time format",
"timeFormatOptions": {
"12h": "12-hour (AM/PM)",
"24h": "24-hour (HH:mm)"
},
"externalUserNameChange": "Your name is managed by your login provider ({provider}). To change it, please update it there instead."
},
"sections": {

View File

@ -7,6 +7,7 @@ import type {SupportedLocale} from '@/i18n'
import type {DefaultProjectViewKind} from '@/modelTypes/IProjectView'
import type {Priority} from '@/constants/priorities'
import type {DateDisplay} from '@/constants/dateDisplay'
import type {TimeFormat} from '@/constants/timeFormat'
import type {IRelationKind} from '@/types/IRelationKind'
export interface IFrontendSettings {
@ -18,6 +19,7 @@ export interface IFrontendSettings {
defaultView?: DefaultProjectViewKind
minimumPriority?: Priority
dateDisplay: DateDisplay
timeFormat: TimeFormat
defaultTaskRelationType: IRelationKind
}

View File

@ -6,6 +6,7 @@ import {PrefixMode} from '@/modules/parseTaskText'
import {DEFAULT_PROJECT_VIEW_SETTINGS} from '@/modelTypes/IProjectView'
import {PRIORITIES} from '@/constants/priorities'
import {DATE_DISPLAY} from '@/constants/dateDisplay'
import {TIME_FORMAT} from '@/constants/timeFormat'
import {RELATION_KIND} from '@/types/IRelationKind'
export default class UserSettingsModel extends AbstractModel<IUserSettings> implements IUserSettings {
@ -27,6 +28,7 @@ export default class UserSettingsModel extends AbstractModel<IUserSettings> impl
defaultView: DEFAULT_PROJECT_VIEW_SETTINGS.FIRST,
minimumPriority: PRIORITIES.MEDIUM,
dateDisplay: DATE_DISPLAY.RELATIVE,
timeFormat: TIME_FORMAT.HOURS_24,
defaultTaskRelationType: RELATION_KIND.RELATED,
}
extraSettingsLinks = {}

View File

@ -23,6 +23,7 @@ import UserSettingsModel from '@/models/userSettings'
import {MILLISECONDS_A_SECOND} from '@/constants/date'
import {PrefixMode} from '@/modules/parseTaskText'
import {DATE_DISPLAY} from '@/constants/dateDisplay'
import {TIME_FORMAT} from '@/constants/timeFormat'
import {RELATION_KIND} from '@/types/IRelationKind'
import type {IProvider} from '@/types/IProvider'
@ -134,6 +135,7 @@ export const useAuthStore = defineStore('auth', () => {
colorSchema: 'auto',
allowIconChanges: true,
dateDisplay: DATE_DISPLAY.RELATIVE,
timeFormat: TIME_FORMAT.HOURS_24,
defaultTaskRelationType: RELATION_KIND.RELATED,
...newSettings.frontendSettings,
},

View File

@ -214,6 +214,25 @@
</div>
</label>
</div>
<div
v-if="settings.frontendSettings.dateDisplay !== 'relative'"
class="field"
>
<label class="two-col">
<span>
{{ $t('user.settings.general.timeFormat') }}
</span>
<div class="select">
<select v-model="settings.frontendSettings.timeFormat">
<option
v-for="(label, value) in timeFormatSettings"
:key="value"
:value="value"
>{{ label }}</option>
</select>
</div>
</label>
</div>
</div>
</Card>
@ -366,6 +385,7 @@ import {isSavedFilter} from '@/services/savedFilter'
import {DEFAULT_PROJECT_VIEW_SETTINGS} from '@/modelTypes/IProjectView'
import {PRIORITIES} from '@/constants/priorities'
import {DATE_DISPLAY} from '@/constants/dateDisplay'
import {TIME_FORMAT} from '@/constants/timeFormat'
import {RELATION_KINDS} from '@/types/IRelationKind'
defineOptions({name: 'UserSettingsGeneral'})
@ -389,8 +409,13 @@ const dateDisplaySettings = computed(() => ({
[DATE_DISPLAY.MM_SLASH_DD_YYYY]: t('user.settings.general.dateDisplayOptions.mm/dd/yyyy'),
[DATE_DISPLAY.DD_SLASH_MM_YYYY]: t('user.settings.general.dateDisplayOptions.dd/mm/yyyy'),
[DATE_DISPLAY.YYYY_SLASH_MM_DD]: t('user.settings.general.dateDisplayOptions.yyyy/mm/dd'),
[DATE_DISPLAY.DAY_MONTH_YEAR]: formatDisplayDateFormat(new Date(), DATE_DISPLAY.DAY_MONTH_YEAR),
[DATE_DISPLAY.WEEKDAY_DAY_MONTH_YEAR]: formatDisplayDateFormat(new Date(), DATE_DISPLAY.WEEKDAY_DAY_MONTH_YEAR),
[DATE_DISPLAY.DAY_MONTH_YEAR]: formatDisplayDateFormat(new Date(), DATE_DISPLAY.DAY_MONTH_YEAR, settings.value?.frontendSettings?.timeFormat),
[DATE_DISPLAY.WEEKDAY_DAY_MONTH_YEAR]: formatDisplayDateFormat(new Date(), DATE_DISPLAY.WEEKDAY_DAY_MONTH_YEAR, settings.value?.frontendSettings?.timeFormat),
}))
const timeFormatSettings = computed(() => ({
[TIME_FORMAT.HOURS_12]: t('user.settings.general.timeFormatOptions.12h'),
[TIME_FORMAT.HOURS_24]: t('user.settings.general.timeFormatOptions.24h'),
}))
const authStore = useAuthStore()
@ -408,6 +433,8 @@ const settings = ref<IUserSettings>({
// Add fallback for old settings that don't have the logo change setting set
allowIconChanges: authStore.settings.frontendSettings.allowIconChanges ?? true,
dateDisplay: authStore.settings.frontendSettings.dateDisplay ?? DATE_DISPLAY.RELATIVE,
// Add fallback for old settings that don't have the time format set
timeFormat: authStore.settings.frontendSettings.timeFormat ?? TIME_FORMAT.HOURS_12,
// Add fallback for old settings that don't have the default task relation type set
defaultTaskRelationType: authStore.settings.frontendSettings.defaultTaskRelationType ?? 'related',
},