Commit 0be6392d authored by Fatih Acet's avatar Fatih Acet

Merge branch '57654-add-time-preferences-for-user-fe' into 'master'

Resolve "Add time preferences for user [FE]"

Closes #57654

See merge request gitlab-org/gitlab-ce!25381
parents 9d445026 3ad6653b
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 => { export const formatUtcOffset = offset => {
const parsed = parseInt(offset, 10); const parsed = parseInt(offset, 10);
...@@ -11,23 +17,28 @@ export const formatUtcOffset = offset => { ...@@ -11,23 +17,28 @@ export const formatUtcOffset = offset => {
export const formatTimezone = item => `[UTC ${formatUtcOffset(item.offset)}] ${item.name}`; export const formatTimezone = item => `[UTC ${formatUtcOffset(item.offset)}] ${item.name}`;
const defaults = { export const findTimezoneByIdentifier = (tzList = [], identifier = null) => {
$inputEl: null, if (tzList && tzList.length && identifier && identifier.length) {
$dropdownEl: null, return tzList.find(tz => tz.identifier === identifier) || null;
onSelectTimezone: null, }
return null;
}; };
export default class TimezoneDropdown { export default class TimezoneDropdown {
constructor({ $dropdownEl, $inputEl, onSelectTimezone } = defaults) { constructor({ $dropdownEl, $inputEl, onSelectTimezone, displayFormat } = 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');
this.onSelectTimezone = onSelectTimezone;
this.displayFormat = displayFormat || defaults.displayFormat;
this.initialTimezone =
findTimezoneByIdentifier(this.timezoneData, this.$input.val()) || defaultTimezone;
this.initDefaultTimezone(); this.initDefaultTimezone();
this.initDropdown(); this.initDropdown();
this.onSelectTimezone = onSelectTimezone;
} }
initDropdown() { initDropdown() {
...@@ -35,7 +46,7 @@ export default class TimezoneDropdown { ...@@ -35,7 +46,7 @@ export default class TimezoneDropdown {
data: this.timezoneData, data: this.timezoneData,
filterable: true, filterable: true,
selectable: true, selectable: true,
toggleLabel: item => item.name, toggleLabel: this.displayFormat,
search: { search: {
fields: ['name'], fields: ['name'],
}, },
...@@ -43,20 +54,17 @@ export default class TimezoneDropdown { ...@@ -43,20 +54,17 @@ export default class TimezoneDropdown {
text: item => formatTimezone(item), text: item => formatTimezone(item),
}); });
this.setDropdownToggle(); this.setDropdownToggle(this.displayFormat(this.initialTimezone));
} }
initDefaultTimezone() { initDefaultTimezone() {
const initialValue = this.$input.val(); if (!this.$input.val()) {
this.$input.val(defaultTimezone.name);
if (!initialValue) {
this.$input.val(defaultTimezone);
} }
} }
setDropdownToggle() { setDropdownToggle(dropdownText) {
const initialValue = this.$input.val(); this.$dropdownToggle.text(dropdownText);
this.$dropdownToggle.text(initialValue);
} }
updateInputValue({ selectedObj, e }) { updateInputValue({ selectedObj, e }) {
......
...@@ -2,6 +2,9 @@ import $ from 'jquery'; ...@@ -2,6 +2,9 @@ import $ from 'jquery';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import flash from '../flash'; import flash from '../flash';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import TimezoneDropdown, {
formatTimezone,
} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
export default class Profile { export default class Profile {
constructor({ form } = {}) { constructor({ form } = {}) {
...@@ -10,6 +13,14 @@ export default class Profile { ...@@ -10,6 +13,14 @@ export default class Profile {
this.setRepoRadio(); this.setRepoRadio();
this.bindEvents(); this.bindEvents();
this.initAvatarGlCrop(); this.initAvatarGlCrop();
this.$inputEl = $('#user_timezone');
this.timezoneDropdown = new TimezoneDropdown({
$inputEl: this.$inputEl,
$dropdownEl: $('.js-timezone-dropdown'),
displayFormat: selectedItem => formatTimezone(selectedItem),
});
} }
initAvatarGlCrop() { initAvatarGlCrop() {
......
...@@ -44,7 +44,9 @@ class Profiles::PreferencesController < Profiles::ApplicationController ...@@ -44,7 +44,9 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:project_view, :project_view,
:theme_id, :theme_id,
:first_day_of_week, :first_day_of_week,
:preferred_language :preferred_language,
:time_display_relative,
:time_format_in_24h
] ]
end end
end end
...@@ -106,6 +106,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -106,6 +106,7 @@ class ProfilesController < Profiles::ApplicationController
:organization, :organization,
:private_profile, :private_profile,
:include_private_contributions, :include_private_contributions,
:timezone,
status: [:emoji, :message] status: [:emoji, :message]
) )
end end
......
...@@ -230,6 +230,9 @@ class User < ApplicationRecord ...@@ -230,6 +230,9 @@ class User < ApplicationRecord
delegate :notes_filter_for, to: :user_preference delegate :notes_filter_for, to: :user_preference
delegate :set_notes_filter, to: :user_preference delegate :set_notes_filter, to: :user_preference
delegate :first_day_of_week, :first_day_of_week=, 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 accepts_nested_attributes_for :user_preference, update_only: true
......
...@@ -10,6 +10,10 @@ class UserPreference < ApplicationRecord ...@@ -10,6 +10,10 @@ class UserPreference < ApplicationRecord
validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true 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 class << self
def notes_filters def notes_filters
{ {
......
...@@ -82,5 +82,31 @@ ...@@ -82,5 +82,31 @@
= f.label :first_day_of_week, class: 'label-bold' do = f.label :first_day_of_week, class: 'label-bold' do
= _('First day of the week') = _('First day of the week')
= f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control' = 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 .form-group
= f.submit _('Save changes'), class: 'btn btn-success' = f.submit _('Save changes'), class: 'btn btn-success'
...@@ -64,6 +64,18 @@ ...@@ -64,6 +64,18 @@
prepend: emoji_button, prepend: emoji_button,
append: reset_message_button, append: reset_message_button,
placeholder: s_("Profiles|What's your status?") 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 %hr
.row .row
......
---
title: Add time preferences for user
merge_request: 25381
author:
type: added
# 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
...@@ -2246,6 +2246,9 @@ ActiveRecord::Schema.define(version: 20190506135400) do ...@@ -2246,6 +2246,9 @@ ActiveRecord::Schema.define(version: 20190506135400) do
t.integer "first_day_of_week" t.integer "first_day_of_week"
t.string "issues_sort" t.string "issues_sort"
t.string "merge_requests_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 t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree
end end
......
...@@ -6974,12 +6974,33 @@ msgstr "" ...@@ -6974,12 +6974,33 @@ msgstr ""
msgid "Preferences saved." msgid "Preferences saved."
msgstr "" msgstr ""
msgid "Preferences|Display time in 24-hour format"
msgstr ""
msgid "Preferences|For example: 30 mins ago."
msgstr ""
msgid "Preferences|Navigation theme" msgid "Preferences|Navigation theme"
msgstr "" 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" msgid "Preferences|This feature is experimental and translations are not complete yet"
msgstr "" 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" msgid "Press %{key}-C to copy"
msgstr "" msgstr ""
...@@ -7190,6 +7211,9 @@ msgstr "" ...@@ -7190,6 +7211,9 @@ msgstr ""
msgid "Profiles|This information will appear on your profile" msgid "Profiles|This information will appear on your profile"
msgstr "" msgstr ""
msgid "Profiles|Time settings"
msgstr ""
msgid "Profiles|Two-Factor Authentication" msgid "Profiles|Two-Factor Authentication"
msgstr "" msgstr ""
...@@ -7232,6 +7256,9 @@ msgstr "" ...@@ -7232,6 +7256,9 @@ msgstr ""
msgid "Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}" msgid "Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}"
msgstr "" msgstr ""
msgid "Profiles|You can set your current timezone here"
msgstr ""
msgid "Profiles|You can upload your avatar here" msgid "Profiles|You can upload your avatar here"
msgstr "" msgstr ""
......
# 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
...@@ -327,5 +327,37 @@ describe 'User edit profile' do ...@@ -327,5 +327,37 @@ describe 'User edit profile' do
end end
end 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
end end
...@@ -3,6 +3,7 @@ import GLDropdown from '~/gl_dropdown'; // eslint-disable-line no-unused-vars ...@@ -3,6 +3,7 @@ import GLDropdown from '~/gl_dropdown'; // eslint-disable-line no-unused-vars
import TimezoneDropdown, { import TimezoneDropdown, {
formatUtcOffset, formatUtcOffset,
formatTimezone, formatTimezone,
findTimezoneByIdentifier,
} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown'; } from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
describe('Timezone Dropdown', function() { describe('Timezone Dropdown', function() {
...@@ -12,6 +13,7 @@ describe('Timezone Dropdown', function() { ...@@ -12,6 +13,7 @@ describe('Timezone Dropdown', function() {
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';
describe('Initialize', () => { describe('Initialize', () => {
describe('with dropdown already loaded', () => { describe('with dropdown already loaded', () => {
...@@ -94,6 +96,36 @@ describe('Timezone Dropdown', function() { ...@@ -94,6 +96,36 @@ describe('Timezone Dropdown', function() {
expect(onSelectTimezone).toHaveBeenCalled(); 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() { ...@@ -164,4 +196,49 @@ describe('Timezone Dropdown', function() {
).toEqual('[UTC 0] Accra'); ).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();
});
});
}); });
...@@ -73,4 +73,10 @@ describe UserPreference do ...@@ -73,4 +73,10 @@ describe UserPreference do
it_behaves_like 'a sort_by preference' it_behaves_like 'a sort_by preference'
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
end end
...@@ -13,6 +13,15 @@ describe Users::UpdateService do ...@@ -13,6 +13,15 @@ describe Users::UpdateService do
expect(user.name).to eq('New Name') expect(user.name).to eq('New Name')
end 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 it 'returns an error result when record cannot be updated' do
result = {} result = {}
expect do expect do
......
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