Added user time settings fields to profile

Udpated user_edit_profile_spec with time preferences

Minor update form fields
This commit is contained in:
Ezekiel Kigbo 2019-05-07 21:58:16 +00:00 committed by Fatih Acet
parent 9d4450263f
commit 3ad6653b3e
17 changed files with 300 additions and 18 deletions

View file

@ -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 }) {

View file

@ -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() {

View file

@ -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

View file

@ -106,6 +106,7 @@ class ProfilesController < Profiles::ApplicationController
:organization,
:private_profile,
:include_private_contributions,
:timezone,
status: [:emoji, :message]
)
end

View file

@ -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

View file

@ -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
{

View file

@ -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'

View file

@ -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

View file

@ -0,0 +1,5 @@
---
title: Add time preferences for user
merge_request: 25381
author:
type: added

View 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

View file

@ -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

View file

@ -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 ""

View 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

View file

@ -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

View file

@ -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();
});
});
});

View file

@ -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

View file

@ -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