Skip to content

Commit 0961dea

Browse files
committed
fix(settings): display birthday correctly regardless of browsers timezone
This also avoid shifting dates after user input. Resolves #49828 Signed-off-by: Oleksandr Dzhychko <hey@oleks.dev>
1 parent a14f248 commit 0961dea

4 files changed

Lines changed: 87 additions & 22 deletions

File tree

apps/settings/src/components/PersonalInfo/BirthdaySection.spec.js

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,17 @@ vi.mock('../../service/PersonalInfo/PersonalInfoService.js', () => ({
2626
savePrimaryAccountProperty,
2727
}))
2828

29+
async function mountBirthdaySection() {
30+
const BirthdaySection = await import('./BirthdaySection.vue')
31+
return mount(BirthdaySection.default, {
32+
mocks: {
33+
t: (_app, text) => text,
34+
},
35+
})
36+
}
37+
2938
afterEach(() => {
39+
vi.unstubAllEnvs()
3040
personalInfoParameters = undefined
3141
vi.resetModules()
3242
})
@@ -42,12 +52,45 @@ describe('BirthdaySection', () => {
4252
savePrimaryAccountProperty.mockReturnValue(Promise.resolve({
4353
ocs: { meta: { status: 'ok' } },
4454
}))
45-
const BirthdaySection = await import('./BirthdaySection.vue')
46-
const wrapper = mount(BirthdaySection.default, {
47-
mocks: {
48-
t: (_app, text) => text,
55+
const wrapper = await mountBirthdaySection()
56+
57+
const input = wrapper.find('input')
58+
await input.setValue('1987-12-01')
59+
60+
await expect.poll(() => savePrimaryAccountProperty.mock.calls.length).toBe(1)
61+
expect(savePrimaryAccountProperty).toHaveBeenCalledWith(
62+
'birthdate',
63+
'1987-12-01T00:00:00.000Z',
64+
)
65+
expect(input.element.value).toBe('1987-12-01')
66+
})
67+
68+
it('displays value when browser timezone is set', async () => {
69+
vi.stubEnv('TZ', 'US/Pacific')
70+
personalInfoParameters = {
71+
birthdate: {
72+
name: 'birthdate',
73+
value: '1987-12-15T00:00:00.000Z',
4974
},
50-
})
75+
}
76+
77+
const wrapper = await mountBirthdaySection()
78+
79+
expect(wrapper.find('input').element.value).toBe('1987-12-15')
80+
})
81+
82+
it('saves value when browser timezone is set', async () => {
83+
vi.stubEnv('TZ', 'US/Pacific')
84+
personalInfoParameters = {
85+
birthdate: {
86+
name: 'birthdate',
87+
value: null,
88+
},
89+
}
90+
savePrimaryAccountProperty.mockReturnValue(Promise.resolve({
91+
ocs: { meta: { status: 'ok' } },
92+
}))
93+
const wrapper = await mountBirthdaySection()
5194

5295
const input = wrapper.find('input')
5396
await input.setValue('1987-12-01')

apps/settings/src/components/PersonalInfo/BirthdaySection.vue

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<NcDateTimePickerNative :id="inputId"
1212
type="date"
1313
label=""
14-
:value="value"
14+
:value="timezoneAdjustedValue"
1515
@input="onInput" />
1616

1717
<p class="property__helper-text-message">
@@ -33,6 +33,16 @@ import HeaderBar from './shared/HeaderBar.vue'
3333
3434
const { birthdate } = loadState('settings', 'personalInfoParameters', {})
3535
36+
/**
37+
* Convert a birthdate string value into a Date.
38+
*
39+
* @param {string } birthdateValue - e.g. "1987-12-01" or "1987-12-01T00:00:00.000Z"
40+
* @return {Date}
41+
*/
42+
function birthdateValueToDate(birthdateValue) {
43+
return new Date(birthdateValue)
44+
}
45+
3646
export default {
3747
name: 'BirthdaySection',
3848
@@ -44,7 +54,7 @@ export default {
4454
data() {
4555
let initialValue = null
4656
if (birthdate.value) {
47-
initialValue = new Date(birthdate.value)
57+
initialValue = birthdateValueToDate(birthdate.value)
4858
}
4959
5060
return {
@@ -60,23 +70,35 @@ export default {
6070
inputId() {
6171
return `account-property-${birthdate.name}`
6272
},
63-
value: {
64-
get() {
65-
return new Date(this.birthdate.value)
66-
},
67-
/** @param {Date} value The date to set */
68-
set(value) {
69-
const day = value.getDate().toString().padStart(2, '0')
70-
const month = (value.getMonth() + 1).toString().padStart(2, '0')
71-
const year = value.getFullYear()
72-
this.birthdate.value = `${year}-${month}-${day}`
73-
},
73+
value() {
74+
return birthdateValueToDate(this.birthdate.value)
75+
},
76+
/**
77+
* Adjusted value for usage with `NcDateTimePickerNative` (internally `<input="date">`)
78+
* The saved value is is UTC and we want to show it the same regardless of the browsers/OSs timezone.
79+
* When the adjusted value is displayed and the users timezone is applied, this adjusted value then looks like the UTC value.
80+
*/
81+
timezoneAdjustedValue() {
82+
// example: this.birthdate.value === '1987-12-01T00:00:00.000Z' or '1987-12-01'
83+
84+
// example: Mon Nov 30 1987 16:00:00 GMT-0800 (Pacific Standard Time)
85+
// `NcDateTimePickerNative` would show this as 11/30/1987
86+
const date = this.value
87+
const timezoneOffsetMilliseconds = date.getTimezoneOffset() * 60 * 1000
88+
const adjustedDate = new Date(date.getTime() + timezoneOffsetMilliseconds)
89+
90+
// example: Tue Dec 01 1987 00:00:00 GMT-0800 (Pacific Standard Time)
91+
// `NcDateTimePickerNative` will show this as 12/01/1987
92+
return adjustedDate
7493
},
7594
},
7695
7796
methods: {
7897
onInput(e) {
79-
this.value = e
98+
const day = e.getDate().toString().padStart(2, '0')
99+
const month = (e.getMonth() + 1).toString().padStart(2, '0')
100+
const year = e.getFullYear()
101+
this.birthdate.value = `${year}-${month}-${day}`
80102
this.debouncePropertyChange(this.value)
81103
},
82104

dist/settings-vue-settings-personal-info.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/settings-vue-settings-personal-info.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)