From 2503241ce54a043c08c2dcb3ef37ce2ebf65e9c5 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 26 Apr 2017 16:18:16 -0400 Subject: [PATCH] FEATURE: rebuild user preferences page to use tabs --- .../discourse/controllers/preferences.js.es6 | 231 +--------- .../controllers/preferences/account.js.es6 | 71 ++++ .../controllers/preferences/categories.js.es6 | 20 + .../controllers/preferences/emails.js.es6 | 62 +++ .../controllers/preferences/interface.js.es6 | 45 ++ .../preferences/notifications.js.es6 | 56 +++ .../controllers/preferences/profile.js.es6 | 84 ++++ .../controllers/preferences/tags.js.es6 | 21 + .../mixins/preferences-tab-controller.js.es6 | 10 + .../javascripts/discourse/models/user.js.es6 | 75 ++-- .../discourse/routes/app-route-map.js.es6 | 9 + .../routes/preferences-account.js.es6 | 8 + .../discourse/routes/preferences-index.js.es6 | 4 +- .../routes/preferences-interface.js.es6 | 11 + .../routes/preferences-profile.js.es6 | 10 + .../discourse/routes/preferences.js.es6 | 6 +- .../templates/components/user-fields/text.hbs | 4 +- .../discourse/templates/preferences.hbs | 399 +----------------- .../templates/preferences/account.hbs | 61 +++ .../discourse/templates/preferences/apps.hbs | 25 ++ .../templates/preferences/categories.hbs | 48 +++ .../templates/preferences/emails.hbs | 56 +++ .../templates/preferences/interface.hbs | 47 +++ .../templates/preferences/notifications.hbs | 46 ++ .../templates/preferences/profile.hbs | 110 +++++ .../discourse/templates/preferences/tags.hbs | 41 ++ app/assets/stylesheets/common/base/login.scss | 5 +- .../stylesheets/common/components/navs.scss | 3 + app/assets/stylesheets/desktop/discourse.scss | 30 ++ app/assets/stylesheets/desktop/user.scss | 32 +- app/assets/stylesheets/mobile/discourse.scss | 32 +- app/assets/stylesheets/mobile/user.scss | 44 +- config/locales/client.en.yml | 10 + config/routes.rb | 8 + .../acceptance/preferences-test.js.es6 | 4 +- 35 files changed, 1039 insertions(+), 689 deletions(-) create mode 100644 app/assets/javascripts/discourse/controllers/preferences/account.js.es6 create mode 100644 app/assets/javascripts/discourse/controllers/preferences/categories.js.es6 create mode 100644 app/assets/javascripts/discourse/controllers/preferences/emails.js.es6 create mode 100644 app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 create mode 100644 app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6 create mode 100644 app/assets/javascripts/discourse/controllers/preferences/profile.js.es6 create mode 100644 app/assets/javascripts/discourse/controllers/preferences/tags.js.es6 create mode 100644 app/assets/javascripts/discourse/mixins/preferences-tab-controller.js.es6 create mode 100644 app/assets/javascripts/discourse/routes/preferences-account.js.es6 create mode 100644 app/assets/javascripts/discourse/routes/preferences-interface.js.es6 create mode 100644 app/assets/javascripts/discourse/routes/preferences-profile.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/preferences/account.hbs create mode 100644 app/assets/javascripts/discourse/templates/preferences/apps.hbs create mode 100644 app/assets/javascripts/discourse/templates/preferences/categories.hbs create mode 100644 app/assets/javascripts/discourse/templates/preferences/emails.hbs create mode 100644 app/assets/javascripts/discourse/templates/preferences/interface.hbs create mode 100644 app/assets/javascripts/discourse/templates/preferences/notifications.hbs create mode 100644 app/assets/javascripts/discourse/templates/preferences/profile.hbs create mode 100644 app/assets/javascripts/discourse/templates/preferences/tags.hbs diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6 index ac848d3ea29..d4180e8b6b4 100644 --- a/app/assets/javascripts/discourse/controllers/preferences.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6 @@ -1,230 +1,3 @@ -import { setting } from 'discourse/lib/computed'; -import CanCheckEmails from 'discourse/mixins/can-check-emails'; -import { popupAjaxError } from 'discourse/lib/ajax-error'; -import { default as computed, observes } from "ember-addons/ember-computed-decorators"; -import { cook } from 'discourse/lib/text'; -import { NotificationLevels } from 'discourse/lib/notification-levels'; -import { listThemes, selectDefaultTheme, previewTheme } from 'discourse/lib/theme-selector'; - -export default Ember.Controller.extend(CanCheckEmails, { - - userSelectableThemes: function(){ - return listThemes(this.site); - }.property(), - - @observes("selectedTheme") - themeKeyChanged() { - let key = this.get("selectedTheme"); - previewTheme(key); - }, - - @computed("model.watchedCategories", "model.trackedCategories", "model.mutedCategories") - selectedCategories(watched, tracked, muted) { - return [].concat(watched, tracked, muted); - }, - - // By default we haven't saved anything - saved: false, - - newNameInput: null, - - @computed("model.user_fields.@each.value") - userFields() { - let siteUserFields = this.site.get('user_fields'); - if (!Ember.isEmpty(siteUserFields)) { - const userFields = this.get('model.user_fields'); - - // Staff can edit fields that are not `editable` - if (!this.get('currentUser.staff')) { - siteUserFields = siteUserFields.filterBy('editable', true); - } - return siteUserFields.sortBy('position').map(function(field) { - const value = userFields ? userFields[field.get('id').toString()] : null; - return Ember.Object.create({ value, field }); - }); - } - }, - - cannotDeleteAccount: Em.computed.not('currentUser.can_delete_account'), - deleteDisabled: Em.computed.or('model.isSaving', 'deleting', 'cannotDeleteAccount'), - - canEditName: setting('enable_names'), - - @computed() - nameInstructions() { - return I18n.t(this.siteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions'); - }, - - @computed("model.has_title_badges") - canSelectTitle(hasTitleBadges) { - return this.siteSettings.enable_badges && hasTitleBadges; - }, - - @computed("model.can_change_bio") - canChangeBio(canChangeBio) - { - return canChangeBio; - }, - - @computed() - canChangePassword() { - return !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins; - }, - - @computed() - availableLocales() { - return this.siteSettings.available_locales.split('|').map(s => ({ name: s, value: s })); - }, - - @computed() - frequencyEstimate() { - var estimate = this.get('model.mailing_list_posts_per_day'); - if (!estimate || estimate < 2) { - return I18n.t('user.mailing_list_mode.few_per_day'); - } else { - return I18n.t('user.mailing_list_mode.many_per_day', { dailyEmailEstimate: estimate }); - } - }, - - @computed() - mailingListModeOptions() { - return [ - {name: I18n.t('user.mailing_list_mode.daily'), value: 0}, - {name: this.get('frequencyEstimate'), value: 1}, - {name: I18n.t('user.mailing_list_mode.individual_no_echo'), value: 2} - ]; - }, - - previousRepliesOptions: [ - {name: I18n.t('user.email_previous_replies.always'), value: 0}, - {name: I18n.t('user.email_previous_replies.unless_emailed'), value: 1}, - {name: I18n.t('user.email_previous_replies.never'), value: 2} - ], - - digestFrequencies: [{ name: I18n.t('user.email_digests.every_30_minutes'), value: 30 }, - { name: I18n.t('user.email_digests.every_hour'), value: 60 }, - { name: I18n.t('user.email_digests.daily'), value: 1440 }, - { name: I18n.t('user.email_digests.every_three_days'), value: 4320 }, - { name: I18n.t('user.email_digests.weekly'), value: 10080 }, - { name: I18n.t('user.email_digests.every_two_weeks'), value: 20160 }], - - likeNotificationFrequencies: [{ name: I18n.t('user.like_notification_frequency.always'), value: 0 }, - { name: I18n.t('user.like_notification_frequency.first_time_and_daily'), value: 1 }, - { name: I18n.t('user.like_notification_frequency.first_time'), value: 2 }, - { name: I18n.t('user.like_notification_frequency.never'), value: 3 }], - - autoTrackDurations: [{ name: I18n.t('user.auto_track_options.never'), value: -1 }, - { name: I18n.t('user.auto_track_options.immediately'), value: 0 }, - { name: I18n.t('user.auto_track_options.after_30_seconds'), value: 30000 }, - { name: I18n.t('user.auto_track_options.after_1_minute'), value: 60000 }, - { name: I18n.t('user.auto_track_options.after_2_minutes'), value: 120000 }, - { name: I18n.t('user.auto_track_options.after_3_minutes'), value: 180000 }, - { name: I18n.t('user.auto_track_options.after_4_minutes'), value: 240000 }, - { name: I18n.t('user.auto_track_options.after_5_minutes'), value: 300000 }, - { name: I18n.t('user.auto_track_options.after_10_minutes'), value: 600000 }], - - notificationLevelsForReplying: [{ name: I18n.t('topic.notifications.watching.title'), value: NotificationLevels.WATCHING }, - { name: I18n.t('topic.notifications.tracking.title'), value: NotificationLevels.TRACKING }, - { name: I18n.t('topic.notifications.regular.title'), value: NotificationLevels.REGULAR }], - - - considerNewTopicOptions: [{ name: I18n.t('user.new_topic_duration.not_viewed'), value: -1 }, - { name: I18n.t('user.new_topic_duration.after_1_day'), value: 60 * 24 }, - { name: I18n.t('user.new_topic_duration.after_2_days'), value: 60 * 48 }, - { name: I18n.t('user.new_topic_duration.after_1_week'), value: 7 * 60 * 24 }, - { name: I18n.t('user.new_topic_duration.after_2_weeks'), value: 2 * 7 * 60 * 24 }, - { name: I18n.t('user.new_topic_duration.last_here'), value: -2 }], - - @computed("model.isSaving") - saveButtonText(isSaving) { - return isSaving ? I18n.t('saving') : I18n.t('save'); - }, - - reset() { - this.setProperties({ - passwordProgress: null - }); - }, - - passwordProgress: null, - - actions: { - - save() { - this.set('saved', false); - - const model = this.get('model'); - - const userFields = this.get('userFields'); - - // Update the user fields - if (!Ember.isEmpty(userFields)) { - const modelFields = model.get('user_fields'); - if (!Ember.isEmpty(modelFields)) { - userFields.forEach(function(uf) { - modelFields[uf.get('field.id').toString()] = uf.get('value'); - }); - } - } - - // Cook the bio for preview - model.set('name', this.get('newNameInput')); - return model.save().then(() => { - if (Discourse.User.currentProp('id') === model.get('id')) { - Discourse.User.currentProp('name', model.get('name')); - } - model.set('bio_cooked', cook(model.get('bio_raw'))); - selectDefaultTheme(this.get('selectedTheme')); - this.set('saved', true); - }).catch(popupAjaxError); - }, - - changePassword() { - if (!this.get('passwordProgress')) { - this.set('passwordProgress', I18n.t("user.change_password.in_progress")); - return this.get('model').changePassword().then(() => { - // password changed - this.setProperties({ - changePasswordProgress: false, - passwordProgress: I18n.t("user.change_password.success") - }); - }).catch(() => { - // password failed to change - this.setProperties({ - changePasswordProgress: false, - passwordProgress: I18n.t("user.change_password.error") - }); - }); - } - }, - - delete() { - this.set('deleting', true); - const self = this, - message = I18n.t('user.delete_account_confirm'), - model = this.get('model'), - buttons = [ - { label: I18n.t("cancel"), - class: "cancel-inline", - link: true, - callback: () => { this.set('deleting', false); } - }, - { label: ' ' + I18n.t("user.delete_account"), - class: "btn btn-danger", - callback() { - model.delete().then(function() { - bootbox.alert(I18n.t('user.deleted_yourself'), function() { - window.location.pathname = Discourse.getURL('/'); - }); - }, function() { - bootbox.alert(I18n.t('user.delete_yourself_not_allowed')); - self.set('deleting', false); - }); - } - } - ]; - bootbox.dialog(message, buttons, {"classes": "delete-account"}); - } - } - +export default Ember.Controller.extend({ + application: Ember.inject.controller() }); diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 new file mode 100644 index 00000000000..a5cd50edc97 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 @@ -0,0 +1,71 @@ +import CanCheckEmails from 'discourse/mixins/can-check-emails'; +import { default as computed } from "ember-addons/ember-computed-decorators"; +import PreferencesTabController from "discourse/mixins/preferences-tab-controller"; + +export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController, { + + passwordProgress: null, + + cannotDeleteAccount: Em.computed.not('currentUser.can_delete_account'), + deleteDisabled: Em.computed.or('model.isSaving', 'deleting', 'cannotDeleteAccount'), + + reset() { + this.setProperties({ + passwordProgress: null + }); + }, + + @computed() + canChangePassword() { + return !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins; + }, + + actions: { + changePassword() { + if (!this.get('passwordProgress')) { + this.set('passwordProgress', I18n.t("user.change_password.in_progress")); + return this.get('model').changePassword().then(() => { + // password changed + this.setProperties({ + changePasswordProgress: false, + passwordProgress: I18n.t("user.change_password.success") + }); + }).catch(() => { + // password failed to change + this.setProperties({ + changePasswordProgress: false, + passwordProgress: I18n.t("user.change_password.error") + }); + }); + } + }, + + delete() { + this.set('deleting', true); + const self = this, + message = I18n.t('user.delete_account_confirm'), + model = this.get('model'), + buttons = [ + { label: I18n.t("cancel"), + class: "cancel-inline", + link: true, + callback: () => { this.set('deleting', false); } + }, + { label: ' ' + I18n.t("user.delete_account"), + class: "btn btn-danger", + callback() { + model.delete().then(function() { + bootbox.alert(I18n.t('user.deleted_yourself'), function() { + window.location.pathname = Discourse.getURL('/'); + }); + }, function() { + bootbox.alert(I18n.t('user.delete_yourself_not_allowed')); + self.set('deleting', false); + }); + } + } + ]; + bootbox.dialog(message, buttons, {"classes": "delete-account"}); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6 new file mode 100644 index 00000000000..66aa5ccc685 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6 @@ -0,0 +1,20 @@ +import PreferencesTabController from "discourse/mixins/preferences-tab-controller"; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Ember.Controller.extend(PreferencesTabController, { + saveAttrNames: [ + 'muted_category_ids', + 'watched_category_ids', + 'tracked_category_ids', + 'watched_first_post_category_ids' + ], + + actions: { + save() { + this.set('saved', false); + return this.get('model').save(this.get('saveAttrNames')).then(() => { + this.set('saved', true); + }).catch(popupAjaxError); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/preferences/emails.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/emails.js.es6 new file mode 100644 index 00000000000..db25accf5a1 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/preferences/emails.js.es6 @@ -0,0 +1,62 @@ +import PreferencesTabController from "discourse/mixins/preferences-tab-controller"; +import { default as computed } from "ember-addons/ember-computed-decorators"; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Ember.Controller.extend(PreferencesTabController, { + + saveAttrNames: [ + 'email_always', + 'mailing_list_mode', + 'mailing_list_mode_frequency', + 'email_digests', + 'email_direct', + 'email_in_reply_to', + 'email_private_messages', + 'email_previous_replies', + 'digest_after_minutes', + 'include_tl0_in_digests' + ], + + @computed() + frequencyEstimate() { + var estimate = this.get('model.mailing_list_posts_per_day'); + if (!estimate || estimate < 2) { + return I18n.t('user.mailing_list_mode.few_per_day'); + } else { + return I18n.t('user.mailing_list_mode.many_per_day', { dailyEmailEstimate: estimate }); + } + }, + + @computed() + mailingListModeOptions() { + return [ + {name: I18n.t('user.mailing_list_mode.daily'), value: 0}, + {name: this.get('frequencyEstimate'), value: 1}, + {name: I18n.t('user.mailing_list_mode.individual_no_echo'), value: 2} + ]; + }, + + previousRepliesOptions: [ + {name: I18n.t('user.email_previous_replies.always'), value: 0}, + {name: I18n.t('user.email_previous_replies.unless_emailed'), value: 1}, + {name: I18n.t('user.email_previous_replies.never'), value: 2} + ], + + digestFrequencies: [ + { name: I18n.t('user.email_digests.every_30_minutes'), value: 30 }, + { name: I18n.t('user.email_digests.every_hour'), value: 60 }, + { name: I18n.t('user.email_digests.daily'), value: 1440 }, + { name: I18n.t('user.email_digests.every_three_days'), value: 4320 }, + { name: I18n.t('user.email_digests.weekly'), value: 10080 }, + { name: I18n.t('user.email_digests.every_two_weeks'), value: 20160 } + ], + + actions: { + save() { + this.set('saved', false); + return this.get('model').save(this.get('saveAttrNames')).then(() => { + this.set('saved', true); + }).catch(popupAjaxError); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 new file mode 100644 index 00000000000..324c3da7d2b --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 @@ -0,0 +1,45 @@ +import PreferencesTabController from "discourse/mixins/preferences-tab-controller"; +import { default as computed, observes } from "ember-addons/ember-computed-decorators"; +import { listThemes, previewTheme } from 'discourse/lib/theme-selector'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; +import { selectDefaultTheme } from 'discourse/lib/theme-selector'; + +export default Ember.Controller.extend(PreferencesTabController, { + + saveAttrNames: [ + 'locale', + 'external_links_in_new_tab', + 'dynamic_favicon', + 'enable_quoting', + 'disable_jump_reply', + 'automatically_unpin_topics' + ], + + preferencesController: Ember.inject.controller('preferences'), + + @computed() + availableLocales() { + return this.siteSettings.available_locales.split('|').map(s => ({ name: s, value: s })); + }, + + userSelectableThemes: function(){ + return listThemes(this.site); + }.property(), + + @observes("selectedTheme") + themeKeyChanged() { + let key = this.get("selectedTheme"); + this.get('preferencesController').set('selectedTheme', key); + previewTheme(key); + }, + + actions: { + save() { + this.set('saved', false); + return this.get('model').save(this.get('saveAttrNames')).then(() => { + this.set('saved', true); + selectDefaultTheme(this.get('selectedTheme')); + }).catch(popupAjaxError); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6 new file mode 100644 index 00000000000..8129641b5dc --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6 @@ -0,0 +1,56 @@ +import PreferencesTabController from "discourse/mixins/preferences-tab-controller"; +import { default as computed } from "ember-addons/ember-computed-decorators"; +import { NotificationLevels } from 'discourse/lib/notification-levels'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Ember.Controller.extend(PreferencesTabController, { + + saveAttrNames:[ + 'muted_usernames', + 'new_topic_duration_minutes', + 'auto_track_topics_after_msecs', + 'notification_level_when_replying', + 'like_notification_frequency' + ], + + @computed("model.watchedCategories", "model.trackedCategories", "model.mutedCategories") + selectedCategories(watched, tracked, muted) { + return [].concat(watched, tracked, muted); + }, + + likeNotificationFrequencies: [{ name: I18n.t('user.like_notification_frequency.always'), value: 0 }, + { name: I18n.t('user.like_notification_frequency.first_time_and_daily'), value: 1 }, + { name: I18n.t('user.like_notification_frequency.first_time'), value: 2 }, + { name: I18n.t('user.like_notification_frequency.never'), value: 3 }], + + autoTrackDurations: [{ name: I18n.t('user.auto_track_options.never'), value: -1 }, + { name: I18n.t('user.auto_track_options.immediately'), value: 0 }, + { name: I18n.t('user.auto_track_options.after_30_seconds'), value: 30000 }, + { name: I18n.t('user.auto_track_options.after_1_minute'), value: 60000 }, + { name: I18n.t('user.auto_track_options.after_2_minutes'), value: 120000 }, + { name: I18n.t('user.auto_track_options.after_3_minutes'), value: 180000 }, + { name: I18n.t('user.auto_track_options.after_4_minutes'), value: 240000 }, + { name: I18n.t('user.auto_track_options.after_5_minutes'), value: 300000 }, + { name: I18n.t('user.auto_track_options.after_10_minutes'), value: 600000 }], + + notificationLevelsForReplying: [{ name: I18n.t('topic.notifications.watching.title'), value: NotificationLevels.WATCHING }, + { name: I18n.t('topic.notifications.tracking.title'), value: NotificationLevels.TRACKING }, + { name: I18n.t('topic.notifications.regular.title'), value: NotificationLevels.REGULAR }], + + considerNewTopicOptions: [{ name: I18n.t('user.new_topic_duration.not_viewed'), value: -1 }, + { name: I18n.t('user.new_topic_duration.after_1_day'), value: 60 * 24 }, + { name: I18n.t('user.new_topic_duration.after_2_days'), value: 60 * 48 }, + { name: I18n.t('user.new_topic_duration.after_1_week'), value: 7 * 60 * 24 }, + { name: I18n.t('user.new_topic_duration.after_2_weeks'), value: 2 * 7 * 60 * 24 }, + { name: I18n.t('user.new_topic_duration.last_here'), value: -2 }], + + actions: { + save() { + this.set('saved', false); + return this.get('model').save(this.get('saveAttrNames')).then(() => { + this.set('saved', true); + }).catch(popupAjaxError); + } + } + +}); diff --git a/app/assets/javascripts/discourse/controllers/preferences/profile.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/profile.js.es6 new file mode 100644 index 00000000000..2cdfd8e6940 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/preferences/profile.js.es6 @@ -0,0 +1,84 @@ +import { default as computed } from "ember-addons/ember-computed-decorators"; +import PreferencesTabController from "discourse/mixins/preferences-tab-controller"; +import { setting } from 'discourse/lib/computed'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; +import { cook } from 'discourse/lib/text'; + +export default Ember.Controller.extend(PreferencesTabController, { + + saveAttrNames: [ + 'name', + 'bio_raw', + 'website', + 'location', + 'custom_fields', + 'user_fields', + 'profile_background', + 'card_background', + 'date_of_birth' + ], + + canEditName: setting('enable_names'), + + newNameInput: null, + + @computed() + nameInstructions() { + return I18n.t(this.siteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions'); + }, + + @computed("model.has_title_badges") + canSelectTitle(hasTitleBadges) { + return this.siteSettings.enable_badges && hasTitleBadges; + }, + + @computed("model.user_fields.@each.value") + userFields() { + let siteUserFields = this.site.get('user_fields'); + if (!Ember.isEmpty(siteUserFields)) { + const userFields = this.get('model.user_fields'); + + // Staff can edit fields that are not `editable` + if (!this.get('currentUser.staff')) { + siteUserFields = siteUserFields.filterBy('editable', true); + } + return siteUserFields.sortBy('position').map(function(field) { + const value = userFields ? userFields[field.get('id').toString()] : null; + return Ember.Object.create({ value, field }); + }); + } + }, + + @computed("model.can_change_bio") + canChangeBio(canChangeBio) + { + return canChangeBio; + }, + + actions: { + + save() { + this.set('saved', false); + + const model = this.get('model'), + userFields = this.get('userFields'); + + model.set('name', this.get('newNameInput')); + + // Update the user fields + if (!Ember.isEmpty(userFields)) { + const modelFields = model.get('user_fields'); + if (!Ember.isEmpty(modelFields)) { + userFields.forEach(function(uf) { + modelFields[uf.get('field.id').toString()] = uf.get('value'); + }); + } + } + + return model.save(this.get('saveAttrNames')).then(() => { + model.set('bio_cooked', cook(model.get('bio_raw'))); + this.set('saved', true); + }).catch(popupAjaxError); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/preferences/tags.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/tags.js.es6 new file mode 100644 index 00000000000..6264d42b2de --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/preferences/tags.js.es6 @@ -0,0 +1,21 @@ +import PreferencesTabController from "discourse/mixins/preferences-tab-controller"; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Ember.Controller.extend(PreferencesTabController, { + + saveAttrNames: [ + 'muted_tags', + 'tracked_tags', + 'watched_tags', + 'watching_first_post_tags' + ], + + actions: { + save() { + this.set('saved', false); + return this.get('model').save(this.get('saveAttrNames')).then(() => { + this.set('saved', true); + }).catch(popupAjaxError); + } + } +}); diff --git a/app/assets/javascripts/discourse/mixins/preferences-tab-controller.js.es6 b/app/assets/javascripts/discourse/mixins/preferences-tab-controller.js.es6 new file mode 100644 index 00000000000..a866ba3de63 --- /dev/null +++ b/app/assets/javascripts/discourse/mixins/preferences-tab-controller.js.es6 @@ -0,0 +1,10 @@ +import { default as computed } from "ember-addons/ember-computed-decorators"; + +export default Ember.Mixin.create({ + saved: false, + + @computed("model.isSaving") + saveButtonText(isSaving) { + return isSaving ? I18n.t('saving') : I18n.t('save'); + } +}); diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 6b45997078f..17fb983eaa7 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -201,8 +201,9 @@ const User = RestModel.extend({ return Discourse.User.create(this.getProperties(Object.keys(this))); }, - save() { - const data = this.getProperties( + save(fields) { + + let userFields = [ 'bio_raw', 'website', 'location', @@ -217,43 +218,55 @@ const User = RestModel.extend({ 'tracked_tags', 'watched_tags', 'watching_first_post_tags', - 'date_of_birth'); + 'date_of_birth' + ]; - ['email_always', - 'mailing_list_mode', - 'mailing_list_mode_frequency', - 'external_links_in_new_tab', - 'email_digests', - 'email_direct', - 'email_in_reply_to', - 'email_private_messages', - 'email_previous_replies', - 'dynamic_favicon', - 'enable_quoting', - 'disable_jump_reply', - 'automatically_unpin_topics', - 'digest_after_minutes', - 'new_topic_duration_minutes', - 'auto_track_topics_after_msecs', - 'notification_level_when_replying', - 'like_notification_frequency', - 'include_tl0_in_digests' - ].forEach(s => { + const data = this.getProperties(fields ? _.intersection(userFields, fields) : userFields); + + let userOptionFields = [ + 'email_always', + 'mailing_list_mode', + 'mailing_list_mode_frequency', + 'external_links_in_new_tab', + 'email_digests', + 'email_direct', + 'email_in_reply_to', + 'email_private_messages', + 'email_previous_replies', + 'dynamic_favicon', + 'enable_quoting', + 'disable_jump_reply', + 'automatically_unpin_topics', + 'digest_after_minutes', + 'new_topic_duration_minutes', + 'auto_track_topics_after_msecs', + 'notification_level_when_replying', + 'like_notification_frequency', + 'include_tl0_in_digests' + ]; + + if (fields) { + userOptionFields = _.intersection(userOptionFields, fields); + } + + userOptionFields.forEach(s => { data[s] = this.get(`user_option.${s}`); }); var updatedState = {}; ['muted','watched','tracked','watched_first_post'].forEach(s => { - let prop = s === "watched_first_post" ? "watchedFirstPostCategories" : s + "Categories"; - let cats = this.get(prop); - if (cats) { - let cat_ids = cats.map(c => c.get('id')); - updatedState[s + '_category_ids'] = cat_ids; + if (fields === undefined || fields.includes(s + '_category_ids')) { + let prop = s === "watched_first_post" ? "watchedFirstPostCategories" : s + "Categories"; + let cats = this.get(prop); + if (cats) { + let cat_ids = cats.map(c => c.get('id')); + updatedState[s + '_category_ids'] = cat_ids; - // HACK: denote lack of categories - if (cats.length === 0) { cat_ids = [-1]; } - data[s + '_category_ids'] = cat_ids; + // HACK: denote lack of categories + if (cats.length === 0) { cat_ids = [-1]; } + data[s + '_category_ids'] = cat_ids; + } } }); diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index cb0bdb8d5d3..65b4b1e1bc8 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -93,6 +93,15 @@ export default function() { }); this.route('preferences', { resetNamespace: true }, function() { + this.route('account'); + this.route('profile'); + this.route('emails'); + this.route('notifications'); + this.route('categories'); + this.route('tags'); + this.route('interface'); + this.route('apps'); + this.route('username'); this.route('email'); this.route('about', { path: '/about-me' }); diff --git a/app/assets/javascripts/discourse/routes/preferences-account.js.es6 b/app/assets/javascripts/discourse/routes/preferences-account.js.es6 new file mode 100644 index 00000000000..32be645d4f3 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/preferences-account.js.es6 @@ -0,0 +1,8 @@ +import RestrictedUserRoute from "discourse/routes/restricted-user"; + +export default RestrictedUserRoute.extend({ + setupController(controller, user) { + controller.reset(); + controller.setProperties({ model: user }); + } +}); diff --git a/app/assets/javascripts/discourse/routes/preferences-index.js.es6 b/app/assets/javascripts/discourse/routes/preferences-index.js.es6 index bb568ff6729..3006f4b8533 100644 --- a/app/assets/javascripts/discourse/routes/preferences-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences-index.js.es6 @@ -1,7 +1,7 @@ import RestrictedUserRoute from "discourse/routes/restricted-user"; export default RestrictedUserRoute.extend({ - renderTemplate: function() { - this.render('preferences', { into: 'user', controller: 'preferences' }); + redirect() { + this.transitionTo('preferences.account'); } }); diff --git a/app/assets/javascripts/discourse/routes/preferences-interface.js.es6 b/app/assets/javascripts/discourse/routes/preferences-interface.js.es6 new file mode 100644 index 00000000000..16f0ba0ca23 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/preferences-interface.js.es6 @@ -0,0 +1,11 @@ +import RestrictedUserRoute from "discourse/routes/restricted-user"; +import { currentThemeKey } from 'discourse/lib/theme-selector'; + +export default RestrictedUserRoute.extend({ + setupController(controller, user) { + controller.setProperties({ + model: user, + selectedTheme: $.cookie('theme_key') || currentThemeKey() + }); + } +}); diff --git a/app/assets/javascripts/discourse/routes/preferences-profile.js.es6 b/app/assets/javascripts/discourse/routes/preferences-profile.js.es6 new file mode 100644 index 00000000000..2f7c61b128d --- /dev/null +++ b/app/assets/javascripts/discourse/routes/preferences-profile.js.es6 @@ -0,0 +1,10 @@ +import RestrictedUserRoute from "discourse/routes/restricted-user"; + +export default RestrictedUserRoute.extend({ + setupController(controller, user) { + controller.setProperties({ + model: user, + newNameInput: user.get('name') + }); + } +}); diff --git a/app/assets/javascripts/discourse/routes/preferences.js.es6 b/app/assets/javascripts/discourse/routes/preferences.js.es6 index 3ff42ca52ea..e1cc50fc5cc 100644 --- a/app/assets/javascripts/discourse/routes/preferences.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences.js.es6 @@ -1,7 +1,6 @@ import RestrictedUserRoute from "discourse/routes/restricted-user"; import showModal from 'discourse/lib/show-modal'; import { popupAjaxError } from 'discourse/lib/ajax-error'; -import { currentThemeKey } from 'discourse/lib/theme-selector'; export default RestrictedUserRoute.extend({ model() { @@ -9,11 +8,8 @@ export default RestrictedUserRoute.extend({ }, setupController(controller, user) { - controller.reset(); controller.setProperties({ - model: user, - newNameInput: user.get('name'), - selectedTheme: $.cookie('theme_key') || currentThemeKey() + model: user }); }, diff --git a/app/assets/javascripts/discourse/templates/components/user-fields/text.hbs b/app/assets/javascripts/discourse/templates/components/user-fields/text.hbs index f008167a800..78621b55de8 100644 --- a/app/assets/javascripts/discourse/templates/components/user-fields/text.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-fields/text.hbs @@ -1,6 +1,6 @@ - +
{{input value=value maxlength=site.user_field_max_length}} {{#if field.required}}*{{/if}} -

{{{field.description}}}

+
{{{field.description}}}
diff --git a/app/assets/javascripts/discourse/templates/preferences.hbs b/app/assets/javascripts/discourse/templates/preferences.hbs index 3a0ec94d69b..818f1b9886e 100644 --- a/app/assets/javascripts/discourse/templates/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/preferences.hbs @@ -1,383 +1,24 @@ -{{#d-section pageClass="user-preferences" class="user-content user-preferences"}} +{{#d-section pageClass="user-preferences" class="user-navigation"}} + {{#mobile-nav class='preferences-nav' desktopClass='preferences-list action-list nav-stacked' currentPath=application.currentPath}} + + + + + + {{#if siteSettings.tagging_enabled}} + + {{/if}} + + {{#if model.userApiKeys}} + + {{/if}} + {{/mobile-nav}} +{{/d-section}} +
{{plugin-outlet name="above-user-preferences"}} -
- -
-
- {{partial 'user/preferences/save-button'}} -
-
- -
- -
- {{model.username}} - {{#if model.can_edit_username}} - {{#link-to "preferences.username" class="btn btn-small pad-left no-text"}}{{/link-to}} - {{/if}} -
-
- {{{i18n 'user.username.short_instructions' username=model.username}}} -
-
- - {{#if canEditName}} -
- -
- {{#if model.can_edit_name}} - {{text-field value=newNameInput classNames="input-xxlarge"}} - {{else}} - {{model.name}} - {{/if}} -
-
- {{nameInstructions}} -
-
- {{/if}} - - {{#if canSelectTitle}} -
- -
- {{model.title}} - {{#link-to "preferences.badgeTitle" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}} -
-
- {{/if}} - - {{#if canCheckEmails}} -
- - {{#if model.email}} -
- {{model.email}} - {{#if model.can_edit_email}} - {{#link-to "preferences.email" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}} - {{/if}} -
-
- {{i18n 'user.email.instructions'}} -
- {{else}} -
- {{d-button action="checkEmail" actionParam=model title="admin.users.check_email.title" icon="envelope-o" label="admin.users.check_email.text"}} -
- {{/if}} -
- {{/if}} - - {{#if canChangePassword}} - - {{/if}} - -
- -
- {{! we want the "huge" version even though we're downsizing it to "large" in CSS }} - {{bound-avatar model "huge"}} - {{#unless siteSettings.sso_overrides_avatar}} - {{d-button action="showAvatarSelector" class="pad-left" icon="pencil"}} - {{/unless}} -
-
- - {{#if siteSettings.allow_profile_backgrounds}} -
- -
- {{image-uploader imageUrl=model.profile_background type="profile_background"}} -
-
- {{i18n 'user.change_profile_background.instructions'}} -
-
- -
- -
- {{image-uploader imageUrl=model.card_background type="card_background"}} -
-
- {{i18n 'user.change_card_background.instructions'}} -
-
- {{/if}} - - {{#if siteSettings.allow_user_locale}} -
- -
- {{combo-box valueAttribute="value" content=availableLocales value=model.locale none="user.locale.default"}} -
-
- {{i18n 'user.locale.instructions'}} -
-
- {{/if}} - - {{#if canChangeBio}} -
- -
- {{d-editor value=model.bio_raw}} -
-
- {{/if}} - - {{#each userFields as |uf|}} - {{user-field field=uf.field value=uf.value}} - {{/each}} -
- -
- -
- {{input type="text" value=model.location class="input-xxlarge" id='edit-location'}} -
-
- -
- -
- {{input type="text" value=model.website class="input-xxlarge"}} -
-
- -
- -
- {{#if model.card_image_badge}} - {{icon-or-image model.card_image_badge}} - {{/if}} - {{#link-to "preferences.card-badge" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}} -
-
- -
- -
- - {{combo-box valueAttribute="value" content=previousRepliesOptions value=model.user_option.email_previous_replies}} -
- {{preference-checkbox labelKey="user.email_in_reply_to" checked=model.user_option.email_in_reply_to}} - {{preference-checkbox labelKey="user.email_private_messages" checked=model.user_option.email_private_messages}} - {{preference-checkbox labelKey="user.email_direct" checked=model.user_option.email_direct}} - {{preference-checkbox labelKey="user.email_always" checked=model.user_option.email_always}} - {{#unless model.user_option.email_always}} -
- {{#if siteSettings.email_time_window_mins}} - {{i18n 'user.email.frequency' count=siteSettings.email_time_window_mins}} - {{else}} - {{i18n 'user.email.frequency_immediately'}} - {{/if}} -
- {{/unless}} -
- - {{#unless siteSettings.disable_digest_emails}} -
- - {{preference-checkbox labelKey="user.email_digests.title" disabled=model.user_option.mailing_list_mode checked=model.user_option.email_digests}} - {{#if model.user_option.email_digests}} -
- {{combo-box valueAttribute="value" content=digestFrequencies value=model.user_option.digest_after_minutes}} -
- {{preference-checkbox labelKey="user.include_tl0_in_digests" disabled=model.user_option.mailing_list_mode checked=model.user_option.include_tl0_in_digests}} - {{/if}} -
- {{/unless}} - - {{#unless siteSettings.disable_mailing_list_mode}} -
- - {{preference-checkbox labelKey="user.mailing_list_mode.enabled" checked=model.user_option.mailing_list_mode}} -
{{{i18n 'user.mailing_list_mode.instructions'}}}
- {{#if model.user_option.mailing_list_mode}} -
- {{combo-box valueAttribute="value" content=mailingListModeOptions value=model.user_option.mailing_list_mode_frequency}} -
- {{/if}} -
- {{/unless}} - -
- - {{desktop-notification-config}} -
{{i18n 'user.desktop_notifications.each_browser_note'}}
-
- -
- - -
- - {{combo-box valueAttribute="value" content=considerNewTopicOptions value=model.user_option.new_topic_duration_minutes}} -
- -
- - {{combo-box valueAttribute="value" content=autoTrackDurations value=model.user_option.auto_track_topics_after_msecs}} -
- -
- - {{combo-box valueAttribute="value" content=notificationLevelsForReplying value=model.user_option.notification_level_when_replying}} -
- -
- - {{combo-box valueAttribute="value" content=likeNotificationFrequencies value=model.user_option.like_notification_frequency}} -
- - {{preference-checkbox labelKey="user.external_links_in_new_tab" checked=model.user_option.external_links_in_new_tab}} - {{preference-checkbox labelKey="user.enable_quoting" checked=model.user_option.enable_quoting}} - {{preference-checkbox labelKey="user.dynamic_favicon" checked=model.user_option.dynamic_favicon}} - {{preference-checkbox labelKey="user.disable_jump_reply" checked=model.user_option.disable_jump_reply}} - {{plugin-outlet name="user-custom-preferences" args=(hash model=model)}} -
- -
- -
- - {{category-selector categories=model.watchedCategories blacklist=selectedCategories}} -
-
{{i18n 'user.watched_categories_instructions'}}
- -
- - {{category-selector categories=model.trackedCategories blacklist=selectedCategories}} -
-
{{i18n 'user.tracked_categories_instructions'}}
- -
- - {{category-selector categories=model.watchedFirstPostCategories}} -
-
{{i18n 'user.watched_first_post_categories_instructions'}}
-
- - {{category-selector categories=model.mutedCategories blacklist=selectedCategories}} -
-
{{i18n 'user.muted_categories_instructions'}}
- -
- - {{#if siteSettings.tagging_enabled}} -
- -
- - {{tag-chooser tags=model.watched_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}} -
-
{{i18n 'user.watched_tags_instructions'}}
-
- - {{tag-chooser tags=model.tracked_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}} -
-
{{i18n 'user.tracked_tags_instructions'}}
-
- - {{tag-chooser tags=model.watching_first_post_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}} -
-
{{i18n 'user.watched_first_post_tags_instructions'}}
-
- - {{tag-chooser tags=model.muted_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}} -
-
{{i18n 'user.muted_tags_instructions'}}
-
- {{/if}} - -
- -
- - {{user-selector excludeCurrentUser=true usernames=model.muted_usernames class="user-selector"}} -
-
{{i18n 'user.muted_users_instructions'}}
-
- - {{#if siteSettings.automatically_unpin_topics}} -
- - {{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.user_option.automatically_unpin_topics}} -
- {{/if}} - - {{#if model.userApiKeys}} -
- -
- {{#each model.userApiKeys as |key|}} -
- {{key.application_name}} - {{#if key.revoked}} - {{d-button action="undoRevokeApiKey" actionParam=key class="btn" label="user.undo_revoke_access"}} - {{else}} - {{d-button action="revokeApiKey" actionParam=key class="btn" label="user.revoke_access"}} - {{/if}} -

-

    - {{#each key.scopes as |scope|}} -
  • {{scope}}
  • - {{/each}} -
-

-

{{i18n "user.api_approved"}} {{bound-date key.created_at}}

-
- {{/each}} -
-
- {{/if}} - - {{#if userSelectableThemes}} -
- -
- {{combo-box content=userSelectableThemes value=selectedTheme}} -
-
- {{/if}} - - {{plugin-outlet name="user-custom-controls" args=(hash model=model)}} - -
-
- {{partial 'user/preferences/save-button'}} -
-
- - {{#if model.canDeleteAccount}} - - {{/if}} + + {{outlet}}
-{{/d-section}} +
diff --git a/app/assets/javascripts/discourse/templates/preferences/account.hbs b/app/assets/javascripts/discourse/templates/preferences/account.hbs new file mode 100644 index 00000000000..fa081a06a33 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/preferences/account.hbs @@ -0,0 +1,61 @@ +
+ +
+ {{model.username}} + {{#if model.can_edit_username}} + {{#link-to "preferences.username" class="btn btn-small pad-left no-text"}}{{/link-to}} + {{/if}} +
+
+ {{{i18n 'user.username.short_instructions' username=model.username}}} +
+
+ +{{#if canCheckEmails}} +
+ + {{#if model.email}} +
+ {{model.email}} + {{#if model.can_edit_email}} + {{#link-to "preferences.email" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}} + {{/if}} +
+
+ {{i18n 'user.email.instructions'}} +
+ {{else}} +
+ {{d-button action="checkEmail" actionParam=model title="admin.users.check_email.title" icon="envelope-o" label="admin.users.check_email.text"}} +
+ {{/if}} +
+{{/if}} + +{{#if canChangePassword}} +
+ + +
+{{/if}} + +
+ +{{#if model.canDeleteAccount}} +
+
+
+ {{d-button action="delete" disabled=deleteDisabled class="btn-danger" icon="trash-o" label="user.delete_account"}} +
+
+{{/if}} diff --git a/app/assets/javascripts/discourse/templates/preferences/apps.hbs b/app/assets/javascripts/discourse/templates/preferences/apps.hbs new file mode 100644 index 00000000000..3bd448c5bc0 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/preferences/apps.hbs @@ -0,0 +1,25 @@ +{{#if model.userApiKeys}} +
+ +
+ {{#each model.userApiKeys as |key|}} +
+ {{key.application_name}} + {{#if key.revoked}} + {{d-button action="undoRevokeApiKey" actionParam=key class="btn" label="user.undo_revoke_access"}} + {{else}} + {{d-button action="revokeApiKey" actionParam=key class="btn" label="user.revoke_access"}} + {{/if}} +

+

    + {{#each key.scopes as |scope|}} +
  • {{scope}}
  • + {{/each}} +
+

+

{{i18n "user.api_approved"}} {{bound-date key.created_at}}

+
+ {{/each}} +
+
+{{/if}} diff --git a/app/assets/javascripts/discourse/templates/preferences/categories.hbs b/app/assets/javascripts/discourse/templates/preferences/categories.hbs new file mode 100644 index 00000000000..99043e5d62b --- /dev/null +++ b/app/assets/javascripts/discourse/templates/preferences/categories.hbs @@ -0,0 +1,48 @@ +
+ + + +
+ + {{category-selector categories=model.watchedCategories blacklist=selectedCategories}} +
+
{{i18n 'user.watched_categories_instructions'}}
+ + +
+ + {{category-selector categories=model.trackedCategories blacklist=selectedCategories}} +
+
{{i18n 'user.tracked_categories_instructions'}}
+ + +
+ + {{category-selector categories=model.watchedFirstPostCategories}} +
+
{{i18n 'user.watched_first_post_categories_instructions'}}
+ +
+ + {{category-selector categories=model.mutedCategories blacklist=selectedCategories}} +
+
{{i18n 'user.muted_categories_instructions'}}
+ + +
+ +
+ +{{plugin-outlet name="user-custom-controls" args=(hash model=model)}} + +
+
+ {{partial 'user/preferences/save-button'}} +
+
diff --git a/app/assets/javascripts/discourse/templates/preferences/emails.hbs b/app/assets/javascripts/discourse/templates/preferences/emails.hbs new file mode 100644 index 00000000000..8c151f50793 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/preferences/emails.hbs @@ -0,0 +1,56 @@ +
+ +
+ + {{combo-box valueAttribute="value" content=previousRepliesOptions value=model.user_option.email_previous_replies}} +
+ {{preference-checkbox labelKey="user.email_in_reply_to" checked=model.user_option.email_in_reply_to}} + {{preference-checkbox labelKey="user.email_private_messages" checked=model.user_option.email_private_messages}} + {{preference-checkbox labelKey="user.email_direct" checked=model.user_option.email_direct}} + {{preference-checkbox labelKey="user.email_always" checked=model.user_option.email_always}} + {{#unless model.user_option.email_always}} +
+ {{#if siteSettings.email_time_window_mins}} + {{i18n 'user.email.frequency' count=siteSettings.email_time_window_mins}} + {{else}} + {{i18n 'user.email.frequency_immediately'}} + {{/if}} +
+ {{/unless}} +
+ +{{#unless siteSettings.disable_digest_emails}} +
+ + {{preference-checkbox labelKey="user.email_digests.title" disabled=model.user_option.mailing_list_mode checked=model.user_option.email_digests}} + {{#if model.user_option.email_digests}} +
+ {{combo-box valueAttribute="value" content=digestFrequencies value=model.user_option.digest_after_minutes}} +
+ {{preference-checkbox labelKey="user.include_tl0_in_digests" disabled=model.user_option.mailing_list_mode checked=model.user_option.include_tl0_in_digests}} + {{/if}} +
+{{/unless}} + +{{#unless siteSettings.disable_mailing_list_mode}} +
+ + {{preference-checkbox labelKey="user.mailing_list_mode.enabled" checked=model.user_option.mailing_list_mode}} +
{{{i18n 'user.mailing_list_mode.instructions'}}}
+ {{#if model.user_option.mailing_list_mode}} +
+ {{combo-box valueAttribute="value" content=mailingListModeOptions value=model.user_option.mailing_list_mode_frequency}} +
+ {{/if}} +
+{{/unless}} + +
+ +{{plugin-outlet name="user-custom-controls" args=(hash model=model)}} + +
+
+ {{partial 'user/preferences/save-button'}} +
+
diff --git a/app/assets/javascripts/discourse/templates/preferences/interface.hbs b/app/assets/javascripts/discourse/templates/preferences/interface.hbs new file mode 100644 index 00000000000..0b38b17ec27 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/preferences/interface.hbs @@ -0,0 +1,47 @@ +{{#if userSelectableThemes}} +
+ +
+ {{combo-box content=userSelectableThemes value=selectedTheme}} +
+
+{{/if}} + +{{#if siteSettings.allow_user_locale}} +
+ +
+ {{combo-box valueAttribute="value" content=availableLocales value=model.locale none="user.locale.default"}} +
+
+ {{i18n 'user.locale.instructions'}} +
+
+{{/if}} + + +
+ + + {{preference-checkbox labelKey="user.external_links_in_new_tab" checked=model.user_option.external_links_in_new_tab}} + {{preference-checkbox labelKey="user.enable_quoting" checked=model.user_option.enable_quoting}} + {{preference-checkbox labelKey="user.dynamic_favicon" checked=model.user_option.dynamic_favicon}} + {{preference-checkbox labelKey="user.disable_jump_reply" checked=model.user_option.disable_jump_reply}} +
+ +{{#if siteSettings.automatically_unpin_topics}} +
+ + {{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.user_option.automatically_unpin_topics}} +
+{{/if}} + +
+ +{{plugin-outlet name="user-custom-controls" args=(hash model=model)}} + +
+
+ {{partial 'user/preferences/save-button'}} +
+
diff --git a/app/assets/javascripts/discourse/templates/preferences/notifications.hbs b/app/assets/javascripts/discourse/templates/preferences/notifications.hbs new file mode 100644 index 00000000000..3feaaa40f1e --- /dev/null +++ b/app/assets/javascripts/discourse/templates/preferences/notifications.hbs @@ -0,0 +1,46 @@ +
+
+ + {{combo-box valueAttribute="value" content=considerNewTopicOptions value=model.user_option.new_topic_duration_minutes}} +
+ +
+ + {{combo-box valueAttribute="value" content=autoTrackDurations value=model.user_option.auto_track_topics_after_msecs}} +
+ +
+ + {{combo-box valueAttribute="value" content=notificationLevelsForReplying value=model.user_option.notification_level_when_replying}} +
+ +
+ + {{combo-box valueAttribute="value" content=likeNotificationFrequencies value=model.user_option.like_notification_frequency}} +
+
+ +
+ + {{desktop-notification-config}} +
{{i18n 'user.desktop_notifications.each_browser_note'}}
+
+ +
+ +
+ + {{user-selector excludeCurrentUser=true usernames=model.muted_usernames class="user-selector"}} +
+
{{i18n 'user.muted_users_instructions'}}
+
+ +
+ +{{plugin-outlet name="user-custom-controls" args=(hash model=model)}} + +
+
+ {{partial 'user/preferences/save-button'}} +
+
diff --git a/app/assets/javascripts/discourse/templates/preferences/profile.hbs b/app/assets/javascripts/discourse/templates/preferences/profile.hbs new file mode 100644 index 00000000000..1f9bb65cfd5 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/preferences/profile.hbs @@ -0,0 +1,110 @@ +{{#if canEditName}} +
+ +
+ {{#if model.can_edit_name}} + {{text-field value=newNameInput classNames="input-xxlarge"}} + {{else}} + {{model.name}} + {{/if}} +
+
+ {{nameInstructions}} +
+
+{{/if}} + +{{#if canSelectTitle}} +
+ +
+ {{model.title}} + {{#link-to "preferences.badgeTitle" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}} +
+
+{{/if}} + +
+ +
+ {{! we want the "huge" version even though we're downsizing it to "large" in CSS }} + {{bound-avatar model "huge"}} + {{#unless siteSettings.sso_overrides_avatar}} + {{d-button action="showAvatarSelector" class="pad-left" icon="pencil"}} + {{/unless}} +
+
+ +{{#if siteSettings.allow_profile_backgrounds}} +
+ +
+ {{image-uploader imageUrl=model.profile_background type="profile_background"}} +
+
+ {{i18n 'user.change_profile_background.instructions'}} +
+
+ +
+ +
+ {{image-uploader imageUrl=model.card_background type="card_background"}} +
+
+ {{i18n 'user.change_card_background.instructions'}} +
+
+{{/if}} + +{{#if canChangeBio}} +
+ +
+ {{d-editor value=model.bio_raw}} +
+
+{{/if}} + +
+ +
+ {{input type="text" value=model.location class="input-xxlarge" id='edit-location'}} +
+
+ +
+ +
+ {{input type="text" value=model.website class="input-xxlarge"}} +
+
+ +{{#each userFields as |uf|}} +
+ {{user-field field=uf.field value=uf.value}} +
+{{/each}} +
+ +
+ +
+ {{#if model.card_image_badge}} + {{icon-or-image model.card_image_badge}} + {{/if}} + {{#link-to "preferences.card-badge" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}} +
+
+ +{{plugin-outlet name="user-custom-preferences" args=(hash model=model)}} + +
+ +{{plugin-outlet name="user-custom-controls" args=(hash model=model)}} + +
+
+ {{partial 'user/preferences/save-button'}} +
+
diff --git a/app/assets/javascripts/discourse/templates/preferences/tags.hbs b/app/assets/javascripts/discourse/templates/preferences/tags.hbs new file mode 100644 index 00000000000..76a12a4245e --- /dev/null +++ b/app/assets/javascripts/discourse/templates/preferences/tags.hbs @@ -0,0 +1,41 @@ +{{#if siteSettings.tagging_enabled}} +
+ + + +
+ + {{tag-chooser tags=model.watched_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}} +
+
{{i18n 'user.watched_tags_instructions'}}
+ +
+ + {{tag-chooser tags=model.tracked_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}} +
+
{{i18n 'user.tracked_tags_instructions'}}
+ +
+ + {{tag-chooser tags=model.watching_first_post_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}} +
+
{{i18n 'user.watched_first_post_tags_instructions'}}
+ +
+ + {{tag-chooser tags=model.muted_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}} +
+
{{i18n 'user.muted_tags_instructions'}}
+ +
+ +
+ +{{plugin-outlet name="user-custom-controls" args=(hash model=model)}} + +
+
+ {{partial 'user/preferences/save-button'}} +
+
+{{/if}} diff --git a/app/assets/stylesheets/common/base/login.scss b/app/assets/stylesheets/common/base/login.scss index 3424a855792..e625ab22945 100644 --- a/app/assets/stylesheets/common/base/login.scss +++ b/app/assets/stylesheets/common/base/login.scss @@ -55,9 +55,12 @@ $input-width: 220px; font-weight: normal; float: auto; } - p { + .instructions { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); margin: 0; + font-size: 0.929em; + font-weight: normal; + line-height: 18px; } } clear: both; diff --git a/app/assets/stylesheets/common/components/navs.scss b/app/assets/stylesheets/common/components/navs.scss index 44952bcf76f..59a34aad62c 100644 --- a/app/assets/stylesheets/common/components/navs.scss +++ b/app/assets/stylesheets/common/components/navs.scss @@ -65,6 +65,9 @@ color: $primary; } } + > li.indent { + padding-left: 15px; + } .active > a, & li > a.active { color: $secondary; diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index d9ff817eb9e..eff53678aec 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -101,6 +101,36 @@ body { } } +body { + .form-vertical input, .form-vertical textarea, .form-vertical select, .form-vertical .input-prepend, .form-vertical .input-append { + display: inline-block; + margin-bottom: 0; + } + + .form-vertical { + .control-group { + margin-bottom: 24px; + &:before { + display: table; + content: ""; + } + &:after { + display: table; + content: ""; + clear: both; + } + } + .control-label { + font-weight: bold; + font-size: 1.2em; + line-height: 2; + } + .controls { + margin-left: 0; + } + } +} + /* bootstrap carryover */ diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index f330c73dc88..197e9003297 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -20,7 +20,9 @@ } .user-preferences { - input.category-selector, input.user-selector { + padding-top: 10px; + + input.category-selector, input.user-selector, input.tag-chooser { width: 530px; } @@ -40,9 +42,9 @@ display: inline-block; } .instructions { + display: inline-block; color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - margin-left: 160px; - margin-top: 5px; + margin-top: 0; margin-bottom: 10px; font-size: 80%; line-height: 1.4em; @@ -66,15 +68,19 @@ } } - .other .controls { + .category-notifications .controls, .tag-notifications .controls { select { width: 280px; } } + + .category-notifications .category-controls, .tag-notifications .tag-controls { + margin-top: 24px; + } } -.form-horizontal .control-group.other { - margin-bottom: 0; +.user-main .user-preferences .user-field.text { + padding-top: 0; } .form-horizontal .control-group.category { @@ -93,11 +99,11 @@ // position: static; } -.user-navigation { +.user-navigation, .user-preferences { display: table-cell; vertical-align: top; width: 170px; - padding-right: 30px; + padding-left: 30px; h3 { color: $primary; @@ -131,7 +137,6 @@ .user-right { width: 900px; - margin-top: 20px; display: table-cell; } @@ -578,12 +583,6 @@ } .user-field { - label { - width: 140px; - float: left; - text-align: right; - font-weight: bold; - } input[type=text] { width: 530px; } @@ -594,7 +593,8 @@ font-weight: normal; float: auto; } - p { + .instructions { + display: block; color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); margin-top: 5px; margin-bottom: 10px; diff --git a/app/assets/stylesheets/mobile/discourse.scss b/app/assets/stylesheets/mobile/discourse.scss index 6354b9c42ec..2f0979efe1f 100644 --- a/app/assets/stylesheets/mobile/discourse.scss +++ b/app/assets/stylesheets/mobile/discourse.scss @@ -102,7 +102,7 @@ h2#site-text-logo } .mobile-view .mobile-nav { - &.messages-nav, &.notifications-nav, &.activity-nav { + &.messages-nav, &.notifications-nav, &.activity-nav, &.preferences-nav { position: absolute; right: 0px; top: -55px; @@ -188,3 +188,33 @@ h2.recent-topics-title { .page-not-found-search h2 { font-size: 1.2em; } + + + +.form-vertical { + input, textarea, select, .input-prepend, .input-append { + display: inline-block; + margin-bottom: 0; + } + + .control-group { + margin-bottom: 12px; + &:before { + display: table; + content: ""; + } + &:after { + display: table; + content: ""; + clear: both; + } + } + .control-label { + font-weight: bold; + font-size: 1.2em; + line-height: 2; + } + .controls { + margin-left: 0; + } +} diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index a82419bd956..7136d89613c 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -2,12 +2,8 @@ .user-preferences { .control-group { - // border-bottom: 1px solid #e5e5e5; padding: 8px 36px 8px 8px; } - .control-label { - font-weight: bold; - } textarea { width: 530px; height: 100px; @@ -39,8 +35,9 @@ width: 520px; } - .other .controls-dropdown { + .controls-dropdown { margin-top: 10px; + margin-bottom: 15px; padding-left: 5px; select { width: 280px; @@ -68,6 +65,16 @@ } textarea {width: 100%;} + + .desktop-notifications button { + float: none; + } + .apps .controls button { + float: right; + } + .category-notifications .category-controls, .tag-notifications .tag-controls { + margin-top: 24px; + } } .profile-image { @@ -123,10 +130,6 @@ } -.form-horizontal .control-group.other { - margin-bottom: 0; -} - .form-horizontal .control-group.category { margin-top: 18px; } @@ -211,9 +214,10 @@ overflow: hidden; color: $secondary; - .secondary { - background: dark-light-diff($primary, $secondary, 90%, -65%); - font-size: 0.929em; + .secondary { + background: dark-light-diff($primary, $secondary, 90%, -65%); + font-size: 0.929em; + .btn { padding: 3px 12px; } dl dd { @@ -549,14 +553,10 @@ .user-field { label { - width: 140px; - float: left; - text-align: right; + width: auto; + text-align: left; font-weight: bold; } - input[type=text] { - width: 530px; - } .controls { label { width: auto; @@ -564,7 +564,7 @@ font-weight: normal; float: auto; } - p { + .instructions { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); margin-top: 5px; margin-bottom: 10px; @@ -581,9 +581,9 @@ margin-top: 20px; .user-content { - width: 100%; - margin-top: 0; - } + width: 100%; + margin-top: 0; + } } .user-archive { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index ab9f96ef6f2..0d2c312851f 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -660,6 +660,16 @@ en: failed_to_move: "Failed to move selected messages (perhaps your network is down)" select_all: "Select All" + preferences_nav: + account: "Account" + profile: "Profile" + emails: "Emails" + notifications: "Notifications" + categories: "Categories" + tags: "Tags" + interface: "Interface" + apps: "Apps" + change_password: success: "(email sent)" in_progress: "(sending email)" diff --git a/config/routes.rb b/config/routes.rb index ba09b3321d8..4fb500853ab 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -347,6 +347,14 @@ Discourse::Application.routes.draw do get "#{root_path}/:username/emails" => "users#check_emails", constraints: {username: USERNAME_ROUTE_FORMAT} get({ "#{root_path}/:username/preferences" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } }.merge(index == 1 ? { as: :email_preferences } : {})) get "#{root_path}/:username/preferences/email" => "users_email#index", constraints: {username: USERNAME_ROUTE_FORMAT} + get "#{root_path}/:username/preferences/account" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} + get "#{root_path}/:username/preferences/profile" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} + get "#{root_path}/:username/preferences/emails" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} + get "#{root_path}/:username/preferences/notifications" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} + get "#{root_path}/:username/preferences/categories" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} + get "#{root_path}/:username/preferences/tags" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} + get "#{root_path}/:username/preferences/interface" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} + get "#{root_path}/:username/preferences/apps" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} put "#{root_path}/:username/preferences/email" => "users_email#update", constraints: {username: USERNAME_ROUTE_FORMAT} get "#{root_path}/:username/preferences/about-me" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} get "#{root_path}/:username/preferences/badge_title" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} diff --git a/test/javascripts/acceptance/preferences-test.js.es6 b/test/javascripts/acceptance/preferences-test.js.es6 index 61fccd74dd5..ec5745ca242 100644 --- a/test/javascripts/acceptance/preferences-test.js.es6 +++ b/test/javascripts/acceptance/preferences-test.js.es6 @@ -6,10 +6,12 @@ test("update some fields", () => { andThen(() => { ok($('body.user-preferences-page').length, "has the body class"); - equal(currentURL(), '/u/eviltrout/preferences', "it doesn't redirect"); + equal(currentURL(), '/u/eviltrout/preferences/account', "defaults to account tab"); ok(exists('.user-preferences'), 'it shows the preferences'); }); + click(".preferences-nav .nav-profile a"); + fillIn("#edit-location", "Westeros"); click('.save-user');