FEATURE: rebuild user preferences page to use tabs

This commit is contained in:
Neil Lalonde 2017-04-26 16:18:16 -04:00
parent f5f4c36795
commit 2503241ce5
35 changed files with 1039 additions and 689 deletions

View File

@ -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: '<i class="fa fa-exclamation-triangle"></i> ' + 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()
});

View File

@ -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: '<i class="fa fa-exclamation-triangle"></i> ' + 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"});
}
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
setupController(controller, user) {
controller.reset();
controller.setProperties({ model: user });
}
});

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<label>{{{field.name}}}</label>
<label class="control-label">{{{field.name}}}</label>
<div class='controls'>
{{input value=value maxlength=site.user_field_max_length}}
{{#if field.required}}<span class='required'>*</span>{{/if}}
<p>{{{field.description}}}</p>
<div class="instructions">{{{field.description}}}</div>
</div>

View File

@ -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}}
<li class='no-glyph nav-account'>{{#link-to 'preferences.account'}}{{i18n 'user.preferences_nav.account'}}{{/link-to}}</li>
<li class='no-glyph nav-profile'>{{#link-to 'preferences.profile'}}{{i18n 'user.preferences_nav.profile'}}{{/link-to}}</li>
<li class='no-glyph nav-emails'>{{#link-to 'preferences.emails'}}{{i18n 'user.preferences_nav.emails'}}{{/link-to}}</li>
<li class='no-glyph nav-notifications'>{{#link-to 'preferences.notifications'}}{{i18n 'user.preferences_nav.notifications'}}{{/link-to}}</li>
<li class='no-glyph indent nav-categories'>{{#link-to 'preferences.categories'}}{{i18n 'user.preferences_nav.categories'}}{{/link-to}}</li>
{{#if siteSettings.tagging_enabled}}
<li class='no-glyph indent nav-tags'>{{#link-to 'preferences.tags'}}{{i18n 'user.preferences_nav.tags'}}{{/link-to}}</li>
{{/if}}
<li class='no-glyph nav-interface'>{{#link-to 'preferences.interface'}}{{i18n 'user.preferences_nav.interface'}}{{/link-to}}</li>
{{#if model.userApiKeys}}
<li class='no-glyph nav-apps'>{{#link-to 'preferences.apps'}}{{i18n 'user.preferences_nav.apps'}}{{/link-to}}</li>
{{/if}}
{{/mobile-nav}}
{{/d-section}}
<section class='user-right user-preferences'>
{{plugin-outlet name="above-user-preferences"}}
<form class="form-horizontal">
<div class="control-group save-button" id='save-button-top'>
<div class="controls">
{{partial 'user/preferences/save-button'}}
</div>
</div>
<div class="control-group pref-username">
<label class="control-label">{{i18n 'user.username.title'}}</label>
<div class="controls">
<span class='static'>{{model.username}}</span>
{{#if model.can_edit_username}}
{{#link-to "preferences.username" class="btn btn-small pad-left no-text"}}<i class="fa fa-pencil"></i>{{/link-to}}
{{/if}}
</div>
<div class='instructions'>
{{{i18n 'user.username.short_instructions' username=model.username}}}
</div>
</div>
{{#if canEditName}}
<div class="control-group pref-name">
<label class="control-label">{{i18n 'user.name.title'}}</label>
<div class="controls">
{{#if model.can_edit_name}}
{{text-field value=newNameInput classNames="input-xxlarge"}}
{{else}}
<span class='static'>{{model.name}}</span>
{{/if}}
</div>
<div class='instructions'>
{{nameInstructions}}
</div>
</div>
{{/if}}
{{#if canSelectTitle}}
<div class="control-group pref-title">
<label class="control-label">{{i18n 'user.title.title'}}</label>
<div class="controls">
<span class="static">{{model.title}}</span>
{{#link-to "preferences.badgeTitle" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}}
</div>
</div>
{{/if}}
{{#if canCheckEmails}}
<div class="control-group pref-email">
<label class="control-label">{{i18n 'user.email.title'}}</label>
{{#if model.email}}
<div class="controls">
<span class='static'>{{model.email}}</span>
{{#if model.can_edit_email}}
{{#link-to "preferences.email" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}}
{{/if}}
</div>
<div class='instructions'>
{{i18n 'user.email.instructions'}}
</div>
{{else}}
<div class="controls">
{{d-button action="checkEmail" actionParam=model title="admin.users.check_email.title" icon="envelope-o" label="admin.users.check_email.text"}}
</div>
{{/if}}
</div>
{{/if}}
{{#if canChangePassword}}
<div class="control-group pref-password">
<label class="control-label">{{i18n 'user.password.title'}}</label>
<div class="controls">
<a href {{action "changePassword"}} class='btn'>
{{fa-icon "envelope"}}
{{#if model.no_password}}
{{i18n 'user.change_password.set_password'}}
{{else}}
{{i18n 'user.change_password.action'}}
{{/if}}
</a>
{{passwordProgress}}
</div>
</div>
{{/if}}
<div class="control-group pref-avatar">
<label class="control-label">{{i18n 'user.avatar.title'}}</label>
<div class="controls">
{{! 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}}
</div>
</div>
{{#if siteSettings.allow_profile_backgrounds}}
<div class="control-group pref-profile-bg">
<label class="control-label">{{i18n 'user.change_profile_background.title'}}</label>
<div class="controls">
{{image-uploader imageUrl=model.profile_background type="profile_background"}}
</div>
<div class='instructions'>
{{i18n 'user.change_profile_background.instructions'}}
</div>
</div>
<div class="control-group pref-profile-bg">
<label class="control-label">{{i18n 'user.change_card_background.title'}}</label>
<div class="controls">
{{image-uploader imageUrl=model.card_background type="card_background"}}
</div>
<div class='instructions'>
{{i18n 'user.change_card_background.instructions'}}
</div>
</div>
{{/if}}
{{#if siteSettings.allow_user_locale}}
<div class="control-group pref-locale">
<label class="control-label">{{i18n 'user.locale.title'}}</label>
<div class="controls">
{{combo-box valueAttribute="value" content=availableLocales value=model.locale none="user.locale.default"}}
</div>
<div class='instructions'>
{{i18n 'user.locale.instructions'}}
</div>
</div>
{{/if}}
{{#if canChangeBio}}
<div class="control-group pref-bio">
<label class="control-label">{{i18n 'user.bio'}}</label>
<div class="controls bio-composer">
{{d-editor value=model.bio_raw}}
</div>
</div>
{{/if}}
{{#each userFields as |uf|}}
{{user-field field=uf.field value=uf.value}}
{{/each}}
<div class='clearfix'></div>
<div class="control-group pref-location">
<label class="control-label">{{i18n 'user.location'}}</label>
<div class="controls">
{{input type="text" value=model.location class="input-xxlarge" id='edit-location'}}
</div>
</div>
<div class="control-group pref-website">
<label class="control-label">{{i18n 'user.website'}}</label>
<div class="controls">
{{input type="text" value=model.website class="input-xxlarge"}}
</div>
</div>
<div class="control-group pref-card-badge">
<label class="control-label">{{i18n 'user.card_badge.title'}}</label>
<div class="controls">
{{#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}}
</div>
</div>
<div class="control-group pref-email-settings">
<label class="control-label">{{i18n 'user.email_settings'}}</label>
<div class='controls controls-dropdown'>
<label>{{i18n 'user.email_previous_replies.title'}}</label>
{{combo-box valueAttribute="value" content=previousRepliesOptions value=model.user_option.email_previous_replies}}
</div>
{{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}}
<div class='instructions'>
{{#if siteSettings.email_time_window_mins}}
{{i18n 'user.email.frequency' count=siteSettings.email_time_window_mins}}
{{else}}
{{i18n 'user.email.frequency_immediately'}}
{{/if}}
</div>
{{/unless}}
</div>
{{#unless siteSettings.disable_digest_emails}}
<div class='control-group pref-activity-summary'>
<label class="control-label">{{i18n 'user.email_activity_summary'}}</label>
{{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}}
<div class='controls controls-dropdown'>
{{combo-box valueAttribute="value" content=digestFrequencies value=model.user_option.digest_after_minutes}}
</div>
{{preference-checkbox labelKey="user.include_tl0_in_digests" disabled=model.user_option.mailing_list_mode checked=model.user_option.include_tl0_in_digests}}
{{/if}}
</div>
{{/unless}}
{{#unless siteSettings.disable_mailing_list_mode}}
<div class='control-group pref-mailing-list-mode'>
<label class="control-label">{{i18n 'user.mailing_list_mode.label'}}</label>
{{preference-checkbox labelKey="user.mailing_list_mode.enabled" checked=model.user_option.mailing_list_mode}}
<div class='instructions'>{{{i18n 'user.mailing_list_mode.instructions'}}}</div>
{{#if model.user_option.mailing_list_mode}}
<div class='controls controls-dropdown'>
{{combo-box valueAttribute="value" content=mailingListModeOptions value=model.user_option.mailing_list_mode_frequency}}
</div>
{{/if}}
</div>
{{/unless}}
<div class="control-group notifications">
<label class="control-label">{{i18n 'user.desktop_notifications.label'}}</label>
{{desktop-notification-config}}
<div class="instructions">{{i18n 'user.desktop_notifications.each_browser_note'}}</div>
</div>
<div class="control-group other">
<label class="control-label">{{i18n 'user.other_settings'}}</label>
<div class="controls controls-dropdown">
<label>{{i18n 'user.new_topic_duration.label'}}</label>
{{combo-box valueAttribute="value" content=considerNewTopicOptions value=model.user_option.new_topic_duration_minutes}}
</div>
<div class="controls controls-dropdown">
<label>{{i18n 'user.auto_track_topics'}}</label>
{{combo-box valueAttribute="value" content=autoTrackDurations value=model.user_option.auto_track_topics_after_msecs}}
</div>
<div class="controls controls-dropdown">
<label>{{i18n 'user.notification_level_when_replying'}}</label>
{{combo-box valueAttribute="value" content=notificationLevelsForReplying value=model.user_option.notification_level_when_replying}}
</div>
<div class="controls controls-dropdown">
<label>{{i18n 'user.like_notification_frequency.title'}}</label>
{{combo-box valueAttribute="value" content=likeNotificationFrequencies value=model.user_option.like_notification_frequency}}
</div>
{{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)}}
</div>
<div class="control-group category">
<label class="control-label">{{i18n 'user.categories_settings'}}</label>
<div class="controls category-controls">
<label><span class="icon fa fa-exclamation-circle watching"></span> {{i18n 'user.watched_categories'}}</label>
{{category-selector categories=model.watchedCategories blacklist=selectedCategories}}
</div>
<div class="instructions">{{i18n 'user.watched_categories_instructions'}}</div>
<div class="controls category-controls">
<a href="{{unbound model.watchingTopicsPath}}">{{i18n 'user.watched_topics_link'}}</a>
</div>
<div class="controls category-controls">
<label><span class="icon fa fa-circle tracking"></span> {{i18n 'user.tracked_categories'}}</label>
{{category-selector categories=model.trackedCategories blacklist=selectedCategories}}
</div>
<div class="instructions">{{i18n 'user.tracked_categories_instructions'}}</div>
<div class="controls category-controls">
<a href="{{unbound model.trackingTopicsPath}}">{{i18n 'user.tracked_topics_link'}}</a>
</div>
<div class="controls category-controls">
<label><span class="icon fa fa-dot-circle-o watching-first-post"></span> {{i18n 'user.watched_first_post_categories'}}</label>
{{category-selector categories=model.watchedFirstPostCategories}}
</div>
<div class="instructions">{{i18n 'user.watched_first_post_categories_instructions'}}</div>
<div class="controls category-controls">
<label><span class="icon fa fa-times-circle muted"></span> {{i18n 'user.muted_categories'}}</label>
{{category-selector categories=model.mutedCategories blacklist=selectedCategories}}
</div>
<div class="instructions">{{i18n 'user.muted_categories_instructions'}}</div>
<div class="controls category-controls">
<a href="{{unbound model.mutedTopicsPath}}">{{i18n 'user.muted_topics_link'}}</a>
</div>
</div>
{{#if siteSettings.tagging_enabled}}
<div class="control-group tags">
<label class="control-label">{{i18n 'user.tag_settings'}}</label>
<div class="controls tag-controls">
<label><span class="icon fa fa-exclamation-circle watching"></span> {{i18n 'user.watched_tags'}}</label>
{{tag-chooser tags=model.watched_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
</div>
<div class="instructions">{{i18n 'user.watched_tags_instructions'}}</div>
<div class="controls tag-controls">
<label><span class="icon fa fa-circle tracking"></span> {{i18n 'user.tracked_tags'}}</label>
{{tag-chooser tags=model.tracked_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
</div>
<div class="instructions">{{i18n 'user.tracked_tags_instructions'}}</div>
<div class="controls tag-controls">
<label><span class="icon fa fa-dot-circle-o watching-first-post"></span> {{i18n 'user.watched_first_post_tags'}}</label>
{{tag-chooser tags=model.watching_first_post_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
</div>
<div class="instructions">{{i18n 'user.watched_first_post_tags_instructions'}}</div>
<div class="controls tag-controls">
<label><span class="icon fa fa-times-circle muted"></span> {{i18n 'user.muted_tags'}}</label>
{{tag-chooser tags=model.muted_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
</div>
<div class="instructions">{{i18n 'user.muted_tags_instructions'}}</div>
</div>
{{/if}}
<div class="control-group muting">
<label class="control-label">{{i18n 'user.users'}}</label>
<div class="controls category-controls">
<label><span class="icon fa fa-times-circle muted"></span> {{i18n 'user.muted_users'}}</label>
{{user-selector excludeCurrentUser=true usernames=model.muted_usernames class="user-selector"}}
</div>
<div class="instructions">{{i18n 'user.muted_users_instructions'}}</div>
</div>
{{#if siteSettings.automatically_unpin_topics}}
<div class="control-group topics">
<label class="control-label">{{i18n 'categories.topics'}}</label>
{{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.user_option.automatically_unpin_topics}}
</div>
{{/if}}
{{#if model.userApiKeys}}
<div class="control-group apps">
<label class="control-label">{{i18n 'user.apps'}}</label>
<div class="controls">
{{#each model.userApiKeys as |key|}}
<div>
<span>{{key.application_name}}</span>
{{#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}}
<p>
<ul>
{{#each key.scopes as |scope|}}
<li>{{scope}}</li>
{{/each}}
</ul>
</p>
<p><span>{{i18n "user.api_approved"}}</span> {{bound-date key.created_at}}</p>
</div>
{{/each}}
</div>
</div>
{{/if}}
{{#if userSelectableThemes}}
<div class="control-group theme">
<label class="control-label">{{i18n 'user.theme'}}</label>
<div class="controls">
{{combo-box content=userSelectableThemes value=selectedTheme}}
</div>
</div>
{{/if}}
{{plugin-outlet name="user-custom-controls" args=(hash model=model)}}
<div class="control-group save-button">
<div class="controls">
{{partial 'user/preferences/save-button'}}
</div>
</div>
{{#if model.canDeleteAccount}}
<div class="control-group delete-account">
<hr/>
<div class="controls">
{{d-button action="delete" disabled=deleteDisabled class="btn-danger" icon="trash-o" label="user.delete_account"}}
</div>
</div>
{{/if}}
<form class="form-vertical">
{{outlet}}
</form>
{{/d-section}}
</section>

View File

@ -0,0 +1,61 @@
<div class="control-group pref-username">
<label class="control-label">{{i18n 'user.username.title'}}</label>
<div class="controls">
<span class='static'>{{model.username}}</span>
{{#if model.can_edit_username}}
{{#link-to "preferences.username" class="btn btn-small pad-left no-text"}}<i class="fa fa-pencil"></i>{{/link-to}}
{{/if}}
</div>
<div class='instructions'>
{{{i18n 'user.username.short_instructions' username=model.username}}}
</div>
</div>
{{#if canCheckEmails}}
<div class="control-group pref-email">
<label class="control-label">{{i18n 'user.email.title'}}</label>
{{#if model.email}}
<div class="controls">
<span class='static'>{{model.email}}</span>
{{#if model.can_edit_email}}
{{#link-to "preferences.email" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}}
{{/if}}
</div>
<div class='instructions'>
{{i18n 'user.email.instructions'}}
</div>
{{else}}
<div class="controls">
{{d-button action="checkEmail" actionParam=model title="admin.users.check_email.title" icon="envelope-o" label="admin.users.check_email.text"}}
</div>
{{/if}}
</div>
{{/if}}
{{#if canChangePassword}}
<div class="control-group pref-password">
<label class="control-label">{{i18n 'user.password.title'}}</label>
<div class="controls">
<a href {{action "changePassword"}} class='btn'>
{{fa-icon "envelope"}}
{{#if model.no_password}}
{{i18n 'user.change_password.set_password'}}
{{else}}
{{i18n 'user.change_password.action'}}
{{/if}}
</a>
{{passwordProgress}}
</div>
</div>
{{/if}}
<br/>
{{#if model.canDeleteAccount}}
<div class="control-group delete-account">
<br/>
<div class="controls">
{{d-button action="delete" disabled=deleteDisabled class="btn-danger" icon="trash-o" label="user.delete_account"}}
</div>
</div>
{{/if}}

View File

@ -0,0 +1,25 @@
{{#if model.userApiKeys}}
<div class="control-group apps">
<label class="control-label">{{i18n 'user.apps'}}</label>
<div class="controls">
{{#each model.userApiKeys as |key|}}
<div>
<span>{{key.application_name}}</span>
{{#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}}
<p>
<ul>
{{#each key.scopes as |scope|}}
<li>{{scope}}</li>
{{/each}}
</ul>
</p>
<p><span>{{i18n "user.api_approved"}}</span> {{bound-date key.created_at}}</p>
</div>
{{/each}}
</div>
</div>
{{/if}}

View File

@ -0,0 +1,48 @@
<div class="control-group category-notifications">
<label class="control-label">{{i18n 'user.categories_settings'}}</label>
<div class="controls category-controls">
<label><span class="icon fa fa-exclamation-circle watching"></span> {{i18n 'user.watched_categories'}}</label>
{{category-selector categories=model.watchedCategories blacklist=selectedCategories}}
</div>
<div class="instructions">{{i18n 'user.watched_categories_instructions'}}</div>
<div class="controls">
<a href="{{unbound model.watchingTopicsPath}}">{{i18n 'user.watched_topics_link'}}</a>
</div>
<div class="controls category-controls">
<label><span class="icon fa fa-circle tracking"></span> {{i18n 'user.tracked_categories'}}</label>
{{category-selector categories=model.trackedCategories blacklist=selectedCategories}}
</div>
<div class="instructions">{{i18n 'user.tracked_categories_instructions'}}</div>
<div class="controls">
<a href="{{unbound model.trackingTopicsPath}}">{{i18n 'user.tracked_topics_link'}}</a>
</div>
<div class="controls category-controls">
<label><span class="icon fa fa-dot-circle-o watching-first-post"></span> {{i18n 'user.watched_first_post_categories'}}</label>
{{category-selector categories=model.watchedFirstPostCategories}}
</div>
<div class="instructions">{{i18n 'user.watched_first_post_categories_instructions'}}</div>
<div class="controls category-controls">
<label><span class="icon fa fa-times-circle muted"></span> {{i18n 'user.muted_categories'}}</label>
{{category-selector categories=model.mutedCategories blacklist=selectedCategories}}
</div>
<div class="instructions">{{i18n 'user.muted_categories_instructions'}}</div>
<div class="controls">
<a href="{{unbound model.mutedTopicsPath}}">{{i18n 'user.muted_topics_link'}}</a>
</div>
</div>
<br/>
{{plugin-outlet name="user-custom-controls" args=(hash model=model)}}
<div class="control-group save-button">
<div class="controls">
{{partial 'user/preferences/save-button'}}
</div>
</div>

View File

@ -0,0 +1,56 @@
<div class="control-group pref-email-settings">
<label class="control-label">{{i18n 'user.email_settings'}}</label>
<div class='controls controls-dropdown'>
<label>{{i18n 'user.email_previous_replies.title'}}</label>
{{combo-box valueAttribute="value" content=previousRepliesOptions value=model.user_option.email_previous_replies}}
</div>
{{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}}
<div class='instructions'>
{{#if siteSettings.email_time_window_mins}}
{{i18n 'user.email.frequency' count=siteSettings.email_time_window_mins}}
{{else}}
{{i18n 'user.email.frequency_immediately'}}
{{/if}}
</div>
{{/unless}}
</div>
{{#unless siteSettings.disable_digest_emails}}
<div class='control-group pref-activity-summary'>
<label class="control-label">{{i18n 'user.email_activity_summary'}}</label>
{{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}}
<div class='controls controls-dropdown'>
{{combo-box valueAttribute="value" content=digestFrequencies value=model.user_option.digest_after_minutes}}
</div>
{{preference-checkbox labelKey="user.include_tl0_in_digests" disabled=model.user_option.mailing_list_mode checked=model.user_option.include_tl0_in_digests}}
{{/if}}
</div>
{{/unless}}
{{#unless siteSettings.disable_mailing_list_mode}}
<div class='control-group pref-mailing-list-mode'>
<label class="control-label">{{i18n 'user.mailing_list_mode.label'}}</label>
{{preference-checkbox labelKey="user.mailing_list_mode.enabled" checked=model.user_option.mailing_list_mode}}
<div class='instructions'>{{{i18n 'user.mailing_list_mode.instructions'}}}</div>
{{#if model.user_option.mailing_list_mode}}
<div class='controls controls-dropdown'>
{{combo-box valueAttribute="value" content=mailingListModeOptions value=model.user_option.mailing_list_mode_frequency}}
</div>
{{/if}}
</div>
{{/unless}}
<br/>
{{plugin-outlet name="user-custom-controls" args=(hash model=model)}}
<div class="control-group save-button">
<div class="controls">
{{partial 'user/preferences/save-button'}}
</div>
</div>

View File

@ -0,0 +1,47 @@
{{#if userSelectableThemes}}
<div class="control-group theme">
<label class="control-label">{{i18n 'user.theme'}}</label>
<div class="controls">
{{combo-box content=userSelectableThemes value=selectedTheme}}
</div>
</div>
{{/if}}
{{#if siteSettings.allow_user_locale}}
<div class="control-group pref-locale">
<label class="control-label">{{i18n 'user.locale.title'}}</label>
<div class="controls">
{{combo-box valueAttribute="value" content=availableLocales value=model.locale none="user.locale.default"}}
</div>
<div class='instructions'>
{{i18n 'user.locale.instructions'}}
</div>
</div>
{{/if}}
<div class="control-group other">
<label class="control-label">{{i18n 'user.other_settings'}}</label>
{{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}}
</div>
{{#if siteSettings.automatically_unpin_topics}}
<div class="control-group topics">
<label class="control-label">{{i18n 'categories.topics'}}</label>
{{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.user_option.automatically_unpin_topics}}
</div>
{{/if}}
<br/>
{{plugin-outlet name="user-custom-controls" args=(hash model=model)}}
<div class="control-group save-button">
<div class="controls">
{{partial 'user/preferences/save-button'}}
</div>
</div>

View File

@ -0,0 +1,46 @@
<div class="control-group notifications">
<div class="controls controls-dropdown">
<label>{{i18n 'user.new_topic_duration.label'}}</label>
{{combo-box valueAttribute="value" content=considerNewTopicOptions value=model.user_option.new_topic_duration_minutes}}
</div>
<div class="controls controls-dropdown">
<label>{{i18n 'user.auto_track_topics'}}</label>
{{combo-box valueAttribute="value" content=autoTrackDurations value=model.user_option.auto_track_topics_after_msecs}}
</div>
<div class="controls controls-dropdown">
<label>{{i18n 'user.notification_level_when_replying'}}</label>
{{combo-box valueAttribute="value" content=notificationLevelsForReplying value=model.user_option.notification_level_when_replying}}
</div>
<div class="controls controls-dropdown">
<label>{{i18n 'user.like_notification_frequency.title'}}</label>
{{combo-box valueAttribute="value" content=likeNotificationFrequencies value=model.user_option.like_notification_frequency}}
</div>
</div>
<div class="control-group desktop-notifications">
<label class="control-label">{{i18n 'user.desktop_notifications.label'}}</label>
{{desktop-notification-config}}
<div class="instructions">{{i18n 'user.desktop_notifications.each_browser_note'}}</div>
</div>
<div class="control-group muting">
<label class="control-label">{{i18n 'user.users'}}</label>
<div class="controls category-controls">
<label><span class="icon fa fa-times-circle muted"></span> {{i18n 'user.muted_users'}}</label>
{{user-selector excludeCurrentUser=true usernames=model.muted_usernames class="user-selector"}}
</div>
<div class="instructions">{{i18n 'user.muted_users_instructions'}}</div>
</div>
<br/>
{{plugin-outlet name="user-custom-controls" args=(hash model=model)}}
<div class="control-group save-button">
<div class="controls">
{{partial 'user/preferences/save-button'}}
</div>
</div>

View File

@ -0,0 +1,110 @@
{{#if canEditName}}
<div class="control-group pref-name">
<label class="control-label">{{i18n 'user.name.title'}}</label>
<div class="controls">
{{#if model.can_edit_name}}
{{text-field value=newNameInput classNames="input-xxlarge"}}
{{else}}
<span class='static'>{{model.name}}</span>
{{/if}}
</div>
<div class='instructions'>
{{nameInstructions}}
</div>
</div>
{{/if}}
{{#if canSelectTitle}}
<div class="control-group pref-title">
<label class="control-label">{{i18n 'user.title.title'}}</label>
<div class="controls">
<span class="static">{{model.title}}</span>
{{#link-to "preferences.badgeTitle" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}}
</div>
</div>
{{/if}}
<div class="control-group pref-avatar">
<label class="control-label">{{i18n 'user.avatar.title'}}</label>
<div class="controls">
{{! 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}}
</div>
</div>
{{#if siteSettings.allow_profile_backgrounds}}
<div class="control-group pref-profile-bg">
<label class="control-label">{{i18n 'user.change_profile_background.title'}}</label>
<div class="controls">
{{image-uploader imageUrl=model.profile_background type="profile_background"}}
</div>
<div class='instructions'>
{{i18n 'user.change_profile_background.instructions'}}
</div>
</div>
<div class="control-group pref-profile-bg">
<label class="control-label">{{i18n 'user.change_card_background.title'}}</label>
<div class="controls">
{{image-uploader imageUrl=model.card_background type="card_background"}}
</div>
<div class='instructions'>
{{i18n 'user.change_card_background.instructions'}}
</div>
</div>
{{/if}}
{{#if canChangeBio}}
<div class="control-group pref-bio">
<label class="control-label">{{i18n 'user.bio'}}</label>
<div class="controls bio-composer">
{{d-editor value=model.bio_raw}}
</div>
</div>
{{/if}}
<div class="control-group pref-location">
<label class="control-label">{{i18n 'user.location'}}</label>
<div class="controls">
{{input type="text" value=model.location class="input-xxlarge" id='edit-location'}}
</div>
</div>
<div class="control-group pref-website">
<label class="control-label">{{i18n 'user.website'}}</label>
<div class="controls">
{{input type="text" value=model.website class="input-xxlarge"}}
</div>
</div>
{{#each userFields as |uf|}}
<div class="control-group">
{{user-field field=uf.field value=uf.value}}
</div>
{{/each}}
<div class='clearfix'></div>
<div class="control-group pref-card-badge">
<label class="control-label">{{i18n 'user.card_badge.title'}}</label>
<div class="controls">
{{#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}}
</div>
</div>
{{plugin-outlet name="user-custom-preferences" args=(hash model=model)}}
<br/>
{{plugin-outlet name="user-custom-controls" args=(hash model=model)}}
<div class="control-group save-button">
<div class="controls">
{{partial 'user/preferences/save-button'}}
</div>
</div>

View File

@ -0,0 +1,41 @@
{{#if siteSettings.tagging_enabled}}
<div class="control-group tag-notifications">
<label class="control-label">{{i18n 'user.tag_settings'}}</label>
<div class="controls tag-controls">
<label><span class="icon fa fa-exclamation-circle watching"></span> {{i18n 'user.watched_tags'}}</label>
{{tag-chooser tags=model.watched_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
</div>
<div class="instructions">{{i18n 'user.watched_tags_instructions'}}</div>
<div class="controls tag-controls">
<label><span class="icon fa fa-circle tracking"></span> {{i18n 'user.tracked_tags'}}</label>
{{tag-chooser tags=model.tracked_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
</div>
<div class="instructions">{{i18n 'user.tracked_tags_instructions'}}</div>
<div class="controls tag-controls">
<label><span class="icon fa fa-dot-circle-o watching-first-post"></span> {{i18n 'user.watched_first_post_tags'}}</label>
{{tag-chooser tags=model.watching_first_post_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
</div>
<div class="instructions">{{i18n 'user.watched_first_post_tags_instructions'}}</div>
<div class="controls tag-controls">
<label><span class="icon fa fa-times-circle muted"></span> {{i18n 'user.muted_tags'}}</label>
{{tag-chooser tags=model.muted_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
</div>
<div class="instructions">{{i18n 'user.muted_tags_instructions'}}</div>
</div>
<br/>
{{plugin-outlet name="user-custom-controls" args=(hash model=model)}}
<div class="control-group save-button">
<div class="controls">
{{partial 'user/preferences/save-button'}}
</div>
</div>
{{/if}}

View File

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

View File

@ -65,6 +65,9 @@
color: $primary;
}
}
> li.indent {
padding-left: 15px;
}
.active > a, & li > a.active
{
color: $secondary;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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