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:
parent
b38780e246
commit
650fb94978
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
})
|
||||
|
|
@ -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]
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue