User settings GUI, including some new components

This commit is contained in:
Toby Zerner 2015-03-28 11:33:18 +10:30
parent e710a2c93e
commit 6dcc14ef49
24 changed files with 472 additions and 7 deletions

View File

@ -25,7 +25,8 @@ export default DropdownButton.extend(HasItemLists, {
items.pushObjectWithTag(Ember.Component.extend({
tagName: 'li',
layout: precompileTemplate('{{#link-to "settings"}}{{fa-icon "cog"}} Settings{{/link-to}}')
layout: precompileTemplate('{{#link-to "user.settings" user}}{{fa-icon "cog"}} Settings{{/link-to}}'),
user: this.get('user')
}));
if (this.get('user.groups').findBy('id', '1')) {

View File

@ -0,0 +1,13 @@
import Ember from 'ember';
/**
A set of fields with a heading.
*/
export default Ember.Component.extend({
layoutName: 'components/ui/field-set',
tagName: 'fieldset',
classNameBindings: ['className'],
label: '',
fields: []
});

View File

@ -0,0 +1,19 @@
import Ember from 'ember';
/**
A toggle switch.
*/
export default Ember.Component.extend({
layoutName: 'components/ui/switch-input',
classNames: ['checkbox', 'checkbox-switch'],
label: '',
toggleState: true,
didInsertElement: function() {
var component = this;
this.$('input').on('change', function() {
component.get('changed')($(this).prop('checked'), component);
});
}
});

View File

@ -0,0 +1,19 @@
import Ember from 'ember';
/**
A toggle switch.
*/
export default Ember.Component.extend({
layoutName: 'components/ui/yesno-input',
tagName: 'label',
classNames: ['yesno-control'],
toggleState: true,
didInsertElement: function() {
var component = this;
this.$('input').on('change', function() {
component.get('changed')($(this).prop('checked'), component);
});
}
});

View File

@ -0,0 +1,105 @@
import Ember from 'ember';
export default Ember.Component.extend({
layoutName: 'components/user/notification-grid',
classNames: ['notification-grid'],
methods: [
{ name: 'alert', icon: 'bell', label: 'Alert' },
{ name: 'email', icon: 'envelope-o', label: 'Email' }
],
didInsertElement: function() {
var component = this;
this.$('thead .toggle-group').bind('mouseenter mouseleave', function(e) {
var i = parseInt($(this).index()) + 1;
component.$('table').find('td:nth-child('+i+')').toggleClass('highlighted', e.type === 'mouseenter');
});
this.$('tbody .toggle-group').bind('mouseenter mouseleave', function(e) {
$(this).parent().find('td').toggleClass('highlighted', e.type === 'mouseenter');
});
},
preferenceKey: function(type, method) {
return 'notify_'+type+'_'+method;
},
grid: Ember.computed('methods', 'notificationTypes', function() {
var grid = [];
var component = this;
var notificationTypes = this.get('notificationTypes');
var methods = this.get('methods');
var user = this.get('user');
notificationTypes.forEach(function(type) {
var row = Ember.Object.create({
type: type,
label: type.label,
cells: []
});
methods.forEach(function(method) {
var preferenceKey = 'preferences.'+component.preferenceKey(type.name, method.name);
var cell = Ember.Object.create({
type: type,
method: method,
enabled: !!user.get(preferenceKey),
loading: false
});
cell.set('save', function(value, component) {
cell.set('loading', true);
user.set(preferenceKey, value).save().then(function() {
cell.set('loading', false);
});
});
row.get('cells').pushObject(cell);
});
grid.pushObject(row);
});
return grid;
}),
toggleCells: function(cells) {
var enabled = !cells[0].get('enabled');
var user = this.get('user');
var component = this;
cells.forEach(function(cell) {
cell.set('loading', true);
cell.set('enabled', enabled);
user.set('preferences.'+component.preferenceKey(cell.get('type.name'), cell.get('method.name')), enabled);
});
user.save().then(function() {
cells.forEach(function(cell) {
cell.set('loading', false);
})
});
},
actions: {
toggleMethod: function(method) {
var grid = this.get('grid');
var component = this;
var cells = [];
grid.forEach(function(row) {
row.get('cells').some(function(cell) {
if (cell.get('method') === method) {
cells.pushObject(cell);
return true;
}
});
});
component.toggleCells(cells);
},
toggleType: function(type) {
var grid = this.get('grid');
var component = this;
grid.some(function(row) {
if (row.get('type') === type) {
component.toggleCells(row.get('cells'));
return true;
}
});
}
}
});

View File

@ -12,6 +12,7 @@ export default DS.Model.extend(HasItemLists, {
avatarUrl: DS.attr('string'),
bio: DS.attr('string'),
bioHtml: DS.attr('string'),
preferences: DS.attr(),
groups: DS.hasMany('group'),

View File

@ -15,9 +15,8 @@ Router.map(function() {
this.resource('user', {path: '/u/:username'}, function() {
this.route('activity', {path: '/'});
this.route('edit');
this.route('settings');
});
this.resource('settings');
});
export default Router;

View File

@ -0,0 +1,7 @@
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return Ember.RSVP.resolve(this.modelFor('user'));
}
});

View File

@ -35,5 +35,6 @@
@import "@{flarum-base}index.less";
@import "@{flarum-base}discussion.less";
@import "@{flarum-base}user.less";
@import "@{flarum-base}settings.less";
@import "@{flarum-base}login.less";
@import "@{flarum-base}signup.less";

View File

@ -84,6 +84,7 @@
@body-bg: @fl-body-bg;
@text-color: @fl-body-color;
@legend-color: @fl-body-color;
@font-size-base: 13px;
@font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;

View File

@ -39,8 +39,11 @@
border-radius: 2px;
line-height: 1;
}
.btn-danger {
.button-variant(#d66, #fdd, #fdd);
}
// Redefine Bootstrap's mixin to make some general changes
// Add to Bootstrap's mixin to make some general changes
.button-variant(@color; @background; @border) {
&:hover,
&:focus,

View File

@ -132,4 +132,7 @@
color: @fl-body-primary-color;
font-weight: bold;
}
& > li.divider {
background: none;
}
}

View File

@ -10,6 +10,12 @@
.box-shadow(none);
}
}
legend {
font-size: 14px;
border: 0;
font-weight: bold;
margin-bottom: 10px;
}
// Search inputs
// @todo Extract some of this into header-specific definitions
@ -69,3 +75,79 @@
pointer-events: none;
color: @fl-body-muted-color;
}
.checkbox-switch {
& label {
padding-left: 65px;
}
& .switch-control {
float: left;
margin-left: -65px;
margin-top: -4px;
}
& .loading-indicator {
display: inline-block;
margin-left: 10px;
}
}
.switch-control, .yesno-control {
& input[type=checkbox] {
display: none;
}
}
.switch {
width: 50px;
height: 28px;
padding: 3px;
position: relative;
border-radius: 14px;
background: @fl-body-control-bg;
.transition(background-color 0.2s);
input:checked + & {
background: @fl-body-primary-color;
}
& .loading-indicator {
opacity: 0;
.loading& {
opacity: 1;
}
}
&:before, & .loading-indicator {
position: absolute;
width: 22px;
height: 22px;
padding: 0;
left: 3px;
.transition(~"opacity 0.2s, left 0.2s");
input:checked + & {
left: 25px;
}
}
&:before {
content: ' ';
background: @fl-body-bg;
border-radius: 11px;
box-shadow: 0 2px 4px @fl-shadow-color;
}
}
.yesno-control {
cursor: pointer;
margin: 0;
}
.yesno {
font-size: 14px;
&.yes {
color: #58A400;
}
&.no {
color: #D0021B;
}
&.disabled {
color: @fl-body-muted-more-color !important;
}
}

View File

@ -0,0 +1,61 @@
.settings {
margin-top: 5px;
& > ul {
list-style: none;
margin: 0;
padding: 0;
& > li {
margin-bottom: 40px;
}
}
& fieldset > ul {
list-style: none;
margin: 0;
padding: 0;
}
}
.settings-account {
& li {
display: inline-block;
margin-right: 5px;
}
}
.notification-grid {
& table {
background: @fl-body-control-bg;
border-radius: @border-radius-base;
}
& table td, & table th {
border-bottom: 1px solid @fl-body-bg;
color: @fl-body-control-color;
}
& td, & th, & .yesno-cell .yesno {
padding: 10px 15px;
}
& thead {
& th {
text-align: center;
padding: 15px 25px;
}
& .fa {
display: block;
font-size: 14px;
width: auto;
margin-bottom: 5px;
}
}
& .yesno-cell {
text-align: center;
cursor: pointer;
padding: 0;
&:hover, &.highlighted {
background: darken(@fl-body-control-bg, 4%);
}
}
& .toggle-group {
cursor: pointer;
}
}

View File

@ -106,10 +106,10 @@
}
}
.user-content .loading-indicator {
.user-activity .loading-indicator {
height: 46px;
}
.user-activity {
.activity-list {
border-left: 3px solid @fl-body-secondary-color;
list-style: none;
margin: 0 0 0 16px;

View File

@ -0,0 +1,3 @@
<legend>{{label}}</legend>
{{ui/item-list items=fields}}

View File

@ -0,0 +1,12 @@
<label>
<div class="switch-control">
{{input type="checkbox" checked=toggleState}}
<div class="switch {{if loading "loading"}}">
</div>
</div>
{{label}}
{{#if loading}}
{{ui/loading-indicator size="tiny"}}
{{/if}}
</label>

View File

@ -0,0 +1,10 @@
{{input type="checkbox" checked=toggleState disabled=disabled}}
<div class="yesno {{if loading "loading"}} {{if disabled "disabled"}} {{if toggleState "yes" "no"}}">
{{#if loading}}
{{ui/loading-indicator size="tiny"}}
{{else if toggleState}}
{{fa-icon "check"}}
{{else}}
{{fa-icon "times"}}
{{/if}}
</div>

View File

@ -0,0 +1,20 @@
<table>
<thead>
<tr>
<td></td>
{{#each methods as |method|}}
<th class="toggle-group" {{action "toggleMethod" method}}>{{fa-icon method.icon}} {{method.label}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each grid as |row|}}
<tr>
<td class="toggle-group" {{action "toggleType" row.type}}>{{row.label}}</td>
{{#each row.cells as |cell|}}
<td class="yesno-cell">{{ui/yesno-input toggleState=cell.enabled changed=cell.save loading=cell.loading}}</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>

View File

@ -1,4 +1,4 @@
<ul class="user-activity">
<ul class="activity-list">
{{#each activity in model}}
{{user/activity-item activity=activity}}
{{/each}}

View File

@ -0,0 +1 @@
{{ui/item-list items=view.settings}}

View File

@ -52,5 +52,15 @@ export default Ember.View.extend(HasItemLists, {
controller: this.get('controller'),
layout: precompileTemplate('{{#link-to "user.activity" (query-params filter="posts")}}{{fa-icon icon}} {{label}} <span class="count">{{badge}}</span>{{/link-to}}')
}), 'posts');
this.addSeparatorItem(items);
if (this.get('controller.model') === this.get('controller.session.user')) {
items.pushObjectWithTag(NavItem.extend({
label: 'Settings',
icon: 'cog',
layout: precompileTemplate('{{#link-to "user.settings"}}{{fa-icon icon}} {{label}}{{/link-to}}')
}), 'settings');
}
}
});

View File

@ -0,0 +1,5 @@
import Ember from 'ember';
export default Ember.View.extend({
classNames: ['user-activity']
});

View File

@ -0,0 +1,89 @@
import Ember from 'ember';
import HasItemLists from 'flarum/mixins/has-item-lists';
import NotificationGrid from 'flarum/components/user/notification-grid';
import FieldSet from 'flarum/components/ui/field-set';
import ActionButton from 'flarum/components/ui/action-button';
import SwitchInput from 'flarum/components/ui/switch-input';
export default Ember.View.extend(HasItemLists, {
itemLists: ['settings'],
classNames: ['settings'],
populateSettings: function(items) {
items.pushObjectWithTag(FieldSet.extend({
label: 'Account',
className: 'settings-account',
fields: this.populateItemList('account')
}), 'account');
items.pushObjectWithTag(FieldSet.extend({
label: 'Notifications',
fields: [NotificationGrid.extend({
notificationTypes: this.populateItemList('notificationTypes'),
user: this.get('controller.model')
})]
}), 'notifications');
items.pushObjectWithTag(FieldSet.extend({
label: 'Privacy',
fields: this.populateItemList('privacy')
}), 'privacy');
},
populateAccount: function(items) {
items.pushObjectWithTag(ActionButton.extend({
label: 'Change Password',
className: 'btn btn-default'
}), 'changePassword');
items.pushObjectWithTag(ActionButton.extend({
label: 'Change Email',
className: 'btn btn-default'
}), 'changeEmail');
items.pushObjectWithTag(ActionButton.extend({
label: 'Delete Account',
className: 'btn btn-default btn-danger'
}), 'deleteAccount');
},
updateSetting: function(key) {
var controller = this.get('controller');
return function(value, component) {
component.set('loading', true);
var user = controller.get('model');
user.set(key, value).save().then(function() {
component.set('loading', false);
});
};
},
populatePrivacy: function(items) {
var self = this;
items.pushObjectWithTag(SwitchInput.extend({
label: 'Allow others to see when I am online',
parentController: this.get('controller'),
toggleState: Ember.computed.alias('parentController.model.preferences.discloseOnline'),
changed: function(value, component) {
self.set('controller.model.lastSeenTime', null);
self.updateSetting('preferences.discloseOnline')(value, component);
}
}), 'discloseOnline');
items.pushObjectWithTag(SwitchInput.extend({
label: 'Allow search engines to index my profile',
parentController: this.get('controller'),
toggleState: Ember.computed.alias('parentController.model.preferences.indexProfile'),
changed: this.updateSetting('preferences.indexProfile')
}), 'indexProfile');
},
populateNotificationTypes: function(items) {
items.pushObjectWithTag({
name: 'discussionRenamed',
label: 'Someone renames a discussion I started'
}, 'discussionRenamed');
}
});