Commit 4840ed7f authored by peterhegman's avatar peterhegman

Remove default value for time zone user preference

This allows us to only display local time on user profiles if a user
has explicitly set their time zone. This commit also allows users to
clear their time zone user preference so local time is not shown on
their profile.

Changelog: changed
parent c1089d13
...@@ -17,7 +17,13 @@ export const formatUtcOffset = (offset) => { ...@@ -17,7 +17,13 @@ export const formatUtcOffset = (offset) => {
return `${prefix} ${Math.abs(offset / 3600)}`; return `${prefix} ${Math.abs(offset / 3600)}`;
}; };
export const formatTimezone = (item) => `[UTC ${formatUtcOffset(item.offset)}] ${item.name}`; export const formatTimezone = (item) => {
if (Object.prototype.hasOwnProperty.call(item, 'offset')) {
return `[UTC ${formatUtcOffset(item.offset)}] ${item.name}`;
}
return item.name;
};
export const findTimezoneByIdentifier = (tzList = [], identifier = null) => { export const findTimezoneByIdentifier = (tzList = [], identifier = null) => {
if (tzList && tzList.length && identifier && identifier.length) { if (tzList && tzList.length && identifier && identifier.length) {
...@@ -27,19 +33,31 @@ export const findTimezoneByIdentifier = (tzList = [], identifier = null) => { ...@@ -27,19 +33,31 @@ export const findTimezoneByIdentifier = (tzList = [], identifier = null) => {
}; };
export default class TimezoneDropdown { export default class TimezoneDropdown {
constructor({ $dropdownEl, $inputEl, onSelectTimezone, displayFormat } = defaults) { constructor({
$dropdownEl,
$inputEl,
onSelectTimezone,
displayFormat,
allowEmpty = false,
} = defaults) {
this.$dropdown = $dropdownEl; this.$dropdown = $dropdownEl;
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text'); this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
this.$input = $inputEl; this.$input = $inputEl;
this.timezoneData = this.$dropdown.data('data'); this.timezoneData = this.$dropdown.data('data') || [];
if (allowEmpty) {
this.timezoneData = [
{
name: this.$dropdownToggle.text(),
},
...this.timezoneData,
];
}
this.onSelectTimezone = onSelectTimezone; this.onSelectTimezone = onSelectTimezone;
this.displayFormat = displayFormat || defaults.displayFormat; this.displayFormat = displayFormat || defaults.displayFormat;
this.allowEmpty = allowEmpty;
this.initialTimezone =
findTimezoneByIdentifier(this.timezoneData, this.$input.val()) || defaultTimezone;
this.initDefaultTimezone();
this.initDropdown(); this.initDropdown();
} }
...@@ -52,24 +70,25 @@ export default class TimezoneDropdown { ...@@ -52,24 +70,25 @@ export default class TimezoneDropdown {
search: { search: {
fields: ['name'], fields: ['name'],
}, },
clicked: (cfg) => this.updateInputValue(cfg), clicked: (cfg) => this.handleDropdownChange(cfg),
text: (item) => formatTimezone(item), text: (item) => formatTimezone(item),
}); });
this.setDropdownToggle(this.displayFormat(this.initialTimezone)); const initialTimezone = findTimezoneByIdentifier(this.timezoneData, this.$input.val());
}
initDefaultTimezone() { if (initialTimezone !== null) {
if (!this.$input.val()) { this.setDropdownValue(initialTimezone);
this.$input.val(defaultTimezone.name); } else if (!this.allowEmpty) {
this.setDropdownValue(defaultTimezone);
} }
} }
setDropdownToggle(dropdownText) { setDropdownValue(timezone) {
this.$dropdownToggle.text(dropdownText); this.$dropdownToggle.text(this.displayFormat(timezone));
this.$input.val(timezone.name);
} }
updateInputValue({ selectedObj, e }) { handleDropdownChange({ selectedObj, e }) {
e.preventDefault(); e.preventDefault();
this.$input.val(selectedObj.identifier); this.$input.val(selectedObj.identifier);
if (this.onSelectTimezone) { if (this.onSelectTimezone) {
......
...@@ -21,6 +21,7 @@ export default class Profile { ...@@ -21,6 +21,7 @@ export default class Profile {
$inputEl: this.$inputEl, $inputEl: this.$inputEl,
$dropdownEl: $('.js-timezone-dropdown'), $dropdownEl: $('.js-timezone-dropdown'),
displayFormat: (selectedItem) => formatTimezone(selectedItem), displayFormat: (selectedItem) => formatTimezone(selectedItem),
allowEmpty: true,
}); });
} }
......
...@@ -33,6 +33,8 @@ module TimeZoneHelper ...@@ -33,6 +33,8 @@ module TimeZoneHelper
end end
def local_time(timezone) def local_time(timezone)
return if timezone.blank?
time_zone_instance = ActiveSupport::TimeZone.new(timezone) || Time.zone time_zone_instance = ActiveSupport::TimeZone.new(timezone) || Time.zone
time_zone_instance.now.strftime("%-l:%M %p") time_zone_instance.now.strftime("%-l:%M %p")
end end
......
...@@ -23,7 +23,6 @@ class UserPreference < ApplicationRecord ...@@ -23,7 +23,6 @@ class UserPreference < ApplicationRecord
ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22' ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
default_value_for :tab_width, value: Gitlab::TabWidth::DEFAULT, allows_nil: false default_value_for :tab_width, value: Gitlab::TabWidth::DEFAULT, allows_nil: false
default_value_for :timezone, value: Time.zone.tzinfo.name, allows_nil: false
default_value_for :time_display_relative, value: true, allows_nil: false default_value_for :time_display_relative, value: true, allows_nil: false
default_value_for :time_format_in_24h, value: false, allows_nil: false default_value_for :time_format_in_24h, value: false, allows_nil: false
default_value_for :render_whitespace_in_code, value: false, allows_nil: false default_value_for :render_whitespace_in_code, value: false, allows_nil: false
......
...@@ -80,7 +80,7 @@ ...@@ -80,7 +80,7 @@
%p= s_("Profiles|Set your local time zone") %p= s_("Profiles|Set your local time zone")
.col-lg-8 .col-lg-8
%h5= _("Time zone") %h5= _("Time zone")
= dropdown_tag(_("Select a time zone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg', title: _("Select a time zone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) = dropdown_tag(_("Select a time zone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg gl-w-full!', title: _("Select a time zone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
%input.hidden{ :type => 'hidden', :id => 'user_timezone', :name => 'user[timezone]', value: @user.timezone } %input.hidden{ :type => 'hidden', :id => 'user_timezone', :name => 'user[timezone]', value: @user.timezone }
.col-lg-12 .col-lg-12
%hr %hr
......
...@@ -84,10 +84,12 @@ ...@@ -84,10 +84,12 @@
= sprite_icon('location', css_class: 'fgray') = sprite_icon('location', css_class: 'fgray')
%span{ itemprop: 'addressLocality' } %span{ itemprop: 'addressLocality' }
= @user.location = @user.location
= render 'middle_dot_divider', stacking: true do - user_local_time = local_time(@user.timezone)
- unless user_local_time.nil?
= render 'middle_dot_divider', stacking: true, data: { testid: 'user-local-time' } do
= sprite_icon('clock', css_class: 'fgray') = sprite_icon('clock', css_class: 'fgray')
%span %span
= local_time(@user.timezone) = user_local_time
- unless work_information(@user).blank? - unless work_information(@user).blank?
= render 'middle_dot_divider', stacking: true do = render 'middle_dot_divider', stacking: true do
= sprite_icon('work', css_class: 'fgray') = sprite_icon('work', css_class: 'fgray')
......
...@@ -48,8 +48,8 @@ gitlab-ctl restart ...@@ -48,8 +48,8 @@ gitlab-ctl restart
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/29669) in GitLab 13.9. > - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/29669) in GitLab 13.9.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/29669) in GitLab 14.1. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/29669) in GitLab 14.1.
A user can set their time zone in their profile. If a user has not set their time zone, it defaults A user can set their time zone in their profile. In GitLab 14.3 and below if a user has not set their time zone, it defaults
to the time zone [configured at the instance level](#changing-your-time-zone). On GitLab.com, the to the time zone [configured at the instance level](#changing-your-time-zone). On GitLab.com, the default time zone is UTC.
default time zone is UTC. In GitLab 14.4 and above there is no default time zone for new users. New users will need to explicitly set their time zone before it is shown on their profile.
For more information, see [Set your time zone](../user/profile/index.md#set-your-time-zone). For more information, see [Set your time zone](../user/profile/index.md#set-your-time-zone).
...@@ -561,14 +561,11 @@ RSpec.describe 'User edit profile' do ...@@ -561,14 +561,11 @@ RSpec.describe 'User edit profile' do
page.find("a", text: "Nuku'alofa").click page.find("a", text: "Nuku'alofa").click
tz = page.find('.user-time-preferences #user_timezone', visible: false) expect(page).to have_field(:user_timezone, with: 'Pacific/Tongatapu', type: :hidden)
expect(tz.value).to eq('Pacific/Tongatapu')
end end
it 'timezone defaults to servers default' do it 'timezone defaults to empty' do
timezone_name = Time.zone.tzinfo.name expect(page).to have_field(:user_timezone, with: '', type: :hidden)
expect(page.find('.user-time-preferences #user_timezone', visible: false).value).to eq(timezone_name)
end end
end end
end end
......
...@@ -81,6 +81,7 @@ RSpec.describe 'User page' do ...@@ -81,6 +81,7 @@ RSpec.describe 'User page' do
context 'timezone' do context 'timezone' do
let_it_be(:timezone) { 'America/Los_Angeles' } let_it_be(:timezone) { 'America/Los_Angeles' }
let_it_be(:local_time_selector) { '[data-testid="user-local-time"]' }
before do before do
travel_to Time.find_zone(timezone).local(2021, 7, 20, 15, 30, 45) travel_to Time.find_zone(timezone).local(2021, 7, 20, 15, 30, 45)
...@@ -92,9 +93,21 @@ RSpec.describe 'User page' do ...@@ -92,9 +93,21 @@ RSpec.describe 'User page' do
it 'shows local time' do it 'shows local time' do
subject subject
within local_time_selector do
expect(page).to have_content('3:30 PM') expect(page).to have_content('3:30 PM')
end end
end end
end
context 'when timezone is not set' do
let_it_be(:user) { create(:user, timezone: nil) }
it 'does not show local time' do
subject
expect(page).not_to have_selector(local_time_selector)
end
end
context 'when timezone is invalid' do context 'when timezone is invalid' do
let_it_be(:user) { create(:user, timezone: 'Foo/Bar') } let_it_be(:user) { create(:user, timezone: 'Foo/Bar') }
...@@ -102,10 +115,12 @@ RSpec.describe 'User page' do ...@@ -102,10 +115,12 @@ RSpec.describe 'User page' do
it 'shows local time using the configured default timezone (UTC in this case)' do it 'shows local time using the configured default timezone (UTC in this case)' do
subject subject
within local_time_selector do
expect(page).to have_content('10:30 PM') expect(page).to have_content('10:30 PM')
end end
end end
end end
end
context 'follow/unfollow and followers/following' do context 'follow/unfollow and followers/following' do
let_it_be(:followee) { create(:user) } let_it_be(:followee) { create(:user) }
......
...@@ -10,7 +10,17 @@ describe('Timezone Dropdown', () => { ...@@ -10,7 +10,17 @@ describe('Timezone Dropdown', () => {
let $dropdownEl = null; let $dropdownEl = null;
let $wrapper = null; let $wrapper = null;
const tzListSel = '.dropdown-content ul li a.is-active'; const tzListSel = '.dropdown-content ul li a.is-active';
const tzDropdownToggleText = '.dropdown-toggle-text';
const initTimezoneDropdown = (options = {}) => {
// eslint-disable-next-line no-new
new TimezoneDropdown({
$inputEl,
$dropdownEl,
...options,
});
};
const findDropdownToggleText = () => $wrapper.find('.dropdown-toggle-text');
describe('Initialize', () => { describe('Initialize', () => {
describe('with dropdown already loaded', () => { describe('with dropdown already loaded', () => {
...@@ -18,16 +28,13 @@ describe('Timezone Dropdown', () => { ...@@ -18,16 +28,13 @@ describe('Timezone Dropdown', () => {
loadFixtures('pipeline_schedules/edit.html'); loadFixtures('pipeline_schedules/edit.html');
$wrapper = $('.dropdown'); $wrapper = $('.dropdown');
$inputEl = $('#schedule_cron_timezone'); $inputEl = $('#schedule_cron_timezone');
$inputEl.val('');
$dropdownEl = $('.js-timezone-dropdown'); $dropdownEl = $('.js-timezone-dropdown');
// eslint-disable-next-line no-new
new TimezoneDropdown({
$inputEl,
$dropdownEl,
});
}); });
it('can take an $inputEl in the constructor', () => { it('can take an $inputEl in the constructor', () => {
initTimezoneDropdown();
const tzStr = '[UTC + 5.5] Sri Jayawardenepura'; const tzStr = '[UTC + 5.5] Sri Jayawardenepura';
const tzValue = 'Asia/Colombo'; const tzValue = 'Asia/Colombo';
...@@ -42,6 +49,8 @@ describe('Timezone Dropdown', () => { ...@@ -42,6 +49,8 @@ describe('Timezone Dropdown', () => {
}); });
it('will format data array of timezones into a list of offsets', () => { it('will format data array of timezones into a list of offsets', () => {
initTimezoneDropdown();
const data = $dropdownEl.data('data'); const data = $dropdownEl.data('data');
const formatted = $wrapper.find(tzListSel).text(); const formatted = $wrapper.find(tzListSel).text();
...@@ -50,6 +59,11 @@ describe('Timezone Dropdown', () => { ...@@ -50,6 +59,11 @@ describe('Timezone Dropdown', () => {
}); });
}); });
describe('when `allowEmpty` property is `false`', () => {
beforeEach(() => {
initTimezoneDropdown();
});
it('will default the timezone to UTC', () => { it('will default the timezone to UTC', () => {
const tz = $inputEl.val(); const tz = $inputEl.val();
...@@ -57,6 +71,25 @@ describe('Timezone Dropdown', () => { ...@@ -57,6 +71,25 @@ describe('Timezone Dropdown', () => {
}); });
}); });
describe('when `allowEmpty` property is `true`', () => {
beforeEach(() => {
initTimezoneDropdown({
allowEmpty: true,
});
});
it('will default the value of the input to an empty string', () => {
expect($inputEl.val()).toBe('');
});
it('will add a new dropdown item for selecting an empty value', () => {
expect(
$(`${tzListSel}:contains('${findDropdownToggleText().html()}')`, $wrapper).length,
).not.toBe(0);
});
});
});
describe('without dropdown loaded', () => { describe('without dropdown loaded', () => {
beforeEach(() => { beforeEach(() => {
loadFixtures('pipeline_schedules/edit.html'); loadFixtures('pipeline_schedules/edit.html');
...@@ -68,23 +101,15 @@ describe('Timezone Dropdown', () => { ...@@ -68,23 +101,15 @@ describe('Timezone Dropdown', () => {
it('will populate the list of UTC offsets after the dropdown is loaded', () => { it('will populate the list of UTC offsets after the dropdown is loaded', () => {
expect($wrapper.find(tzListSel).length).toEqual(0); expect($wrapper.find(tzListSel).length).toEqual(0);
// eslint-disable-next-line no-new initTimezoneDropdown();
new TimezoneDropdown({
$inputEl,
$dropdownEl,
});
expect($wrapper.find(tzListSel).length).toEqual($($dropdownEl).data('data').length); expect($wrapper.find(tzListSel).length).toEqual($($dropdownEl).data('data').length);
}); });
it('will call a provided handler when a new timezone is selected', () => { it('will call a provided handler when a new timezone is selected', () => {
const onSelectTimezone = jest.fn(); const onSelectTimezone = jest.fn();
// eslint-disable-next-line no-new
new TimezoneDropdown({ initTimezoneDropdown({ onSelectTimezone });
$inputEl,
$dropdownEl,
onSelectTimezone,
});
$wrapper.find(tzListSel).first().trigger('click'); $wrapper.find(tzListSel).first().trigger('click');
...@@ -94,24 +119,15 @@ describe('Timezone Dropdown', () => { ...@@ -94,24 +119,15 @@ describe('Timezone Dropdown', () => {
it('will correctly set the dropdown label if a timezone identifier is set on the inputEl', () => { it('will correctly set the dropdown label if a timezone identifier is set on the inputEl', () => {
$inputEl.val('America/St_Johns'); $inputEl.val('America/St_Johns');
// eslint-disable-next-line no-new initTimezoneDropdown({ displayFormat: (selectedItem) => formatTimezone(selectedItem) });
new TimezoneDropdown({
$inputEl,
$dropdownEl,
displayFormat: (selectedItem) => formatTimezone(selectedItem),
});
expect($wrapper.find(tzDropdownToggleText).html()).toEqual('[UTC - 2.5] Newfoundland'); expect(findDropdownToggleText().html()).toEqual('[UTC - 2.5] Newfoundland');
}); });
it('will call a provided `displayFormat` handler to format the dropdown value', () => { it('will call a provided `displayFormat` handler to format the dropdown value', () => {
const displayFormat = jest.fn(); const displayFormat = jest.fn();
// eslint-disable-next-line no-new
new TimezoneDropdown({ initTimezoneDropdown({ displayFormat });
$inputEl,
$dropdownEl,
displayFormat,
});
$wrapper.find(tzListSel).first().trigger('click'); $wrapper.find(tzListSel).first().trigger('click');
...@@ -186,6 +202,14 @@ describe('Timezone Dropdown', () => { ...@@ -186,6 +202,14 @@ describe('Timezone Dropdown', () => {
}), }),
).toEqual('[UTC 0] Accra'); ).toEqual('[UTC 0] Accra');
}); });
it('returns name when no offset is provided', () => {
expect(
formatTimezone({
name: 'Foo bar',
}),
).toEqual('Foo bar');
});
}); });
describe('findTimezoneByIdentifier', () => { describe('findTimezoneByIdentifier', () => {
......
...@@ -76,6 +76,18 @@ RSpec.describe TimeZoneHelper, :aggregate_failures do ...@@ -76,6 +76,18 @@ RSpec.describe TimeZoneHelper, :aggregate_failures do
travel_to Time.find_zone(timezone).local(2021, 7, 20, 15, 30, 45) travel_to Time.find_zone(timezone).local(2021, 7, 20, 15, 30, 45)
end end
context 'when timezone is `nil`' do
it 'returns `nil`' do
expect(helper.local_time(nil)).to eq(nil)
end
end
context 'when timezone is blank' do
it 'returns `nil`' do
expect(helper.local_time('')).to eq(nil)
end
end
context 'when a valid timezone is passed' do context 'when a valid timezone is passed' do
it 'returns local time' do it 'returns local time' do
expect(helper.local_time(timezone)).to eq('3:30 PM') expect(helper.local_time(timezone)).to eq('3:30 PM')
......
...@@ -80,12 +80,6 @@ RSpec.describe UserPreference do ...@@ -80,12 +80,6 @@ RSpec.describe UserPreference do
end end
end end
describe '#timezone' do
it 'returns server time as default' do
expect(user_preference.timezone).to eq(Time.zone.tzinfo.name)
end
end
describe '#tab_width' do describe '#tab_width' do
it 'is set to 8 by default' do it 'is set to 8 by default' do
# Intentionally not using factory here to test the constructor. # Intentionally not using factory here to test the constructor.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment