mirror of
https://github.com/flarum/framework.git
synced 2025-02-21 03:59:39 +08:00
User settings GUI, including some new components
This commit is contained in:
parent
e710a2c93e
commit
6dcc14ef49
@ -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')) {
|
||||
|
13
ember/app/components/ui/field-set.js
Normal file
13
ember/app/components/ui/field-set.js
Normal 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: []
|
||||
});
|
19
ember/app/components/ui/switch-input.js
Normal file
19
ember/app/components/ui/switch-input.js
Normal 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);
|
||||
});
|
||||
}
|
||||
});
|
19
ember/app/components/ui/yesno-input.js
Normal file
19
ember/app/components/ui/yesno-input.js
Normal 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);
|
||||
});
|
||||
}
|
||||
});
|
105
ember/app/components/user/notification-grid.js
Normal file
105
ember/app/components/user/notification-grid.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -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'),
|
||||
|
||||
|
@ -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;
|
||||
|
7
ember/app/routes/user/settings.js
Normal file
7
ember/app/routes/user/settings.js
Normal file
@ -0,0 +1,7 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
model: function() {
|
||||
return Ember.RSVP.resolve(this.modelFor('user'));
|
||||
}
|
||||
});
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -132,4 +132,7 @@
|
||||
color: @fl-body-primary-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
& > li.divider {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
61
ember/app/styles/flarum/settings.less
Normal file
61
ember/app/styles/flarum/settings.less
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
3
ember/app/templates/components/ui/field-set.hbs
Normal file
3
ember/app/templates/components/ui/field-set.hbs
Normal file
@ -0,0 +1,3 @@
|
||||
<legend>{{label}}</legend>
|
||||
|
||||
{{ui/item-list items=fields}}
|
12
ember/app/templates/components/ui/switch-input.hbs
Normal file
12
ember/app/templates/components/ui/switch-input.hbs
Normal 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>
|
10
ember/app/templates/components/ui/yesno-input.hbs
Normal file
10
ember/app/templates/components/ui/yesno-input.hbs
Normal 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>
|
20
ember/app/templates/components/user/notification-grid.hbs
Normal file
20
ember/app/templates/components/user/notification-grid.hbs
Normal 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>
|
@ -1,4 +1,4 @@
|
||||
<ul class="user-activity">
|
||||
<ul class="activity-list">
|
||||
{{#each activity in model}}
|
||||
{{user/activity-item activity=activity}}
|
||||
{{/each}}
|
||||
|
1
ember/app/templates/user/settings.hbs
Normal file
1
ember/app/templates/user/settings.hbs
Normal file
@ -0,0 +1 @@
|
||||
{{ui/item-list items=view.settings}}
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
5
ember/app/views/user/activity.js
Normal file
5
ember/app/views/user/activity.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.View.extend({
|
||||
classNames: ['user-activity']
|
||||
});
|
89
ember/app/views/user/settings.js
Normal file
89
ember/app/views/user/settings.js
Normal 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');
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user