1
0
mirror of https://github.com/flarum/core.git synced 2025-07-12 04:16:24 +02:00

Run prettier for all JS files

This commit is contained in:
Franz Liedke
2020-04-17 11:57:55 +02:00
parent 84cf938379
commit 89ef14faf1
145 changed files with 2374 additions and 2050 deletions

View File

@ -12,9 +12,9 @@ export default class AdminApplication extends Application {
canGoBack: () => true, canGoBack: () => true,
getPrevious: () => {}, getPrevious: () => {},
backUrl: () => this.forum.attribute('baseUrl'), backUrl: () => this.forum.attribute('baseUrl'),
back: function() { back: function () {
window.location = this.backUrl(); window.location = this.backUrl();
} },
}; };
constructor() { constructor() {
@ -27,7 +27,7 @@ export default class AdminApplication extends Application {
* @inheritdoc * @inheritdoc
*/ */
mount() { mount() {
m.mount(document.getElementById('app-navigation'), Navigation.component({className: 'App-backControl', drawer: true})); m.mount(document.getElementById('app-navigation'), Navigation.component({ className: 'App-backControl', drawer: true }));
m.mount(document.getElementById('header-navigation'), Navigation.component()); m.mount(document.getElementById('header-navigation'), Navigation.component());
m.mount(document.getElementById('header-primary'), HeaderPrimary.component()); m.mount(document.getElementById('header-primary'), HeaderPrimary.component());
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component()); m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
@ -59,5 +59,5 @@ export default class AdminApplication extends Application {
} }
return required; return required;
}; }
} }

View File

@ -58,6 +58,6 @@ export default Object.assign(compat, {
'components/AdminNav': AdminNav, 'components/AdminNav': AdminNav,
'components/EditCustomCssModal': EditCustomCssModal, 'components/EditCustomCssModal': EditCustomCssModal,
'components/EditGroupModal': EditGroupModal, 'components/EditGroupModal': EditGroupModal,
'routes': routes, routes: routes,
'AdminApplication': AdminApplication AdminApplication: AdminApplication,
}); });

View File

@ -22,8 +22,10 @@ export default class AddExtensionModal extends Modal {
return ( return (
<div className="Modal-body"> <div className="Modal-body">
<p>{app.translator.trans('core.admin.add_extension.temporary_text')}</p> <p>{app.translator.trans('core.admin.add_extension.temporary_text')}</p>
<p>{app.translator.trans('core.admin.add_extension.install_text', {a: <a href="https://discuss.flarum.org/t/extensions" target="_blank"/>})}</p> <p>
<p>{app.translator.trans('core.admin.add_extension.developer_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</p> {app.translator.trans('core.admin.add_extension.install_text', { a: <a href="https://discuss.flarum.org/t/extensions" target="_blank" /> })}
</p>
<p>{app.translator.trans('core.admin.add_extension.developer_text', { a: <a href="http://flarum.org/docs/extend" target="_blank" /> })}</p>
</div> </div>
); );
} }

View File

@ -13,11 +13,7 @@ export default class AdminLinkButton extends LinkButton {
getButtonContent() { getButtonContent() {
const content = super.getButtonContent(); const content = super.getButtonContent();
content.push( content.push(<div className="AdminLinkButton-description">{this.props.description}</div>);
<div className="AdminLinkButton-description">
{this.props.description}
</div>
);
return content; return content;
} }

View File

@ -15,9 +15,7 @@ import ItemList from '../../common/utils/ItemList';
export default class AdminNav extends Component { export default class AdminNav extends Component {
view() { view() {
return ( return (
<SelectDropdown <SelectDropdown className="AdminNav App-titleControl" buttonClassName="Button">
className="AdminNav App-titleControl"
buttonClassName="Button">
{this.items().toArray()} {this.items().toArray()}
</SelectDropdown> </SelectDropdown>
); );
@ -31,47 +29,65 @@ export default class AdminNav extends Component {
items() { items() {
const items = new ItemList(); const items = new ItemList();
items.add('dashboard', AdminLinkButton.component({ items.add(
'dashboard',
AdminLinkButton.component({
href: app.route('dashboard'), href: app.route('dashboard'),
icon: 'far fa-chart-bar', icon: 'far fa-chart-bar',
children: app.translator.trans('core.admin.nav.dashboard_button'), children: app.translator.trans('core.admin.nav.dashboard_button'),
description: app.translator.trans('core.admin.nav.dashboard_text') description: app.translator.trans('core.admin.nav.dashboard_text'),
})); })
);
items.add('basics', AdminLinkButton.component({ items.add(
'basics',
AdminLinkButton.component({
href: app.route('basics'), href: app.route('basics'),
icon: 'fas fa-pencil-alt', icon: 'fas fa-pencil-alt',
children: app.translator.trans('core.admin.nav.basics_button'), children: app.translator.trans('core.admin.nav.basics_button'),
description: app.translator.trans('core.admin.nav.basics_text') description: app.translator.trans('core.admin.nav.basics_text'),
})); })
);
items.add('mail', AdminLinkButton.component({ items.add(
'mail',
AdminLinkButton.component({
href: app.route('mail'), href: app.route('mail'),
icon: 'fas fa-envelope', icon: 'fas fa-envelope',
children: app.translator.trans('core.admin.nav.email_button'), children: app.translator.trans('core.admin.nav.email_button'),
description: app.translator.trans('core.admin.nav.email_text') description: app.translator.trans('core.admin.nav.email_text'),
})); })
);
items.add('permissions', AdminLinkButton.component({ items.add(
'permissions',
AdminLinkButton.component({
href: app.route('permissions'), href: app.route('permissions'),
icon: 'fas fa-key', icon: 'fas fa-key',
children: app.translator.trans('core.admin.nav.permissions_button'), children: app.translator.trans('core.admin.nav.permissions_button'),
description: app.translator.trans('core.admin.nav.permissions_text') description: app.translator.trans('core.admin.nav.permissions_text'),
})); })
);
items.add('appearance', AdminLinkButton.component({ items.add(
'appearance',
AdminLinkButton.component({
href: app.route('appearance'), href: app.route('appearance'),
icon: 'fas fa-paint-brush', icon: 'fas fa-paint-brush',
children: app.translator.trans('core.admin.nav.appearance_button'), children: app.translator.trans('core.admin.nav.appearance_button'),
description: app.translator.trans('core.admin.nav.appearance_text') description: app.translator.trans('core.admin.nav.appearance_text'),
})); })
);
items.add('extensions', AdminLinkButton.component({ items.add(
'extensions',
AdminLinkButton.component({
href: app.route('extensions'), href: app.route('extensions'),
icon: 'fas fa-puzzle-piece', icon: 'fas fa-puzzle-piece',
children: app.translator.trans('core.admin.nav.extensions_button'), children: app.translator.trans('core.admin.nav.extensions_button'),
description: app.translator.trans('core.admin.nav.extensions_text') description: app.translator.trans('core.admin.nav.extensions_text'),
})); })
);
return items; return items;
} }

View File

@ -24,85 +24,85 @@ export default class AppearancePage extends Page {
<form onsubmit={this.onsubmit.bind(this)}> <form onsubmit={this.onsubmit.bind(this)}>
<fieldset className="AppearancePage-colors"> <fieldset className="AppearancePage-colors">
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
<div className="helpText"> <div className="helpText">{app.translator.trans('core.admin.appearance.colors_text')}</div>
{app.translator.trans('core.admin.appearance.colors_text')}
</div>
<div className="AppearancePage-colors-input"> <div className="AppearancePage-colors-input">
<input className="FormControl" type="text" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/> <input
<input className="FormControl" type="text" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/> className="FormControl"
type="text"
placeholder="#aaaaaa"
value={this.primaryColor()}
onchange={m.withAttr('value', this.primaryColor)}
/>
<input
className="FormControl"
type="text"
placeholder="#aaaaaa"
value={this.secondaryColor()}
onchange={m.withAttr('value', this.secondaryColor)}
/>
</div> </div>
{Switch.component({ {Switch.component({
state: this.darkMode(), state: this.darkMode(),
children: app.translator.trans('core.admin.appearance.dark_mode_label'), children: app.translator.trans('core.admin.appearance.dark_mode_label'),
onchange: this.darkMode onchange: this.darkMode,
})} })}
{Switch.component({ {Switch.component({
state: this.coloredHeader(), state: this.coloredHeader(),
children: app.translator.trans('core.admin.appearance.colored_header_label'), children: app.translator.trans('core.admin.appearance.colored_header_label'),
onchange: this.coloredHeader onchange: this.coloredHeader,
})} })}
{Button.component({ {Button.component({
className: 'Button Button--primary', className: 'Button Button--primary',
type: 'submit', type: 'submit',
children: app.translator.trans('core.admin.appearance.submit_button'), children: app.translator.trans('core.admin.appearance.submit_button'),
loading: this.loading loading: this.loading,
})} })}
</fieldset> </fieldset>
</form> </form>
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend>
<div className="helpText"> <div className="helpText">{app.translator.trans('core.admin.appearance.logo_text')}</div>
{app.translator.trans('core.admin.appearance.logo_text')} <UploadImageButton name="logo" />
</div>
<UploadImageButton name="logo"/>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend>
<div className="helpText"> <div className="helpText">{app.translator.trans('core.admin.appearance.favicon_text')}</div>
{app.translator.trans('core.admin.appearance.favicon_text')} <UploadImageButton name="favicon" />
</div>
<UploadImageButton name="favicon"/>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend>
<div className="helpText"> <div className="helpText">{app.translator.trans('core.admin.appearance.custom_header_text')}</div>
{app.translator.trans('core.admin.appearance.custom_header_text')}
</div>
{Button.component({ {Button.component({
className: 'Button', className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_header_button'), children: app.translator.trans('core.admin.appearance.edit_header_button'),
onclick: () => app.modal.show(new EditCustomHeaderModal()) onclick: () => app.modal.show(new EditCustomHeaderModal()),
})} })}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend>
<div className="helpText"> <div className="helpText">{app.translator.trans('core.admin.appearance.custom_footer_text')}</div>
{app.translator.trans('core.admin.appearance.custom_footer_text')}
</div>
{Button.component({ {Button.component({
className: 'Button', className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_footer_button'), children: app.translator.trans('core.admin.appearance.edit_footer_button'),
onclick: () => app.modal.show(new EditCustomFooterModal()) onclick: () => app.modal.show(new EditCustomFooterModal()),
})} })}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
<div className="helpText"> <div className="helpText">{app.translator.trans('core.admin.appearance.custom_styles_text')}</div>
{app.translator.trans('core.admin.appearance.custom_styles_text')}
</div>
{Button.component({ {Button.component({
className: 'Button', className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_css_button'), children: app.translator.trans('core.admin.appearance.edit_css_button'),
onclick: () => app.modal.show(new EditCustomCssModal()) onclick: () => app.modal.show(new EditCustomCssModal()),
})} })}
</fieldset> </fieldset>
</div> </div>
@ -126,7 +126,7 @@ export default class AppearancePage extends Page {
theme_primary_color: this.primaryColor(), theme_primary_color: this.primaryColor(),
theme_secondary_color: this.secondaryColor(), theme_secondary_color: this.secondaryColor(),
theme_dark_mode: this.darkMode(), theme_dark_mode: this.darkMode(),
theme_colored_header: this.coloredHeader() theme_colored_header: this.coloredHeader(),
}).then(() => window.location.reload()); }).then(() => window.location.reload());
} }
} }

View File

@ -20,12 +20,12 @@ export default class BasicsPage extends Page {
'show_language_selector', 'show_language_selector',
'default_route', 'default_route',
'welcome_title', 'welcome_title',
'welcome_message' 'welcome_message',
]; ];
this.values = {}; this.values = {};
const settings = app.data.settings; const settings = app.data.settings;
this.fields.forEach(key => this.values[key] = m.prop(settings[key])); this.fields.forEach((key) => (this.values[key] = m.prop(settings[key])));
this.localeOptions = {}; this.localeOptions = {};
const locales = app.data.locales; const locales = app.data.locales;
@ -33,7 +33,7 @@ export default class BasicsPage extends Page {
this.localeOptions[i] = `${locales[i]} (${i})`; this.localeOptions[i] = `${locales[i]} (${i})`;
} }
if (typeof this.values.show_language_selector() !== "number") this.values.show_language_selector(1); if (typeof this.values.show_language_selector() !== 'number') this.values.show_language_selector(1);
} }
view() { view() {
@ -43,19 +43,19 @@ export default class BasicsPage extends Page {
<form onsubmit={this.onsubmit.bind(this)}> <form onsubmit={this.onsubmit.bind(this)}>
{FieldSet.component({ {FieldSet.component({
label: app.translator.trans('core.admin.basics.forum_title_heading'), label: app.translator.trans('core.admin.basics.forum_title_heading'),
children: [ children: [<input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)} />],
<input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)}/>
]
})} })}
{FieldSet.component({ {FieldSet.component({
label: app.translator.trans('core.admin.basics.forum_description_heading'), label: app.translator.trans('core.admin.basics.forum_description_heading'),
children: [ children: [
<div className="helpText"> <div className="helpText">{app.translator.trans('core.admin.basics.forum_description_text')}</div>,
{app.translator.trans('core.admin.basics.forum_description_text')} <textarea
</div>, className="FormControl"
<textarea className="FormControl" value={this.values.forum_description()} oninput={m.withAttr('value', this.values.forum_description)}/> value={this.values.forum_description()}
] oninput={m.withAttr('value', this.values.forum_description)}
/>,
],
})} })}
{Object.keys(this.localeOptions).length > 1 {Object.keys(this.localeOptions).length > 1
@ -65,14 +65,14 @@ export default class BasicsPage extends Page {
Select.component({ Select.component({
options: this.localeOptions, options: this.localeOptions,
value: this.values.default_locale(), value: this.values.default_locale(),
onchange: this.values.default_locale onchange: this.values.default_locale,
}), }),
Switch.component({ Switch.component({
state: this.values.show_language_selector(), state: this.values.show_language_selector(),
onchange: this.values.show_language_selector, onchange: this.values.show_language_selector,
children: app.translator.trans('core.admin.basics.show_language_selector_label'), children: app.translator.trans('core.admin.basics.show_language_selector_label'),
}) }),
] ],
}) })
: ''} : ''}
@ -80,30 +80,38 @@ export default class BasicsPage extends Page {
label: app.translator.trans('core.admin.basics.home_page_heading'), label: app.translator.trans('core.admin.basics.home_page_heading'),
className: 'BasicsPage-homePage', className: 'BasicsPage-homePage',
children: [ children: [
<div className="helpText"> <div className="helpText">{app.translator.trans('core.admin.basics.home_page_text')}</div>,
{app.translator.trans('core.admin.basics.home_page_text')} this.homePageItems()
</div>, .toArray()
this.homePageItems().toArray().map(({path, label}) => .map(({ path, label }) => (
<label className="checkbox"> <label className="checkbox">
<input type="radio" name="homePage" value={path} checked={this.values.default_route() === path} onclick={m.withAttr('value', this.values.default_route)}/> <input
type="radio"
name="homePage"
value={path}
checked={this.values.default_route() === path}
onclick={m.withAttr('value', this.values.default_route)}
/>
{label} {label}
</label> </label>
) )),
] ],
})} })}
{FieldSet.component({ {FieldSet.component({
label: app.translator.trans('core.admin.basics.welcome_banner_heading'), label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
className: 'BasicsPage-welcomeBanner', className: 'BasicsPage-welcomeBanner',
children: [ children: [
<div className="helpText"> <div className="helpText">{app.translator.trans('core.admin.basics.welcome_banner_text')}</div>,
{app.translator.trans('core.admin.basics.welcome_banner_text')}
</div>,
<div className="BasicsPage-welcomeBanner-input"> <div className="BasicsPage-welcomeBanner-input">
<input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)}/> <input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)} />
<textarea className="FormControl" value={this.values.welcome_message()} oninput={m.withAttr('value', this.values.welcome_message)}/> <textarea
</div> className="FormControl"
] value={this.values.welcome_message()}
oninput={m.withAttr('value', this.values.welcome_message)}
/>
</div>,
],
})} })}
{Button.component({ {Button.component({
@ -111,7 +119,7 @@ export default class BasicsPage extends Page {
className: 'Button Button--primary', className: 'Button Button--primary',
children: app.translator.trans('core.admin.basics.submit_button'), children: app.translator.trans('core.admin.basics.submit_button'),
loading: this.loading, loading: this.loading,
disabled: !this.changed() disabled: !this.changed(),
})} })}
</form> </form>
</div> </div>
@ -120,7 +128,7 @@ export default class BasicsPage extends Page {
} }
changed() { changed() {
return this.fields.some(key => this.values[key]() !== app.data.settings[key]); return this.fields.some((key) => this.values[key]() !== app.data.settings[key]);
} }
/** /**
@ -135,7 +143,7 @@ export default class BasicsPage extends Page {
items.add('allDiscussions', { items.add('allDiscussions', {
path: '/all', path: '/all',
label: app.translator.trans('core.admin.basics.all_discussions_label') label: app.translator.trans('core.admin.basics.all_discussions_label'),
}); });
return items; return items;
@ -151,11 +159,11 @@ export default class BasicsPage extends Page {
const settings = {}; const settings = {};
this.fields.forEach(key => settings[key] = this.values[key]()); this.fields.forEach((key) => (settings[key] = this.values[key]()));
saveSettings(settings) saveSettings(settings)
.then(() => { .then(() => {
app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.translator.trans('core.admin.basics.saved_message')})); app.alerts.show((this.successAlert = new Alert({ type: 'success', children: app.translator.trans('core.admin.basics.saved_message') })));
}) })
.catch(() => {}) .catch(() => {})
.then(() => { .then(() => {

View File

@ -5,14 +5,12 @@ export default class DashboardPage extends Page {
view() { view() {
return ( return (
<div className="DashboardPage"> <div className="DashboardPage">
<div className="container"> <div className="container">{this.availableWidgets()}</div>
{this.availableWidgets()}
</div>
</div> </div>
); );
} }
availableWidgets() { availableWidgets() {
return [<StatusWidget/>]; return [<StatusWidget />];
} }
} }

View File

@ -11,11 +11,7 @@ import Component from '../../common/Component';
export default class Widget extends Component { export default class Widget extends Component {
view() { view() {
return ( return <div className={'Widget ' + this.className()}>{this.content()}</div>;
<div className={"Widget "+this.className()}>
{this.content()}
</div>
);
} }
/** /**

View File

@ -11,10 +11,14 @@ export default class EditCustomCssModal extends SettingsModal {
form() { form() {
return [ return [
<p>{app.translator.trans('core.admin.edit_css.customize_text', {a: <a href="https://github.com/flarum/core/tree/master/less" target="_blank"/>})}</p>, <p>
{app.translator.trans('core.admin.edit_css.customize_text', {
a: <a href="https://github.com/flarum/core/tree/master/less" target="_blank" />,
})}
</p>,
<div className="Form-group"> <div className="Form-group">
<textarea className="FormControl" rows="30" bidi={this.setting('custom_less')}/> <textarea className="FormControl" rows="30" bidi={this.setting('custom_less')} />
</div> </div>,
]; ];
} }

View File

@ -13,8 +13,8 @@ export default class EditCustomFooterModal extends SettingsModal {
return [ return [
<p>{app.translator.trans('core.admin.edit_footer.customize_text')}</p>, <p>{app.translator.trans('core.admin.edit_footer.customize_text')}</p>,
<div className="Form-group"> <div className="Form-group">
<textarea className="FormControl" rows="30" bidi={this.setting('custom_footer')}/> <textarea className="FormControl" rows="30" bidi={this.setting('custom_footer')} />
</div> </div>,
]; ];
} }

View File

@ -13,8 +13,8 @@ export default class EditCustomHeaderModal extends SettingsModal {
return [ return [
<p>{app.translator.trans('core.admin.edit_header.customize_text')}</p>, <p>{app.translator.trans('core.admin.edit_header.customize_text')}</p>,
<div className="Form-group"> <div className="Form-group">
<textarea className="FormControl" rows="30" bidi={this.setting('custom_header')}/> <textarea className="FormControl" rows="30" bidi={this.setting('custom_header')} />
</div> </div>,
]; ];
} }

View File

@ -24,21 +24,21 @@ export default class EditGroupModal extends Modal {
title() { title() {
return [ return [
this.color() || this.icon() ? Badge.component({ this.color() || this.icon()
? Badge.component({
icon: this.icon(), icon: this.icon(),
style: {backgroundColor: this.color()} style: { backgroundColor: this.color() },
}) : '', })
: '',
' ', ' ',
this.namePlural() || app.translator.trans('core.admin.edit_group.title') this.namePlural() || app.translator.trans('core.admin.edit_group.title'),
]; ];
} }
content() { content() {
return ( return (
<div className="Modal-body"> <div className="Modal-body">
<div className="Form"> <div className="Form">{this.fields().toArray()}</div>
{this.fields().toArray()}
</div>
</div> </div>
); );
} }
@ -46,40 +46,68 @@ export default class EditGroupModal extends Modal {
fields() { fields() {
const items = new ItemList(); const items = new ItemList();
items.add('name', <div className="Form-group"> items.add(
'name',
<div className="Form-group">
<label>{app.translator.trans('core.admin.edit_group.name_label')}</label> <label>{app.translator.trans('core.admin.edit_group.name_label')}</label>
<div className="EditGroupModal-name-input"> <div className="EditGroupModal-name-input">
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.singular_placeholder')} value={this.nameSingular()} oninput={m.withAttr('value', this.nameSingular)}/> <input
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.plural_placeholder')} value={this.namePlural()} oninput={m.withAttr('value', this.namePlural)}/> className="FormControl"
placeholder={app.translator.trans('core.admin.edit_group.singular_placeholder')}
value={this.nameSingular()}
oninput={m.withAttr('value', this.nameSingular)}
/>
<input
className="FormControl"
placeholder={app.translator.trans('core.admin.edit_group.plural_placeholder')}
value={this.namePlural()}
oninput={m.withAttr('value', this.namePlural)}
/>
</div> </div>
</div>, 30); </div>,
30
);
items.add('color', <div className="Form-group"> items.add(
'color',
<div className="Form-group">
<label>{app.translator.trans('core.admin.edit_group.color_label')}</label> <label>{app.translator.trans('core.admin.edit_group.color_label')}</label>
<input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)}/> <input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)} />
</div>, 20); </div>,
20
);
items.add('icon', <div className="Form-group"> items.add(
'icon',
<div className="Form-group">
<label>{app.translator.trans('core.admin.edit_group.icon_label')}</label> <label>{app.translator.trans('core.admin.edit_group.icon_label')}</label>
<div className="helpText"> <div className="helpText">
{app.translator.trans('core.admin.edit_group.icon_text', {a: <a href="https://fontawesome.com/icons?m=free" tabindex="-1"/>})} {app.translator.trans('core.admin.edit_group.icon_text', { a: <a href="https://fontawesome.com/icons?m=free" tabindex="-1" /> })}
</div> </div>
<input className="FormControl" placeholder="fas fa-bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)}/> <input className="FormControl" placeholder="fas fa-bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)} />
</div>, 10); </div>,
10
);
items.add('submit', <div className="Form-group"> items.add(
'submit',
<div className="Form-group">
{Button.component({ {Button.component({
type: 'submit', type: 'submit',
className: 'Button Button--primary EditGroupModal-save', className: 'Button Button--primary EditGroupModal-save',
loading: this.loading, loading: this.loading,
children: app.translator.trans('core.admin.edit_group.submit_button') children: app.translator.trans('core.admin.edit_group.submit_button'),
})} })}
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? ( {this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}> <button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
{app.translator.trans('core.admin.edit_group.delete_button')} {app.translator.trans('core.admin.edit_group.delete_button')}
</button> </button>
) : ''} ) : (
</div>, -10); ''
)}
</div>,
-10
);
return items; return items;
} }
@ -89,7 +117,7 @@ export default class EditGroupModal extends Modal {
nameSingular: this.nameSingular(), nameSingular: this.nameSingular(),
namePlural: this.namePlural(), namePlural: this.namePlural(),
color: this.color(), color: this.color(),
icon: this.icon() icon: this.icon(),
}; };
} }
@ -98,7 +126,8 @@ export default class EditGroupModal extends Modal {
this.loading = true; this.loading = true;
this.group.save(this.submitData(), {errorHandler: this.onerror.bind(this)}) this.group
.save(this.submitData(), { errorHandler: this.onerror.bind(this) })
.then(this.hide.bind(this)) .then(this.hide.bind(this))
.catch(() => { .catch(() => {
this.loading = false; this.loading = false;

View File

@ -19,7 +19,7 @@ export default class ExtensionsPage extends Page {
children: app.translator.trans('core.admin.extensions.add_button'), children: app.translator.trans('core.admin.extensions.add_button'),
icon: 'fas fa-plus', icon: 'fas fa-plus',
className: 'Button Button--primary', className: 'Button Button--primary',
onclick: () => app.modal.show(new AddExtensionModal()) onclick: () => app.modal.show(new AddExtensionModal()),
})} })}
</div> </div>
</div> </div>
@ -27,12 +27,12 @@ export default class ExtensionsPage extends Page {
<div className="ExtensionsPage-list"> <div className="ExtensionsPage-list">
<div className="container"> <div className="container">
<ul className="ExtensionList"> <ul className="ExtensionList">
{Object.keys(app.data.extensions) {Object.keys(app.data.extensions).map((id) => {
.map(id => {
const extension = app.data.extensions[id]; const extension = app.data.extensions[id];
const controls = this.controlItems(extension.id).toArray(); const controls = this.controlItems(extension.id).toArray();
return <li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}> return (
<li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}>
<div className="ExtensionListItem-content"> <div className="ExtensionListItem-content">
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}> <span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? icon(extension.icon.name) : ''} {extension.icon ? icon(extension.icon.name) : ''}
@ -42,20 +42,24 @@ export default class ExtensionsPage extends Page {
className="ExtensionListItem-controls" className="ExtensionListItem-controls"
buttonClassName="Button Button--icon Button--flat" buttonClassName="Button Button--icon Button--flat"
menuClassName="Dropdown-menu--right" menuClassName="Dropdown-menu--right"
icon="fas fa-ellipsis-h"> icon="fas fa-ellipsis-h"
>
{controls} {controls}
</Dropdown> </Dropdown>
) : ''} ) : (
''
)}
<div className="ExtensionListItem-main"> <div className="ExtensionListItem-main">
<label className="ExtensionListItem-title"> <label className="ExtensionListItem-title">
<input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)}/> {' '} <input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)} />{' '}
{extension.extra['flarum-extension'].title} {extension.extra['flarum-extension'].title}
</label> </label>
<div className="ExtensionListItem-version">{extension.version}</div> <div className="ExtensionListItem-version">{extension.version}</div>
<div className="ExtensionListItem-description">{extension.description}</div> <div className="ExtensionListItem-description">{extension.description}</div>
</div> </div>
</div> </div>
</li>; </li>
);
})} })}
</ul> </ul>
</div> </div>
@ -69,26 +73,34 @@ export default class ExtensionsPage extends Page {
const enabled = this.isEnabled(name); const enabled = this.isEnabled(name);
if (app.extensionSettings[name]) { if (app.extensionSettings[name]) {
items.add('settings', Button.component({ items.add(
'settings',
Button.component({
icon: 'fas fa-cog', icon: 'fas fa-cog',
children: app.translator.trans('core.admin.extensions.settings_button'), children: app.translator.trans('core.admin.extensions.settings_button'),
onclick: app.extensionSettings[name] onclick: app.extensionSettings[name],
})); })
);
} }
if (!enabled) { if (!enabled) {
items.add('uninstall', Button.component({ items.add(
'uninstall',
Button.component({
icon: 'far fa-trash-alt', icon: 'far fa-trash-alt',
children: app.translator.trans('core.admin.extensions.uninstall_button'), children: app.translator.trans('core.admin.extensions.uninstall_button'),
onclick: () => { onclick: () => {
app.request({ app
.request({
url: app.forum.attribute('apiUrl') + '/extensions/' + name, url: app.forum.attribute('apiUrl') + '/extensions/' + name,
method: 'DELETE' method: 'DELETE',
}).then(() => window.location.reload()); })
.then(() => window.location.reload());
app.modal.show(new LoadingModal()); app.modal.show(new LoadingModal());
} },
})); })
);
} }
return items; return items;
@ -103,11 +115,13 @@ export default class ExtensionsPage extends Page {
toggle(id) { toggle(id) {
const enabled = this.isEnabled(id); const enabled = this.isEnabled(id);
app.request({ app
.request({
url: app.forum.attribute('apiUrl') + '/extensions/' + id, url: app.forum.attribute('apiUrl') + '/extensions/' + id,
method: 'PATCH', method: 'PATCH',
data: {enabled: !enabled} data: { enabled: !enabled },
}).then(() => { })
.then(() => {
if (!enabled) localStorage.setItem('enabledExtension', id); if (!enabled) localStorage.setItem('enabledExtension', id);
window.location.reload(); window.location.reload();
}); });

View File

@ -8,11 +8,7 @@ import listItems from '../../common/helpers/listItems';
*/ */
export default class HeaderPrimary extends Component { export default class HeaderPrimary extends Component {
view() { view() {
return ( return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
<ul className="Header-controls">
{listItems(this.items().toArray())}
</ul>
);
} }
config(isInitialized, context) { config(isInitialized, context) {

View File

@ -8,11 +8,7 @@ import listItems from '../../common/helpers/listItems';
*/ */
export default class HeaderSecondary extends Component { export default class HeaderSecondary extends Component {
view() { view() {
return ( return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
<ul className="Header-controls">
{listItems(this.items().toArray())}
</ul>
);
} }
config(isInitialized, context) { config(isInitialized, context) {

View File

@ -20,15 +20,17 @@ export default class MailPage extends Page {
this.driverFields = {}; this.driverFields = {};
this.fields = ['mail_driver', 'mail_from']; this.fields = ['mail_driver', 'mail_from'];
this.values = {}; this.values = {};
this.status = {sending: false, errors: {}}; this.status = { sending: false, errors: {} };
const settings = app.data.settings; const settings = app.data.settings;
this.fields.forEach(key => this.values[key] = m.prop(settings[key])); this.fields.forEach((key) => (this.values[key] = m.prop(settings[key])));
app.request({ app
.request({
method: 'GET', method: 'GET',
url: app.forum.attribute('apiUrl') + '/mail-settings' url: app.forum.attribute('apiUrl') + '/mail-settings',
}).then(response => { })
.then((response) => {
this.driverFields = response['data']['attributes']['fields']; this.driverFields = response['data']['attributes']['fields'];
this.status.sending = response['data']['attributes']['sending']; this.status.sending = response['data']['attributes']['sending'];
this.status.errors = response['data']['attributes']['errors']; this.status.errors = response['data']['attributes']['errors'];
@ -64,9 +66,7 @@ export default class MailPage extends Page {
<div className="container"> <div className="container">
<form onsubmit={this.onsubmit.bind(this)}> <form onsubmit={this.onsubmit.bind(this)}>
<h2>{app.translator.trans('core.admin.email.heading')}</h2> <h2>{app.translator.trans('core.admin.email.heading')}</h2>
<div className="helpText"> <div className="helpText">{app.translator.trans('core.admin.email.text')}</div>
{app.translator.trans('core.admin.email.text')}
</div>
{FieldSet.component({ {FieldSet.component({
label: app.translator.trans('core.admin.email.addresses_heading'), label: app.translator.trans('core.admin.email.addresses_heading'),
@ -77,8 +77,8 @@ export default class MailPage extends Page {
{app.translator.trans('core.admin.email.from_label')} {app.translator.trans('core.admin.email.from_label')}
<input className="FormControl" value={this.values.mail_from() || ''} oninput={m.withAttr('value', this.values.mail_from)} /> <input className="FormControl" value={this.values.mail_from() || ''} oninput={m.withAttr('value', this.values.mail_from)} />
</label> </label>
</div> </div>,
] ],
})} })}
{FieldSet.component({ {FieldSet.component({
@ -88,38 +88,44 @@ export default class MailPage extends Page {
<div className="MailPage-MailSettings-input"> <div className="MailPage-MailSettings-input">
<label> <label>
{app.translator.trans('core.admin.email.driver_label')} {app.translator.trans('core.admin.email.driver_label')}
<Select value={this.values.mail_driver()} options={Object.keys(this.driverFields).reduce((memo, val) => ({...memo, [val]: val}), {})} onchange={this.values.mail_driver} /> <Select
value={this.values.mail_driver()}
options={Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {})}
onchange={this.values.mail_driver}
/>
</label> </label>
</div> </div>,
] ],
})} })}
{this.status.sending || Alert.component({ {this.status.sending ||
Alert.component({
children: app.translator.trans('core.admin.email.not_sending_message'), children: app.translator.trans('core.admin.email.not_sending_message'),
dismissible: false, dismissible: false,
})} })}
{fieldKeys.length > 0 && FieldSet.component({ {fieldKeys.length > 0 &&
FieldSet.component({
label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`), label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`),
className: 'MailPage-MailSettings', className: 'MailPage-MailSettings',
children: [ children: [
<div className="MailPage-MailSettings-input"> <div className="MailPage-MailSettings-input">
{fieldKeys.map(field => [ {fieldKeys.map((field) => [
<label> <label>
{app.translator.trans(`core.admin.email.${field}_label`)} {app.translator.trans(`core.admin.email.${field}_label`)}
{this.renderField(field)} {this.renderField(field)}
</label>, </label>,
this.status.errors[field] && <p className='ValidationError'>{this.status.errors[field]}</p>, this.status.errors[field] && <p className="ValidationError">{this.status.errors[field]}</p>,
])} ])}
</div> </div>,
] ],
})} })}
{Button.component({ {Button.component({
type: 'submit', type: 'submit',
className: 'Button Button--primary', className: 'Button Button--primary',
children: app.translator.trans('core.admin.email.submit_button'), children: app.translator.trans('core.admin.email.submit_button'),
disabled: !this.changed() disabled: !this.changed(),
})} })}
</form> </form>
</div> </div>
@ -140,7 +146,7 @@ export default class MailPage extends Page {
} }
changed() { changed() {
return this.fields.some(key => this.values[key]() !== app.data.settings[key]); return this.fields.some((key) => this.values[key]() !== app.data.settings[key]);
} }
onsubmit(e) { onsubmit(e) {
@ -153,11 +159,11 @@ export default class MailPage extends Page {
const settings = {}; const settings = {};
this.fields.forEach(key => settings[key] = this.values[key]()); this.fields.forEach((key) => (settings[key] = this.values[key]()));
saveSettings(settings) saveSettings(settings)
.then(() => { .then(() => {
app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.translator.trans('core.admin.basics.saved_message')})); app.alerts.show((this.successAlert = new Alert({ type: 'success', children: app.translator.trans('core.admin.basics.saved_message') })));
}) })
.catch(() => {}) .catch(() => {})
.then(() => { .then(() => {

View File

@ -8,22 +8,21 @@ import GroupBadge from '../../common/components/GroupBadge';
function badgeForId(id) { function badgeForId(id) {
const group = app.store.getById('groups', id); const group = app.store.getById('groups', id);
return group ? GroupBadge.component({group, label: null}) : ''; return group ? GroupBadge.component({ group, label: null }) : '';
} }
function filterByRequiredPermissions(groupIds, permission) { function filterByRequiredPermissions(groupIds, permission) {
app.getRequiredPermissions(permission) app.getRequiredPermissions(permission).forEach((required) => {
.forEach(required => {
const restrictToGroupIds = app.data.permissions[required] || []; const restrictToGroupIds = app.data.permissions[required] || [];
if (restrictToGroupIds.indexOf(Group.GUEST_ID) !== -1) { if (restrictToGroupIds.indexOf(Group.GUEST_ID) !== -1) {
// do nothing // do nothing
} else if (restrictToGroupIds.indexOf(Group.MEMBER_ID) !== -1) { } else if (restrictToGroupIds.indexOf(Group.MEMBER_ID) !== -1) {
groupIds = groupIds.filter(id => id !== Group.GUEST_ID); groupIds = groupIds.filter((id) => id !== Group.GUEST_ID);
} else if (groupIds.indexOf(Group.MEMBER_ID) !== -1) { } else if (groupIds.indexOf(Group.MEMBER_ID) !== -1) {
groupIds = restrictToGroupIds; groupIds = restrictToGroupIds;
} else { } else {
groupIds = restrictToGroupIds.filter(id => groupIds.indexOf(id) !== -1); groupIds = restrictToGroupIds.filter((id) => groupIds.indexOf(id) !== -1);
} }
groupIds = filterByRequiredPermissions(groupIds, required); groupIds = filterByRequiredPermissions(groupIds, required);
@ -52,34 +51,31 @@ export default class PermissionDropdown extends Dropdown {
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID); const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
if (everyone) { if (everyone) {
this.props.label = Badge.component({icon: 'fas fa-globe'}); this.props.label = Badge.component({ icon: 'fas fa-globe' });
} else if (members) { } else if (members) {
this.props.label = Badge.component({icon: 'fas fa-user'}); this.props.label = Badge.component({ icon: 'fas fa-user' });
} else { } else {
this.props.label = [ this.props.label = [badgeForId(Group.ADMINISTRATOR_ID), groupIds.map(badgeForId)];
badgeForId(Group.ADMINISTRATOR_ID),
groupIds.map(badgeForId)
];
} }
if (this.showing) { if (this.showing) {
if (this.props.allowGuest) { if (this.props.allowGuest) {
this.props.children.push( this.props.children.push(
Button.component({ Button.component({
children: [Badge.component({icon: 'fas fa-globe'}), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')], children: [Badge.component({ icon: 'fas fa-globe' }), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')],
icon: everyone ? 'fas fa-check' : true, icon: everyone ? 'fas fa-check' : true,
onclick: () => this.save([Group.GUEST_ID]), onclick: () => this.save([Group.GUEST_ID]),
disabled: this.isGroupDisabled(Group.GUEST_ID) disabled: this.isGroupDisabled(Group.GUEST_ID),
}) })
); );
} }
this.props.children.push( this.props.children.push(
Button.component({ Button.component({
children: [Badge.component({icon: 'fas fa-user'}), ' ', app.translator.trans('core.admin.permissions_controls.members_button')], children: [Badge.component({ icon: 'fas fa-user' }), ' ', app.translator.trans('core.admin.permissions_controls.members_button')],
icon: members ? 'fas fa-check' : true, icon: members ? 'fas fa-check' : true,
onclick: () => this.save([Group.MEMBER_ID]), onclick: () => this.save([Group.MEMBER_ID]),
disabled: this.isGroupDisabled(Group.MEMBER_ID) disabled: this.isGroupDisabled(Group.MEMBER_ID),
}), }),
Separator.component(), Separator.component(),
@ -88,26 +84,29 @@ export default class PermissionDropdown extends Dropdown {
children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()], children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()],
icon: !everyone && !members ? 'fas fa-check' : true, icon: !everyone && !members ? 'fas fa-check' : true,
disabled: !everyone && !members, disabled: !everyone && !members,
onclick: e => { onclick: (e) => {
if (e.shiftKey) e.stopPropagation(); if (e.shiftKey) e.stopPropagation();
this.save([]); this.save([]);
} },
}) })
); );
[].push.apply( [].push.apply(
this.props.children, this.props.children,
app.store.all('groups') app.store
.filter(group => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1) .all('groups')
.map(group => Button.component({ .filter((group) => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
.map((group) =>
Button.component({
children: [badgeForId(group.id()), ' ', group.namePlural()], children: [badgeForId(group.id()), ' ', group.namePlural()],
icon: groupIds.indexOf(group.id()) !== -1 ? 'fas fa-check' : true, icon: groupIds.indexOf(group.id()) !== -1 ? 'fas fa-check' : true,
onclick: (e) => { onclick: (e) => {
if (e.shiftKey) e.stopPropagation(); if (e.shiftKey) e.stopPropagation();
this.toggle(group.id()); this.toggle(group.id());
}, },
disabled: this.isGroupDisabled(group.id()) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID) disabled: this.isGroupDisabled(group.id()) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID),
})) })
)
); );
} }
@ -122,7 +121,7 @@ export default class PermissionDropdown extends Dropdown {
app.request({ app.request({
method: 'POST', method: 'POST',
url: app.forum.attribute('apiUrl') + '/permission', url: app.forum.attribute('apiUrl') + '/permission',
data: {permission, groupIds} data: { permission, groupIds },
}); });
} }
@ -137,7 +136,7 @@ export default class PermissionDropdown extends Dropdown {
groupIds.splice(index, 1); groupIds.splice(index, 1);
} else { } else {
groupIds.push(groupId); groupIds.push(groupId);
groupIds = groupIds.filter(id => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(id) === -1); groupIds = groupIds.filter((id) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(id) === -1);
} }
this.save(groupIds); this.save(groupIds);

View File

@ -13,12 +13,8 @@ export default class PermissionGrid extends Component {
view() { view() {
const scopes = this.scopeItems().toArray(); const scopes = this.scopeItems().toArray();
const permissionCells = permission => { const permissionCells = (permission) => {
return scopes.map(scope => ( return scopes.map((scope) => <td>{scope.render(permission)}</td>);
<td>
{scope.render(permission)}
</td>
));
}; };
return ( return (
@ -26,27 +22,32 @@ export default class PermissionGrid extends Component {
<thead> <thead>
<tr> <tr>
<td></td> <td></td>
{scopes.map(scope => ( {scopes.map((scope) => (
<th> <th>
{scope.label}{' '} {scope.label}{' '}
{scope.onremove ? Button.component({icon: 'fas fa-times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove}) : ''} {scope.onremove
? Button.component({ icon: 'fas fa-times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove })
: ''}
</th> </th>
))} ))}
<th>{this.scopeControlItems().toArray()}</th> <th>{this.scopeControlItems().toArray()}</th>
</tr> </tr>
</thead> </thead>
{this.permissions.map(section => ( {this.permissions.map((section) => (
<tbody> <tbody>
<tr className="PermissionGrid-section"> <tr className="PermissionGrid-section">
<th>{section.label}</th> <th>{section.label}</th>
{permissionCells(section)} {permissionCells(section)}
<td/> <td />
</tr> </tr>
{section.children.map(child => ( {section.children.map((child) => (
<tr className="PermissionGrid-child"> <tr className="PermissionGrid-child">
<th>{icon(child.icon)}{child.label}</th> <th>
{icon(child.icon)}
{child.label}
</th>
{permissionCells(child)} {permissionCells(child)}
<td/> <td />
</tr> </tr>
))} ))}
</tbody> </tbody>
@ -58,25 +59,41 @@ export default class PermissionGrid extends Component {
permissionItems() { permissionItems() {
const items = new ItemList(); const items = new ItemList();
items.add('view', { items.add(
'view',
{
label: app.translator.trans('core.admin.permissions.read_heading'), label: app.translator.trans('core.admin.permissions.read_heading'),
children: this.viewItems().toArray() children: this.viewItems().toArray(),
}, 100); },
100
);
items.add('start', { items.add(
'start',
{
label: app.translator.trans('core.admin.permissions.create_heading'), label: app.translator.trans('core.admin.permissions.create_heading'),
children: this.startItems().toArray() children: this.startItems().toArray(),
}, 90); },
90
);
items.add('reply', { items.add(
'reply',
{
label: app.translator.trans('core.admin.permissions.participate_heading'), label: app.translator.trans('core.admin.permissions.participate_heading'),
children: this.replyItems().toArray() children: this.replyItems().toArray(),
}, 80); },
80
);
items.add('moderate', { items.add(
'moderate',
{
label: app.translator.trans('core.admin.permissions.moderate_heading'), label: app.translator.trans('core.admin.permissions.moderate_heading'),
children: this.moderateItems().toArray() children: this.moderateItems().toArray(),
}, 70); },
70
);
return items; return items;
} }
@ -84,31 +101,44 @@ export default class PermissionGrid extends Component {
viewItems() { viewItems() {
const items = new ItemList(); const items = new ItemList();
items.add('viewDiscussions', { items.add(
'viewDiscussions',
{
icon: 'fas fa-eye', icon: 'fas fa-eye',
label: app.translator.trans('core.admin.permissions.view_discussions_label'), label: app.translator.trans('core.admin.permissions.view_discussions_label'),
permission: 'viewDiscussions', permission: 'viewDiscussions',
allowGuest: true allowGuest: true,
}, 100); },
100
);
items.add('viewUserList', { items.add(
'viewUserList',
{
icon: 'fas fa-users', icon: 'fas fa-users',
label: app.translator.trans('core.admin.permissions.view_user_list_label'), label: app.translator.trans('core.admin.permissions.view_user_list_label'),
permission: 'viewUserList', permission: 'viewUserList',
allowGuest: true allowGuest: true,
}, 100); },
100
);
items.add('signUp', { items.add(
'signUp',
{
icon: 'fas fa-user-plus', icon: 'fas fa-user-plus',
label: app.translator.trans('core.admin.permissions.sign_up_label'), label: app.translator.trans('core.admin.permissions.sign_up_label'),
setting: () => SettingDropdown.component({ setting: () =>
SettingDropdown.component({
key: 'allow_sign_up', key: 'allow_sign_up',
options: [ options: [
{value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button')}, { value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button') },
{value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button')} { value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button') },
] ],
}) }),
}, 90); },
90
);
items.add('viewLastSeenAt', { items.add('viewLastSeenAt', {
icon: 'far fa-clock', icon: 'far fa-clock',
@ -122,13 +152,19 @@ export default class PermissionGrid extends Component {
startItems() { startItems() {
const items = new ItemList(); const items = new ItemList();
items.add('start', { items.add(
'start',
{
icon: 'fas fa-edit', icon: 'fas fa-edit',
label: app.translator.trans('core.admin.permissions.start_discussions_label'), label: app.translator.trans('core.admin.permissions.start_discussions_label'),
permission: 'startDiscussion' permission: 'startDiscussion',
}, 100); },
100
);
items.add('allowRenaming', { items.add(
'allowRenaming',
{
icon: 'fas fa-i-cursor', icon: 'fas fa-i-cursor',
label: app.translator.trans('core.admin.permissions.allow_renaming_label'), label: app.translator.trans('core.admin.permissions.allow_renaming_label'),
setting: () => { setting: () => {
@ -136,17 +172,19 @@ export default class PermissionGrid extends Component {
return SettingDropdown.component({ return SettingDropdown.component({
defaultLabel: minutes defaultLabel: minutes
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, {count: minutes}) ? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, { count: minutes })
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'), : app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
key: 'allow_renaming', key: 'allow_renaming',
options: [ options: [
{value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')}, { value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') },
{value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')}, { value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') },
{value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button')} { value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') },
] ],
}); });
} },
}, 90); },
90
);
return items; return items;
} }
@ -154,13 +192,19 @@ export default class PermissionGrid extends Component {
replyItems() { replyItems() {
const items = new ItemList(); const items = new ItemList();
items.add('reply', { items.add(
'reply',
{
icon: 'fas fa-reply', icon: 'fas fa-reply',
label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'), label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'),
permission: 'discussion.reply' permission: 'discussion.reply',
}, 100); },
100
);
items.add('allowPostEditing', { items.add(
'allowPostEditing',
{
icon: 'fas fa-pencil-alt', icon: 'fas fa-pencil-alt',
label: app.translator.trans('core.admin.permissions.allow_post_editing_label'), label: app.translator.trans('core.admin.permissions.allow_post_editing_label'),
setting: () => { setting: () => {
@ -168,17 +212,19 @@ export default class PermissionGrid extends Component {
return SettingDropdown.component({ return SettingDropdown.component({
defaultLabel: minutes defaultLabel: minutes
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, {count: minutes}) ? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, { count: minutes })
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'), : app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
key: 'allow_post_editing', key: 'allow_post_editing',
options: [ options: [
{value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')}, { value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') },
{value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')}, { value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') },
{value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button')} { value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') },
] ],
}); });
} },
}, 90); },
90
);
return items; return items;
} }
@ -186,59 +232,95 @@ export default class PermissionGrid extends Component {
moderateItems() { moderateItems() {
const items = new ItemList(); const items = new ItemList();
items.add('viewIpsPosts', { items.add(
'viewIpsPosts',
{
icon: 'fas fa-bullseye', icon: 'fas fa-bullseye',
label: app.translator.trans('core.admin.permissions.view_post_ips_label'), label: app.translator.trans('core.admin.permissions.view_post_ips_label'),
permission: 'discussion.viewIpsPosts' permission: 'discussion.viewIpsPosts',
}, 110); },
110
);
items.add('renameDiscussions', { items.add(
'renameDiscussions',
{
icon: 'fas fa-i-cursor', icon: 'fas fa-i-cursor',
label: app.translator.trans('core.admin.permissions.rename_discussions_label'), label: app.translator.trans('core.admin.permissions.rename_discussions_label'),
permission: 'discussion.rename' permission: 'discussion.rename',
}, 100); },
100
);
items.add('hideDiscussions', { items.add(
'hideDiscussions',
{
icon: 'far fa-trash-alt', icon: 'far fa-trash-alt',
label: app.translator.trans('core.admin.permissions.delete_discussions_label'), label: app.translator.trans('core.admin.permissions.delete_discussions_label'),
permission: 'discussion.hide' permission: 'discussion.hide',
}, 90); },
90
);
items.add('deleteDiscussions', { items.add(
'deleteDiscussions',
{
icon: 'fas fa-times', icon: 'fas fa-times',
label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'), label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'),
permission: 'discussion.delete' permission: 'discussion.delete',
}, 80); },
80
);
items.add('postWithoutThrottle', { items.add(
'postWithoutThrottle',
{
icon: 'fas fa-swimmer', icon: 'fas fa-swimmer',
label: app.translator.trans('core.admin.permissions.post_without_throttle_label'), label: app.translator.trans('core.admin.permissions.post_without_throttle_label'),
permission: 'postWithoutThrottle' permission: 'postWithoutThrottle',
}, 70); },
70
);
items.add('editPosts', { items.add(
'editPosts',
{
icon: 'fas fa-pencil-alt', icon: 'fas fa-pencil-alt',
label: app.translator.trans('core.admin.permissions.edit_posts_label'), label: app.translator.trans('core.admin.permissions.edit_posts_label'),
permission: 'discussion.editPosts' permission: 'discussion.editPosts',
}, 70); },
70
);
items.add('hidePosts', { items.add(
'hidePosts',
{
icon: 'far fa-trash-alt', icon: 'far fa-trash-alt',
label: app.translator.trans('core.admin.permissions.delete_posts_label'), label: app.translator.trans('core.admin.permissions.delete_posts_label'),
permission: 'discussion.hidePosts' permission: 'discussion.hidePosts',
}, 60); },
60
);
items.add('deletePosts', { items.add(
'deletePosts',
{
icon: 'fas fa-times', icon: 'fas fa-times',
label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'), label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'),
permission: 'discussion.deletePosts' permission: 'discussion.deletePosts',
}, 60); },
60
);
items.add('userEdit', { items.add(
'userEdit',
{
icon: 'fas fa-user-cog', icon: 'fas fa-user-cog',
label: app.translator.trans('core.admin.permissions.edit_users_label'), label: app.translator.trans('core.admin.permissions.edit_users_label'),
permission: 'user.edit' permission: 'user.edit',
}, 60); },
60
);
return items; return items;
} }
@ -246,21 +328,25 @@ export default class PermissionGrid extends Component {
scopeItems() { scopeItems() {
const items = new ItemList(); const items = new ItemList();
items.add('global', { items.add(
'global',
{
label: app.translator.trans('core.admin.permissions.global_heading'), label: app.translator.trans('core.admin.permissions.global_heading'),
render: item => { render: (item) => {
if (item.setting) { if (item.setting) {
return item.setting(); return item.setting();
} else if (item.permission) { } else if (item.permission) {
return PermissionDropdown.component({ return PermissionDropdown.component({
permission: item.permission, permission: item.permission,
allowGuest: item.allowGuest allowGuest: item.allowGuest,
}); });
} }
return ''; return '';
} },
}, 100); },
100
);
return items; return items;
} }

View File

@ -11,29 +11,28 @@ export default class PermissionsPage extends Page {
<div className="PermissionsPage"> <div className="PermissionsPage">
<div className="PermissionsPage-groups"> <div className="PermissionsPage-groups">
<div className="container"> <div className="container">
{app.store.all('groups') {app.store
.filter(group => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1) .all('groups')
.map(group => ( .filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
<button className="Button Group" onclick={() => app.modal.show(new EditGroupModal({group}))}> .map((group) => (
<button className="Button Group" onclick={() => app.modal.show(new EditGroupModal({ group }))}>
{GroupBadge.component({ {GroupBadge.component({
group, group,
className: 'Group-icon', className: 'Group-icon',
label: null label: null,
})} })}
<span className="Group-name">{group.namePlural()}</span> <span className="Group-name">{group.namePlural()}</span>
</button> </button>
))} ))}
<button className="Button Group Group--add" onclick={() => app.modal.show(new EditGroupModal())}> <button className="Button Group Group--add" onclick={() => app.modal.show(new EditGroupModal())}>
{icon('fas fa-plus', {className: 'Group-icon'})} {icon('fas fa-plus', { className: 'Group-icon' })}
<span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span> <span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
</button> </button>
</div> </div>
</div> </div>
<div className="PermissionsPage-permissions"> <div className="PermissionsPage-permissions">
<div className="container"> <div className="container">{PermissionGrid.component()}</div>
{PermissionGrid.component()}
</div>
</div> </div>
</div> </div>
); );

View File

@ -26,10 +26,7 @@ export default class SessionDropdown extends Dropdown {
getButtonContent() { getButtonContent() {
const user = app.session.user; const user = app.session.user;
return [ return [avatar(user), ' ', <span className="Button-label">{username(user)}</span>];
avatar(user), ' ',
<span className="Button-label">{username(user)}</span>
];
} }
/** /**
@ -40,11 +37,12 @@ export default class SessionDropdown extends Dropdown {
items() { items() {
const items = new ItemList(); const items = new ItemList();
items.add('logOut', items.add(
'logOut',
Button.component({ Button.component({
icon: 'fas fa-sign-out-alt', icon: 'fas fa-sign-out-alt',
children: app.translator.trans('core.admin.header.log_out_button'), children: app.translator.trans('core.admin.header.log_out_button'),
onclick: app.session.logout.bind(app.session) onclick: app.session.logout.bind(app.session),
}), }),
-100 -100
); );

View File

@ -11,14 +11,14 @@ export default class SettingDropdown extends SelectDropdown {
props.caretIcon = 'fas fa-caret-down'; props.caretIcon = 'fas fa-caret-down';
props.defaultLabel = 'Custom'; props.defaultLabel = 'Custom';
props.children = props.options.map(({value, label}) => { props.children = props.options.map(({ value, label }) => {
const active = app.data.settings[props.key] === value; const active = app.data.settings[props.key] === value;
return Button.component({ return Button.component({
children: label, children: label,
icon: active ? 'fas fa-check' : true, icon: active ? 'fas fa-check' : true,
onclick: saveSettings.bind(this, {[props.key]: value}), onclick: saveSettings.bind(this, { [props.key]: value }),
active active,
}); });
}); });
} }

View File

@ -18,9 +18,7 @@ export default class SettingsModal extends Modal {
<div className="Form"> <div className="Form">
{this.form()} {this.form()}
<div className="Form-group"> <div className="Form-group">{this.submitButton()}</div>
{this.submitButton()}
</div>
</div> </div>
</div> </div>
); );
@ -28,11 +26,7 @@ export default class SettingsModal extends Modal {
submitButton() { submitButton() {
return ( return (
<Button <Button type="submit" className="Button Button--primary" loading={this.loading} disabled={!this.changed()}>
type="submit"
className="Button Button--primary"
loading={this.loading}
disabled={!this.changed()}>
{app.translator.trans('core.admin.settings.submit_button')} {app.translator.trans('core.admin.settings.submit_button')}
</Button> </Button>
); );
@ -47,7 +41,7 @@ export default class SettingsModal extends Modal {
dirty() { dirty() {
const dirty = {}; const dirty = {};
Object.keys(this.settings).forEach(key => { Object.keys(this.settings).forEach((key) => {
const value = this.settings[key](); const value = this.settings[key]();
if (value !== app.data.settings[key]) { if (value !== app.data.settings[key]) {
@ -67,10 +61,7 @@ export default class SettingsModal extends Modal {
this.loading = true; this.loading = true;
saveSettings(this.dirty()).then( saveSettings(this.dirty()).then(this.onsaved.bind(this), this.loaded.bind(this));
this.onsaved.bind(this),
this.loaded.bind(this)
);
} }
onsaved() { onsaved() {

View File

@ -20,29 +20,27 @@ export default class StatusWidget extends DashboardWidget {
} }
content() { content() {
return ( return <ul>{listItems(this.items().toArray())}</ul>;
<ul>{listItems(this.items().toArray())}</ul>
);
} }
items() { items() {
const items = new ItemList(); const items = new ItemList();
items.add('tools', ( items.add(
'tools',
<Dropdown <Dropdown
label={app.translator.trans('core.admin.dashboard.tools_button')} label={app.translator.trans('core.admin.dashboard.tools_button')}
icon="fas fa-cog" icon="fas fa-cog"
buttonClassName="Button" buttonClassName="Button"
menuClassName="Dropdown-menu--right"> menuClassName="Dropdown-menu--right"
<Button onclick={this.handleClearCache.bind(this)}> >
{app.translator.trans('core.admin.dashboard.clear_cache_button')} <Button onclick={this.handleClearCache.bind(this)}>{app.translator.trans('core.admin.dashboard.clear_cache_button')}</Button>
</Button>
</Dropdown> </Dropdown>
)); );
items.add('version-flarum', [<strong>Flarum</strong>, <br/>, app.forum.attribute('version')]); items.add('version-flarum', [<strong>Flarum</strong>, <br />, app.forum.attribute('version')]);
items.add('version-php', [<strong>PHP</strong>, <br/>, app.data.phpVersion]); items.add('version-php', [<strong>PHP</strong>, <br />, app.data.phpVersion]);
items.add('version-mysql', [<strong>MySQL</strong>, <br/>, app.data.mysqlVersion]); items.add('version-mysql', [<strong>MySQL</strong>, <br />, app.data.mysqlVersion]);
return items; return items;
} }
@ -50,9 +48,11 @@ export default class StatusWidget extends DashboardWidget {
handleClearCache(e) { handleClearCache(e) {
app.modal.show(new LoadingModal()); app.modal.show(new LoadingModal());
app.request({ app
.request({
method: 'DELETE', method: 'DELETE',
url: app.forum.attribute('apiUrl') + '/cache' url: app.forum.attribute('apiUrl') + '/cache',
}).then(() => window.location.reload()); })
.then(() => window.location.reload());
} }
} }

View File

@ -15,7 +15,9 @@ export default class UploadImageButton extends Button {
return ( return (
<div> <div>
<p><img src={app.forum.attribute(this.props.name+'Url')} alt=""/></p> <p>
<img src={app.forum.attribute(this.props.name + 'Url')} alt="" />
</p>
<p>{super.view()}</p> <p>{super.view()}</p>
</div> </div>
); );
@ -35,22 +37,25 @@ export default class UploadImageButton extends Button {
const $input = $('<input type="file">'); const $input = $('<input type="file">');
$input.appendTo('body').hide().click().on('change', e => { $input
.appendTo('body')
.hide()
.click()
.on('change', (e) => {
const data = new FormData(); const data = new FormData();
data.append(this.props.name, $(e.target)[0].files[0]); data.append(this.props.name, $(e.target)[0].files[0]);
this.loading = true; this.loading = true;
m.redraw(); m.redraw();
app.request({ app
.request({
method: 'POST', method: 'POST',
url: this.resourceUrl(), url: this.resourceUrl(),
serialize: raw => raw, serialize: (raw) => raw,
data data,
}).then( })
this.success.bind(this), .then(this.success.bind(this), this.failure.bind(this));
this.failure.bind(this)
);
}); });
} }
@ -61,13 +66,12 @@ export default class UploadImageButton extends Button {
this.loading = true; this.loading = true;
m.redraw(); m.redraw();
app.request({ app
.request({
method: 'DELETE', method: 'DELETE',
url: this.resourceUrl() url: this.resourceUrl(),
}).then( })
this.success.bind(this), .then(this.success.bind(this), this.failure.bind(this));
this.failure.bind(this)
);
} }
resourceUrl() { resourceUrl() {

View File

@ -11,11 +11,7 @@ import Component from '../../common/Component';
export default class DashboardWidget extends Component { export default class DashboardWidget extends Component {
view() { view() {
return ( return <div className={'DashboardWidget ' + this.className()}>{this.content()}</div>;
<div className={"DashboardWidget "+this.className()}>
{this.content()}
</div>
);
} }
/** /**

View File

@ -9,7 +9,6 @@ export { app };
// Export public API // Export public API
// Export compat API // Export compat API
import compat from './compat'; import compat from './compat';

View File

@ -10,13 +10,13 @@ import MailPage from './components/MailPage';
* *
* @param {App} app * @param {App} app
*/ */
export default function(app) { export default function (app) {
app.routes = { app.routes = {
'dashboard': {path: '/', component: DashboardPage.component()}, dashboard: { path: '/', component: DashboardPage.component() },
'basics': {path: '/basics', component: BasicsPage.component()}, basics: { path: '/basics', component: BasicsPage.component() },
'permissions': {path: '/permissions', component: PermissionsPage.component()}, permissions: { path: '/permissions', component: PermissionsPage.component() },
'appearance': {path: '/appearance', component: AppearancePage.component()}, appearance: { path: '/appearance', component: AppearancePage.component() },
'extensions': {path: '/extensions', component: ExtensionsPage.component()}, extensions: { path: '/extensions', component: ExtensionsPage.component() },
'mail': {path: '/mail', component: MailPage.component()} mail: { path: '/mail', component: MailPage.component() },
}; };
} }

View File

@ -3,11 +3,13 @@ export default function saveSettings(settings) {
Object.assign(app.data.settings, settings); Object.assign(app.data.settings, settings);
return app.request({ return app
.request({
method: 'POST', method: 'POST',
url: app.forum.attribute('apiUrl') + '/settings', url: app.forum.attribute('apiUrl') + '/settings',
data: settings data: settings,
}).catch(error => { })
.catch((error) => {
app.data.settings = oldSettings; app.data.settings = oldSettings;
throw error; throw error;
}); });

View File

@ -86,7 +86,7 @@ export default class Application {
discussions: Discussion, discussions: Discussion,
posts: Post, posts: Post,
groups: Group, groups: Group,
notifications: Notification notifications: Notification,
}); });
/** /**
@ -126,22 +126,19 @@ export default class Application {
} }
boot() { boot() {
this.initializers.toArray().forEach(initializer => initializer(this)); this.initializers.toArray().forEach((initializer) => initializer(this));
this.store.pushPayload({data: this.data.resources}); this.store.pushPayload({ data: this.data.resources });
this.forum = this.store.getById('forums', 1); this.forum = this.store.getById('forums', 1);
this.session = new Session( this.session = new Session(this.store.getById('users', this.data.session.userId), this.data.session.csrfToken);
this.store.getById('users', this.data.session.userId),
this.data.session.csrfToken
);
this.mount(); this.mount();
} }
bootExtensions(extensions) { bootExtensions(extensions) {
Object.keys(extensions).forEach(name => { Object.keys(extensions).forEach((name) => {
const extension = extensions[name]; const extension = extensions[name];
const extenders = flattenDeep(extension.extend); const extenders = flattenDeep(extension.extend);
@ -153,26 +150,20 @@ export default class Application {
} }
mount(basePath = '') { mount(basePath = '') {
this.modal = m.mount(document.getElementById('modal'), <ModalManager/>); this.modal = m.mount(document.getElementById('modal'), <ModalManager />);
this.alerts = m.mount(document.getElementById('alerts'), <AlertManager/>); this.alerts = m.mount(document.getElementById('alerts'), <AlertManager />);
this.drawer = new Drawer(); this.drawer = new Drawer();
m.route( m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath));
document.getElementById('content'),
basePath + '/',
mapRoutes(this.routes, basePath)
);
// Add a class to the body which indicates that the page has been scrolled // Add a class to the body which indicates that the page has been scrolled
// down. // down.
new ScrollListener(top => { new ScrollListener((top) => {
const $app = $('#app'); const $app = $('#app');
const offset = $app.offset().top; const offset = $app.offset().top;
$app $app.toggleClass('affix', top >= offset).toggleClass('scrolled', top > offset);
.toggleClass('affix', top >= offset)
.toggleClass('scrolled', top > offset);
}).start(); }).start();
$(() => { $(() => {
@ -220,9 +211,7 @@ export default class Application {
} }
updateTitle() { updateTitle() {
document.title = (this.titleCount ? `(${this.titleCount}) ` : '') + document.title = (this.titleCount ? `(${this.titleCount}) ` : '') + (this.title ? this.title + ' - ' : '') + this.forum.attribute('title');
(this.title ? this.title + ' - ' : '') +
this.forum.attribute('title');
} }
/** /**
@ -256,9 +245,11 @@ export default class Application {
// When we deserialize JSON data, if for some reason the server has provided // When we deserialize JSON data, if for some reason the server has provided
// a dud response, we don't want the application to crash. We'll show an // a dud response, we don't want the application to crash. We'll show an
// error message to the user instead. // error message to the user instead.
options.deserialize = options.deserialize || (responseText => responseText); options.deserialize = options.deserialize || ((responseText) => responseText);
options.errorHandler = options.errorHandler || (error => { options.errorHandler =
options.errorHandler ||
((error) => {
throw error; throw error;
}); });
@ -266,7 +257,7 @@ export default class Application {
// response code and show an error message to the user if something's gone // response code and show an error message to the user if something's gone
// awry. // awry.
const original = options.extract; const original = options.extract;
options.extract = xhr => { options.extract = (xhr) => {
let responseText; let responseText;
if (original) { if (original) {
@ -299,7 +290,9 @@ export default class Application {
// returned and show an alert containing its contents. // returned and show an alert containing its contents.
const deferred = m.deferred(); const deferred = m.deferred();
m.request(options).then(response => deferred.resolve(response), error => { m.request(options).then(
(response) => deferred.resolve(response),
(error) => {
this.requestError = error; this.requestError = error;
let children; let children;
@ -307,7 +300,7 @@ export default class Application {
switch (error.status) { switch (error.status) {
case 422: case 422:
children = error.response.errors children = error.response.errors
.map(error => [error.detail, <br/>]) .map((error) => [error.detail, <br />])
.reduce((a, b) => a.concat(b), []) .reduce((a, b) => a.concat(b), [])
.slice(0, -1); .slice(0, -1);
break; break;
@ -336,8 +329,10 @@ export default class Application {
type: 'error', type: 'error',
children, children,
controls: isDebug && [ controls: isDebug && [
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error)}>Debug</Button> <Button className="Button Button--link" onclick={this.showDebug.bind(this, error)}>
] Debug
</Button>,
],
}); });
try { try {
@ -347,7 +342,8 @@ export default class Application {
} }
deferred.reject(error); deferred.reject(error);
}); }
);
return deferred.promise; return deferred.promise;
} }
@ -359,7 +355,7 @@ export default class Application {
showDebug(error) { showDebug(error) {
this.alerts.dismiss(this.requestError.alert); this.alerts.dismiss(this.requestError.alert);
this.modal.show(new RequestErrorModal({error})); this.modal.show(new RequestErrorModal({ error }));
} }
/** /**

View File

@ -70,8 +70,7 @@ export default class Component {
* *
* @protected * @protected
*/ */
init() { init() {}
}
/** /**
* Called when the component is destroyed, i.e. after a redraw where it is no * Called when the component is destroyed, i.e. after a redraw where it is no
@ -81,8 +80,7 @@ export default class Component {
* @param {Object} e * @param {Object} e
* @public * @public
*/ */
onunload() { onunload() {}
}
/** /**
* Get the renderable virtual DOM that represents the component's view. * Get the renderable virtual DOM that represents the component's view.
@ -99,7 +97,7 @@ export default class Component {
* @public * @public
*/ */
render() { render() {
const vdom = this.retain ? {subtree: 'retain'} : this.view(); const vdom = this.retain ? { subtree: 'retain' } : this.view();
// Override the root element's config attribute with our own function, which // Override the root element's config attribute with our own function, which
// will set the component instance's element property to the root DOM // will set the component instance's element property to the root DOM
@ -148,8 +146,7 @@ export default class Component {
* @param {Object} vdom * @param {Object} vdom
* @public * @public
*/ */
config() { config() {}
}
/** /**
* Get the virtual DOM that represents the component's view. * Get the virtual DOM that represents the component's view.
@ -201,14 +198,14 @@ export default class Component {
controller: this.bind(undefined, componentProps), controller: this.bind(undefined, componentProps),
view: view, view: view,
props: componentProps, props: componentProps,
component: this component: this,
}; };
// If a `key` prop was set, then we'll assume that we want that to actually // If a `key` prop was set, then we'll assume that we want that to actually
// show up as an attribute on the component object so that Mithril's key // show up as an attribute on the component object so that Mithril's key
// algorithm can be applied. // algorithm can be applied.
if (componentProps.key) { if (componentProps.key) {
output.attrs = {key: componentProps.key}; output.attrs = { key: componentProps.key };
} }
return output; return output;
@ -220,6 +217,5 @@ export default class Component {
* @param {Object} props * @param {Object} props
* @public * @public
*/ */
static initProps(props) { static initProps(props) {}
}
} }

View File

@ -88,7 +88,7 @@ export default class Model {
// relationship data object. // relationship data object.
for (const innerKey in data[key]) { for (const innerKey in data[key]) {
if (data[key][innerKey] instanceof Model) { if (data[key][innerKey] instanceof Model) {
data[key][innerKey] = {data: Model.getIdentifier(data[key][innerKey])}; data[key][innerKey] = { data: Model.getIdentifier(data[key][innerKey]) };
} }
this.data[key][innerKey] = data[key][innerKey]; this.data[key][innerKey] = data[key][innerKey];
} }
@ -109,7 +109,7 @@ export default class Model {
* @public * @public
*/ */
pushAttributes(attributes) { pushAttributes(attributes) {
this.pushData({attributes}); this.pushData({ attributes });
} }
/** /**
@ -125,7 +125,7 @@ export default class Model {
const data = { const data = {
type: this.data.type, type: this.data.type,
id: this.data.id, id: this.data.id,
attributes attributes,
}; };
// If a 'relationships' key exists, extract it from the attributes hash and // If a 'relationships' key exists, extract it from the attributes hash and
@ -138,9 +138,7 @@ export default class Model {
const model = attributes.relationships[key]; const model = attributes.relationships[key];
data.relationships[key] = { data.relationships[key] = {
data: model instanceof Array data: model instanceof Array ? model.map(Model.getIdentifier) : Model.getIdentifier(model),
? model.map(Model.getIdentifier)
: Model.getIdentifier(model)
}; };
} }
@ -154,18 +152,25 @@ export default class Model {
this.pushData(data); this.pushData(data);
const request = {data}; const request = { data };
if (options.meta) request.meta = options.meta; if (options.meta) request.meta = options.meta;
return app.request(Object.assign({ return app
.request(
Object.assign(
{
method: this.exists ? 'PATCH' : 'POST', method: this.exists ? 'PATCH' : 'POST',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(), url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data: request data: request,
}, options)).then( },
options
)
)
.then(
// If everything went well, we'll make sure the store knows that this // If everything went well, we'll make sure the store knows that this
// model exists now (if it didn't already), and we'll push the data that // model exists now (if it didn't already), and we'll push the data that
// the API returned into the store. // the API returned into the store.
payload => { (payload) => {
this.store.data[payload.data.type] = this.store.data[payload.data.type] || {}; this.store.data[payload.data.type] = this.store.data[payload.data.type] || {};
this.store.data[payload.data.type][payload.data.id] = this; this.store.data[payload.data.type][payload.data.id] = this;
return this.store.pushPayload(payload); return this.store.pushPayload(payload);
@ -173,7 +178,7 @@ export default class Model {
// If something went wrong, though... good thing we backed up our model's // If something went wrong, though... good thing we backed up our model's
// old data! We'll revert to that and let others handle the error. // old data! We'll revert to that and let others handle the error.
response => { (response) => {
this.pushData(oldData); this.pushData(oldData);
m.lazyRedraw(); m.lazyRedraw();
throw response; throw response;
@ -192,11 +197,18 @@ export default class Model {
delete(data, options = {}) { delete(data, options = {}) {
if (!this.exists) return m.deferred().resolve().promise; if (!this.exists) return m.deferred().resolve().promise;
return app.request(Object.assign({ return app
.request(
Object.assign(
{
method: 'DELETE', method: 'DELETE',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(), url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data data,
}, options)).then(() => { },
options
)
)
.then(() => {
this.exists = false; this.exists = false;
this.store.remove(this); this.store.remove(this);
}); });
@ -225,7 +237,7 @@ export default class Model {
* @public * @public
*/ */
static attribute(name, transform) { static attribute(name, transform) {
return function() { return function () {
const value = this.data.attributes && this.data.attributes[name]; const value = this.data.attributes && this.data.attributes[name];
return transform ? transform(value) : value; return transform ? transform(value) : value;
@ -243,7 +255,7 @@ export default class Model {
* @public * @public
*/ */
static hasOne(name) { static hasOne(name) {
return function() { return function () {
if (this.data.relationships) { if (this.data.relationships) {
const relationship = this.data.relationships[name]; const relationship = this.data.relationships[name];
@ -267,12 +279,12 @@ export default class Model {
* @public * @public
*/ */
static hasMany(name) { static hasMany(name) {
return function() { return function () {
if (this.data.relationships) { if (this.data.relationships) {
const relationship = this.data.relationships[name]; const relationship = this.data.relationships[name];
if (relationship) { if (relationship) {
return relationship.data.map(data => app.store.getById(data.type, data.id)); return relationship.data.map((data) => app.store.getById(data.type, data.id));
} }
} }
@ -301,7 +313,7 @@ export default class Model {
static getIdentifier(model) { static getIdentifier(model) {
return { return {
type: model.data.type, type: model.data.type,
id: model.data.id id: model.data.id,
}; };
} }
} }

View File

@ -31,11 +31,16 @@ export default class Session {
* @public * @public
*/ */
login(data, options = {}) { login(data, options = {}) {
return app.request(Object.assign({ return app.request(
Object.assign(
{
method: 'POST', method: 'POST',
url: app.forum.attribute('baseUrl') + '/login', url: app.forum.attribute('baseUrl') + '/login',
data data,
}, options)); },
options
)
);
} }
/** /**

View File

@ -34,9 +34,7 @@ export default class Store {
pushPayload(payload) { pushPayload(payload) {
if (payload.included) payload.included.map(this.pushObject.bind(this)); if (payload.included) payload.included.map(this.pushObject.bind(this));
const result = payload.data instanceof Array const result = payload.data instanceof Array ? payload.data.map(this.pushObject.bind(this)) : this.pushObject(payload.data);
? payload.data.map(this.pushObject.bind(this))
: this.pushObject(payload.data);
// Attach the original payload to the model that we give back. This is // Attach the original payload to the model that we give back. This is
// useful to consumers as it allows them to access meta information // useful to consumers as it allows them to access meta information
@ -58,7 +56,7 @@ export default class Store {
pushObject(data) { pushObject(data) {
if (!this.models[data.type]) return null; if (!this.models[data.type]) return null;
const type = this.data[data.type] = this.data[data.type] || {}; const type = (this.data[data.type] = this.data[data.type] || {});
if (type[data.id]) { if (type[data.id]) {
type[data.id].pushData(data); type[data.id].pushData(data);
@ -95,11 +93,18 @@ export default class Store {
url += '/' + id; url += '/' + id;
} }
return app.request(Object.assign({ return app
.request(
Object.assign(
{
method: 'GET', method: 'GET',
url, url,
data data,
}, options)).then(this.pushPayload.bind(this)); },
options
)
)
.then(this.pushPayload.bind(this));
} }
/** /**
@ -124,7 +129,7 @@ export default class Store {
* @public * @public
*/ */
getBy(type, key, value) { getBy(type, key, value) {
return this.all(type).filter(model => model[key]() === value)[0]; return this.all(type).filter((model) => model[key]() === value)[0];
} }
/** /**
@ -137,7 +142,7 @@ export default class Store {
all(type) { all(type) {
const records = this.data[type]; const records = this.data[type];
return records ? Object.keys(records).map(id => records[id]) : []; return records ? Object.keys(records).map((id) => records[id]) : [];
} }
/** /**
@ -160,6 +165,6 @@ export default class Store {
createRecord(type, data = {}) { createRecord(type, data = {}) {
data.type = data.type || type; data.type = data.type || type;
return new (this.models[type])(data, this); return new this.models[type](data, this);
} }
} }

View File

@ -67,7 +67,7 @@ export default class Translator {
const hydrated = []; const hydrated = [];
const open = [hydrated]; const open = [hydrated];
translation.forEach(part => { translation.forEach((part) => {
const match = part.match(new RegExp('{([a-z0-9_]+)}|<(/?)([a-z0-9_]+)>', 'i')); const match = part.match(new RegExp('{([a-z0-9_]+)}|<(/?)([a-z0-9_]+)>', 'i'));
if (match) { if (match) {
@ -77,7 +77,7 @@ export default class Translator {
if (match[2]) { if (match[2]) {
open.shift(); open.shift();
} else { } else {
let tag = input[match[3]] || {tag: match[3], children: []}; let tag = input[match[3]] || { tag: match[3], children: [] };
open[0].push(tag); open[0].push(tag);
open.unshift(tag.children || tag); open.unshift(tag.children || tag);
} }
@ -87,7 +87,7 @@ export default class Translator {
} }
}); });
return hydrated.filter(part => part); return hydrated.filter((part) => part);
} }
pluralize(translation, number) { pluralize(translation, number) {
@ -97,7 +97,7 @@ export default class Translator {
standardRules = [], standardRules = [],
explicitRules = []; explicitRules = [];
translation.split('|').forEach(part => { translation.split('|').forEach((part) => {
if (cPluralRegex.test(part)) { if (cPluralRegex.test(part)) {
const matches = part.match(cPluralRegex); const matches = part.match(cPluralRegex);
explicitRules[matches[0]] = matches[matches.length - 1]; explicitRules[matches[0]] = matches[matches.length - 1];
@ -125,8 +125,10 @@ export default class Translator {
var leftNumber = this.convertNumber(matches[4]); var leftNumber = this.convertNumber(matches[4]);
var rightNumber = this.convertNumber(matches[5]); var rightNumber = this.convertNumber(matches[5]);
if (('[' === matches[3] ? number >= leftNumber : number > leftNumber) && if (
(']' === matches[6] ? number <= rightNumber : number < rightNumber)) { ('[' === matches[3] ? number >= leftNumber : number > leftNumber) &&
(']' === matches[6] ? number <= rightNumber : number < rightNumber)
) {
return explicitRules[e]; return explicitRules[e];
} }
} }
@ -223,7 +225,7 @@ export default class Translator {
case 'tr': case 'tr':
case 'ur': case 'ur':
case 'zu': case 'zu':
return (number == 1) ? 0 : 1; return number == 1 ? 0 : 1;
case 'am': case 'am':
case 'bh': case 'bh':
@ -237,7 +239,7 @@ export default class Translator {
case 'xbr': case 'xbr':
case 'ti': case 'ti':
case 'wa': case 'wa':
return ((number === 0) || (number == 1)) ? 0 : 1; return number === 0 || number == 1 ? 0 : 1;
case 'be': case 'be':
case 'bs': case 'bs':
@ -245,41 +247,41 @@ export default class Translator {
case 'ru': case 'ru':
case 'sr': case 'sr':
case 'uk': case 'uk':
return ((number % 10 == 1) && (number % 100 != 11)) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2); return number % 10 == 1 && number % 100 != 11 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
case 'cs': case 'cs':
case 'sk': case 'sk':
return (number == 1) ? 0 : (((number >= 2) && (number <= 4)) ? 1 : 2); return number == 1 ? 0 : number >= 2 && number <= 4 ? 1 : 2;
case 'ga': case 'ga':
return (number == 1) ? 0 : ((number == 2) ? 1 : 2); return number == 1 ? 0 : number == 2 ? 1 : 2;
case 'lt': case 'lt':
return ((number % 10 == 1) && (number % 100 != 11)) ? 0 : (((number % 10 >= 2) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2); return number % 10 == 1 && number % 100 != 11 ? 0 : number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
case 'sl': case 'sl':
return (number % 100 == 1) ? 0 : ((number % 100 == 2) ? 1 : (((number % 100 == 3) || (number % 100 == 4)) ? 2 : 3)); return number % 100 == 1 ? 0 : number % 100 == 2 ? 1 : number % 100 == 3 || number % 100 == 4 ? 2 : 3;
case 'mk': case 'mk':
return (number % 10 == 1) ? 0 : 1; return number % 10 == 1 ? 0 : 1;
case 'mt': case 'mt':
return (number == 1) ? 0 : (((number === 0) || ((number % 100 > 1) && (number % 100 < 11))) ? 1 : (((number % 100 > 10) && (number % 100 < 20)) ? 2 : 3)); return number == 1 ? 0 : number === 0 || (number % 100 > 1 && number % 100 < 11) ? 1 : number % 100 > 10 && number % 100 < 20 ? 2 : 3;
case 'lv': case 'lv':
return (number === 0) ? 0 : (((number % 10 == 1) && (number % 100 != 11)) ? 1 : 2); return number === 0 ? 0 : number % 10 == 1 && number % 100 != 11 ? 1 : 2;
case 'pl': case 'pl':
return (number == 1) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 12) || (number % 100 > 14))) ? 1 : 2); return number == 1 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14) ? 1 : 2;
case 'cy': case 'cy':
return (number == 1) ? 0 : ((number == 2) ? 1 : (((number == 8) || (number == 11)) ? 2 : 3)); return number == 1 ? 0 : number == 2 ? 1 : number == 8 || number == 11 ? 2 : 3;
case 'ro': case 'ro':
return (number == 1) ? 0 : (((number === 0) || ((number % 100 > 0) && (number % 100 < 20))) ? 1 : 2); return number == 1 ? 0 : number === 0 || (number % 100 > 0 && number % 100 < 20) ? 1 : 2;
case 'ar': case 'ar':
return (number === 0) ? 0 : ((number == 1) ? 1 : ((number == 2) ? 2 : (((number >= 3) && (number <= 10)) ? 3 : (((number >= 11) && (number <= 99)) ? 4 : 5)))); return number === 0 ? 0 : number == 1 ? 1 : number == 2 ? 2 : number >= 3 && number <= 10 ? 3 : number >= 11 && number <= 99 ? 4 : 5;
default: default:
return 0; return 0;

View File

@ -62,9 +62,9 @@ import userOnline from './helpers/userOnline';
import listItems from './helpers/listItems'; import listItems from './helpers/listItems';
export default { export default {
'extend': extend, extend: extend,
'Session': Session, Session: Session,
'Store': Store, Store: Store,
'utils/evented': evented, 'utils/evented': evented,
'utils/liveHumanTimes': liveHumanTimes, 'utils/liveHumanTimes': liveHumanTimes,
'utils/ItemList': ItemList, 'utils/ItemList': ItemList,
@ -91,8 +91,8 @@ export default {
'models/Discussion': Discussion, 'models/Discussion': Discussion,
'models/Group': Group, 'models/Group': Group,
'models/Forum': Forum, 'models/Forum': Forum,
'Component': Component, Component: Component,
'Translator': Translator, Translator: Translator,
'components/AlertManager': AlertManager, 'components/AlertManager': AlertManager,
'components/Switch': Switch, 'components/Switch': Switch,
'components/Badge': Badge, 'components/Badge': Badge,
@ -113,8 +113,8 @@ export default {
'components/Button': Button, 'components/Button': Button,
'components/Modal': Modal, 'components/Modal': Modal,
'components/GroupBadge': GroupBadge, 'components/GroupBadge': GroupBadge,
'Model': Model, Model: Model,
'Application': Application, Application: Application,
'helpers/fullTime': fullTime, 'helpers/fullTime': fullTime,
'helpers/avatar': avatar, 'helpers/avatar': avatar,
'helpers/icon': icon, 'helpers/icon': icon,
@ -123,5 +123,5 @@ export default {
'helpers/highlight': highlight, 'helpers/highlight': highlight,
'helpers/username': username, 'helpers/username': username,
'helpers/userOnline': userOnline, 'helpers/userOnline': userOnline,
'helpers/listItems': listItems 'helpers/listItems': listItems,
}; };

View File

@ -35,22 +35,13 @@ export default class Alert extends Component {
const dismissControl = []; const dismissControl = [];
if (dismissible || dismissible === undefined) { if (dismissible || dismissible === undefined) {
dismissControl.push( dismissControl.push(<Button icon="fas fa-times" className="Button Button--link Button--icon Alert-dismiss" onclick={ondismiss} />);
<Button
icon="fas fa-times"
className="Button Button--link Button--icon Alert-dismiss"
onclick={ondismiss}/>
);
} }
return ( return (
<div {...attrs}> <div {...attrs}>
<span className="Alert-body"> <span className="Alert-body">{children}</span>
{children} <ul className="Alert-controls">{listItems(controls.concat(dismissControl))}</ul>
</span>
<ul className="Alert-controls">
{listItems(controls.concat(dismissControl))}
</ul>
</div> </div>
); );
} }

View File

@ -19,7 +19,9 @@ export default class AlertManager extends Component {
view() { view() {
return ( return (
<div className="AlertManager"> <div className="AlertManager">
{this.components.map(component => <div className="AlertManager-alert">{component}</div>)} {this.components.map((component) => (
<div className="AlertManager-alert">{component}</div>
))}
</div> </div>
); );
} }

View File

@ -24,16 +24,12 @@ export default class Badge extends Component {
attrs.className = 'Badge ' + (type ? 'Badge--' + type : '') + ' ' + (attrs.className || ''); attrs.className = 'Badge ' + (type ? 'Badge--' + type : '') + ' ' + (attrs.className || '');
attrs.title = extract(attrs, 'label') || ''; attrs.title = extract(attrs, 'label') || '';
return ( return <span {...attrs}>{iconName ? icon(iconName, { className: 'Badge-icon' }) : m.trust('&nbsp;')}</span>;
<span {...attrs}>
{iconName ? icon(iconName, {className: 'Badge-icon'}) : m.trust('&nbsp;')}
</span>
);
} }
config(isInitialized) { config(isInitialized) {
if (isInitialized) return; if (isInitialized) return;
if (this.props.label) this.$().tooltip({container: 'body'}); if (this.props.label) this.$().tooltip({ container: 'body' });
} }
} }

View File

@ -62,9 +62,9 @@ export default class Button extends Component {
const iconName = this.props.icon; const iconName = this.props.icon;
return [ return [
iconName && iconName !== true ? icon(iconName, {className: 'Button-icon'}) : '', iconName && iconName !== true ? icon(iconName, { className: 'Button-icon' }) : '',
this.props.children ? <span className="Button-label">{this.props.children}</span> : '', this.props.children ? <span className="Button-label">{this.props.children}</span> : '',
this.props.loading ? LoadingIndicator.component({size: 'tiny', className: 'LoadingIndicator--inline'}) : '' this.props.loading ? LoadingIndicator.component({ size: 'tiny', className: 'LoadingIndicator--inline' }) : '',
]; ];
} }
} }

View File

@ -31,13 +31,8 @@ export default class Checkbox extends Component {
return ( return (
<label className={className}> <label className={className}>
<input type="checkbox" <input type="checkbox" checked={this.props.state} disabled={this.props.disabled} onchange={m.withAttr('checked', this.onchange.bind(this))} />
checked={this.props.state} <div className="Checkbox-display">{this.getDisplay()}</div>
disabled={this.props.disabled}
onchange={m.withAttr('checked', this.onchange.bind(this))}/>
<div className="Checkbox-display">
{this.getDisplay()}
</div>
{this.props.children} {this.props.children}
</label> </label>
); );
@ -50,9 +45,7 @@ export default class Checkbox extends Component {
* @protected * @protected
*/ */
getDisplay() { getDisplay() {
return this.loading return this.loading ? LoadingIndicator.component({ size: 'tiny' }) : icon(this.props.state ? 'fas fa-check' : 'fas fa-times');
? LoadingIndicator.component({size: 'tiny'})
: icon(this.props.state ? 'fas fa-check' : 'fas fa-times');
} }
/** /**

View File

@ -64,19 +64,13 @@ export default class Dropdown extends Component {
$menu.removeClass('Dropdown-menu--top Dropdown-menu--right'); $menu.removeClass('Dropdown-menu--top Dropdown-menu--right');
$menu.toggleClass( $menu.toggleClass('Dropdown-menu--top', $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height());
'Dropdown-menu--top',
$menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height()
);
if ($menu.offset().top < 0) { if ($menu.offset().top < 0) {
$menu.removeClass('Dropdown-menu--top'); $menu.removeClass('Dropdown-menu--top');
} }
$menu.toggleClass( $menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width());
'Dropdown-menu--right',
isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width()
);
}); });
this.$().on('hidden.bs.dropdown', () => { this.$().on('hidden.bs.dropdown', () => {
@ -98,10 +92,7 @@ export default class Dropdown extends Component {
*/ */
getButton() { getButton() {
return ( return (
<button <button className={'Dropdown-toggle ' + this.props.buttonClassName} data-toggle="dropdown" onclick={this.props.onclick}>
className={'Dropdown-toggle ' + this.props.buttonClassName}
data-toggle="dropdown"
onclick={this.props.onclick}>
{this.getButtonContent()} {this.getButtonContent()}
</button> </button>
); );
@ -115,17 +106,13 @@ export default class Dropdown extends Component {
*/ */
getButtonContent() { getButtonContent() {
return [ return [
this.props.icon ? icon(this.props.icon, {className: 'Button-icon'}) : '', this.props.icon ? icon(this.props.icon, { className: 'Button-icon' }) : '',
<span className="Button-label">{this.props.label}</span>, <span className="Button-label">{this.props.label}</span>,
this.props.caretIcon ? icon(this.props.caretIcon, {className: 'Button-caret'}) : '' this.props.caretIcon ? icon(this.props.caretIcon, { className: 'Button-caret' }) : '',
]; ];
} }
getMenu(items) { getMenu(items) {
return ( return <ul className={'Dropdown-menu dropdown-menu ' + this.props.menuClassName}>{items}</ul>;
<ul className={'Dropdown-menu dropdown-menu ' + this.props.menuClassName}>
{items}
</ul>
);
} }
} }

View File

@ -6,7 +6,7 @@ export default class GroupBadge extends Badge {
if (props.group) { if (props.group) {
props.icon = props.group.icon(); props.icon = props.group.icon();
props.style = {backgroundColor: props.group.color()}; props.style = { backgroundColor: props.group.color() };
props.label = typeof props.label === 'undefined' ? props.group.nameSingular() : props.label; props.label = typeof props.label === 'undefined' ? props.group.nameSingular() : props.label;
props.type = 'group--' + props.group.id(); props.type = 'group--' + props.group.id();

View File

@ -33,8 +33,6 @@ export default class LinkButton extends Button {
* @return {Boolean} * @return {Boolean}
*/ */
static isActive(props) { static isActive(props) {
return typeof props.active !== 'undefined' return typeof props.active !== 'undefined' ? props.active : m.route() === props.href;
? props.active
: m.route() === props.href;
} }
} }

View File

@ -31,10 +31,12 @@ export default class Modal extends Component {
{Button.component({ {Button.component({
icon: 'fas fa-times', icon: 'fas fa-times',
onclick: this.hide.bind(this), onclick: this.hide.bind(this),
className: 'Button Button--icon Button--link' className: 'Button Button--icon Button--link',
})} })}
</div> </div>
) : ''} ) : (
''
)}
<form onsubmit={this.onsubmit.bind(this)}> <form onsubmit={this.onsubmit.bind(this)}>
<div className="Modal-header"> <div className="Modal-header">
@ -65,8 +67,7 @@ export default class Modal extends Component {
* @return {String} * @return {String}
* @abstract * @abstract
*/ */
className() { className() {}
}
/** /**
* Get the title of the modal dialog. * Get the title of the modal dialog.
@ -74,8 +75,7 @@ export default class Modal extends Component {
* @return {String} * @return {String}
* @abstract * @abstract
*/ */
title() { title() {}
}
/** /**
* Get the content of the modal. * Get the content of the modal.
@ -83,16 +83,14 @@ export default class Modal extends Component {
* @return {VirtualElement} * @return {VirtualElement}
* @abstract * @abstract
*/ */
content() { content() {}
}
/** /**
* Handle the modal form's submit event. * Handle the modal form's submit event.
* *
* @param {Event} e * @param {Event} e
*/ */
onsubmit() { onsubmit() {}
}
/** /**
* Focus on the first input when the modal is ready to be used. * Focus on the first input when the modal is ready to be used.
@ -101,8 +99,7 @@ export default class Modal extends Component {
this.$('form').find('input, select, textarea').first().focus().select(); this.$('form').find('input, select, textarea').first().focus().select();
} }
onhide() { onhide() {}
}
/** /**
* Hide the modal. * Hide the modal.

View File

@ -13,11 +13,7 @@ export default class ModalManager extends Component {
} }
view() { view() {
return ( return <div className="ModalManager modal fade">{this.component && this.component.render()}</div>;
<div className="ModalManager modal fade">
{this.component && this.component.render()}
</div>
);
} }
config(isInitialized, context) { config(isInitialized, context) {
@ -28,9 +24,7 @@ export default class ModalManager extends Component {
// to be retained across route changes. // to be retained across route changes.
context.retain = true; context.retain = true;
this.$() this.$().on('hidden.bs.modal', this.clear.bind(this)).on('shown.bs.modal', this.onready.bind(this));
.on('hidden.bs.modal', this.clear.bind(this))
.on('shown.bs.modal', this.onready.bind(this));
} }
/** /**
@ -54,10 +48,12 @@ export default class ModalManager extends Component {
m.redraw(true); m.redraw(true);
const dismissible = !!this.component.isDismissible(); const dismissible = !!this.component.isDismissible();
this.$().modal({ this.$()
.modal({
backdrop: dismissible || 'static', backdrop: dismissible || 'static',
keyboard: dismissible keyboard: dismissible,
}).modal('show'); })
.modal('show');
} }
/** /**

View File

@ -19,15 +19,15 @@ import LinkButton from './LinkButton';
*/ */
export default class Navigation extends Component { export default class Navigation extends Component {
view() { view() {
const {history, pane} = app; const { history, pane } = app;
return ( return (
<div className={'Navigation ButtonGroup ' + (this.props.className || '')} <div
className={'Navigation ButtonGroup ' + (this.props.className || '')}
onmouseenter={pane && pane.show.bind(pane)} onmouseenter={pane && pane.show.bind(pane)}
onmouseleave={pane && pane.onmouseleave.bind(pane)}> onmouseleave={pane && pane.onmouseleave.bind(pane)}
{history.canGoBack() >
? [this.getBackButton(), this.getPaneButton()] {history.canGoBack() ? [this.getBackButton(), this.getPaneButton()] : this.getDrawerButton()}
: this.getDrawerButton()}
</div> </div>
); );
} }
@ -46,7 +46,7 @@ export default class Navigation extends Component {
* @protected * @protected
*/ */
getBackButton() { getBackButton() {
const {history} = app; const { history } = app;
const previous = history.getPrevious() || {}; const previous = history.getPrevious() || {};
return LinkButton.component({ return LinkButton.component({
@ -55,11 +55,11 @@ export default class Navigation extends Component {
icon: 'fas fa-chevron-left', icon: 'fas fa-chevron-left',
title: previous.title, title: previous.title,
config: () => {}, config: () => {},
onclick: e => { onclick: (e) => {
if (e.shiftKey || e.ctrlKey || e.metaKey || e.which === 2) return; if (e.shiftKey || e.ctrlKey || e.metaKey || e.which === 2) return;
e.preventDefault(); e.preventDefault();
history.back(); history.back();
} },
}); });
} }
@ -70,14 +70,14 @@ export default class Navigation extends Component {
* @protected * @protected
*/ */
getPaneButton() { getPaneButton() {
const {pane} = app; const { pane } = app;
if (!pane || !pane.active) return ''; if (!pane || !pane.active) return '';
return Button.component({ return Button.component({
className: 'Button Button--icon Navigation-pin' + (pane.pinned ? ' active' : ''), className: 'Button Button--icon Navigation-pin' + (pane.pinned ? ' active' : ''),
onclick: pane.togglePinned.bind(pane), onclick: pane.togglePinned.bind(pane),
icon: 'fas fa-thumbtack' icon: 'fas fa-thumbtack',
}); });
} }
@ -90,17 +90,16 @@ export default class Navigation extends Component {
getDrawerButton() { getDrawerButton() {
if (!this.props.drawer) return ''; if (!this.props.drawer) return '';
const {drawer} = app; const { drawer } = app;
const user = app.session.user; const user = app.session.user;
return Button.component({ return Button.component({
className: 'Button Button--icon Navigation-drawer' + className: 'Button Button--icon Navigation-drawer' + (user && user.newNotificationCount() ? ' new' : ''),
(user && user.newNotificationCount() ? ' new' : ''), onclick: (e) => {
onclick: e => {
e.stopPropagation(); e.stopPropagation();
drawer.show(); drawer.show();
}, },
icon: 'fas fa-bars' icon: 'fas fa-bars',
}); });
} }
} }

View File

@ -6,9 +6,7 @@ export default class RequestErrorModal extends Modal {
} }
title() { title() {
return this.props.error.xhr return this.props.error.xhr ? this.props.error.xhr.status + ' ' + this.props.error.xhr.statusText : '';
? this.props.error.xhr.status+' '+this.props.error.xhr.statusText
: '';
} }
content() { content() {
@ -20,11 +18,15 @@ export default class RequestErrorModal extends Modal {
responseText = this.props.error.responseText; responseText = this.props.error.responseText;
} }
return <div className="Modal-body"> return (
<div className="Modal-body">
<pre> <pre>
{this.props.error.options.method} {this.props.error.options.url}<br/><br/> {this.props.error.options.method} {this.props.error.options.url}
<br />
<br />
{responseText} {responseText}
</pre> </pre>
</div>; </div>
);
} }
} }

View File

@ -12,14 +12,21 @@ import icon from '../helpers/icon';
*/ */
export default class Select extends Component { export default class Select extends Component {
view() { view() {
const {options, onchange, value, disabled} = this.props; const { options, onchange, value, disabled } = this.props;
return ( return (
<span className="Select"> <span className="Select">
<select className="Select-input FormControl" onchange={onchange ? m.withAttr('value', onchange.bind(this)) : undefined} value={value} disabled={disabled}> <select
{Object.keys(options).map(key => <option value={key}>{options[key]}</option>)} className="Select-input FormControl"
onchange={onchange ? m.withAttr('value', onchange.bind(this)) : undefined}
value={value}
disabled={disabled}
>
{Object.keys(options).map((key) => (
<option value={key}>{options[key]}</option>
))}
</select> </select>
{icon('fas fa-sort', {className: 'Select-caret'})} {icon('fas fa-sort', { className: 'Select-caret' })}
</span> </span>
); );
} }

View File

@ -21,14 +21,11 @@ export default class SelectDropdown extends Dropdown {
} }
getButtonContent() { getButtonContent() {
const activeChild = this.props.children.filter(child => child.props.active)[0]; const activeChild = this.props.children.filter((child) => child.props.active)[0];
let label = activeChild && activeChild.props.children || this.props.defaultLabel; let label = (activeChild && activeChild.props.children) || this.props.defaultLabel;
if (label instanceof Array) label = label[0]; if (label instanceof Array) label = label[0];
return [ return [<span className="Button-label">{label}</span>, icon(this.props.caretIcon, { className: 'Button-caret' })];
<span className="Button-label">{label}</span>,
icon(this.props.caretIcon, {className: 'Button-caret'})
];
} }
} }

View File

@ -5,7 +5,7 @@ import Component from '../Component';
*/ */
class Separator extends Component { class Separator extends Component {
view() { view() {
return <li className="Dropdown-separator"/>; return <li className="Dropdown-separator" />;
} }
} }

View File

@ -24,12 +24,10 @@ export default class SplitDropdown extends Dropdown {
return [ return [
Button.component(buttonProps), Button.component(buttonProps),
<button <button className={'Dropdown-toggle Button Button--icon ' + this.props.buttonClassName} data-toggle="dropdown">
className={'Dropdown-toggle Button Button--icon ' + this.props.buttonClassName} {icon(this.props.icon, { className: 'Button-icon' })}
data-toggle="dropdown"> {icon('fas fa-caret-down', { className: 'Button-caret' })}
{icon(this.props.icon, {className: 'Button-icon'})} </button>,
{icon('fas fa-caret-down', {className: 'Button-caret'})}
</button>
]; ];
} }

View File

@ -21,7 +21,7 @@
export function extend(object, method, callback) { export function extend(object, method, callback) {
const original = object[method]; const original = object[method];
object[method] = function(...args) { object[method] = function (...args) {
const value = original ? original.apply(this, args) : undefined; const value = original ? original.apply(this, args) : undefined;
callback.apply(this, [value].concat(args)); callback.apply(this, [value].concat(args));
@ -57,7 +57,7 @@ export function extend(object, method, callback) {
export function override(object, method, newMethod) { export function override(object, method, newMethod) {
const original = object[method]; const original = object[method];
object[method] = function(...args) { object[method] = function (...args) {
return newMethod.apply(this, [original.bind(this)].concat(args)); return newMethod.apply(this, [original.bind(this)].concat(args));
}; };

View File

@ -34,8 +34,8 @@ export default class Routes {
const model = app.store.models[this.type]; const model = app.store.models[this.type];
this.attributes.forEach(name => model.prototype[name] = model.attribute(name)); this.attributes.forEach((name) => (model.prototype[name] = model.attribute(name)));
this.hasOnes.forEach(name => model.prototype[name] = model.hasOne(name)); this.hasOnes.forEach((name) => (model.prototype[name] = model.hasOne(name)));
this.hasManys.forEach(name => model.prototype[name] = model.hasMany(name)); this.hasManys.forEach((name) => (model.prototype[name] = model.hasMany(name)));
} }
} }

View File

@ -25,11 +25,11 @@ export default function avatar(user, attrs = {}) {
if (hasTitle) attrs.title = attrs.title || username; if (hasTitle) attrs.title = attrs.title || username;
if (avatarUrl) { if (avatarUrl) {
return <img {...attrs} src={avatarUrl}/>; return <img {...attrs} src={avatarUrl} />;
} }
content = username.charAt(0).toUpperCase(); content = username.charAt(0).toUpperCase();
attrs.style = {background: user.color()}; attrs.style = { background: user.color() };
} }
return <span {...attrs}>{content}</span>; return <span {...attrs}>{content}</span>;

View File

@ -11,5 +11,9 @@ export default function fullTime(time) {
const datetime = mo.format(); const datetime = mo.format();
const full = mo.format('LLLL'); const full = mo.format('LLLL');
return <time pubdate datetime={datetime}>{full}</time>; return (
<time pubdate datetime={datetime}>
{full}
</time>
);
} }

View File

@ -15,5 +15,9 @@ export default function humanTime(time) {
const full = mo.format('LLLL'); const full = mo.format('LLLL');
const ago = humanTimeUtil(time); const ago = humanTimeUtil(time);
return <time pubdate datetime={datetime} title={full} data-humantime>{ago}</time>; return (
<time pubdate datetime={datetime} title={full} data-humantime>
{ago}
</time>
);
} }

View File

@ -8,5 +8,5 @@
export default function icon(fontClass, attrs = {}) { export default function icon(fontClass, attrs = {}) {
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || ''); attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
return <i {...attrs}/>; return <i {...attrs} />;
} }

View File

@ -29,7 +29,7 @@ function withoutUnnecessarySeparators(items) {
export default function listItems(items) { export default function listItems(items) {
if (!(items instanceof Array)) items = [items]; if (!(items instanceof Array)) items = [items];
return withoutUnnecessarySeparators(items).map(item => { return withoutUnnecessarySeparators(items).map((item) => {
const isListItem = item.component && item.component.isListItem; const isListItem = item.component && item.component.isListItem;
const active = item.component && item.component.isActive && item.component.isActive(item.props); const active = item.component && item.component.isActive && item.component.isActive(item.props);
const className = item.props ? item.props.itemClassName : item.itemClassName; const className = item.props ? item.props.itemClassName : item.itemClassName;
@ -39,15 +39,12 @@ export default function listItems(items) {
item.attrs.key = item.attrs.key || item.itemName; item.attrs.key = item.attrs.key || item.itemName;
} }
return isListItem return isListItem ? (
? item item
: <li className={classList([ ) : (
(item.itemName ? 'item-' + item.itemName : ''), <li className={classList([item.itemName ? 'item-' + item.itemName : '', className, active ? 'active' : ''])} key={item.itemName}>
className,
(active ? 'active' : '')
])}
key={item.itemName}>
{item} {item}
</li>; </li>
);
}); });
} }

View File

@ -13,7 +13,7 @@ export default function punctuateSeries(items) {
if (items.length === 2) { if (items.length === 2) {
return app.translator.trans('core.lib.series.two_text', { return app.translator.trans('core.lib.series.two_text', {
first: items[0], first: items[0],
second: items[1] second: items[1],
}); });
} else if (items.length >= 3) { } else if (items.length >= 3) {
// If there are three or more items, we will join all but the first and // If there are three or more items, we will join all but the first and
@ -27,7 +27,7 @@ export default function punctuateSeries(items) {
return app.translator.trans('core.lib.series.three_text', { return app.translator.trans('core.lib.series.three_text', {
first: items[0], first: items[0],
second, second,
third: items[items.length - 1] third: items[items.length - 1],
}); });
} }

View File

@ -19,18 +19,18 @@ Object.assign(Discussion.prototype, {
lastPostNumber: Model.attribute('lastPostNumber'), lastPostNumber: Model.attribute('lastPostNumber'),
commentCount: Model.attribute('commentCount'), commentCount: Model.attribute('commentCount'),
replyCount: computed('commentCount', commentCount => Math.max(0, commentCount - 1)), replyCount: computed('commentCount', (commentCount) => Math.max(0, commentCount - 1)),
posts: Model.hasMany('posts'), posts: Model.hasMany('posts'),
mostRelevantPost: Model.hasOne('mostRelevantPost'), mostRelevantPost: Model.hasOne('mostRelevantPost'),
lastReadAt: Model.attribute('lastReadAt', Model.transformDate), lastReadAt: Model.attribute('lastReadAt', Model.transformDate),
lastReadPostNumber: Model.attribute('lastReadPostNumber'), lastReadPostNumber: Model.attribute('lastReadPostNumber'),
isUnread: computed('unreadCount', unreadCount => !!unreadCount), isUnread: computed('unreadCount', (unreadCount) => !!unreadCount),
isRead: computed('unreadCount', unreadCount => app.session.user && !unreadCount), isRead: computed('unreadCount', (unreadCount) => app.session.user && !unreadCount),
hiddenAt: Model.attribute('hiddenAt', Model.transformDate), hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
hiddenUser: Model.hasOne('hiddenUser'), hiddenUser: Model.hasOne('hiddenUser'),
isHidden: computed('hiddenAt', hiddenAt => !!hiddenAt), isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt),
canReply: Model.attribute('canReply'), canReply: Model.attribute('canReply'),
canRename: Model.attribute('canRename'), canRename: Model.attribute('canRename'),
@ -84,7 +84,7 @@ Object.assign(Discussion.prototype, {
const items = new ItemList(); const items = new ItemList();
if (this.isHidden()) { if (this.isHidden()) {
items.add('hidden', <Badge type="hidden" icon="fas fa-trash" label={app.translator.trans('core.lib.badge.hidden_tooltip')}/>); items.add('hidden', <Badge type="hidden" icon="fas fa-trash" label={app.translator.trans('core.lib.badge.hidden_tooltip')} />);
} }
return items; return items;
@ -99,6 +99,6 @@ Object.assign(Discussion.prototype, {
postIds() { postIds() {
const posts = this.data.relationships.posts; const posts = this.data.relationships.posts;
return posts ? posts.data.map(link => link.id) : []; return posts ? posts.data.map((link) => link.id) : [];
} },
}); });

View File

@ -6,7 +6,7 @@ Object.assign(Group.prototype, {
nameSingular: Model.attribute('nameSingular'), nameSingular: Model.attribute('nameSingular'),
namePlural: Model.attribute('namePlural'), namePlural: Model.attribute('namePlural'),
color: Model.attribute('color'), color: Model.attribute('color'),
icon: Model.attribute('icon') icon: Model.attribute('icon'),
}); });
Group.ADMINISTRATOR_ID = '1'; Group.ADMINISTRATOR_ID = '1';

View File

@ -11,5 +11,5 @@ Object.assign(Notification.prototype, {
user: Model.hasOne('user'), user: Model.hasOne('user'),
fromUser: Model.hasOne('fromUser'), fromUser: Model.hasOne('fromUser'),
subject: Model.hasOne('subject') subject: Model.hasOne('subject'),
}); });

View File

@ -17,13 +17,13 @@ Object.assign(Post.prototype, {
editedAt: Model.attribute('editedAt', Model.transformDate), editedAt: Model.attribute('editedAt', Model.transformDate),
editedUser: Model.hasOne('editedUser'), editedUser: Model.hasOne('editedUser'),
isEdited: computed('editedAt', editedAt => !!editedAt), isEdited: computed('editedAt', (editedAt) => !!editedAt),
hiddenAt: Model.attribute('hiddenAt', Model.transformDate), hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
hiddenUser: Model.hasOne('hiddenUser'), hiddenUser: Model.hasOne('hiddenUser'),
isHidden: computed('hiddenAt', hiddenAt => !!hiddenAt), isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt),
canEdit: Model.attribute('canEdit'), canEdit: Model.attribute('canEdit'),
canHide: Model.attribute('canHide'), canHide: Model.attribute('canHide'),
canDelete: Model.attribute('canDelete') canDelete: Model.attribute('canDelete'),
}); });

View File

@ -32,7 +32,7 @@ Object.assign(User.prototype, {
canDelete: Model.attribute('canDelete'), canDelete: Model.attribute('canDelete'),
avatarColor: null, avatarColor: null,
color: computed('username', 'avatarUrl', 'avatarColor', function(username, avatarUrl, avatarColor) { color: computed('username', 'avatarUrl', 'avatarColor', function (username, avatarUrl, avatarColor) {
// If we've already calculated and cached the dominant color of the user's // If we've already calculated and cached the dominant color of the user's
// avatar, then we can return that in RGB format. If we haven't, we'll want // avatar, then we can return that in RGB format. If we haven't, we'll want
// to calculate it. Unless the user doesn't have an avatar, in which case // to calculate it. Unless the user doesn't have an avatar, in which case
@ -67,8 +67,8 @@ Object.assign(User.prototype, {
const groups = this.groups(); const groups = this.groups();
if (groups) { if (groups) {
groups.forEach(group => { groups.forEach((group) => {
items.add('group' + group.id(), GroupBadge.component({group})); items.add('group' + group.id(), GroupBadge.component({ group }));
}); });
} }
@ -85,7 +85,7 @@ Object.assign(User.prototype, {
const image = new Image(); const image = new Image();
const user = this; const user = this;
image.onload = function() { image.onload = function () {
const colorThief = new ColorThief(); const colorThief = new ColorThief();
user.avatarColor = colorThief.getColor(this); user.avatarColor = colorThief.getColor(this);
user.freshness = new Date(); user.freshness = new Date();
@ -106,6 +106,6 @@ Object.assign(User.prototype, {
Object.assign(preferences, newPreferences); Object.assign(preferences, newPreferences);
return this.save({preferences}); return this.save({ preferences });
} },
}); });

View File

@ -7,7 +7,7 @@ export default class Drawer {
constructor() { constructor() {
// Set up an event handler so that whenever the content area is tapped, // Set up an event handler so that whenever the content area is tapped,
// the drawer will close. // the drawer will close.
$('#content').click(e => { $('#content').click((e) => {
if (this.isOpen()) { if (this.isOpen()) {
e.preventDefault(); e.preventDefault();
this.hide(); this.hide();

View File

@ -28,7 +28,7 @@ export default class ItemList {
*/ */
isEmpty() { isEmpty() {
for (const i in this.items) { for (const i in this.items) {
if(this.items.hasOwnProperty(i)) { if (this.items.hasOwnProperty(i)) {
return false; return false;
} }
} }
@ -147,14 +147,15 @@ export default class ItemList {
} }
} }
return items.sort((a, b) => { return items
.sort((a, b) => {
if (a.priority === b.priority) { if (a.priority === b.priority) {
return a.key - b.key; return a.key - b.key;
} else if (a.priority > b.priority) { } else if (a.priority > b.priority) {
return -1; return -1;
} }
return 1; return 1;
}).map(item => item.content); })
.map((item) => item.content);
} }
} }

View File

@ -1,9 +1,10 @@
const later = window.requestAnimationFrame || const later =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame || window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame || window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame || window.msRequestAnimationFrame ||
window.oRequestAnimationFrame || window.oRequestAnimationFrame ||
(callback => window.setTimeout(callback, 1000 / 60)); ((callback) => window.setTimeout(callback, 1000 / 60));
/** /**
* The `ScrollListener` class sets up a listener that handles window scroll * The `ScrollListener` class sets up a listener that handles window scroll
@ -57,10 +58,7 @@ export default class ScrollListener {
*/ */
start() { start() {
if (!this.active) { if (!this.active) {
window.addEventListener( window.addEventListener('scroll', (this.active = this.loop.bind(this)));
'scroll',
this.active = this.loop.bind(this)
);
} }
} }

View File

@ -44,7 +44,7 @@ export default class SubtreeRetainer {
} }
}); });
return needsRebuild ? false : {subtree: 'retain'}; return needsRebuild ? false : { subtree: 'retain' };
} }
/** /**

View File

@ -13,7 +13,7 @@ export default function classList(classes) {
let classNames; let classNames;
if (classes instanceof Array) { if (classes instanceof Array) {
classNames = classes.filter(name => name); classNames = classes.filter((name) => name);
} else { } else {
classNames = []; classNames = [];

View File

@ -14,12 +14,12 @@ export default function computed(...dependentKeys) {
const dependentValues = {}; const dependentValues = {};
let computedValue; let computedValue;
return function() { return function () {
let recompute = false; let recompute = false;
// Read all of the dependent values. If any of them have changed since last // Read all of the dependent values. If any of them have changed since last
// time, then we'll want to recompute our output. // time, then we'll want to recompute our output.
keys.forEach(key => { keys.forEach((key) => {
const value = typeof this[key] === 'function' ? this[key]() : this[key]; const value = typeof this[key] === 'function' ? this[key]() : this[key];
if (dependentValues[key] !== value) { if (dependentValues[key] !== value) {
@ -29,7 +29,10 @@ export default function computed(...dependentKeys) {
}); });
if (recompute) { if (recompute) {
computedValue = compute.apply(this, keys.map(key => dependentValues[key])); computedValue = compute.apply(
this,
keys.map((key) => dependentValues[key])
);
} }
return computedValue; return computedValue;

View File

@ -34,7 +34,7 @@ export default {
* @public * @public
*/ */
trigger(event, ...args) { trigger(event, ...args) {
this.getHandlers(event).forEach(handler => handler.apply(this, args)); this.getHandlers(event).forEach((handler) => handler.apply(this, args));
}, },
/** /**
@ -55,7 +55,7 @@ export default {
* @param {function} handler The function to handle the event. * @param {function} handler The function to handle the event.
*/ */
one(event, handler) { one(event, handler) {
const wrapper = function() { const wrapper = function () {
handler.apply(this, arguments); handler.apply(this, arguments);
this.off(event, wrapper); this.off(event, wrapper);
@ -77,5 +77,5 @@ export default {
if (index !== -1) { if (index !== -1) {
handlers.splice(index, 1); handlers.splice(index, 1);
} }
} },
} };

View File

@ -6,7 +6,7 @@
*/ */
export default function extractText(vdom) { export default function extractText(vdom) {
if (vdom instanceof Array) { if (vdom instanceof Array) {
return vdom.map(element => extractText(element)).join(''); return vdom.map((element) => extractText(element)).join('');
} else if (typeof vdom === 'object' && vdom !== null) { } else if (typeof vdom === 'object' && vdom !== null) {
return extractText(vdom.children); return extractText(vdom.children);
} else { } else {

View File

@ -33,4 +33,4 @@ export default function humanTime(time) {
} }
return ago; return ago;
}; }

View File

@ -1,7 +1,7 @@
import humanTimeUtil from './humanTime'; import humanTimeUtil from './humanTime';
function updateHumanTimes() { function updateHumanTimes() {
$('[data-humantime]').each(function() { $('[data-humantime]').each(function () {
const $this = $(this); const $this = $(this);
const ago = humanTimeUtil($this.attr('datetime')); const ago = humanTimeUtil($this.attr('datetime'));

View File

@ -12,7 +12,7 @@
export default function mixin(Parent, ...mixins) { export default function mixin(Parent, ...mixins) {
class Mixed extends Parent {} class Mixed extends Parent {}
mixins.forEach(object => { mixins.forEach((object) => {
Object.assign(Mixed.prototype, object); Object.assign(Mixed.prototype, object);
}); });

View File

@ -3,11 +3,11 @@ import Component from '../Component';
export default function patchMithril(global) { export default function patchMithril(global) {
const mo = global.m; const mo = global.m;
const m = function(comp, ...args) { const m = function (comp, ...args) {
if (comp.prototype && comp.prototype instanceof Component) { if (comp.prototype && comp.prototype instanceof Component) {
let children = args.slice(1); let children = args.slice(1);
if (children.length === 1 && Array.isArray(children[0])) { if (children.length === 1 && Array.isArray(children[0])) {
children = children[0] children = children[0];
} }
return comp.component(args[0], children); return comp.component(args[0], children);
@ -29,14 +29,14 @@ export default function patchMithril(global) {
return node; return node;
}; };
Object.keys(mo).forEach(key => m[key] = mo[key]); Object.keys(mo).forEach((key) => (m[key] = mo[key]));
/** /**
* Redraw only if not in the middle of a computation (e.g. a route change). * Redraw only if not in the middle of a computation (e.g. a route change).
* *
* @return {void} * @return {void}
*/ */
m.lazyRedraw = function() { m.lazyRedraw = function () {
m.startComputation(); m.startComputation();
m.endComputation(); m.endComputation();
}; };

View File

@ -7,9 +7,7 @@
* @return {String} * @return {String}
*/ */
export function truncate(string, length, start = 0) { export function truncate(string, length, start = 0) {
return (start > 0 ? '...' : '') + return (start > 0 ? '...' : '') + string.substring(start, start + length) + (string.length > start + length ? '...' : '');
string.substring(start, start + length) +
(string.length > start + length ? '...' : '');
} }
/** /**
@ -24,7 +22,8 @@ export function truncate(string, length, start = 0) {
* @return {String} * @return {String}
*/ */
export function slug(string) { export function slug(string) {
return string.toLowerCase() return string
.toLowerCase()
.replace(/[^a-z0-9]/gi, '-') .replace(/[^a-z0-9]/gi, '-')
.replace(/-+/g, '-') .replace(/-+/g, '-')
.replace(/-$|^-/g, ''); .replace(/-$|^-/g, '');
@ -38,9 +37,7 @@ export function slug(string) {
* @return {String} * @return {String}
*/ */
export function getPlainContent(string) { export function getPlainContent(string) {
const html = string const html = string.replace(/(<\/p>|<br>)/g, '$1 &nbsp;').replace(/<img\b[^>]*>/gi, ' ');
.replace(/(<\/p>|<br>)/g, '$1 &nbsp;')
.replace(/<img\b[^>]*>/ig, ' ');
const dom = $('<div/>').html(html); const dom = $('<div/>').html(html);

View File

@ -10,18 +10,42 @@ function hsvToRgb(h, s, v) {
const t = v * (1 - (1 - f) * s); const t = v * (1 - (1 - f) * s);
switch (i % 6) { switch (i % 6) {
case 0: r = v; g = t; b = p; break; case 0:
case 1: r = q; g = v; b = p; break; r = v;
case 2: r = p; g = v; b = t; break; g = t;
case 3: r = p; g = q; b = v; break; b = p;
case 4: r = t; g = p; b = v; break; break;
case 5: r = v; g = p; b = q; break; case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
case 5:
r = v;
g = p;
b = q;
break;
} }
return { return {
r: Math.floor(r * 255), r: Math.floor(r * 255),
g: Math.floor(g * 255), g: Math.floor(g * 255),
b: Math.floor(b * 255) b: Math.floor(b * 255),
}; };
} }

View File

@ -22,7 +22,7 @@ export default class ForumApplication extends Application {
* @type {Object} * @type {Object}
*/ */
notificationComponents = { notificationComponents = {
discussionRenamed: DiscussionRenamedNotification discussionRenamed: DiscussionRenamedNotification,
}; };
/** /**
* A map of post types to their components. * A map of post types to their components.
@ -31,7 +31,7 @@ export default class ForumApplication extends Application {
*/ */
postComponents = { postComponents = {
comment: CommentPost, comment: CommentPost,
discussionRenamed: DiscussionRenamedPost discussionRenamed: DiscussionRenamedPost,
}; };
/** /**
@ -87,7 +87,7 @@ export default class ForumApplication extends Application {
this.routes[defaultAction].path = '/'; this.routes[defaultAction].path = '/';
this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/'); this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/');
m.mount(document.getElementById('app-navigation'), Navigation.component({className: 'App-backControl', drawer: true})); m.mount(document.getElementById('app-navigation'), Navigation.component({ className: 'App-backControl', drawer: true }));
m.mount(document.getElementById('header-navigation'), Navigation.component()); m.mount(document.getElementById('header-navigation'), Navigation.component());
m.mount(document.getElementById('header-primary'), HeaderPrimary.component()); m.mount(document.getElementById('header-primary'), HeaderPrimary.component());
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component()); m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
@ -102,7 +102,7 @@ export default class ForumApplication extends Application {
// Route the home link back home when clicked. We do not want it to register // 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. // if the user is opening it in a new tab, however.
$('#home-link').click(e => { $('#home-link').click((e) => {
if (e.ctrlKey || e.metaKey || e.which === 2) return; if (e.ctrlKey || e.metaKey || e.which === 2) return;
e.preventDefault(); e.preventDefault();
app.history.home(); app.history.home();
@ -123,9 +123,11 @@ export default class ForumApplication extends Application {
* @return {Boolean} * @return {Boolean}
*/ */
composingReplyTo(discussion) { composingReplyTo(discussion) {
return this.composer.component instanceof ReplyComposer && return (
this.composer.component instanceof ReplyComposer &&
this.composer.component.props.discussion === discussion && this.composer.component.props.discussion === discussion &&
this.composer.position !== Composer.PositionEnum.HIDDEN; this.composer.position !== Composer.PositionEnum.HIDDEN
);
} }
/** /**
@ -135,8 +137,7 @@ export default class ForumApplication extends Application {
* @return {Boolean} * @return {Boolean}
*/ */
viewingDiscussion(discussion) { viewingDiscussion(discussion) {
return this.current instanceof DiscussionPage && return this.current instanceof DiscussionPage && this.current.discussion === discussion;
this.current.discussion === discussion;
} }
/** /**

View File

@ -134,6 +134,6 @@ export default Object.assign(compat, {
'components/DiscussionListItem': DiscussionListItem, 'components/DiscussionListItem': DiscussionListItem,
'components/LoadingPost': LoadingPost, 'components/LoadingPost': LoadingPost,
'components/PostsUserPage': PostsUserPage, 'components/PostsUserPage': PostsUserPage,
'routes': routes, routes: routes,
'ForumApplication': ForumApplication ForumApplication: ForumApplication,
}); });

View File

@ -44,7 +44,8 @@ export default class AvatarEditor extends Component {
return ( return (
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '') + (this.isDraggedOver ? ' dragover' : '')}> <div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '') + (this.isDraggedOver ? ' dragover' : '')}>
{avatar(user)} {avatar(user)}
<a className={ user.avatarUrl() ? "Dropdown-toggle" : "Dropdown-toggle AvatarEditor--noAvatar" } <a
className={user.avatarUrl() ? 'Dropdown-toggle' : 'Dropdown-toggle AvatarEditor--noAvatar'}
title={app.translator.trans('core.forum.user.avatar_upload_tooltip')} title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
data-toggle="dropdown" data-toggle="dropdown"
onclick={this.quickUpload.bind(this)} onclick={this.quickUpload.bind(this)}
@ -52,12 +53,11 @@ export default class AvatarEditor extends Component {
ondragenter={this.enableDragover.bind(this)} ondragenter={this.enableDragover.bind(this)}
ondragleave={this.disableDragover.bind(this)} ondragleave={this.disableDragover.bind(this)}
ondragend={this.disableDragover.bind(this)} ondragend={this.disableDragover.bind(this)}
ondrop={this.dropUpload.bind(this)}> ondrop={this.dropUpload.bind(this)}
{this.loading ? LoadingIndicator.component() : (user.avatarUrl() ? icon('fas fa-pencil-alt') : icon('fas fa-plus-circle'))} >
{this.loading ? LoadingIndicator.component() : user.avatarUrl() ? icon('fas fa-pencil-alt') : icon('fas fa-plus-circle')}
</a> </a>
<ul className="Dropdown-menu Menu"> <ul className="Dropdown-menu Menu">{listItems(this.controlItems().toArray())}</ul>
{listItems(this.controlItems().toArray())}
</ul>
</div> </div>
); );
} }
@ -70,19 +70,21 @@ export default class AvatarEditor extends Component {
controlItems() { controlItems() {
const items = new ItemList(); const items = new ItemList();
items.add('upload', items.add(
'upload',
Button.component({ Button.component({
icon: 'fas fa-upload', icon: 'fas fa-upload',
children: app.translator.trans('core.forum.user.avatar_upload_button'), children: app.translator.trans('core.forum.user.avatar_upload_button'),
onclick: this.openPicker.bind(this) onclick: this.openPicker.bind(this),
}) })
); );
items.add('remove', items.add(
'remove',
Button.component({ Button.component({
icon: 'fas fa-times', icon: 'fas fa-times',
children: app.translator.trans('core.forum.user.avatar_remove_button'), children: app.translator.trans('core.forum.user.avatar_remove_button'),
onclick: this.remove.bind(this) onclick: this.remove.bind(this),
}) })
); );
@ -150,7 +152,11 @@ export default class AvatarEditor extends Component {
const user = this.props.user; const user = this.props.user;
const $input = $('<input type="file">'); const $input = $('<input type="file">');
$input.appendTo('body').hide().click().on('input', e => { $input
.appendTo('body')
.hide()
.click()
.on('input', (e) => {
this.upload($(e.target)[0].files[0]); this.upload($(e.target)[0].files[0]);
}); });
} }
@ -170,15 +176,14 @@ export default class AvatarEditor extends Component {
this.loading = true; this.loading = true;
m.redraw(); m.redraw();
app.request({ app
.request({
method: 'POST', method: 'POST',
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar', url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
serialize: raw => raw, serialize: (raw) => raw,
data data,
}).then( })
this.success.bind(this), .then(this.success.bind(this), this.failure.bind(this));
this.failure.bind(this)
);
} }
/** /**
@ -190,13 +195,12 @@ export default class AvatarEditor extends Component {
this.loading = true; this.loading = true;
m.redraw(); m.redraw();
app.request({ app
.request({
method: 'DELETE', method: 'DELETE',
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar' url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
}).then( })
this.success.bind(this), .then(this.success.bind(this), this.failure.bind(this));
this.failure.bind(this)
);
} }
/** /**

View File

@ -44,7 +44,9 @@ export default class ChangeEmailModal extends Modal {
return ( return (
<div className="Modal-body"> <div className="Modal-body">
<div className="Form Form--centered"> <div className="Form Form--centered">
<p className="helpText">{app.translator.trans('core.forum.change_email.confirmation_message', {email: <strong>{this.email()}</strong>})}</p> <p className="helpText">
{app.translator.trans('core.forum.change_email.confirmation_message', { email: <strong>{this.email()}</strong> })}
</p>
<div className="Form-group"> <div className="Form-group">
<Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}> <Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
{app.translator.trans('core.forum.change_email.dismiss_button')} {app.translator.trans('core.forum.change_email.dismiss_button')}
@ -59,23 +61,31 @@ export default class ChangeEmailModal extends Modal {
<div className="Modal-body"> <div className="Modal-body">
<div className="Form Form--centered"> <div className="Form Form--centered">
<div className="Form-group"> <div className="Form-group">
<input type="email" name="email" className="FormControl" <input
type="email"
name="email"
className="FormControl"
placeholder={app.session.user.email()} placeholder={app.session.user.email()}
bidi={this.email} bidi={this.email}
disabled={this.loading}/> disabled={this.loading}
/>
</div> </div>
<div className="Form-group"> <div className="Form-group">
<input type="password" name="password" className="FormControl" <input
type="password"
name="password"
className="FormControl"
placeholder={app.translator.trans('core.forum.change_email.confirm_password_placeholder')} placeholder={app.translator.trans('core.forum.change_email.confirm_password_placeholder')}
bidi={this.password} bidi={this.password}
disabled={this.loading}/> disabled={this.loading}
/>
</div> </div>
<div className="Form-group"> <div className="Form-group">
{Button.component({ {Button.component({
className: 'Button Button--primary Button--block', className: 'Button Button--primary Button--block',
type: 'submit', type: 'submit',
loading: this.loading, loading: this.loading,
children: app.translator.trans('core.forum.change_email.submit_button') children: app.translator.trans('core.forum.change_email.submit_button'),
})} })}
</div> </div>
</div> </div>
@ -97,11 +107,15 @@ export default class ChangeEmailModal extends Modal {
this.loading = true; this.loading = true;
app.session.user.save({email: this.email()}, { app.session.user
.save(
{ email: this.email() },
{
errorHandler: this.onerror.bind(this), errorHandler: this.onerror.bind(this),
meta: {password: this.password()} meta: { password: this.password() },
}) }
.then(() => this.success = true) )
.then(() => (this.success = true))
.catch(() => {}) .catch(() => {})
.then(this.loaded.bind(this)); .then(this.loaded.bind(this));
} }

View File

@ -24,7 +24,7 @@ export default class ChangePasswordModal extends Modal {
className: 'Button Button--primary Button--block', className: 'Button Button--primary Button--block',
type: 'submit', type: 'submit',
loading: this.loading, loading: this.loading,
children: app.translator.trans('core.forum.change_password.send_button') children: app.translator.trans('core.forum.change_password.send_button'),
})} })}
</div> </div>
</div> </div>
@ -37,13 +37,12 @@ export default class ChangePasswordModal extends Modal {
this.loading = true; this.loading = true;
app.request({ app
.request({
method: 'POST', method: 'POST',
url: app.forum.attribute('apiUrl') + '/forgot', url: app.forum.attribute('apiUrl') + '/forgot',
data: {email: app.session.user.email()} data: { email: app.session.user.email() },
}).then( })
this.hide.bind(this), .then(this.hide.bind(this), this.loaded.bind(this));
this.loaded.bind(this)
);
} }
} }

View File

@ -33,7 +33,7 @@ export default class CommentPost extends Post {
// Create an instance of the component that displays the post's author so // Create an instance of the component that displays the post's author so
// that we can force the post to rerender when the user card is shown. // that we can force the post to rerender when the user card is shown.
this.postUser = new PostUser({post: this.props.post}); this.postUser = new PostUser({ post: this.props.post });
this.subtree.check( this.subtree.check(
() => this.postUser.cardVisible, () => this.postUser.cardVisible,
() => this.isEditing() () => this.isEditing()
@ -44,13 +44,13 @@ export default class CommentPost extends Post {
// Note: we avoid using JSX for the <ul> below because it results in some // Note: we avoid using JSX for the <ul> below because it results in some
// weirdness in Mithril.js 0.1.x (see flarum/core#975). This workaround can // weirdness in Mithril.js 0.1.x (see flarum/core#975). This workaround can
// be reverted when we upgrade to Mithril 1.0. // be reverted when we upgrade to Mithril 1.0.
return super.content().concat([ return super
.content()
.concat([
<header className="Post-header">{m('ul', listItems(this.headerItems().toArray()))}</header>, <header className="Post-header">{m('ul', listItems(this.headerItems().toArray()))}</header>,
<div className="Post-body"> <div className="Post-body">
{this.isEditing() {this.isEditing() ? <div className="Post-preview" config={this.configPreview.bind(this)} /> : m.trust(this.props.post.contentHtml())}
? <div className="Post-preview" config={this.configPreview.bind(this)}/> </div>,
: m.trust(this.props.post.contentHtml())}
</div>
]); ]);
} }
@ -63,7 +63,7 @@ export default class CommentPost extends Post {
// all of the <script> tags in the content and evaluate them. This is // all of the <script> tags in the content and evaluate them. This is
// necessary because TextFormatter outputs them for e.g. syntax highlighting. // necessary because TextFormatter outputs them for e.g. syntax highlighting.
if (context.contentHtml !== contentHtml) { if (context.contentHtml !== contentHtml) {
this.$('.Post-body script').each(function() { this.$('.Post-body script').each(function () {
eval.call(window, $(this).text()); eval.call(window, $(this).text());
}); });
} }
@ -72,20 +72,22 @@ export default class CommentPost extends Post {
} }
isEditing() { isEditing() {
return app.composer.component instanceof EditPostComposer && return app.composer.component instanceof EditPostComposer && app.composer.component.props.post === this.props.post;
app.composer.component.props.post === this.props.post;
} }
attrs() { attrs() {
const post = this.props.post; const post = this.props.post;
const attrs = super.attrs(); const attrs = super.attrs();
attrs.className = (attrs.className || '') + ' ' + classList({ attrs.className =
'CommentPost': true, (attrs.className || '') +
' ' +
classList({
CommentPost: true,
'Post--hidden': post.isHidden(), 'Post--hidden': post.isHidden(),
'Post--edited': post.isEdited(), 'Post--edited': post.isEdited(),
'revealContent': this.revealContent, revealContent: this.revealContent,
'editing': this.isEditing() editing: this.isEditing(),
}); });
return attrs; return attrs;
@ -127,7 +129,7 @@ export default class CommentPost extends Post {
headerItems() { headerItems() {
const items = new ItemList(); const items = new ItemList();
const post = this.props.post; const post = this.props.post;
const props = {post}; const props = { post };
items.add('user', this.postUser.render(), 100); items.add('user', this.postUser.render(), 100);
items.add('meta', PostMeta.component(props)); items.add('meta', PostMeta.component(props));
@ -139,13 +141,14 @@ export default class CommentPost extends Post {
// If the post is hidden, add a button that allows toggling the visibility // If the post is hidden, add a button that allows toggling the visibility
// of the post's content. // of the post's content.
if (post.isHidden()) { if (post.isHidden()) {
items.add('toggle', ( items.add(
'toggle',
Button.component({ Button.component({
className: 'Button Button--default Button--more', className: 'Button Button--default Button--more',
icon: 'fas fa-ellipsis-h', icon: 'fas fa-ellipsis-h',
onclick: this.toggleContent.bind(this) onclick: this.toggleContent.bind(this),
}) })
)); );
} }
return items; return items;

View File

@ -36,10 +36,10 @@ class Composer extends Component {
view() { view() {
const classes = { const classes = {
'normal': this.position === Composer.PositionEnum.NORMAL, normal: this.position === Composer.PositionEnum.NORMAL,
'minimized': this.position === Composer.PositionEnum.MINIMIZED, minimized: this.position === Composer.PositionEnum.MINIMIZED,
'fullScreen': this.position === Composer.PositionEnum.FULLSCREEN, fullScreen: this.position === Composer.PositionEnum.FULLSCREEN,
'active': this.active active: this.active,
}; };
classes.visible = classes.normal || classes.minimized || classes.fullScreen; classes.visible = classes.normal || classes.minimized || classes.fullScreen;
@ -52,7 +52,7 @@ class Composer extends Component {
return ( return (
<div className={'Composer ' + classList(classes)}> <div className={'Composer ' + classList(classes)}>
<div className="Composer-handle" config={this.configHandle.bind(this)}/> <div className="Composer-handle" config={this.configHandle.bind(this)} />
<ul className="Composer-controls">{listItems(this.controlItems().toArray())}</ul> <ul className="Composer-controls">{listItems(this.controlItems().toArray())}</ul>
<div className="Composer-content" onclick={showIfMinimized}> <div className="Composer-content" onclick={showIfMinimized}>
{this.component ? this.component.render() : ''} {this.component ? this.component.render() : ''}
@ -77,7 +77,7 @@ class Composer extends Component {
// Whenever any of the inputs inside the composer are have focus, we want to // Whenever any of the inputs inside the composer are have focus, we want to
// add a class to the composer to draw attention to it. // add a class to the composer to draw attention to it.
this.$().on('focus blur', ':input', e => { this.$().on('focus blur', ':input', (e) => {
this.active = e.type === 'focusin'; this.active = e.type === 'focusin';
m.redraw(); m.redraw();
}); });
@ -94,18 +94,18 @@ class Composer extends Component {
const handlers = {}; const handlers = {};
$(window).on('resize', handlers.onresize = this.updateHeight.bind(this)).resize(); $(window)
.on('resize', (handlers.onresize = this.updateHeight.bind(this)))
.resize();
$(document) $(document)
.on('mousemove', handlers.onmousemove = this.onmousemove.bind(this)) .on('mousemove', (handlers.onmousemove = this.onmousemove.bind(this)))
.on('mouseup', handlers.onmouseup = this.onmouseup.bind(this)); .on('mouseup', (handlers.onmouseup = this.onmouseup.bind(this)));
context.onunload = () => { context.onunload = () => {
$(window).off('resize', handlers.onresize); $(window).off('resize', handlers.onresize);
$(document) $(document).off('mousemove', handlers.onmousemove).off('mouseup', handlers.onmouseup);
.off('mousemove', handlers.onmousemove)
.off('mouseup', handlers.onmouseup);
}; };
} }
@ -121,9 +121,10 @@ class Composer extends Component {
const composer = this; const composer = this;
$(element).css('cursor', 'row-resize') $(element)
.bind('dragstart mousedown', e => e.preventDefault()) .css('cursor', 'row-resize')
.mousedown(function(e) { .bind('dragstart mousedown', (e) => e.preventDefault())
.mousedown(function (e) {
composer.mouseStart = e.clientY; composer.mouseStart = e.clientY;
composer.heightStart = composer.$().height(); composer.heightStart = composer.$().height();
composer.handle = $(this); composer.handle = $(this);
@ -191,15 +192,12 @@ class Composer extends Component {
* scrolled right to the bottom. * scrolled right to the bottom.
*/ */
updateBodyPadding() { updateBodyPadding() {
const visible = this.position !== Composer.PositionEnum.HIDDEN && const visible =
this.position !== Composer.PositionEnum.MINIMIZED && this.position !== Composer.PositionEnum.HIDDEN && this.position !== Composer.PositionEnum.MINIMIZED && this.$().css('position') !== 'absolute';
this.$().css('position') !== 'absolute';
const paddingBottom = visible const paddingBottom = visible ? this.computedHeight() - parseInt($('#app').css('padding-bottom'), 10) : 0;
? this.computedHeight() - parseInt($('#app').css('padding-bottom'), 10)
: 0;
$('#content').css({paddingBottom}); $('#content').css({ paddingBottom });
} }
/** /**
@ -289,12 +287,12 @@ class Composer extends Component {
const newHeight = $composer.outerHeight(); const newHeight = $composer.outerHeight();
if (oldPosition === Composer.PositionEnum.HIDDEN) { if (oldPosition === Composer.PositionEnum.HIDDEN) {
$composer.css({bottom: -newHeight, height: newHeight}); $composer.css({ bottom: -newHeight, height: newHeight });
} else { } else {
$composer.css({height: oldHeight}); $composer.css({ height: oldHeight });
} }
$composer.animate({bottom: 0, height: newHeight}, 'fast', () => this.component.focus()); $composer.animate({ bottom: 0, height: newHeight }, 'fast', () => this.component.focus());
this.updateBodyPadding(); this.updateBodyPadding();
$(window).scrollTop(scrollTop); $(window).scrollTop(scrollTop);
@ -304,9 +302,7 @@ class Composer extends Component {
* Show the Composer backdrop. * Show the Composer backdrop.
*/ */
showBackdrop() { showBackdrop() {
this.$backdrop = $('<div/>') this.$backdrop = $('<div/>').addClass('composer-backdrop').appendTo('body');
.addClass('composer-backdrop')
.appendTo('body');
} }
/** /**
@ -346,7 +342,7 @@ class Composer extends Component {
// Animate the composer sliding down off the bottom edge of the viewport. // Animate the composer sliding down off the bottom edge of the viewport.
// Only when the animation is completed, update the Composer state flag and // Only when the animation is completed, update the Composer state flag and
// other elements on the page. // other elements on the page.
$composer.stop(true).animate({bottom: -$composer.height()}, 'fast', () => { $composer.stop(true).animate({ bottom: -$composer.height() }, 'fast', () => {
this.position = Composer.PositionEnum.HIDDEN; this.position = Composer.PositionEnum.HIDDEN;
this.clear(); this.clear();
m.redraw(); m.redraw();
@ -421,32 +417,44 @@ class Composer extends Component {
const items = new ItemList(); const items = new ItemList();
if (this.position === Composer.PositionEnum.FULLSCREEN) { if (this.position === Composer.PositionEnum.FULLSCREEN) {
items.add('exitFullScreen', ComposerButton.component({ items.add(
'exitFullScreen',
ComposerButton.component({
icon: 'fas fa-compress', icon: 'fas fa-compress',
title: app.translator.trans('core.forum.composer.exit_full_screen_tooltip'), title: app.translator.trans('core.forum.composer.exit_full_screen_tooltip'),
onclick: this.exitFullScreen.bind(this) onclick: this.exitFullScreen.bind(this),
})); })
);
} else { } else {
if (this.position !== Composer.PositionEnum.MINIMIZED) { if (this.position !== Composer.PositionEnum.MINIMIZED) {
items.add('minimize', ComposerButton.component({ items.add(
'minimize',
ComposerButton.component({
icon: 'fas fa-minus minimize', icon: 'fas fa-minus minimize',
title: app.translator.trans('core.forum.composer.minimize_tooltip'), title: app.translator.trans('core.forum.composer.minimize_tooltip'),
onclick: this.minimize.bind(this), onclick: this.minimize.bind(this),
itemClassName: 'App-backControl' itemClassName: 'App-backControl',
})); })
);
items.add('fullScreen', ComposerButton.component({ items.add(
'fullScreen',
ComposerButton.component({
icon: 'fas fa-expand', icon: 'fas fa-expand',
title: app.translator.trans('core.forum.composer.full_screen_tooltip'), title: app.translator.trans('core.forum.composer.full_screen_tooltip'),
onclick: this.fullScreen.bind(this) onclick: this.fullScreen.bind(this),
})); })
);
} }
items.add('close', ComposerButton.component({ items.add(
'close',
ComposerButton.component({
icon: 'fas fa-times', icon: 'fas fa-times',
title: app.translator.trans('core.forum.composer.close_tooltip'), title: app.translator.trans('core.forum.composer.close_tooltip'),
onclick: this.close.bind(this) onclick: this.close.bind(this),
})); })
);
} }
return items; return items;
@ -524,7 +532,7 @@ Composer.PositionEnum = {
HIDDEN: 'hidden', HIDDEN: 'hidden',
NORMAL: 'normal', NORMAL: 'normal',
MINIMIZED: 'minimized', MINIMIZED: 'minimized',
FULLSCREEN: 'fullScreen' FULLSCREEN: 'fullScreen',
}; };
export default Composer; export default Composer;

View File

@ -47,7 +47,7 @@ export default class ComposerBody extends Component {
placeholder: this.props.placeholder, placeholder: this.props.placeholder,
onchange: this.content, onchange: this.content,
onsubmit: this.onsubmit.bind(this), onsubmit: this.onsubmit.bind(this),
value: this.content() value: this.content(),
}); });
} }
@ -57,12 +57,12 @@ export default class ComposerBody extends Component {
return ( return (
<div className={'ComposerBody ' + (this.props.className || '')}> <div className={'ComposerBody ' + (this.props.className || '')}>
{avatar(this.props.user, {className: 'ComposerBody-avatar'})} {avatar(this.props.user, { className: 'ComposerBody-avatar' })}
<div className="ComposerBody-content"> <div className="ComposerBody-content">
<ul className="ComposerBody-header">{listItems(this.headerItems().toArray())}</ul> <ul className="ComposerBody-header">{listItems(this.headerItems().toArray())}</ul>
<div className="ComposerBody-editor">{this.editor.render()}</div> <div className="ComposerBody-editor">{this.editor.render()}</div>
</div> </div>
{LoadingIndicator.component({className: 'ComposerBody-loading' + (this.loading ? ' active' : '')})} {LoadingIndicator.component({ className: 'ComposerBody-loading' + (this.loading ? ' active' : '') })}
</div> </div>
); );
} }
@ -100,8 +100,7 @@ export default class ComposerBody extends Component {
* *
* @abstract * @abstract
*/ */
onsubmit() { onsubmit() {}
}
/** /**
* Stop loading. * Stop loading.

View File

@ -39,16 +39,19 @@ export default class DiscussionComposer extends ComposerBody {
items.add('title', <h3>{app.translator.trans('core.forum.composer_discussion.title')}</h3>, 100); items.add('title', <h3>{app.translator.trans('core.forum.composer_discussion.title')}</h3>, 100);
items.add('discussionTitle', ( items.add(
'discussionTitle',
<h3> <h3>
<input className="FormControl" <input
className="FormControl"
value={this.title()} value={this.title()}
oninput={m.withAttr('value', this.title)} oninput={m.withAttr('value', this.title)}
placeholder={this.props.titlePlaceholder} placeholder={this.props.titlePlaceholder}
disabled={!!this.props.disabled} disabled={!!this.props.disabled}
onkeydown={this.onkeydown.bind(this)}/> onkeydown={this.onkeydown.bind(this)}
/>
</h3> </h3>
)); );
return items; return items;
} }
@ -60,7 +63,8 @@ export default class DiscussionComposer extends ComposerBody {
* @param {Event} e * @param {Event} e
*/ */
onkeydown(e) { onkeydown(e) {
if (e.which === 13) { // Return if (e.which === 13) {
// Return
e.preventDefault(); e.preventDefault();
this.editor.setSelectionRange(0, 0); this.editor.setSelectionRange(0, 0);
} }
@ -80,7 +84,7 @@ export default class DiscussionComposer extends ComposerBody {
data() { data() {
return { return {
title: this.title(), title: this.title(),
content: this.content() content: this.content(),
}; };
} }
@ -89,13 +93,13 @@ export default class DiscussionComposer extends ComposerBody {
const data = this.data(); const data = this.data();
app.store.createRecord('discussions').save(data).then( app.store
discussion => { .createRecord('discussions')
.save(data)
.then((discussion) => {
app.composer.hide(); app.composer.hide();
app.cache.discussionList.refresh(); app.cache.discussionList.refresh();
m.route(app.route.discussion(discussion)); m.route(app.route.discussion(discussion));
}, }, this.loaded.bind(this));
this.loaded.bind(this)
);
} }
} }

View File

@ -48,33 +48,27 @@ export default class DiscussionList extends Component {
loading = Button.component({ loading = Button.component({
children: app.translator.trans('core.forum.discussion_list.load_more_button'), children: app.translator.trans('core.forum.discussion_list.load_more_button'),
className: 'Button', className: 'Button',
onclick: this.loadMore.bind(this) onclick: this.loadMore.bind(this),
}); });
} }
if (this.discussions.length === 0 && !this.loading) { if (this.discussions.length === 0 && !this.loading) {
const text = app.translator.trans('core.forum.discussion_list.empty_text'); const text = app.translator.trans('core.forum.discussion_list.empty_text');
return ( return <div className="DiscussionList">{Placeholder.component({ text })}</div>;
<div className="DiscussionList">
{Placeholder.component({text})}
</div>
);
} }
return ( return (
<div className={'DiscussionList'+(this.props.params.q ? ' DiscussionList--searchResults' : '')}> <div className={'DiscussionList' + (this.props.params.q ? ' DiscussionList--searchResults' : '')}>
<ul className="DiscussionList-discussions"> <ul className="DiscussionList-discussions">
{this.discussions.map(discussion => { {this.discussions.map((discussion) => {
return ( return (
<li key={discussion.id()} data-id={discussion.id()}> <li key={discussion.id()} data-id={discussion.id()}>
{DiscussionListItem.component({discussion, params})} {DiscussionListItem.component({ discussion, params })}
</li> </li>
); );
})} })}
</ul> </ul>
<div className="DiscussionList-loadMore"> <div className="DiscussionList-loadMore">{loading}</div>
{loading}
</div>
</div> </div>
); );
} }
@ -87,7 +81,7 @@ export default class DiscussionList extends Component {
* @api * @api
*/ */
requestParams() { requestParams() {
const params = {include: ['user', 'lastPostedUser'], filter: {}}; const params = { include: ['user', 'lastPostedUser'], filter: {} };
params.sort = this.sortMap()[this.props.params.sort]; params.sort = this.sortMap()[this.props.params.sort];
@ -132,7 +126,7 @@ export default class DiscussionList extends Component {
} }
return this.loadResults().then( return this.loadResults().then(
results => { (results) => {
this.discussions = []; this.discussions = [];
this.parseResults(results); this.parseResults(results);
}, },
@ -157,7 +151,7 @@ export default class DiscussionList extends Component {
} }
const params = this.requestParams(); const params = this.requestParams();
params.page = {offset}; params.page = { offset };
params.include = params.include.join(','); params.include = params.include.join(',');
return app.store.find('discussions', params); return app.store.find('discussions', params);
@ -171,8 +165,7 @@ export default class DiscussionList extends Component {
loadMore() { loadMore() {
this.loading = true; this.loading = true;
this.loadResults(this.discussions.length) this.loadResults(this.discussions.length).then(this.parseResults.bind(this));
.then(this.parseResults.bind(this));
} }
/** /**

View File

@ -47,8 +47,8 @@ export default class DiscussionListItem extends Component {
className: classList([ className: classList([
'DiscussionListItem', 'DiscussionListItem',
this.active() ? 'active' : '', this.active() ? 'active' : '',
this.props.discussion.isHidden() ? 'DiscussionListItem--hidden' : '' this.props.discussion.isHidden() ? 'DiscussionListItem--hidden' : '',
]) ]),
}; };
} }
@ -73,50 +73,56 @@ export default class DiscussionListItem extends Component {
} }
const phrase = this.props.params.q; const phrase = this.props.params.q;
this.highlightRegExp = new RegExp(phrase+'|'+phrase.trim().replace(/\s+/g, '|'), 'gi'); this.highlightRegExp = new RegExp(phrase + '|' + phrase.trim().replace(/\s+/g, '|'), 'gi');
} else { } else {
jumpTo = Math.min(discussion.lastPostNumber(), (discussion.lastReadPostNumber() || 0) + 1); jumpTo = Math.min(discussion.lastPostNumber(), (discussion.lastReadPostNumber() || 0) + 1);
} }
return ( return (
<div {...attrs}> <div {...attrs}>
{controls.length ? Dropdown.component({ {controls.length
? Dropdown.component({
icon: 'fas fa-ellipsis-v', icon: 'fas fa-ellipsis-v',
children: controls, children: controls,
className: 'DiscussionListItem-controls', className: 'DiscussionListItem-controls',
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right' buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right',
}) : ''} })
: ''}
<a className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')} <a
onclick={this.markAsRead.bind(this)}> className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')}
onclick={this.markAsRead.bind(this)}
>
{icon('fas fa-check')} {icon('fas fa-check')}
</a> </a>
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '') + (isRead ? ' read' : '')}> <div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '') + (isRead ? ' read' : '')}>
<a href={user ? app.route.user(user) : '#'} <a
href={user ? app.route.user(user) : '#'}
className="DiscussionListItem-author" className="DiscussionListItem-author"
title={extractText(app.translator.trans('core.forum.discussion_list.started_text', {user: user, ago: humanTime(discussion.createdAt())}))} title={extractText(
config={function(element) { app.translator.trans('core.forum.discussion_list.started_text', { user: user, ago: humanTime(discussion.createdAt()) })
$(element).tooltip({placement: 'right'}); )}
config={function (element) {
$(element).tooltip({ placement: 'right' });
m.route.apply(this, arguments); m.route.apply(this, arguments);
}}> }}
{avatar(user, {title: ''})} >
{avatar(user, { title: '' })}
</a> </a>
<ul className="DiscussionListItem-badges badges"> <ul className="DiscussionListItem-badges badges">{listItems(discussion.badges().toArray())}</ul>
{listItems(discussion.badges().toArray())}
</ul>
<a href={app.route.discussion(discussion, jumpTo)} <a href={app.route.discussion(discussion, jumpTo)} config={m.route} className="DiscussionListItem-main">
config={m.route}
className="DiscussionListItem-main">
<h3 className="DiscussionListItem-title">{highlight(discussion.title(), this.highlightRegExp)}</h3> <h3 className="DiscussionListItem-title">{highlight(discussion.title(), this.highlightRegExp)}</h3>
<ul className="DiscussionListItem-info">{listItems(this.infoItems().toArray())}</ul> <ul className="DiscussionListItem-info">{listItems(this.infoItems().toArray())}</ul>
</a> </a>
<span className="DiscussionListItem-count" <span
className="DiscussionListItem-count"
onclick={this.markAsRead.bind(this)} onclick={this.markAsRead.bind(this)}
title={showUnread ? app.translator.trans('core.forum.discussion_list.mark_as_read_tooltip') : ''}> title={showUnread ? app.translator.trans('core.forum.discussion_list.mark_as_read_tooltip') : ''}
>
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'replyCount']())} {abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'replyCount']())}
</span> </span>
</div> </div>
@ -133,8 +139,7 @@ export default class DiscussionListItem extends Component {
if ('ontouchstart' in window) { if ('ontouchstart' in window) {
const slidableInstance = slidable(this.$().addClass('Slidable')); const slidableInstance = slidable(this.$().addClass('Slidable'));
this.$('.DiscussionListItem-controls') this.$('.DiscussionListItem-controls').on('hidden.bs.dropdown', () => slidableInstance.reset());
.on('hidden.bs.dropdown', () => slidableInstance.reset());
} }
} }
@ -177,7 +182,7 @@ export default class DiscussionListItem extends Component {
const discussion = this.props.discussion; const discussion = this.props.discussion;
if (discussion.isUnread()) { if (discussion.isUnread()) {
discussion.save({lastReadPostNumber: discussion.lastPostNumber()}); discussion.save({ lastReadPostNumber: discussion.lastPostNumber() });
m.redraw(); m.redraw();
} }
} }
@ -199,10 +204,11 @@ export default class DiscussionListItem extends Component {
items.add('excerpt', excerpt, -100); items.add('excerpt', excerpt, -100);
} }
} else { } else {
items.add('terminalPost', items.add(
'terminalPost',
TerminalPost.component({ TerminalPost.component({
discussion: this.props.discussion, discussion: this.props.discussion,
lastPost: !this.showFirstPost() lastPost: !this.showFirstPost(),
}) })
); );
} }

View File

@ -90,26 +90,26 @@ export default class DiscussionPage extends Page {
return ( return (
<div className="DiscussionPage"> <div className="DiscussionPage">
{app.cache.discussionList {app.cache.discussionList ? (
? <div className="DiscussionPage-list" config={this.configPane.bind(this)}> <div className="DiscussionPage-list" config={this.configPane.bind(this)}>
{!$('.App-navigation').is(':visible') ? app.cache.discussionList.render() : ''} {!$('.App-navigation').is(':visible') ? app.cache.discussionList.render() : ''}
</div> </div>
: ''} ) : (
''
)}
<div className="DiscussionPage-discussion"> <div className="DiscussionPage-discussion">
{discussion {discussion
? [ ? [
DiscussionHero.component({discussion}), DiscussionHero.component({ discussion }),
<div className="container"> <div className="container">
<nav className="DiscussionPage-nav"> <nav className="DiscussionPage-nav">
<ul>{listItems(this.sidebarItems().toArray())}</ul> <ul>{listItems(this.sidebarItems().toArray())}</ul>
</nav> </nav>
<div className="DiscussionPage-stream"> <div className="DiscussionPage-stream">{this.stream.render()}</div>
{this.stream.render()} </div>,
</div>
</div>
] ]
: LoadingIndicator.component({className: 'LoadingIndicator--block'})} : LoadingIndicator.component({ className: 'LoadingIndicator--block' })}
</div> </div>
</div> </div>
); );
@ -140,8 +140,7 @@ export default class DiscussionPage extends Page {
} else { } else {
const params = this.requestParams(); const params = this.requestParams();
app.store.find('discussions', m.route.param('id').split('-')[0], params) app.store.find('discussions', m.route.param('id').split('-')[0], params).then(this.show.bind(this));
.then(this.show.bind(this));
} }
m.lazyRedraw(); m.lazyRedraw();
@ -155,7 +154,7 @@ export default class DiscussionPage extends Page {
*/ */
requestParams() { requestParams() {
return { return {
page: {near: this.near} page: { near: this.near },
}; };
} }
@ -182,11 +181,14 @@ export default class DiscussionPage extends Page {
const discussionId = discussion.id(); const discussionId = discussion.id();
includedPosts = discussion.payload.included includedPosts = discussion.payload.included
.filter(record => record.type === 'posts' .filter(
&& record.relationships (record) =>
&& record.relationships.discussion record.type === 'posts' &&
&& record.relationships.discussion.data.id === discussionId) record.relationships &&
.map(record => app.store.getById('posts', record.id)) record.relationships.discussion &&
record.relationships.discussion.data.id === discussionId
)
.map((record) => app.store.getById('posts', record.id))
.sort((a, b) => a.id() - b.id()) .sort((a, b) => a.id() - b.id())
.slice(0, 20); .slice(0, 20);
} }
@ -194,7 +196,7 @@ export default class DiscussionPage extends Page {
// Set up the post stream for this discussion, along with the first page of // Set up the post stream for this discussion, along with the first page of
// posts we want to display. Tell the stream to scroll down and highlight // posts we want to display. Tell the stream to scroll down and highlight
// the specific post that was routed to. // the specific post that was routed to.
this.stream = new PostStream({discussion, includedPosts}); this.stream = new PostStream({ discussion, includedPosts });
this.stream.on('positionChanged', this.positionChanged.bind(this)); this.stream.on('positionChanged', this.positionChanged.bind(this));
this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true); this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true);
} }
@ -219,7 +221,7 @@ export default class DiscussionPage extends Page {
const pane = app.pane; const pane = app.pane;
$list.hover(pane.show.bind(pane), pane.onmouseleave.bind(pane)); $list.hover(pane.show.bind(pane), pane.onmouseleave.bind(pane));
const hotEdge = e => { const hotEdge = (e) => {
if (e.pageX < 10) pane.show(); if (e.pageX < 10) pane.show();
}; };
$(document).on('mousemove', hotEdge); $(document).on('mousemove', hotEdge);
@ -249,19 +251,21 @@ export default class DiscussionPage extends Page {
sidebarItems() { sidebarItems() {
const items = new ItemList(); const items = new ItemList();
items.add('controls', items.add(
'controls',
SplitDropdown.component({ SplitDropdown.component({
children: DiscussionControls.controls(this.discussion, this).toArray(), children: DiscussionControls.controls(this.discussion, this).toArray(),
icon: 'fas fa-ellipsis-v', icon: 'fas fa-ellipsis-v',
className: 'App-primaryControl', className: 'App-primaryControl',
buttonClassName: 'Button--primary' buttonClassName: 'Button--primary',
}) })
); );
items.add('scrubber', items.add(
'scrubber',
PostStreamScrubber.component({ PostStreamScrubber.component({
stream: this.stream, stream: this.stream,
className: 'App-titleControl' className: 'App-titleControl',
}), }),
-100 -100
); );
@ -281,7 +285,7 @@ export default class DiscussionPage extends Page {
// Construct a URL to this discussion with the updated position, then // Construct a URL to this discussion with the updated position, then
// replace it into the window's history and our own history stack. // replace it into the window's history and our own history stack.
const url = app.route.discussion(discussion, this.near = startNumber); const url = app.route.discussion(discussion, (this.near = startNumber));
m.route(url, true); m.route(url, true);
window.history.replaceState(null, document.title, url); window.history.replaceState(null, document.title, url);
@ -291,7 +295,7 @@ export default class DiscussionPage extends Page {
// If the user hasn't read past here before, then we'll update their read // If the user hasn't read past here before, then we'll update their read
// state and redraw. // state and redraw.
if (app.session.user && endNumber > (discussion.lastReadPostNumber() || 0)) { if (app.session.user && endNumber > (discussion.lastReadPostNumber() || 0)) {
discussion.save({lastReadPostNumber: endNumber}); discussion.save({ lastReadPostNumber: endNumber });
m.redraw(); m.redraw();
} }
} }

View File

@ -20,6 +20,6 @@ export default class DiscussionRenamedNotification extends Notification {
} }
content() { content() {
return app.translator.trans('core.forum.notifications.discussion_renamed_text', {user: this.props.notification.fromUser()}); return app.translator.trans('core.forum.notifications.discussion_renamed_text', { user: this.props.notification.fromUser() });
} }
} }

View File

@ -27,8 +27,8 @@ export default class DiscussionRenamedPost extends EventPost {
const newTitle = post.content()[1]; const newTitle = post.content()[1];
return { return {
'old': oldTitle, old: oldTitle,
'new': <strong className="DiscussionRenamedPost-new">{newTitle}</strong> new: <strong className="DiscussionRenamedPost-new">{newTitle}</strong>,
}; };
} }
} }

View File

@ -18,12 +18,12 @@ export default class DiscussionsSearchSource {
this.results[query] = []; this.results[query] = [];
const params = { const params = {
filter: {q: query}, filter: { q: query },
page: {limit: 3}, page: { limit: 3 },
include: 'mostRelevantPost' include: 'mostRelevantPost',
}; };
return app.store.find('discussions', params).then(results => this.results[query] = results); return app.store.find('discussions', params).then((results) => (this.results[query] = results));
} }
view(query) { view(query) {
@ -36,11 +36,11 @@ export default class DiscussionsSearchSource {
<li> <li>
{LinkButton.component({ {LinkButton.component({
icon: 'fas fa-search', icon: 'fas fa-search',
children: app.translator.trans('core.forum.search.all_discussions_button', {query}), children: app.translator.trans('core.forum.search.all_discussions_button', { query }),
href: app.route('index', {q: query}) href: app.route('index', { q: query }),
})} })}
</li>, </li>,
results.map(discussion => { results.map((discussion) => {
const mostRelevantPost = discussion.mostRelevantPost(); const mostRelevantPost = discussion.mostRelevantPost();
return ( return (
@ -51,7 +51,7 @@ export default class DiscussionsSearchSource {
</a> </a>
</li> </li>
); );
}) }),
]; ];
} }
} }

View File

@ -18,8 +18,8 @@ export default class DiscussionsUserPage extends UserPage {
{DiscussionList.component({ {DiscussionList.component({
params: { params: {
q: 'author:' + this.user.username(), q: 'author:' + this.user.username(),
sort: 'newest' sort: 'newest',
} },
})} })}
</div> </div>
); );

View File

@ -22,7 +22,7 @@ export default class EditPostComposer extends ComposerBody {
init() { init() {
super.init(); super.init();
this.editor.props.preview = e => { this.editor.props.preview = (e) => {
minimizeComposerIfFullScreen(e); minimizeComposerIfFullScreen(e);
m.route(app.route.post(this.props.post)); m.route(app.route.post(this.props.post));
@ -44,20 +44,21 @@ export default class EditPostComposer extends ComposerBody {
const items = super.headerItems(); const items = super.headerItems();
const post = this.props.post; const post = this.props.post;
const routeAndMinimize = function(element, isInitialized) { const routeAndMinimize = function (element, isInitialized) {
if (isInitialized) return; if (isInitialized) return;
$(element).on('click', minimizeComposerIfFullScreen); $(element).on('click', minimizeComposerIfFullScreen);
m.route.apply(this, arguments); m.route.apply(this, arguments);
}; };
items.add('title', ( items.add(
'title',
<h3> <h3>
{icon('fas fa-pencil-alt')} {' '} {icon('fas fa-pencil-alt')}{' '}
<a href={app.route.discussion(post.discussion(), post.number())} config={routeAndMinimize}> <a href={app.route.discussion(post.discussion(), post.number())} config={routeAndMinimize}>
{app.translator.trans('core.forum.composer_edit.post_link', {number: post.number(), discussion: post.discussion().title()})} {app.translator.trans('core.forum.composer_edit.post_link', { number: post.number(), discussion: post.discussion().title() })}
</a> </a>
</h3> </h3>
)); );
return items; return items;
} }
@ -69,7 +70,7 @@ export default class EditPostComposer extends ComposerBody {
*/ */
data() { data() {
return { return {
content: this.content() content: this.content(),
}; };
} }
@ -78,9 +79,6 @@ export default class EditPostComposer extends ComposerBody {
const data = this.data(); const data = this.data();
this.props.post.save(data).then( this.props.post.save(data).then(() => app.composer.hide(), this.loaded.bind(this));
() => app.composer.hide(),
this.loaded.bind(this)
);
} }
} }

View File

@ -21,9 +21,10 @@ export default class EditUserModal extends Modal {
this.password = m.prop(user.password() || ''); this.password = m.prop(user.password() || '');
this.groups = {}; this.groups = {};
app.store.all('groups') app.store
.filter(group => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1) .all('groups')
.forEach(group => this.groups[group.id()] = m.prop(user.groups().indexOf(group) !== -1)); .filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
.forEach((group) => (this.groups[group.id()] = m.prop(user.groups().indexOf(group) !== -1)));
} }
className() { className() {
@ -37,9 +38,7 @@ export default class EditUserModal extends Modal {
content() { content() {
return ( return (
<div className="Modal-body"> <div className="Modal-body">
<div className="Form"> <div className="Form">{this.fields().toArray()}</div>
{this.fields().toArray()}
</div>
</div> </div>
); );
} }
@ -47,19 +46,22 @@ export default class EditUserModal extends Modal {
fields() { fields() {
const items = new ItemList(); const items = new ItemList();
items.add('username', <div className="Form-group"> items.add(
'username',
<div className="Form-group">
<label>{app.translator.trans('core.forum.edit_user.username_heading')}</label> <label>{app.translator.trans('core.forum.edit_user.username_heading')}</label>
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.username_label'))} <input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.username_label'))} bidi={this.username} />
bidi={this.username} /> </div>,
</div>, 40); 40
);
if (app.session.user !== this.props.user) { if (app.session.user !== this.props.user) {
items.add('email', <div className="Form-group"> items.add(
'email',
<div className="Form-group">
<label>{app.translator.trans('core.forum.edit_user.email_heading')}</label> <label>{app.translator.trans('core.forum.edit_user.email_heading')}</label>
<div> <div>
<input className="FormControl" <input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.email_label'))} bidi={this.email} />
placeholder={extractText(app.translator.trans('core.forum.edit_user.email_label'))}
bidi={this.email}/>
</div> </div>
{!this.isEmailConfirmed() ? ( {!this.isEmailConfirmed() ? (
<div> <div>
@ -67,58 +69,84 @@ export default class EditUserModal extends Modal {
className: 'Button Button--block', className: 'Button Button--block',
children: app.translator.trans('core.forum.edit_user.activate_button'), children: app.translator.trans('core.forum.edit_user.activate_button'),
loading: this.loading, loading: this.loading,
onclick: this.activate.bind(this) onclick: this.activate.bind(this),
})} })}
</div> </div>
) : ''} ) : (
</div>, 30); ''
)}
</div>,
30
);
items.add('password', <div className="Form-group"> items.add(
'password',
<div className="Form-group">
<label>{app.translator.trans('core.forum.edit_user.password_heading')}</label> <label>{app.translator.trans('core.forum.edit_user.password_heading')}</label>
<div> <div>
<label className="checkbox"> <label className="checkbox">
<input type="checkbox" onchange={e => { <input
type="checkbox"
onchange={(e) => {
this.setPassword(e.target.checked); this.setPassword(e.target.checked);
m.redraw(true); m.redraw(true);
if (e.target.checked) this.$('[name=password]').select(); if (e.target.checked) this.$('[name=password]').select();
m.redraw.strategy('none'); m.redraw.strategy('none');
}}/> }}
/>
{app.translator.trans('core.forum.edit_user.set_password_label')} {app.translator.trans('core.forum.edit_user.set_password_label')}
</label> </label>
{this.setPassword() ? ( {this.setPassword() ? (
<input className="FormControl" type="password" name="password" <input
className="FormControl"
type="password"
name="password"
placeholder={extractText(app.translator.trans('core.forum.edit_user.password_label'))} placeholder={extractText(app.translator.trans('core.forum.edit_user.password_label'))}
bidi={this.password}/> bidi={this.password}
) : ''} />
) : (
''
)}
</div> </div>
</div>, 20); </div>,
20
);
} }
items.add('groups', <div className="Form-group EditUserModal-groups"> items.add(
'groups',
<div className="Form-group EditUserModal-groups">
<label>{app.translator.trans('core.forum.edit_user.groups_heading')}</label> <label>{app.translator.trans('core.forum.edit_user.groups_heading')}</label>
<div> <div>
{Object.keys(this.groups) {Object.keys(this.groups)
.map(id => app.store.getById('groups', id)) .map((id) => app.store.getById('groups', id))
.map(group => ( .map((group) => (
<label className="checkbox"> <label className="checkbox">
<input type="checkbox" <input
type="checkbox"
bidi={this.groups[group.id()]} bidi={this.groups[group.id()]}
disabled={this.props.user.id() === '1' && group.id() === Group.ADMINISTRATOR_ID} /> disabled={this.props.user.id() === '1' && group.id() === Group.ADMINISTRATOR_ID}
{GroupBadge.component({group, label: ''})} {group.nameSingular()} />
{GroupBadge.component({ group, label: '' })} {group.nameSingular()}
</label> </label>
))} ))}
</div> </div>
</div>, 10); </div>,
10
);
items.add('submit', <div className="Form-group"> items.add(
'submit',
<div className="Form-group">
{Button.component({ {Button.component({
className: 'Button Button--primary', className: 'Button Button--primary',
type: 'submit', type: 'submit',
loading: this.loading, loading: this.loading,
children: app.translator.trans('core.forum.edit_user.submit_button') children: app.translator.trans('core.forum.edit_user.submit_button'),
})} })}
</div>, -10); </div>,
-10
);
return items; return items;
} }
@ -129,7 +157,8 @@ export default class EditUserModal extends Modal {
username: this.username(), username: this.username(),
isEmailConfirmed: true, isEmailConfirmed: true,
}; };
this.props.user.save(data, {errorHandler: this.onerror.bind(this)}) this.props.user
.save(data, { errorHandler: this.onerror.bind(this) })
.then(() => { .then(() => {
this.isEmailConfirmed(true); this.isEmailConfirmed(true);
this.loading = false; this.loading = false;
@ -143,12 +172,12 @@ export default class EditUserModal extends Modal {
data() { data() {
const groups = Object.keys(this.groups) const groups = Object.keys(this.groups)
.filter(id => this.groups[id]()) .filter((id) => this.groups[id]())
.map(id => app.store.getById('groups', id)); .map((id) => app.store.getById('groups', id));
const data = { const data = {
username: this.username(), username: this.username(),
relationships: {groups} relationships: { groups },
}; };
if (app.session.user !== this.props.user) { if (app.session.user !== this.props.user) {
@ -167,7 +196,8 @@ export default class EditUserModal extends Modal {
this.loading = true; this.loading = true;
this.props.user.save(this.data(), {errorHandler: this.onerror.bind(this)}) this.props.user
.save(this.data(), { errorHandler: this.onerror.bind(this) })
.then(this.hide.bind(this)) .then(this.hide.bind(this))
.catch(() => { .catch(() => {
this.loading = false; this.loading = false;

View File

@ -28,17 +28,16 @@ export default class EventPost extends Post {
const username = usernameHelper(user); const username = usernameHelper(user);
const data = Object.assign(this.descriptionData(), { const data = Object.assign(this.descriptionData(), {
user, user,
username: user username: user ? (
? <a className="EventPost-user" href={app.route.user(user)} config={m.route}>{username}</a> <a className="EventPost-user" href={app.route.user(user)} config={m.route}>
: username {username}
</a>
) : (
username
),
}); });
return super.content().concat([ return super.content().concat([icon(this.icon(), { className: 'EventPost-icon' }), <div class="EventPost-info">{this.description(data)}</div>]);
icon(this.icon(), {className: 'EventPost-icon'}),
<div class="EventPost-info">
{this.description(data)}
</div>
]);
} }
/** /**

View File

@ -59,17 +59,22 @@ export default class ForgotPasswordModal extends Modal {
<div className="Form Form--centered"> <div className="Form Form--centered">
<p className="helpText">{app.translator.trans('core.forum.forgot_password.text')}</p> <p className="helpText">{app.translator.trans('core.forum.forgot_password.text')}</p>
<div className="Form-group"> <div className="Form-group">
<input className="FormControl" name="email" type="email" placeholder={extractText(app.translator.trans('core.forum.forgot_password.email_placeholder'))} <input
className="FormControl"
name="email"
type="email"
placeholder={extractText(app.translator.trans('core.forum.forgot_password.email_placeholder'))}
value={this.email()} value={this.email()}
onchange={m.withAttr('value', this.email)} onchange={m.withAttr('value', this.email)}
disabled={this.loading} /> disabled={this.loading}
/>
</div> </div>
<div className="Form-group"> <div className="Form-group">
{Button.component({ {Button.component({
className: 'Button Button--primary Button--block', className: 'Button Button--primary Button--block',
type: 'submit', type: 'submit',
loading: this.loading, loading: this.loading,
children: app.translator.trans('core.forum.forgot_password.submit_button') children: app.translator.trans('core.forum.forgot_password.submit_button'),
})} })}
</div> </div>
</div> </div>
@ -82,11 +87,12 @@ export default class ForgotPasswordModal extends Modal {
this.loading = true; this.loading = true;
app.request({ app
.request({
method: 'POST', method: 'POST',
url: app.forum.attribute('apiUrl') + '/forgot', url: app.forum.attribute('apiUrl') + '/forgot',
data: {email: this.email()}, data: { email: this.email() },
errorHandler: this.onerror.bind(this) errorHandler: this.onerror.bind(this),
}) })
.then(() => { .then(() => {
this.success = true; this.success = true;

View File

@ -8,11 +8,7 @@ import listItems from '../../common/helpers/listItems';
*/ */
export default class HeaderPrimary extends Component { export default class HeaderPrimary extends Component {
view() { view() {
return ( return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
<ul className="Header-controls">
{listItems(this.items().toArray())}
</ul>
);
} }
config(isInitialized, context) { config(isInitialized, context) {

Some files were not shown because too many files have changed in this diff Show More