Merge pull request #61737 from nextcloud/backport/61586/stable34
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Integration sqlite / changes (push) Waiting to run
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, --tags ~@large files_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, capabilities_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, collaboration_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, comments_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, dav_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, federation_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, file_conversions) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, files_reminders) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, filesdrop_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, guests_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, ldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, openldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, openldap_numerical_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, remoteapi_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, routing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, setup_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, sharees_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, sharing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, theming_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, videoverification_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite-summary (push) Blocked by required conditions

[stable34]  fix(settings): display birthday correctly regardless of browsers timezone
This commit is contained in:
Andy Scherzinger 2026-07-09 15:00:18 +02:00 committed by GitHub
commit cfd2e3426b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 145 additions and 18 deletions

View file

@ -0,0 +1,105 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { mount } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
let personalInfoParameters
vi.mock('@nextcloud/initial-state', () => ({
loadState(app, key, fallback) {
if (app === 'settings' && key === 'personalInfoParameters' && personalInfoParameters !== undefined) {
return personalInfoParameters
}
if (fallback !== undefined) {
return fallback
}
console.error('Unexpected loadState call without fallback', { app, key })
throw new Error()
},
}))
const savePrimaryAccountProperty = vi.hoisted(() => vi.fn())
vi.mock('../../service/PersonalInfo/PersonalInfoService.js', () => ({
savePrimaryAccountProperty,
}))
async function mountBirthdaySection() {
const BirthdaySection = await import('./BirthdaySection.vue')
return mount(BirthdaySection.default, {
mocks: {
t: (_app, text) => text,
},
})
}
afterEach(() => {
vi.unstubAllEnvs()
personalInfoParameters = undefined
vi.resetModules()
})
describe('BirthdaySection', () => {
it('saves value', async () => {
personalInfoParameters = {
birthdate: {
name: 'birthdate',
value: null,
},
}
savePrimaryAccountProperty.mockReturnValue(Promise.resolve({
ocs: { meta: { status: 'ok' } },
}))
const wrapper = await mountBirthdaySection()
const input = wrapper.find('input')
await input.setValue('1987-12-01')
await expect.poll(() => savePrimaryAccountProperty.mock.calls.length).toBe(1)
expect(savePrimaryAccountProperty).toHaveBeenCalledWith(
'birthdate',
'1987-12-01T00:00:00.000Z',
)
expect(input.element.value).toBe('1987-12-01')
})
it('displays value when browser timezone is set', async () => {
vi.stubEnv('TZ', 'US/Pacific')
personalInfoParameters = {
birthdate: {
name: 'birthdate',
value: '1987-12-15T00:00:00.000Z',
},
}
const wrapper = await mountBirthdaySection()
expect(wrapper.find('input').element.value).toBe('1987-12-15')
})
it('saves value when browser timezone is set', async () => {
vi.stubEnv('TZ', 'US/Pacific')
personalInfoParameters = {
birthdate: {
name: 'birthdate',
value: null,
},
}
savePrimaryAccountProperty.mockReturnValue(Promise.resolve({
ocs: { meta: { status: 'ok' } },
}))
const wrapper = await mountBirthdaySection()
const input = wrapper.find('input')
await input.setValue('1987-12-01')
await expect.poll(() => savePrimaryAccountProperty.mock.calls.length).toBe(1)
expect(savePrimaryAccountProperty).toHaveBeenCalledWith(
'birthdate',
'1987-12-01T00:00:00.000Z',
)
expect(input.element.value).toBe('1987-12-01')
})
})

View file

@ -13,7 +13,7 @@
:id="inputId"
type="date"
label=""
:model-value="value"
:model-value="timezoneAdjustedValue"
@input="onInput" />
<p class="property__helper-text-message">
@ -33,6 +33,16 @@ import { handleError } from '../../utils/handlers.js'
const { birthdate } = loadState('settings', 'personalInfoParameters', {})
/**
* Convert a birthdate string value into a Date.
*
* @param {string } birthdateValue - e.g. "1987-12-01" or "1987-12-01T00:00:00.000Z"
* @return {Date}
*/
function birthdateValueToDate(birthdateValue) {
return new Date(birthdateValue)
}
export default {
name: 'BirthdaySection',
@ -44,7 +54,7 @@ export default {
data() {
let initialValue = null
if (birthdate.value) {
initialValue = new Date(birthdate.value)
initialValue = birthdateValueToDate(birthdate.value)
}
return {
@ -62,24 +72,36 @@ export default {
return `account-property-${birthdate.name}`
},
value: {
get() {
return new Date(this.birthdate.value)
},
value() {
return birthdateValueToDate(this.birthdate.value)
},
/** @param {Date} value The date to set */
set(value) {
const day = value.getDate().toString().padStart(2, '0')
const month = (value.getMonth() + 1).toString().padStart(2, '0')
const year = value.getFullYear()
this.birthdate.value = `${year}-${month}-${day}`
},
/**
* Adjusted value for usage with `NcDateTimePickerNative` (internally `<input="date">`)
* The saved value is is UTC and we want to show it the same regardless of the browsers/OSs timezone.
* When the adjusted value is displayed and the users timezone is applied, this adjusted value then looks like the UTC value.
*/
timezoneAdjustedValue() {
// example: this.birthdate.value === '1987-12-01T00:00:00.000Z' or '1987-12-01'
// example: Mon Nov 30 1987 16:00:00 GMT-0800 (Pacific Standard Time)
// `NcDateTimePickerNative` would show this as 11/30/1987
const date = this.value
const timezoneOffsetMilliseconds = date.getTimezoneOffset() * 60 * 1000
const adjustedDate = new Date(date.getTime() + timezoneOffsetMilliseconds)
// example: Tue Dec 01 1987 00:00:00 GMT-0800 (Pacific Standard Time)
// `NcDateTimePickerNative` will show this as 12/01/1987
return adjustedDate
},
},
methods: {
onInput(e) {
this.value = e
const day = e.getDate().toString().padStart(2, '0')
const month = (e.getMonth() + 1).toString().padStart(2, '0')
const year = e.getFullYear()
this.birthdate.value = `${year}-${month}-${day}`
this.debouncePropertyChange(this.value)
},
@ -91,7 +113,7 @@ export default {
try {
const responseData = await savePrimaryAccountProperty(
this.birthdate.name,
value,
value.toISOString(),
)
this.handleResponse({
value,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long