mirror of
https://github.com/flarum/core.git
synced 2025-08-04 15:37:51 +02:00
Add notifications, and frontend framework rewrite changes changelog file
This commit is contained in:
19
FRONTEND FRAMEWORK REWRITE CHANGES.md
Normal file
19
FRONTEND FRAMEWORK REWRITE CHANGES.md
Normal file
@@ -0,0 +1,19 @@
|
||||
### Changes
|
||||
|
||||
* Mithril
|
||||
- See changes from v0.2.x @ https://mithril.js.org/migration-v02x.html
|
||||
- Kept `m.prop` and `m.withAttr`
|
||||
- Actual Promises are used now instead of `m.deferred`
|
||||
* Component
|
||||
- Use new Mithril lifecycle hooks (`component.config` is gone)
|
||||
- `component.render` is gone
|
||||
* Application
|
||||
- New different methods
|
||||
- `app.bus` for some event hooking
|
||||
* Translator
|
||||
- Added `app.translator.transText`, automatically extracts text from `translator.trans` output
|
||||
|
||||
#### Forum
|
||||
* Forum Application
|
||||
- Renamed to `Forum`
|
||||
- `app.search` is no longer global, extend using `extend`
|
@@ -7,6 +7,7 @@ import Store from './Store';
|
||||
|
||||
import extract from './utils/extract';
|
||||
import mapRoutes from './utils/mapRoutes';
|
||||
import Drawer from './utils/Drawer';
|
||||
import {extend} from './extend';
|
||||
|
||||
import Forum from './models/Forum';
|
||||
@@ -14,6 +15,7 @@ import Discussion from './models/Discussion';
|
||||
import User from './models/User';
|
||||
import Post from './models/Post';
|
||||
import Group from './models/Group';
|
||||
import Notification from './models/Notification';
|
||||
|
||||
import RequestError from './utils/RequestError';
|
||||
import Alert from './components/Alert';
|
||||
@@ -32,7 +34,7 @@ export default abstract class Application {
|
||||
*/
|
||||
forum: Forum;
|
||||
|
||||
data: ApplicationData | undefined;
|
||||
data: ApplicationData;
|
||||
|
||||
translator = new Translator();
|
||||
bus = new Bus();
|
||||
@@ -51,9 +53,17 @@ export default abstract class Application {
|
||||
discussions: Discussion,
|
||||
posts: Post,
|
||||
groups: Group,
|
||||
// notifications: Notification
|
||||
notifications: Notification
|
||||
});
|
||||
|
||||
drawer = new Drawer();
|
||||
|
||||
/**
|
||||
* A local cache that can be used to store data at the application level, so
|
||||
* that is persists between different routes.
|
||||
*/
|
||||
cache = {};
|
||||
|
||||
routes = {};
|
||||
|
||||
title = '';
|
||||
@@ -69,8 +79,6 @@ export default abstract class Application {
|
||||
// this.modal = m.mount(document.getElementById('modal'), <ModalManager />);
|
||||
// this.alerts = m.mount(document.getElementById('alerts'), <AlertManager />);
|
||||
|
||||
// this.drawer = new Drawer();
|
||||
|
||||
m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath));
|
||||
}
|
||||
|
||||
@@ -151,7 +159,7 @@ export default abstract class Application {
|
||||
route(name: string, params: object = {}): string {
|
||||
const route = this.routes[name];
|
||||
|
||||
if (!route) throw new Error(`Route ${name} does not exist`);
|
||||
if (!route) throw new Error(`Route '${name}' does not exist`);
|
||||
|
||||
const url = route.path.replace(/:([^\/]+)/g, (m, key) => extract(params, key));
|
||||
const queryString = m.buildQueryString(params);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import extract from './utils/extract';
|
||||
import extractText from './utils/extractText';
|
||||
import username from './helpers/username';
|
||||
import extractText from "./utils/extractText";
|
||||
|
||||
type Translations = { [key: string]: string };
|
||||
|
||||
|
18
js/src/common/models/Notification.ts
Normal file
18
js/src/common/models/Notification.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import Model from '../Model';
|
||||
import User from './User';
|
||||
|
||||
export default class Notification extends Model {
|
||||
static ADMINISTRATOR_ID = '1';
|
||||
static GUEST_ID = '2';
|
||||
static MEMBER_ID = '3';
|
||||
|
||||
contentType = Model.attribute('contentType') as () => string;
|
||||
content = Model.attribute('content') as () => string;
|
||||
createdAt = Model.attribute('createdAt', Model.transformDate) as () => Date;
|
||||
|
||||
isRead = Model.attribute('isRead') as () => boolean;
|
||||
|
||||
user = Model.hasOne('user') as () => User;
|
||||
fromUser = Model.hasOne('fromUser') as () => User;
|
||||
subject = Model.hasOne('subhect') as () => any;
|
||||
}
|
51
js/src/common/utils/Drawer.ts
Normal file
51
js/src/common/utils/Drawer.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* The `Drawer` class controls the page's drawer. The drawer is the area the
|
||||
* slides out from the left on mobile devices; it contains the header and the
|
||||
* footer.
|
||||
*/
|
||||
export default class Drawer {
|
||||
private $backdrop?: ZeptoCollection;
|
||||
|
||||
constructor() {
|
||||
// Set up an event handler so that whenever the content area is tapped,
|
||||
// the drawer will close.
|
||||
$('#content').click(e => {
|
||||
if (this.isOpen()) {
|
||||
e.preventDefault();
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not the drawer is currently open.
|
||||
*/
|
||||
isOpen(): boolean {
|
||||
return $('#app').hasClass('drawerOpen');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the drawer.
|
||||
*/
|
||||
hide() {
|
||||
$('#app').removeClass('drawerOpen');
|
||||
|
||||
if (this.$backdrop) this.$backdrop.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the drawer.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
show() {
|
||||
$('#app').addClass('drawerOpen');
|
||||
|
||||
this.$backdrop = $('<div/>')
|
||||
.addClass('drawer-backdrop fade')
|
||||
.appendTo('body')
|
||||
.click(() => this.hide());
|
||||
|
||||
setTimeout(() => this.$backdrop.addClass('in'));
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
class Item {
|
||||
content: any;
|
||||
class Item<T> {
|
||||
content: T;
|
||||
priority: number;
|
||||
key: number = 0;
|
||||
|
||||
@@ -9,8 +9,8 @@ class Item {
|
||||
}
|
||||
}
|
||||
|
||||
export default class ItemList {
|
||||
private items: { [key: string]: Item } = {};
|
||||
export default class ItemList<T = any> {
|
||||
private items: { [key: string]: Item<T> } = {};
|
||||
|
||||
/**
|
||||
* Check whether the list is empty.
|
||||
@@ -30,9 +30,6 @@ export default class ItemList {
|
||||
|
||||
/**
|
||||
* Check whether an item is present in the list.
|
||||
*
|
||||
* @param key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has(key: any): boolean {
|
||||
return !!this.items[key];
|
||||
@@ -40,12 +37,8 @@ export default class ItemList {
|
||||
|
||||
/**
|
||||
* Get the content of an item.
|
||||
*
|
||||
* @param {String} key
|
||||
* @return {*}
|
||||
* @public
|
||||
*/
|
||||
get(key: any) {
|
||||
get(key: any): T {
|
||||
return this.items[key].content;
|
||||
}
|
||||
|
||||
@@ -65,8 +58,8 @@ export default class ItemList {
|
||||
return this;
|
||||
}
|
||||
|
||||
toArray<T>(): T[] {
|
||||
const items: Item[] = [];
|
||||
toArray(): T[] {
|
||||
const items: Item<T>[] = [];
|
||||
|
||||
for (const i in this.items) {
|
||||
if (this.items.hasOwnProperty(i)) {
|
||||
|
@@ -30,7 +30,7 @@ export default class Forum extends Application {
|
||||
}
|
||||
|
||||
this.routes[defaultAction].path = '/';
|
||||
this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/');
|
||||
this.history.push(defaultAction, this.translator.transText('core.forum.header.back_to_index_tooltip'), '/');
|
||||
|
||||
// m.mount(document.getElementById('app-navigation'), Navigation.component({className: 'App-backControl', drawer: true}));
|
||||
// m.mount(document.getElementById('header-navigation'), Navigation.component());
|
||||
@@ -47,7 +47,7 @@ export default class Forum extends Application {
|
||||
|
||||
// Route the home link back home when clicked. We do not want it to register
|
||||
// if the user is opening it in a new tab, however.
|
||||
$('#home-link').click(e => {
|
||||
$('#home-link').click((e: MouseEvent) => {
|
||||
if (e.ctrlKey || e.metaKey || e.which === 2) return;
|
||||
e.preventDefault();
|
||||
app.history.home();
|
||||
|
@@ -2,9 +2,9 @@ import Component from '../../common/Component';
|
||||
import Button from '../../common/components/Button';
|
||||
// import LogInModal from './LogInModal';
|
||||
// import SignUpModal from './SignUpModal';
|
||||
// import SessionDropdown from './SessionDropdown';
|
||||
import SessionDropdown from './SessionDropdown';
|
||||
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||
// import NotificationsDropdown from './NotificationsDropdown';
|
||||
import NotificationsDropdown from './NotificationsDropdown';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
|
||||
@@ -32,7 +32,7 @@ export default class HeaderSecondary extends Component {
|
||||
|
||||
items.add('search', Search.component(), 30);
|
||||
|
||||
if (app.forum.attribute("showLanguageSelector") && Object.keys(app.data.locales).length > 0) {
|
||||
if (app.forum.attribute("showLanguageSelector") && Object.keys(app.data.locales).length > 1) {
|
||||
const locales = [];
|
||||
|
||||
for (const locale in app.data.locales) {
|
||||
@@ -58,8 +58,8 @@ export default class HeaderSecondary extends Component {
|
||||
}
|
||||
|
||||
if (app.session.user) {
|
||||
// items.add('notifications', NotificationsDropdown.component(), 10);
|
||||
// items.add('session', SessionDropdown.component(), 0);
|
||||
items.add('notifications', NotificationsDropdown.component(), 10);
|
||||
items.add('session', SessionDropdown.component(), 0);
|
||||
} else {
|
||||
if (app.forum.attribute('allowSignUp')) {
|
||||
items.add('signUp',
|
||||
|
201
js/src/forum/components/NotificationList.tsx
Normal file
201
js/src/forum/components/NotificationList.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import Component from '../../common/Component';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import Button from '../../common/components/Button';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import Notification from '../../common/models/Notification';
|
||||
import Discussion from '../../common/models/Discussion';
|
||||
|
||||
/**
|
||||
* The `NotificationList` component displays a list of the logged-in user's
|
||||
* notifications, grouped by discussion.
|
||||
*/
|
||||
export default class NotificationList extends Component {
|
||||
/**
|
||||
* Whether or not the notifications are loading.
|
||||
*/
|
||||
loading: boolean = false;
|
||||
|
||||
/**
|
||||
* Whether or not there are more results that can be loaded.
|
||||
*/
|
||||
moreResults: boolean = false;
|
||||
|
||||
private $scrollParent: ZeptoCollection;
|
||||
private scrollHandler: () => void;
|
||||
|
||||
view() {
|
||||
const pages = app.cache.notifications || [];
|
||||
|
||||
return (
|
||||
<div className="NotificationList">
|
||||
<div className="NotificationList-header">
|
||||
<div className="App-primaryControl">
|
||||
{Button.component({
|
||||
className: 'Button Button--icon Button--link',
|
||||
icon: 'fas fa-check',
|
||||
title: app.translator.transText('core.forum.notifications.mark_all_as_read_tooltip'),
|
||||
onclick: this.markAllAsRead.bind(this)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h4 className="App-titleControl App-titleControl--text">{app.translator.trans('core.forum.notifications.title')}</h4>
|
||||
</div>
|
||||
|
||||
<div className="NotificationList-content">
|
||||
{pages.length ? pages.map(notifications => {
|
||||
const groups = [];
|
||||
const discussions = {};
|
||||
|
||||
notifications.forEach(notification => {
|
||||
const subject = notification.subject();
|
||||
|
||||
if (typeof subject === 'undefined') return;
|
||||
|
||||
// Get the discussion that this notification is related to. If it's not
|
||||
// directly related to a discussion, it may be related to a post or
|
||||
// other entity which is related to a discussion.
|
||||
let discussion: any = false;
|
||||
if (subject instanceof Discussion) discussion = subject;
|
||||
else if (subject && subject.discussion) discussion = subject.discussion();
|
||||
|
||||
// If the notification is not related to a discussion directly or
|
||||
// indirectly, then we will assign it to a neutral group.
|
||||
const key = discussion ? discussion.id() : 0;
|
||||
discussions[key] = discussions[key] || {discussion: discussion, notifications: []};
|
||||
discussions[key].notifications.push(notification);
|
||||
|
||||
if (groups.indexOf(discussions[key]) === -1) {
|
||||
groups.push(discussions[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return groups.map(group => {
|
||||
const badges = group.discussion && group.discussion.badges().toArray();
|
||||
|
||||
return (
|
||||
<div className="NotificationGroup">
|
||||
{group.discussion
|
||||
? (
|
||||
<a className="NotificationGroup-header"
|
||||
href={app.route.discussion(group.discussion)}
|
||||
config={m.route}>
|
||||
{badges && badges.length ? <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul> : ''}
|
||||
{group.discussion.title()}
|
||||
</a>
|
||||
) : (
|
||||
<div className="NotificationGroup-header">
|
||||
{app.forum.attribute('title')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ul className="NotificationGroup-content">
|
||||
{group.notifications.map(notification => {
|
||||
const NotificationComponent = app.notificationComponents[notification.contentType()];
|
||||
return NotificationComponent ? <li>{NotificationComponent.component({notification})}</li> : '';
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}) : ''}
|
||||
{this.loading
|
||||
? <LoadingIndicator className="LoadingIndicator--block" />
|
||||
: (pages.length ? '' : <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
const $notifications = this.$('.NotificationList-content');
|
||||
const $scrollParent = this.$scrollParent = $notifications.css('overflow') === 'auto' ? $notifications : $(window);
|
||||
|
||||
this.scrollHandler = () => {
|
||||
const scrollTop = $scrollParent.scrollTop();
|
||||
const viewportHeight = $scrollParent.height();
|
||||
const contentTop = $scrollParent === $notifications ? 0 : $notifications.offset().top;
|
||||
const contentHeight = $notifications[0].scrollHeight;
|
||||
|
||||
if (this.moreResults && !this.loading && scrollTop + viewportHeight >= contentTop + contentHeight) {
|
||||
this.loadMore();
|
||||
}
|
||||
};
|
||||
|
||||
$scrollParent.on('scroll', this.scrollHandler);
|
||||
}
|
||||
|
||||
onremove(vnode) {
|
||||
super.onremove(vnode);
|
||||
|
||||
this.$scrollParent.off('scroll', this.scrollHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load notifications into the application's cache if they haven't already
|
||||
* been loaded.
|
||||
*/
|
||||
load() {
|
||||
if (app.session.user.newNotificationCount()) {
|
||||
delete app.cache.notifications;
|
||||
}
|
||||
|
||||
if (app.cache.notifications) {
|
||||
return;
|
||||
}
|
||||
|
||||
app.session.user.pushAttributes({newNotificationCount: 0});
|
||||
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the next page of notification results.
|
||||
*/
|
||||
loadMore() {
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
const params = app.cache.notifications ? {page: {offset: app.cache.notifications.length * 10}} : null;
|
||||
|
||||
return app.store.find('notifications', params)
|
||||
.then(this.parseResults.bind(this))
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse results and append them to the notification list.
|
||||
*/
|
||||
parseResults(results: Notification[]|any): Notification[]|any {
|
||||
app.cache.notifications = app.cache.notifications || [];
|
||||
|
||||
if (results.length) app.cache.notifications.push(results);
|
||||
|
||||
this.moreResults = !!results.payload.links.next;
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all of the notifications as read.
|
||||
*/
|
||||
markAllAsRead() {
|
||||
if (!app.cache.notifications) return;
|
||||
|
||||
app.session.user.pushAttributes({unreadNotificationCount: 0});
|
||||
|
||||
app.cache.notifications.forEach(notifications => {
|
||||
notifications.forEach(notification => notification.pushAttributes({isRead: true}))
|
||||
});
|
||||
|
||||
app.request({
|
||||
url: app.forum.attribute('apiUrl') + '/notifications/read',
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
}
|
73
js/src/forum/components/NotificationsDropdown.tsx
Normal file
73
js/src/forum/components/NotificationsDropdown.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import Dropdown from '../../common/components/Dropdown';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import NotificationList from './NotificationList';
|
||||
|
||||
export default class NotificationsDropdown extends Dropdown {
|
||||
list = new NotificationList();
|
||||
|
||||
static initProps(props) {
|
||||
props.className = props.className || 'NotificationsDropdown';
|
||||
props.buttonClassName = props.buttonClassName || 'Button Button--flat';
|
||||
props.menuClassName = props.menuClassName || 'Dropdown-menu--right';
|
||||
props.label = props.label || app.translator.trans('core.forum.notifications.tooltip');
|
||||
props.icon = props.icon || 'fas fa-bell';
|
||||
|
||||
super.initProps(props);
|
||||
}
|
||||
|
||||
getButton() {
|
||||
const newNotifications = this.getNewCount();
|
||||
const vdom = super.getButton();
|
||||
|
||||
vdom.attrs.title = this.props.label;
|
||||
|
||||
vdom.attrs.className += (newNotifications ? ' new' : '');
|
||||
vdom.attrs.onclick = this.onclick.bind(this);
|
||||
|
||||
return vdom;
|
||||
}
|
||||
|
||||
getButtonContent() {
|
||||
const unread = this.getUnreadCount();
|
||||
|
||||
return [
|
||||
icon(this.props.icon, {className: 'Button-icon'}),
|
||||
unread ? <span className="NotificationsDropdown-unread">{unread}</span> : '',
|
||||
<span className="Button-label">{this.props.label}</span>
|
||||
];
|
||||
}
|
||||
|
||||
getMenu() {
|
||||
return (
|
||||
<div className={'Dropdown-menu ' + this.props.menuClassName} onclick={this.menuClick.bind(this)}>
|
||||
{this.showing ? m(this.list) : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onclick() {
|
||||
if (app.drawer.isOpen()) {
|
||||
this.goToRoute();
|
||||
} else {
|
||||
this.list.load();
|
||||
}
|
||||
}
|
||||
|
||||
goToRoute() {
|
||||
m.route(app.route('notifications'));
|
||||
}
|
||||
|
||||
getUnreadCount() {
|
||||
return app.session.user.unreadNotificationCount();
|
||||
}
|
||||
|
||||
getNewCount() {
|
||||
return app.session.user.newNotificationCount();
|
||||
}
|
||||
|
||||
menuClick(e) {
|
||||
// Don't close the notifications dropdown if the user is opening a link in a
|
||||
// new tab or window.
|
||||
if (e.shiftKey || e.metaKey || e.ctrlKey || e.which === 2) e.stopPropagation();
|
||||
}
|
||||
}
|
@@ -7,6 +7,8 @@ import DiscussionsSearchSource from './DiscussionsSearchSource';
|
||||
import UsersSearchSource from './UsersSearchSource';
|
||||
import SearchSource from './SearchSource';
|
||||
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
/**
|
||||
* The `Search` component displays a menu of as-you-type results from a variety
|
||||
* of sources.
|
||||
@@ -20,7 +22,7 @@ export default class Search extends Component {
|
||||
/**
|
||||
* The value of the search input.
|
||||
*/
|
||||
value: Function = m.prop('');
|
||||
value: Stream<string> = m.prop('');
|
||||
|
||||
/**
|
||||
* Whether or not the search input has focus.
|
||||
@@ -72,8 +74,6 @@ export default class Search extends Component {
|
||||
// Hide the search view if no sources were loaded
|
||||
if (!this.sources.length) return <div/>;
|
||||
|
||||
console.log('Search#view - loading:', this.loadingSources)
|
||||
|
||||
return (
|
||||
<div className={'Search ' + classNames({
|
||||
open: this.value() && this.hasFocus,
|
||||
|
89
js/src/forum/components/SessionDropdown.tsx
Normal file
89
js/src/forum/components/SessionDropdown.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import avatar from '../../common/helpers/avatar';
|
||||
import username from '../../common/helpers/username';
|
||||
import Dropdown from '../../common/components/Dropdown';
|
||||
import LinkButton from '../../common/components/LinkButton';
|
||||
import Button from '../../common/components/Button';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import Separator from '../../common/components/Separator';
|
||||
import Group from '../../common/models/Group';
|
||||
|
||||
/**
|
||||
* The `SessionDropdown` component shows a button with the current user's
|
||||
* avatar/name, with a dropdown of session controls.
|
||||
*/
|
||||
export default class SessionDropdown extends Dropdown {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.className = 'SessionDropdown';
|
||||
props.buttonClassName = 'Button Button--user Button--flat';
|
||||
props.menuClassName = 'Dropdown-menu--right';
|
||||
}
|
||||
|
||||
view() {
|
||||
this.props.children = this.items().toArray();
|
||||
|
||||
return super.view();
|
||||
}
|
||||
|
||||
getButtonContent() {
|
||||
const user = app.session.user;
|
||||
|
||||
return [
|
||||
avatar(user), ' ',
|
||||
<span className="Button-label">{username(user)}</span>
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the contents of the dropdown menu.
|
||||
*/
|
||||
items(): ItemList {
|
||||
const items = new ItemList();
|
||||
const user = app.session.user;
|
||||
|
||||
// items.add('profile',
|
||||
// LinkButton.component({
|
||||
// icon: 'fas fa-user',
|
||||
// children: app.translator.trans('core.forum.header.profile_button'),
|
||||
// href: app.route.user(user)
|
||||
// }),
|
||||
// 100
|
||||
// );
|
||||
|
||||
// items.add('settings',
|
||||
// LinkButton.component({
|
||||
// icon: 'fas fa-cog',
|
||||
// children: app.translator.trans('core.forum.header.settings_button'),
|
||||
// href: app.route('settings')
|
||||
// }),
|
||||
// 50
|
||||
// );
|
||||
|
||||
if (app.forum.attribute('adminUrl')) {
|
||||
items.add('administration',
|
||||
LinkButton.component({
|
||||
icon: 'fas fa-wrench',
|
||||
children: app.translator.trans('core.forum.header.admin_button'),
|
||||
href: app.forum.attribute('adminUrl'),
|
||||
target: '_blank',
|
||||
config: () => {}
|
||||
}),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
items.add('separator', Separator.component(), -90);
|
||||
|
||||
items.add('logOut',
|
||||
Button.component({
|
||||
icon: 'fas fa-sign-out-alt',
|
||||
children: app.translator.trans('core.forum.header.log_out_button'),
|
||||
onclick: app.session.logout.bind(app.session)
|
||||
}),
|
||||
-100
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user