1
0
mirror of https://github.com/flarum/core.git synced 2025-07-12 12:26:23 +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

@ -14,7 +14,7 @@ export default class AdminApplication extends Application {
backUrl: () => this.forum.attribute('baseUrl'), backUrl: () => this.forum.attribute('baseUrl'),
back: function () { back: function () {
window.location = this.backUrl(); window.location = this.backUrl();
} },
}; };
constructor() { constructor() {
@ -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,7 +22,9 @@ 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>
{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> <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')}
</div>
<UploadImageButton name="logo" /> <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')}
</div>
<UploadImageButton name="favicon" /> <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,9 +5,7 @@ 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>
); );
} }

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

@ -14,7 +14,7 @@ export default class EditCustomFooterModal extends SettingsModal {
<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

@ -14,7 +14,7 @@ export default class EditCustomHeaderModal extends SettingsModal {
<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,10 +42,13 @@ 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)} />{' '}
@ -55,7 +58,8 @@ export default class ExtensionsPage extends Page {
<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

@ -23,12 +23,14 @@ export default class MailPage extends Page {
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

@ -12,18 +12,17 @@ function badgeForId(id) {
} }
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);
@ -56,10 +55,7 @@ export default class PermissionDropdown extends Dropdown {
} 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) {
@ -69,7 +65,7 @@ export default class PermissionDropdown extends Dropdown {
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),
}) })
); );
} }
@ -79,7 +75,7 @@ export default class PermissionDropdown extends Dropdown {
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,25 +22,30 @@ 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>
@ -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: () => {
@ -142,11 +178,13 @@ export default class PermissionGrid extends Component {
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: () => {
@ -174,11 +218,13 @@ export default class PermissionGrid extends Component {
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,14 +11,15 @@ 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)
.map((group) => (
<button className="Button Group" onclick={() => app.modal.show(new EditGroupModal({ 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>
@ -31,9 +32,7 @@ export default class PermissionsPage extends Page {
</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

@ -18,7 +18,7 @@ export default class SettingDropdown extends SelectDropdown {
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,25 +20,23 @@ 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]);
@ -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

@ -12,11 +12,11 @@ import MailPage from './components/MailPage';
*/ */
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);
@ -158,21 +155,15 @@ export default class Application {
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;
} }

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.
@ -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,7 +198,7 @@ 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
@ -220,6 +217,5 @@ export default class Component {
* @param {Object} props * @param {Object} props
* @public * @public
*/ */
static initProps(props) { static initProps(props) {}
}
} }

View File

@ -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)
}; };
} }
@ -157,15 +155,22 @@ export default class Model {
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);
}); });
@ -272,7 +284,7 @@ export default class Model {
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) {
@ -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,11 +24,7 @@ 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) {

View File

@ -64,7 +64,7 @@ export default class Button extends Component {
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>
); );
@ -117,15 +108,11 @@ export default class Dropdown extends Component {
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

@ -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

@ -22,12 +22,12 @@ export default class Navigation extends Component {
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>
); );
} }
@ -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();
} },
}); });
} }
@ -77,7 +77,7 @@ export default class Navigation extends Component {
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',
}); });
} }
@ -94,13 +94,12 @@ export default class Navigation extends Component {
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

@ -16,8 +16,15 @@ export default class Select extends Component {
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

@ -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}
data-toggle="dropdown">
{icon(this.props.icon, { className: 'Button-icon' })} {icon(this.props.icon, { className: 'Button-icon' })}
{icon('fas fa-caret-down', { className: 'Button-caret' })} {icon('fas fa-caret-down', { className: 'Button-caret' })}
</button> </button>,
]; ];
} }

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

@ -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

@ -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'),
@ -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

@ -67,7 +67,7 @@ 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 }));
}); });
} }
@ -107,5 +107,5 @@ 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

@ -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

@ -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

@ -19,7 +19,7 @@ export default function computed(...dependentKeys) {
// 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));
}, },
/** /**
@ -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

@ -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

@ -7,7 +7,7 @@ export default function patchMithril(global) {
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,7 +29,7 @@ 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).

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,
}; };
/** /**
@ -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

@ -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>
]); ]);
} }
@ -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;
@ -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;
@ -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,8 +121,9 @@ 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')
.bind('dragstart mousedown', (e) => e.preventDefault())
.mousedown(function (e) { .mousedown(function (e) {
composer.mouseStart = e.clientY; composer.mouseStart = e.clientY;
composer.heightStart = composer.$().height(); composer.heightStart = composer.$().height();
@ -191,13 +192,10 @@ 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 });
} }
@ -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');
} }
/** /**
@ -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(),
}); });
} }
@ -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,23 +48,19 @@ 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 })}
@ -72,9 +68,7 @@ export default class DiscussionList extends Component {
); );
})} })}
</ul> </ul>
<div className="DiscussionList-loadMore"> <div className="DiscussionList-loadMore">{loading}</div>
{loading}
</div>
</div> </div>
); );
} }
@ -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);
}, },
@ -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' : '',
]) ]),
}; };
} }
@ -80,43 +80,49 @@ export default class DiscussionListItem extends Component {
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(
app.translator.trans('core.forum.discussion_list.started_text', { user: user, ago: humanTime(discussion.createdAt()) })
)}
config={function (element) { config={function (element) {
$(element).tooltip({ placement: 'right' }); $(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());
} }
} }
@ -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,11 +90,13 @@ 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
@ -104,10 +106,8 @@ export default class DiscussionPage extends Page {
<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>
@ -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);
} }
@ -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);

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

@ -20,10 +20,10 @@ export default class DiscussionsSearchSource {
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) {
@ -37,10 +37,10 @@ export default class DiscussionsSearchSource {
{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));
@ -50,14 +50,15 @@ export default class EditPostComposer extends ComposerBody {
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) {

View File

@ -15,11 +15,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) {
@ -39,11 +35,12 @@ export default class HeaderSecondary extends Component {
items.add('search', app.search.render(), 30); items.add('search', app.search.render(), 30);
if (app.forum.attribute("showLanguageSelector") && Object.keys(app.data.locales).length > 1) { if (app.forum.attribute('showLanguageSelector') && Object.keys(app.data.locales).length > 1) {
const locales = []; const locales = [];
for (const locale in app.data.locales) { for (const locale in app.data.locales) {
locales.push(Button.component({ locales.push(
Button.component({
active: app.data.locale === locale, active: app.data.locale === locale,
children: app.data.locales[locale], children: app.data.locales[locale],
icon: app.data.locale === locale ? 'fas fa-check' : true, icon: app.data.locale === locale ? 'fas fa-check' : true,
@ -54,14 +51,19 @@ export default class HeaderSecondary extends Component {
document.cookie = `locale=${locale}; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT`; document.cookie = `locale=${locale}; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT`;
window.location.reload(); window.location.reload();
} }
} },
})); })
);
} }
items.add('locale', SelectDropdown.component({ items.add(
'locale',
SelectDropdown.component({
children: locales, children: locales,
buttonClassName: 'Button Button--link' buttonClassName: 'Button Button--link',
}), 20); }),
20
);
} }
if (app.session.user) { if (app.session.user) {
@ -69,21 +71,25 @@ export default class HeaderSecondary extends Component {
items.add('session', SessionDropdown.component(), 0); items.add('session', SessionDropdown.component(), 0);
} else { } else {
if (app.forum.attribute('allowSignUp')) { if (app.forum.attribute('allowSignUp')) {
items.add('signUp', items.add(
'signUp',
Button.component({ Button.component({
children: app.translator.trans('core.forum.header.sign_up_link'), children: app.translator.trans('core.forum.header.sign_up_link'),
className: 'Button Button--link', className: 'Button Button--link',
onclick: () => app.modal.show(new SignUpModal()) onclick: () => app.modal.show(new SignUpModal()),
}), 10 }),
10
); );
} }
items.add('logIn', items.add(
'logIn',
Button.component({ Button.component({
children: app.translator.trans('core.forum.header.log_in_link'), children: app.translator.trans('core.forum.header.log_in_link'),
className: 'Button Button--link', className: 'Button Button--link',
onclick: () => app.modal.show(new LogInModal()) onclick: () => app.modal.show(new LogInModal()),
}), 0 }),
0
); );
} }

View File

@ -43,7 +43,7 @@ export default class IndexPage extends Page {
// are currently present in the cached discussion list. If they differ, we // are currently present in the cached discussion list. If they differ, we
// will clear the cache and set up a new discussion list component with // will clear the cache and set up a new discussion list component with
// the new parameters. // the new parameters.
Object.keys(params).some(key => { Object.keys(params).some((key) => {
if (app.cache.discussionList.props.params[key] !== params[key]) { if (app.cache.discussionList.props.params[key] !== params[key]) {
app.cache.discussionList = null; app.cache.discussionList = null;
return true; return true;
@ -102,7 +102,7 @@ export default class IndexPage extends Page {
// previous hero. Maintain the same scroll position relative to the bottom // previous hero. Maintain the same scroll position relative to the bottom
// of the hero so that the sidebar doesn't jump around. // of the hero so that the sidebar doesn't jump around.
const oldHeroHeight = app.cache.heroHeight; const oldHeroHeight = app.cache.heroHeight;
const heroHeight = app.cache.heroHeight = this.$('.Hero').outerHeight() || 0; const heroHeight = (app.cache.heroHeight = this.$('.Hero').outerHeight() || 0);
const scrollTop = app.cache.scrollTop; const scrollTop = app.cache.scrollTop;
$('#app').css('min-height', $(window).height() + heroHeight); $('#app').css('min-height', $(window).height() + heroHeight);
@ -153,22 +153,26 @@ export default class IndexPage extends Page {
const items = new ItemList(); const items = new ItemList();
const canStartDiscussion = app.forum.attribute('canStartDiscussion') || !app.session.user; const canStartDiscussion = app.forum.attribute('canStartDiscussion') || !app.session.user;
items.add('newDiscussion', items.add(
'newDiscussion',
Button.component({ Button.component({
children: app.translator.trans(canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button'), children: app.translator.trans(
canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button'
),
icon: 'fas fa-edit', icon: 'fas fa-edit',
className: 'Button Button--primary IndexPage-newDiscussion', className: 'Button Button--primary IndexPage-newDiscussion',
itemClassName: 'App-primaryControl', itemClassName: 'App-primaryControl',
onclick: this.newDiscussionAction.bind(this), onclick: this.newDiscussionAction.bind(this),
disabled: !canStartDiscussion disabled: !canStartDiscussion,
}) })
); );
items.add('nav', items.add(
'nav',
SelectDropdown.component({ SelectDropdown.component({
children: this.navItems(this).toArray(), children: this.navItems(this).toArray(),
buttonClassName: 'Button', buttonClassName: 'Button',
className: 'App-titleControl' className: 'App-titleControl',
}) })
); );
@ -185,11 +189,12 @@ export default class IndexPage extends Page {
const items = new ItemList(); const items = new ItemList();
const params = this.stickyParams(); const params = this.stickyParams();
items.add('allDiscussions', items.add(
'allDiscussions',
LinkButton.component({ LinkButton.component({
href: app.route('index', params), href: app.route('index', params),
children: app.translator.trans('core.forum.index.all_discussions_link'), children: app.translator.trans('core.forum.index.all_discussions_link'),
icon: 'far fa-comments' icon: 'far fa-comments',
}), }),
100 100
); );
@ -213,11 +218,12 @@ export default class IndexPage extends Page {
sortOptions[i] = app.translator.trans('core.forum.index_sort.' + i + '_button'); sortOptions[i] = app.translator.trans('core.forum.index_sort.' + i + '_button');
} }
items.add('sort', items.add(
'sort',
Dropdown.component({ Dropdown.component({
buttonClassName: 'Button', buttonClassName: 'Button',
label: sortOptions[this.params().sort] || Object.keys(sortMap).map(key => sortOptions[key])[0], label: sortOptions[this.params().sort] || Object.keys(sortMap).map((key) => sortOptions[key])[0],
children: Object.keys(sortOptions).map(value => { children: Object.keys(sortOptions).map((value) => {
const label = sortOptions[value]; const label = sortOptions[value];
const active = (this.params().sort || Object.keys(sortMap)[0]) === value; const active = (this.params().sort || Object.keys(sortMap)[0]) === value;
@ -226,7 +232,7 @@ export default class IndexPage extends Page {
icon: active ? 'fas fa-check' : true, icon: active ? 'fas fa-check' : true,
onclick: this.changeSort.bind(this, value), onclick: this.changeSort.bind(this, value),
active: active, active: active,
}) });
}), }),
}) })
); );
@ -243,7 +249,8 @@ export default class IndexPage extends Page {
actionItems() { actionItems() {
const items = new ItemList(); const items = new ItemList();
items.add('refresh', items.add(
'refresh',
Button.component({ Button.component({
title: app.translator.trans('core.forum.index.refresh_tooltip'), title: app.translator.trans('core.forum.index.refresh_tooltip'),
icon: 'fas fa-sync', icon: 'fas fa-sync',
@ -254,17 +261,18 @@ export default class IndexPage extends Page {
app.store.find('users', app.session.user.id()); app.store.find('users', app.session.user.id());
m.redraw(); m.redraw();
} }
} },
}) })
); );
if (app.session.user) { if (app.session.user) {
items.add('markAllAsRead', items.add(
'markAllAsRead',
Button.component({ Button.component({
title: app.translator.trans('core.forum.index.mark_all_as_read_tooltip'), title: app.translator.trans('core.forum.index.mark_all_as_read_tooltip'),
icon: 'fas fa-check', icon: 'fas fa-check',
className: 'Button Button--icon', className: 'Button Button--icon',
onclick: this.markAllAsRead.bind(this) onclick: this.markAllAsRead.bind(this),
}) })
); );
} }
@ -321,7 +329,7 @@ export default class IndexPage extends Page {
stickyParams() { stickyParams() {
return { return {
sort: m.route.param('sort'), sort: m.route.param('sort'),
q: m.route.param('q') q: m.route.param('q'),
}; };
} }

View File

@ -17,12 +17,15 @@ export default class LogInButton extends Button {
const height = 400; const height = 400;
const $window = $(window); const $window = $(window);
window.open(app.forum.attribute('baseUrl') + props.path, 'logInPopup', window.open(
app.forum.attribute('baseUrl') + props.path,
'logInPopup',
`width=${width},` + `width=${width},` +
`height=${height},` + `height=${height},` +
`top=${$window.height() / 2 - height / 2},` + `top=${$window.height() / 2 - height / 2},` +
`left=${$window.width() / 2 - width / 2},` + `left=${$window.width() / 2 - width / 2},` +
'status=no,scrollbars=yes,resizable=no'); 'status=no,scrollbars=yes,resizable=no'
);
}; };
super.initProps(props); super.initProps(props);

View File

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

View File

@ -49,58 +49,71 @@ export default class LogInModal extends Modal {
} }
content() { content() {
return [ return [<div className="Modal-body">{this.body()}</div>, <div className="Modal-footer">{this.footer()}</div>];
<div className="Modal-body">
{this.body()}
</div>,
<div className="Modal-footer">
{this.footer()}
</div>
];
} }
body() { body() {
return [ return [<LogInButtons />, <div className="Form Form--centered">{this.fields().toArray()}</div>];
<LogInButtons/>,
<div className="Form Form--centered">
{this.fields().toArray()}
</div>
];
} }
fields() { fields() {
const items = new ItemList(); const items = new ItemList();
items.add('identification', <div className="Form-group"> items.add(
<input className="FormControl" name="identification" type="text" placeholder={extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'))} 'identification',
<div className="Form-group">
<input
className="FormControl"
name="identification"
type="text"
placeholder={extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'))}
bidi={this.identification} bidi={this.identification}
disabled={this.loading} /> disabled={this.loading}
</div>, 30); />
</div>,
30
);
items.add('password', <div className="Form-group"> items.add(
<input className="FormControl" name="password" type="password" placeholder={extractText(app.translator.trans('core.forum.log_in.password_placeholder'))} 'password',
<div className="Form-group">
<input
className="FormControl"
name="password"
type="password"
placeholder={extractText(app.translator.trans('core.forum.log_in.password_placeholder'))}
bidi={this.password} bidi={this.password}
disabled={this.loading} /> disabled={this.loading}
</div>, 20); />
</div>,
20
);
items.add('remember', <div className="Form-group"> items.add(
'remember',
<div className="Form-group">
<div> <div>
<label className="checkbox"> <label className="checkbox">
<input type="checkbox" bidi={this.remember} disabled={this.loading} /> <input type="checkbox" bidi={this.remember} disabled={this.loading} />
{app.translator.trans('core.forum.log_in.remember_me_label')} {app.translator.trans('core.forum.log_in.remember_me_label')}
</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 Button--block', className: 'Button Button--primary Button--block',
type: 'submit', type: 'submit',
loading: this.loading, loading: this.loading,
children: app.translator.trans('core.forum.log_in.submit_button') children: app.translator.trans('core.forum.log_in.submit_button'),
})} })}
</div>, -10); </div>,
-10
);
return items; return items;
} }
@ -112,10 +125,10 @@ export default class LogInModal extends Modal {
</p>, </p>,
app.forum.attribute('allowSignUp') ? ( app.forum.attribute('allowSignUp') ? (
<p className="LogInModal-signUp"> <p className="LogInModal-signUp">{app.translator.trans('core.forum.log_in.sign_up_text', { a: <a onclick={this.signUp.bind(this)} /> })}</p>
{app.translator.trans('core.forum.log_in.sign_up_text', {a: <a onclick={this.signUp.bind(this)}/>})} ) : (
</p> ''
) : '' ),
]; ];
} }
@ -159,11 +172,9 @@ export default class LogInModal extends Modal {
const password = this.password(); const password = this.password();
const remember = this.remember(); const remember = this.remember();
app.session.login({identification, password, remember}, {errorHandler: this.onerror.bind(this)}) app.session
.then( .login({ identification, password, remember }, { errorHandler: this.onerror.bind(this) })
() => window.location.reload(), .then(() => window.location.reload(), this.loaded.bind(this));
this.loaded.bind(this)
);
} }
onerror(error) { onerror(error) {

View File

@ -20,31 +20,32 @@ export default class Notification extends Component {
const href = this.href(); const href = this.href();
return ( return (
<a className={'Notification Notification--' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')} <a
className={'Notification Notification--' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')}
href={href} href={href}
config={function (element, isInitialized) { config={function (element, isInitialized) {
if (href.indexOf('://') === -1) m.route.apply(this, arguments); if (href.indexOf('://') === -1) m.route.apply(this, arguments);
if (!isInitialized) $(element).click(this.markAsRead.bind(this)); if (!isInitialized) $(element).click(this.markAsRead.bind(this));
}}> }}
{!notification.isRead() && Button.component({ >
{!notification.isRead() &&
Button.component({
className: 'Notification-action Button Button--icon Button--link', className: 'Notification-action Button Button--icon Button--link',
icon: 'fas fa-check', icon: 'fas fa-check',
title: app.translator.trans('core.forum.notifications.mark_as_read_tooltip'), title: app.translator.trans('core.forum.notifications.mark_as_read_tooltip'),
onclick: e => { onclick: (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.markAsRead(); this.markAsRead();
} },
})} })}
{avatar(notification.fromUser())} {avatar(notification.fromUser())}
{icon(this.icon(), { className: 'Notification-icon' })} {icon(this.icon(), { className: 'Notification-icon' })}
<span className="Notification-content">{this.content()}</span> <span className="Notification-content">{this.content()}</span>
{humanTime(notification.createdAt())} {humanTime(notification.createdAt())}
<div className="Notification-excerpt"> <div className="Notification-excerpt">{this.excerpt()}</div>
{this.excerpt()}
</div>
</a> </a>
); );
} }
@ -55,8 +56,7 @@ export default class Notification extends Component {
* @return {String} * @return {String}
* @abstract * @abstract
*/ */
icon() { icon() {}
}
/** /**
* Get the URL that the notification should link to. * Get the URL that the notification should link to.
@ -64,8 +64,7 @@ export default class Notification extends Component {
* @return {String} * @return {String}
* @abstract * @abstract
*/ */
href() { href() {}
}
/** /**
* Get the content of the notification. * Get the content of the notification.
@ -73,8 +72,7 @@ export default class Notification extends Component {
* @return {VirtualElement} * @return {VirtualElement}
* @abstract * @abstract
*/ */
content() { content() {}
}
/** /**
* Get the excerpt of the notification. * Get the excerpt of the notification.
@ -82,8 +80,7 @@ export default class Notification extends Component {
* @return {VirtualElement} * @return {VirtualElement}
* @abstract * @abstract
*/ */
excerpt() { excerpt() {}
}
/** /**
* Mark the notification as read. * Mark the notification as read.

View File

@ -37,15 +37,15 @@ export default class NotificationGrid extends Component {
// For each of the notification type-method combinations, create and store a // For each of the notification type-method combinations, create and store a
// new checkbox component instance, which we will render in the view. // new checkbox component instance, which we will render in the view.
this.types.forEach(type => { this.types.forEach((type) => {
this.methods.forEach(method => { this.methods.forEach((method) => {
const key = this.preferenceKey(type.name, method.name); const key = this.preferenceKey(type.name, method.name);
const preference = this.props.user.preferences()[key]; const preference = this.props.user.preferences()[key];
this.inputs[key] = new Checkbox({ this.inputs[key] = new Checkbox({
state: !!preference, state: !!preference,
disabled: typeof preference === 'undefined', disabled: typeof preference === 'undefined',
onchange: () => this.toggle([key]) onchange: () => this.toggle([key]),
}); });
}); });
}); });
@ -57,7 +57,7 @@ export default class NotificationGrid extends Component {
<thead> <thead>
<tr> <tr>
<td /> <td />
{this.methods.map(method => ( {this.methods.map((method) => (
<th className="NotificationGrid-groupToggle" onclick={this.toggleMethod.bind(this, method.name)}> <th className="NotificationGrid-groupToggle" onclick={this.toggleMethod.bind(this, method.name)}>
{icon(method.icon)} {method.label} {icon(method.icon)} {method.label}
</th> </th>
@ -66,15 +66,13 @@ export default class NotificationGrid extends Component {
</thead> </thead>
<tbody> <tbody>
{this.types.map(type => ( {this.types.map((type) => (
<tr> <tr>
<td className="NotificationGrid-groupToggle" onclick={this.toggleType.bind(this, type.name)}> <td className="NotificationGrid-groupToggle" onclick={this.toggleType.bind(this, type.name)}>
{icon(type.icon)} {type.label} {icon(type.icon)} {type.label}
</td> </td>
{this.methods.map(method => ( {this.methods.map((method) => (
<td className="NotificationGrid-checkbox"> <td className="NotificationGrid-checkbox">{this.inputs[this.preferenceKey(type.name, method.name)].render()}</td>
{this.inputs[this.preferenceKey(type.name, method.name)].render()}
</td>
))} ))}
</tr> </tr>
))} ))}
@ -88,11 +86,17 @@ export default class NotificationGrid extends Component {
this.$('thead .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function (e) { this.$('thead .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function (e) {
const i = parseInt($(this).index(), 10) + 1; const i = parseInt($(this).index(), 10) + 1;
$(this).parents('table').find('td:nth-child(' + i + ')').toggleClass('highlighted', e.type === 'mouseenter'); $(this)
.parents('table')
.find('td:nth-child(' + i + ')')
.toggleClass('highlighted', e.type === 'mouseenter');
}); });
this.$('tbody .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function (e) { this.$('tbody .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function (e) {
$(this).parent().find('td').toggleClass('highlighted', e.type === 'mouseenter'); $(this)
.parent()
.find('td')
.toggleClass('highlighted', e.type === 'mouseenter');
}); });
} }
@ -107,7 +111,7 @@ export default class NotificationGrid extends Component {
const preferences = user.preferences(); const preferences = user.preferences();
const enabled = !preferences[keys[0]]; const enabled = !preferences[keys[0]];
keys.forEach(key => { keys.forEach((key) => {
const control = this.inputs[key]; const control = this.inputs[key];
control.loading = true; control.loading = true;
@ -117,7 +121,7 @@ export default class NotificationGrid extends Component {
m.redraw(); m.redraw();
user.save({ preferences }).then(() => { user.save({ preferences }).then(() => {
keys.forEach(key => this.inputs[key].loading = false); keys.forEach((key) => (this.inputs[key].loading = false));
m.redraw(); m.redraw();
}); });
@ -129,9 +133,7 @@ export default class NotificationGrid extends Component {
* @param {String} method * @param {String} method
*/ */
toggleMethod(method) { toggleMethod(method) {
const keys = this.types const keys = this.types.map((type) => this.preferenceKey(type.name, method)).filter((key) => !this.inputs[key].props.disabled);
.map(type => this.preferenceKey(type.name, method))
.filter(key => !this.inputs[key].props.disabled);
this.toggle(keys); this.toggle(keys);
} }
@ -142,9 +144,7 @@ export default class NotificationGrid extends Component {
* @param {String} type * @param {String} type
*/ */
toggleType(type) { toggleType(type) {
const keys = this.methods const keys = this.methods.map((method) => this.preferenceKey(type, method.name)).filter((key) => !this.inputs[key].props.disabled);
.map(method => this.preferenceKey(type, method.name))
.filter(key => !this.inputs[key].props.disabled);
this.toggle(keys); this.toggle(keys);
} }
@ -207,7 +207,7 @@ export default class NotificationGrid extends Component {
items.add('discussionRenamed', { items.add('discussionRenamed', {
name: 'discussionRenamed', name: 'discussionRenamed',
icon: 'fas fa-pencil-alt', icon: 'fas fa-pencil-alt',
label: app.translator.trans('core.forum.settings.notify_discussion_renamed_label') label: app.translator.trans('core.forum.settings.notify_discussion_renamed_label'),
}); });
return items; return items;

View File

@ -36,7 +36,7 @@ export default class NotificationList extends Component {
className: 'Button Button--icon Button--link', className: 'Button Button--icon Button--link',
icon: 'fas fa-check', icon: 'fas fa-check',
title: app.translator.trans('core.forum.notifications.mark_all_as_read_tooltip'), title: app.translator.trans('core.forum.notifications.mark_all_as_read_tooltip'),
onclick: this.markAllAsRead.bind(this) onclick: this.markAllAsRead.bind(this),
})} })}
</div> </div>
@ -44,11 +44,12 @@ export default class NotificationList extends Component {
</div> </div>
<div className="NotificationList-content"> <div className="NotificationList-content">
{pages.length ? pages.map(notifications => { {pages.length
? pages.map((notifications) => {
const groups = []; const groups = [];
const discussions = {}; const discussions = {};
notifications.forEach(notification => { notifications.forEach((notification) => {
const subject = notification.subject(); const subject = notification.subject();
if (typeof subject === 'undefined') return; if (typeof subject === 'undefined') return;
@ -71,27 +72,22 @@ export default class NotificationList extends Component {
} }
}); });
return groups.map(group => { return groups.map((group) => {
const badges = group.discussion && group.discussion.badges().toArray(); const badges = group.discussion && group.discussion.badges().toArray();
return ( return (
<div className="NotificationGroup"> <div className="NotificationGroup">
{group.discussion {group.discussion ? (
? ( <a className="NotificationGroup-header" href={app.route.discussion(group.discussion)} config={m.route}>
<a className="NotificationGroup-header"
href={app.route.discussion(group.discussion)}
config={m.route}>
{badges && badges.length ? <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul> : ''} {badges && badges.length ? <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul> : ''}
{group.discussion.title()} {group.discussion.title()}
</a> </a>
) : ( ) : (
<div className="NotificationGroup-header"> <div className="NotificationGroup-header">{app.forum.attribute('title')}</div>
{app.forum.attribute('title')}
</div>
)} )}
<ul className="NotificationGroup-content"> <ul className="NotificationGroup-content">
{group.notifications.map(notification => { {group.notifications.map((notification) => {
const NotificationComponent = app.notificationComponents[notification.contentType()]; const NotificationComponent = app.notificationComponents[notification.contentType()];
return NotificationComponent ? <li>{NotificationComponent.component({ notification })}</li> : ''; return NotificationComponent ? <li>{NotificationComponent.component({ notification })}</li> : '';
})} })}
@ -99,10 +95,15 @@ export default class NotificationList extends Component {
</div> </div>
); );
}); });
}) : ''} })
{this.loading : ''}
? <LoadingIndicator className="LoadingIndicator--block" /> {this.loading ? (
: (pages.length ? '' : <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>)} <LoadingIndicator className="LoadingIndicator--block" />
) : pages.length ? (
''
) : (
<div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>
)}
</div> </div>
</div> </div>
); );
@ -161,7 +162,8 @@ export default class NotificationList extends Component {
const params = app.cache.notifications ? { page: { offset: app.cache.notifications.length * 10 } } : null; const params = app.cache.notifications ? { page: { offset: app.cache.notifications.length * 10 } } : null;
return app.store.find('notifications', params) return app.store
.find('notifications', params)
.then(this.parseResults.bind(this)) .then(this.parseResults.bind(this))
.catch(() => {}) .catch(() => {})
.then(() => { .then(() => {
@ -194,13 +196,13 @@ export default class NotificationList extends Component {
app.session.user.pushAttributes({ unreadNotificationCount: 0 }); app.session.user.pushAttributes({ unreadNotificationCount: 0 });
app.cache.notifications.forEach(notifications => { app.cache.notifications.forEach((notifications) => {
notifications.forEach(notification => notification.pushAttributes({isRead: true})) notifications.forEach((notification) => notification.pushAttributes({ isRead: true }));
}); });
app.request({ app.request({
url: app.forum.attribute('apiUrl') + '/notifications/read', url: app.forum.attribute('apiUrl') + '/notifications/read',
method: 'POST' method: 'POST',
}); });
} }
} }

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