From 3ad6653b3e64ad81278cbbc24dacd15bd2d32e6f Mon Sep 17 00:00:00 2001 From: Ezekiel Kigbo Date: Tue, 7 May 2019 21:58:16 +0000 Subject: Added user time settings fields to profile Udpated user_edit_profile_spec with time preferences Minor update form fields --- .../shared/components/timezone_dropdown.js | 42 +++++++----- app/assets/javascripts/profile/profile.js | 11 ++++ app/controllers/profiles/preferences_controller.rb | 4 +- app/controllers/profiles_controller.rb | 1 + app/models/user.rb | 3 + app/models/user_preference.rb | 4 ++ app/views/profiles/preferences/show.html.haml | 26 ++++++++ app/views/profiles/show.html.haml | 12 ++++ .../57654-add-time-preferences-for-user-fe.yml | 5 ++ ...0190325105715_add_fields_to_user_preferences.rb | 24 +++++++ db/schema.rb | 3 + locale/gitlab.pot | 27 ++++++++ .../profiles/user_edit_preferences_spec.rb | 32 +++++++++ spec/features/profiles/user_edit_profile_spec.rb | 32 +++++++++ .../shared/components/timezone_dropdown_spec.js | 77 ++++++++++++++++++++++ spec/models/user_preference_spec.rb | 6 ++ spec/services/users/update_service_spec.rb | 9 +++ 17 files changed, 300 insertions(+), 18 deletions(-) create mode 100644 changelogs/unreleased/57654-add-time-preferences-for-user-fe.yml create mode 100644 db/migrate/20190325105715_add_fields_to_user_preferences.rb create mode 100644 spec/features/profiles/user_edit_preferences_spec.rb diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js index c1f6edf2f27..a20a0526f12 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js @@ -1,4 +1,10 @@ -const defaultTimezone = 'UTC'; +const defaultTimezone = { name: 'UTC', offset: 0 }; +const defaults = { + $inputEl: null, + $dropdownEl: null, + onSelectTimezone: null, + displayFormat: item => item.name, +}; export const formatUtcOffset = offset => { const parsed = parseInt(offset, 10); @@ -11,23 +17,28 @@ export const formatUtcOffset = offset => { export const formatTimezone = item => `[UTC ${formatUtcOffset(item.offset)}] ${item.name}`; -const defaults = { - $inputEl: null, - $dropdownEl: null, - onSelectTimezone: null, +export const findTimezoneByIdentifier = (tzList = [], identifier = null) => { + if (tzList && tzList.length && identifier && identifier.length) { + return tzList.find(tz => tz.identifier === identifier) || null; + } + return null; }; export default class TimezoneDropdown { - constructor({ $dropdownEl, $inputEl, onSelectTimezone } = defaults) { + constructor({ $dropdownEl, $inputEl, onSelectTimezone, displayFormat } = defaults) { this.$dropdown = $dropdownEl; this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text'); this.$input = $inputEl; this.timezoneData = this.$dropdown.data('data'); + this.onSelectTimezone = onSelectTimezone; + this.displayFormat = displayFormat || defaults.displayFormat; + + this.initialTimezone = + findTimezoneByIdentifier(this.timezoneData, this.$input.val()) || defaultTimezone; + this.initDefaultTimezone(); this.initDropdown(); - - this.onSelectTimezone = onSelectTimezone; } initDropdown() { @@ -35,7 +46,7 @@ export default class TimezoneDropdown { data: this.timezoneData, filterable: true, selectable: true, - toggleLabel: item => item.name, + toggleLabel: this.displayFormat, search: { fields: ['name'], }, @@ -43,20 +54,17 @@ export default class TimezoneDropdown { text: item => formatTimezone(item), }); - this.setDropdownToggle(); + this.setDropdownToggle(this.displayFormat(this.initialTimezone)); } initDefaultTimezone() { - const initialValue = this.$input.val(); - - if (!initialValue) { - this.$input.val(defaultTimezone); + if (!this.$input.val()) { + this.$input.val(defaultTimezone.name); } } - setDropdownToggle() { - const initialValue = this.$input.val(); - this.$dropdownToggle.text(initialValue); + setDropdownToggle(dropdownText) { + this.$dropdownToggle.text(dropdownText); } updateInputValue({ selectedObj, e }) { diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index deacff5abe7..6e3800021b4 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -2,6 +2,9 @@ import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; import flash from '../flash'; import { parseBoolean } from '~/lib/utils/common_utils'; +import TimezoneDropdown, { + formatTimezone, +} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown'; export default class Profile { constructor({ form } = {}) { @@ -10,6 +13,14 @@ export default class Profile { this.setRepoRadio(); this.bindEvents(); this.initAvatarGlCrop(); + + this.$inputEl = $('#user_timezone'); + + this.timezoneDropdown = new TimezoneDropdown({ + $inputEl: this.$inputEl, + $dropdownEl: $('.js-timezone-dropdown'), + displayFormat: selectedItem => formatTimezone(selectedItem), + }); } initAvatarGlCrop() { diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index 0e30df1b15b..62f98d9e549 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -44,7 +44,9 @@ class Profiles::PreferencesController < Profiles::ApplicationController :project_view, :theme_id, :first_day_of_week, - :preferred_language + :preferred_language, + :time_display_relative, + :time_format_in_24h ] end end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index b9c52618d4b..d3746248bd3 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -106,6 +106,7 @@ class ProfilesController < Profiles::ApplicationController :organization, :private_profile, :include_private_contributions, + :timezone, status: [:emoji, :message] ) end diff --git a/app/models/user.rb b/app/models/user.rb index 4a1bf5514fe..60f69659a6b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -230,6 +230,9 @@ class User < ApplicationRecord delegate :notes_filter_for, to: :user_preference delegate :set_notes_filter, to: :user_preference delegate :first_day_of_week, :first_day_of_week=, to: :user_preference + delegate :timezone, :timezone=, to: :user_preference + delegate :time_display_relative, :time_display_relative=, to: :user_preference + delegate :time_format_in_24h, :time_format_in_24h=, to: :user_preference accepts_nested_attributes_for :user_preference, update_only: true diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 282b192167f..f1326f4c8cb 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -10,6 +10,10 @@ class UserPreference < ApplicationRecord validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true + 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_format_in_24h, value: false, allows_nil: false + class << self def notes_filters { diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index bfe1c3ddf33..58f2eb229ba 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -82,5 +82,31 @@ = f.label :first_day_of_week, class: 'label-bold' do = _('First day of the week') = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control' + - if Feature.enabled?(:user_time_settings) + .col-sm-12 + %hr + .col-lg-4.profile-settings-sidebar + %h4.prepend-top-0= s_('Preferences|Time preferences') + %p= s_('Preferences|These settings will update how dates and times are displayed for you.') + .col-lg-8 + .form-group + %h5= s_('Preferences|Time format') + .checkbox-icon-inline-wrapper.form-check + - time_format_label = capture do + = s_('Preferences|Display time in 24-hour format') + = f.check_box :time_format_in_24h, class: 'form-check-input' + = f.label :time_format_in_24h do + = time_format_label + %h5= s_('Preferences|Time display') + .checkbox-icon-inline-wrapper.form-check + - time_display_label = capture do + = s_('Preferences|Use relative times') + = f.check_box :time_display_relative, class: 'form-check-input' + = f.label :time_display_relative do + = time_display_label + .text-muted + = s_('Preferences|For example: 30 mins ago.') + .col-lg-4.profile-settings-sidebar + .col-lg-8 .form-group = f.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 02c750a92c3..45b6453b5ff 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -64,6 +64,18 @@ prepend: emoji_button, append: reset_message_button, placeholder: s_("Profiles|What's your status?") + - if Feature.enabled?(:user_time_settings) + %hr + .row.user-time-preferences + .col-lg-4.profile-settings-sidebar + %h4.prepend-top-0= s_("Profiles|Time settings") + %p= s_("Profiles|You can set your current timezone here") + .col-lg-8 + -# TODO: might need an entry in user/profile.md to describe some of these settings + -# https://gitlab.com/gitlab-org/gitlab-ce/issues/60070 + %h5= ("Time zone") + = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown input-lg', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) + %input.hidden{ :type => 'hidden', :id => 'user_timezone', :name => 'user[timezone]', value: @user.timezone } %hr .row diff --git a/changelogs/unreleased/57654-add-time-preferences-for-user-fe.yml b/changelogs/unreleased/57654-add-time-preferences-for-user-fe.yml new file mode 100644 index 00000000000..f4ce3a51724 --- /dev/null +++ b/changelogs/unreleased/57654-add-time-preferences-for-user-fe.yml @@ -0,0 +1,5 @@ +--- +title: Add time preferences for user +merge_request: 25381 +author: +type: added diff --git a/db/migrate/20190325105715_add_fields_to_user_preferences.rb b/db/migrate/20190325105715_add_fields_to_user_preferences.rb new file mode 100644 index 00000000000..9ea3b4f9cd8 --- /dev/null +++ b/db/migrate/20190325105715_add_fields_to_user_preferences.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddFieldsToUserPreferences < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column(:user_preferences, :timezone, :string) + add_column(:user_preferences, :time_display_relative, :boolean) + add_column(:user_preferences, :time_format_in_24h, :boolean) + end + + def down + remove_column(:user_preferences, :timezone) + remove_column(:user_preferences, :time_display_relative) + remove_column(:user_preferences, :time_format_in_24h) + end +end diff --git a/db/schema.rb b/db/schema.rb index deaf406fe3d..1be3afd5282 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2246,6 +2246,9 @@ ActiveRecord::Schema.define(version: 20190506135400) do t.integer "first_day_of_week" t.string "issues_sort" t.string "merge_requests_sort" + t.string "timezone" + t.boolean "time_display_relative" + t.boolean "time_format_in_24h" t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2907430bd51..b9fc3e00cff 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6974,12 +6974,33 @@ msgstr "" msgid "Preferences saved." msgstr "" +msgid "Preferences|Display time in 24-hour format" +msgstr "" + +msgid "Preferences|For example: 30 mins ago." +msgstr "" + msgid "Preferences|Navigation theme" msgstr "" +msgid "Preferences|These settings will update how dates and times are displayed for you." +msgstr "" + msgid "Preferences|This feature is experimental and translations are not complete yet" msgstr "" +msgid "Preferences|Time display" +msgstr "" + +msgid "Preferences|Time format" +msgstr "" + +msgid "Preferences|Time preferences" +msgstr "" + +msgid "Preferences|Use relative times" +msgstr "" + msgid "Press %{key}-C to copy" msgstr "" @@ -7190,6 +7211,9 @@ msgstr "" msgid "Profiles|This information will appear on your profile" msgstr "" +msgid "Profiles|Time settings" +msgstr "" + msgid "Profiles|Two-Factor Authentication" msgstr "" @@ -7232,6 +7256,9 @@ msgstr "" msgid "Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}" msgstr "" +msgid "Profiles|You can set your current timezone here" +msgstr "" + msgid "Profiles|You can upload your avatar here" msgstr "" diff --git a/spec/features/profiles/user_edit_preferences_spec.rb b/spec/features/profiles/user_edit_preferences_spec.rb new file mode 100644 index 00000000000..2d2da222998 --- /dev/null +++ b/spec/features/profiles/user_edit_preferences_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe 'User edit preferences profile' do + let(:user) { create(:user) } + + before do + stub_feature_flags(user_time_settings: true) + sign_in(user) + visit(profile_preferences_path) + end + + it 'allows the user to toggle their time format preference' do + field = page.find_field("user[time_format_in_24h]") + + expect(field).not_to be_checked + + field.click + + expect(field).to be_checked + end + + it 'allows the user to toggle their time display preference' do + field = page.find_field("user[time_display_relative]") + + expect(field).to be_checked + + field.click + + expect(field).not_to be_checked + end +end diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb index b43711f6ef6..a53da94ef7d 100644 --- a/spec/features/profiles/user_edit_profile_spec.rb +++ b/spec/features/profiles/user_edit_profile_spec.rb @@ -327,5 +327,37 @@ describe 'User edit profile' do end end end + + context 'User time preferences', :js do + let(:issue) { create(:issue, project: project)} + let(:project) { create(:project) } + + before do + stub_feature_flags(user_time_settings: true) + end + + it 'shows the user time preferences form' do + expect(page).to have_content('Time settings') + end + + it 'allows the user to select a time zone from a dropdown list of options' do + expect(page.find('.user-time-preferences .dropdown')).not_to have_css('.show') + + page.find('.user-time-preferences .js-timezone-dropdown').click + + expect(page.find('.user-time-preferences .dropdown')).to have_css('.show') + + page.find("a", text: "Nuku'alofa").click + + tz = page.find('.user-time-preferences #user_timezone', visible: false) + + expect(tz.value).to eq('Pacific/Tongatapu') + end + + it 'timezone defaults to servers default' do + timezone_name = Time.zone.tzinfo.name + expect(page.find('.user-time-preferences #user_timezone', visible: false).value).to eq(timezone_name) + end + end end end diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js index a89952ee435..5f4dba5ecb9 100644 --- a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js +++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js @@ -3,6 +3,7 @@ import GLDropdown from '~/gl_dropdown'; // eslint-disable-line no-unused-vars import TimezoneDropdown, { formatUtcOffset, formatTimezone, + findTimezoneByIdentifier, } from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown'; describe('Timezone Dropdown', function() { @@ -12,6 +13,7 @@ describe('Timezone Dropdown', function() { let $dropdownEl = null; let $wrapper = null; const tzListSel = '.dropdown-content ul li a.is-active'; + const tzDropdownToggleText = '.dropdown-toggle-text'; describe('Initialize', () => { describe('with dropdown already loaded', () => { @@ -94,6 +96,36 @@ describe('Timezone Dropdown', function() { expect(onSelectTimezone).toHaveBeenCalled(); }); + + it('will correctly set the dropdown label if a timezone identifier is set on the inputEl', () => { + $inputEl.val('America/St_Johns'); + + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + displayFormat: selectedItem => formatTimezone(selectedItem), + }); + + expect($wrapper.find(tzDropdownToggleText).html()).toEqual('[UTC - 2.5] Newfoundland'); + }); + + it('will call a provided `displayFormat` handler to format the dropdown value', () => { + const displayFormat = jasmine.createSpy('displayFormat'); + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + displayFormat, + }); + + $wrapper + .find(tzListSel) + .first() + .trigger('click'); + + expect(displayFormat).toHaveBeenCalled(); + }); }); }); @@ -164,4 +196,49 @@ describe('Timezone Dropdown', function() { ).toEqual('[UTC 0] Accra'); }); }); + + describe('findTimezoneByIdentifier', () => { + const tzList = [ + { + identifier: 'Asia/Tokyo', + name: 'Sapporo', + offset: 32400, + }, + { + identifier: 'Asia/Hong_Kong', + name: 'Hong Kong', + offset: 28800, + }, + { + identifier: 'Asia/Dhaka', + name: 'Dhaka', + offset: 21600, + }, + ]; + + const identifier = 'Asia/Dhaka'; + it('returns the correct object if the identifier exists', () => { + const res = findTimezoneByIdentifier(tzList, identifier); + + expect(res).toBeTruthy(); + expect(res).toBe(tzList[2]); + }); + + it('returns null if it doesnt find the identifier', () => { + const res = findTimezoneByIdentifier(tzList, 'Australia/Melbourne'); + + expect(res).toBeNull(); + }); + + it('returns null if there is no identifier given', () => { + expect(findTimezoneByIdentifier(tzList)).toBeNull(); + expect(findTimezoneByIdentifier(tzList, '')).toBeNull(); + }); + + it('returns null if there is an empty or invalid array given', () => { + expect(findTimezoneByIdentifier([], identifier)).toBeNull(); + expect(findTimezoneByIdentifier(null, identifier)).toBeNull(); + expect(findTimezoneByIdentifier(undefined, identifier)).toBeNull(); + }); + }); }); diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb index b2ef17a81d4..e09c91e874a 100644 --- a/spec/models/user_preference_spec.rb +++ b/spec/models/user_preference_spec.rb @@ -73,4 +73,10 @@ describe UserPreference do it_behaves_like 'a sort_by preference' end end + + describe '#timezone' do + it 'returns server time as default' do + expect(user_preference.timezone).to eq(Time.zone.tzinfo.name) + end + end end diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb index e04c05418b0..9384287f98a 100644 --- a/spec/services/users/update_service_spec.rb +++ b/spec/services/users/update_service_spec.rb @@ -13,6 +13,15 @@ describe Users::UpdateService do expect(user.name).to eq('New Name') end + it 'updates time preferences' do + result = update_user(user, timezone: 'Europe/Warsaw', time_display_relative: true, time_format_in_24h: false) + + expect(result).to eq(status: :success) + expect(user.reload.timezone).to eq('Europe/Warsaw') + expect(user.time_display_relative).to eq(true) + expect(user.time_format_in_24h).to eq(false) + end + it 'returns an error result when record cannot be updated' do result = {} expect do -- cgit v1.2.1