1
0
mirror of https://github.com/flarum/core.git synced 2025-08-07 08:56:38 +02:00

forum: add NotificationGrid and SettingsPage - missing modals

This commit is contained in:
David Sevilla Martin
2020-01-31 18:29:26 -05:00
parent 66745916b3
commit 6656820f24
8 changed files with 360 additions and 8 deletions

2
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,14 @@ import computed from '../utils/computed';
import GroupBadge from '../components/GroupBadge';
import Group from './Group';
export interface UserPreferences {
discloseOnline?: boolean;
indexProfile?: boolean;
locale?: string;
[key: string]: any;
}
export default class User extends Model {
username = Model.attribute('username') as () => string;
@@ -14,7 +22,7 @@ export default class User extends Model {
password = Model.attribute('password') as () => string;
avatarUrl = Model.attribute('avatarUrl') as () => string;
preferences = Model.attribute('preferences') as () => string;
preferences = Model.attribute('preferences') as () => UserPreferences;
groups = Model.hasMany('groups') as () => Group[];
joinTime = Model.attribute('joinTime', Model.transformDate) as () => Date;

View File

@@ -7,6 +7,8 @@ import HeaderSecondary from './components/HeaderSecondary';
import Page from './components/Page';
import IndexPage from './components/IndexPage';
import PostsUserPage from './components/PostsUserPage';
import SettingsPage from './components/SettingsPage';
import User from '../common/models/User';
import Post from '../common/models/Post';
import Discussion from '../common/models/Discussion';
@@ -22,7 +24,7 @@ export default class Forum extends Application {
'user.posts': { path: '/u/:username', component: PostsUserPage },
'user.discussions': { path: '/u/:username/discussions', component: PostsUserPage },
settings: { path: '/settings', component: PostsUserPage },
settings: { path: '/settings', component: SettingsPage },
'index.filter': { path: '/:filter', component: IndexPage },
};

View File

@@ -0,0 +1,208 @@
import Component, { ComponentProps } from '../../common/Component';
import Checkbox from '../../common/components/Checkbox';
import icon from '../../common/helpers/icon';
import ItemList from '../../common/utils/ItemList';
import User from '../../common/models/User';
export interface NotificationGridProps extends ComponentProps {
user: User;
}
export type NotificationItem = {
/**
* The name of the notification type/method.
*/
name: string;
/**
* The icon to display in the column header/notificatio grid row.
*/
icon: string;
/**
* The label to display in the column header/notification grid row.
*/
label: string;
};
/**
* The `NotificationGrid` component displays a table of notification types and
* methods, allowing the user to toggle each combination.
*/
export default class NotificationGrid extends Component<NotificationGridProps> {
/**
* Information about the available notification methods.
*/
methods = this.notificationMethods().toArray();
/**
* A map of notification type-method combinations to the checkbox instances
* that represent them.
*/
inputs = {};
/**
* Information about the available notification types.
*/
types = this.notificationTypes().toArray();
oninit(vnode) {
super.oninit(vnode);
// For each of the notification type-method combinations, create and store a
// new checkbox component instance, which we will render in the view.
this.types.forEach(type =>
this.methods.forEach(method => {
const key = this.preferenceKey(type.name, method.name);
const preference = this.props.user.preferences()[key];
this.inputs[key] = new Checkbox({
state: !!preference,
disabled: typeof preference === 'undefined',
onchange: () => this.toggle([key]),
});
})
);
}
view() {
return (
<table className="NotificationGrid">
<thead>
<tr>
<td />
{this.methods.map(method => (
<th className="NotificationGrid-groupToggle" onclick={this.toggleMethod.bind(this, method.name)}>
{icon(method.icon)} {method.label}
</th>
))}
</tr>
</thead>
<tbody>
{this.types.map(type => (
<tr>
<td className="NotificationGrid-groupToggle" onclick={this.toggleType.bind(this, type.name)}>
{icon(type.icon)} {type.label}
</td>
{this.methods.map(method => (
<td className="NotificationGrid-checkbox">{this.inputs[this.preferenceKey(type.name, method.name)].render()}</td>
))}
</tr>
))}
</tbody>
</table>
);
}
oncreate(vnode) {
super.oncreate(vnode);
this.$('thead .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function(e) {
const i = parseInt($(this).index(), 10) + 1;
$(this)
.parents('table')
.find('td:nth-child(' + i + ')')
.toggleClass('highlighted', e.type === 'mouseenter');
});
this.$('tbody .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function(e) {
$(this)
.parent()
.find('td')
.toggleClass('highlighted', e.type === 'mouseenter');
});
}
/**
* Toggle the state of the given preferences, based on the value of the first
* one.
*/
toggle(keys: string[]) {
const user = this.props.user;
const preferences = user.preferences();
const enabled = !preferences[keys[0]];
keys.forEach(key => {
const control = this.inputs[key];
control.loading = true;
control.props.state = enabled;
preferences[key] = control.props.state = enabled;
});
m.redraw();
user.save({ preferences }).then(() => {
keys.forEach(key => (this.inputs[key].loading = false));
m.redraw();
});
}
/**
* Toggle all notification types for the given method.
*/
toggleMethod(method: string) {
const keys = this.types.map(type => this.preferenceKey(type.name, method)).filter(key => !this.inputs[key].props.disabled);
this.toggle(keys);
}
/**
* Toggle all notification methods for the given type.
*/
toggleType(type: string) {
const keys = this.methods.map(method => this.preferenceKey(type, method.name)).filter(key => !this.inputs[key].props.disabled);
this.toggle(keys);
}
/**
* Get the name of the preference key for the given notification type-method
* combination.
*/
preferenceKey(type: string, method: string): string {
return `notify_${type}_${method}`;
}
/**
* Build an item list for the notification methods to display in the grid.
*
* @see {NotificationItem}
*/
notificationMethods() {
const items = new ItemList<NotificationItem>();
items.add('alert', {
name: 'alert',
icon: 'fas fa-bell',
label: app.translator.trans('core.forum.settings.notify_by_web_heading'),
});
items.add('email', {
name: 'email',
icon: 'far fa-envelope',
label: app.translator.trans('core.forum.settings.notify_by_email_heading'),
});
return items;
}
/**
* Build an item list for the notification types to display in the grid.
*
* @see {NotificationItem}
*/
notificationTypes() {
const items = new ItemList<NotificationItem>();
items.add('discussionRenamed', {
name: 'discussionRenamed',
icon: 'fas fa-pencil-alt',
label: app.translator.trans('core.forum.settings.notify_discussion_renamed_label'),
});
return items;
}
}

View File

@@ -0,0 +1,134 @@
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';
import Button from '../../common/components/Button';
import FieldSet from '../../common/components/FieldSet';
import Switch from '../../common/components/Switch';
import UserPage from './UserPage';
import NotificationGrid from './NotificationGrid';
export default class SettingsPage extends UserPage {
oninit(vnode) {
super.oninit(vnode);
this.show(app.session.user);
app.setTitle(app.translator.transText('core.forum.settings.title'));
}
content() {
return (
<div className="SettingsPage">
<ul>{listItems(this.settingsItems().toArray())}</ul>
</div>
);
}
/**
* Build an item list for the user's settings controls.
*/
settingsItems(): ItemList {
const items = new ItemList();
items.add(
'account',
FieldSet.component({
label: app.translator.trans('core.forum.settings.account_heading'),
className: 'Settings-account',
children: this.accountItems().toArray(),
})
);
items.add(
'notifications',
FieldSet.component({
label: app.translator.trans('core.forum.settings.notifications_heading'),
className: 'Settings-notifications',
children: this.notificationsItems().toArray(),
})
);
items.add(
'privacy',
FieldSet.component({
label: app.translator.trans('core.forum.settings.privacy_heading'),
className: 'Settings-privacy',
children: this.privacyItems().toArray(),
})
);
return items;
}
/**
* Build an item list for the user's account settings.
*/
accountItems(): ItemList {
const items = new ItemList();
items.add(
'changePassword',
Button.component({
children: app.translator.trans('core.forum.settings.change_password_button'),
className: 'Button',
onclick: () => app.modal.show(new ChangePasswordModal()),
})
);
items.add(
'changeEmail',
Button.component({
children: app.translator.trans('core.forum.settings.change_email_button'),
className: 'Button',
onclick: () => app.modal.show(new ChangeEmailModal()),
})
);
return items;
}
/**
* Build an item list for the user's notification settings.
*/
notificationsItems(): ItemList {
const items = new ItemList();
items.add('notificationGrid', NotificationGrid.component({ user: this.user }));
return items;
}
/**
* Generate a callback that will save a value to the given preference.
*/
preferenceSaver(key: string): Function {
return (value, component) => {
if (component) component.loading = true;
m.redraw();
this.user.savePreferences({ [key]: value }).then(() => {
if (component) component.loading = false;
m.redraw();
});
};
}
/**
* Build an item list for the user's privacy settings.
*/
privacyItems(): ItemList {
const items = new ItemList();
items.add(
'discloseOnline',
Switch.component({
children: app.translator.trans('core.forum.settings.privacy_disclose_online_label'),
state: this.user.preferences().discloseOnline,
onchange: (value, component) => {
this.user.pushAttributes({ lastSeenAt: null });
this.preferenceSaver('discloseOnline')(value, component);
},
})
);
return items;
}
}