mirror of
https://github.com/nextcloud/server.git
synced 2026-07-10 02:01:38 -04:00
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
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:
commit
cfd2e3426b
4 changed files with 145 additions and 18 deletions
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
4
dist/settings-vue-settings-personal-info.js
vendored
4
dist/settings-vue-settings-personal-info.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue