Added user time settings fields to profile
Udpated user_edit_profile_spec with time preferences Minor update form fields
This commit is contained in:
parent
9d4450263f
commit
3ad6653b3e
17 changed files with 300 additions and 18 deletions
|
@ -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 }) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -106,6 +106,7 @@ class ProfilesController < Profiles::ApplicationController
|
|||
:organization,
|
||||
:private_profile,
|
||||
:include_private_contributions,
|
||||
:timezone,
|
||||
status: [:emoji, :message]
|
||||
)
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add time preferences for user
|
||||
merge_request: 25381
|
||||
author:
|
||||
type: added
|
24
db/migrate/20190325105715_add_fields_to_user_preferences.rb
Normal file
24
db/migrate/20190325105715_add_fields_to_user_preferences.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
32
spec/features/profiles/user_edit_preferences_spec.rb
Normal file
32
spec/features/profiles/user_edit_preferences_spec.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue