1
0
mirror of https://github.com/flarum/core.git synced 2025-08-13 20:04:24 +02:00

Compare commits

..

2 Commits

Author SHA1 Message Date
David Sevilla Martin
4242a82b80 Use composer.json instead of composer.lock 2019-11-06 19:25:30 -05:00
David Sevilla Martin
cee4cb6840 Cache composer dependencies, skip installing dependencies if cache was used 2019-11-06 19:24:03 -05:00
726 changed files with 6279 additions and 8063 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
github: flarum
open_collective: flarum
tidelift: packagist/flarum/core

26
.github/stale.yml vendored
View File

@@ -1,26 +0,0 @@
daysUntilStale: 90
daysUntilClose: 30
staleLabel: stale
exemptLabels:
- org/keep
- type/bug
- type/regression
- critical
- security
exemptAssignees: true
exemptMilestones: true
exemptProjects: true
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. We do this
to keep the amount of open issues to a manageable minimum.
In any case, thanks for taking an interest in this software and contributing
by opening the issue in the first place!
closeComment: >
We are closing this issue as it seems to have grown stale. If you still
encounter this problem with the latest version, feel free to re-open it.

View File

@@ -1,31 +0,0 @@
name: Lint code
on:
push:
paths:
- 'js/src/**'
pull_request:
paths:
- 'js/src/**'
jobs:
prettier:
runs-on: ubuntu-latest
name: Lint JS code with Prettier
steps:
- uses: actions/checkout@master
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: "12"
- name: Install JS dependencies
run: npm ci
working-directory: ./js
- name: Check JS code for formatting
run: node_modules/.bin/prettier --check src
working-directory: ./js

View File

@@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
php: [7.2, 7.3, 7.4]
php: [7.1, 7.2, 7.3]
service: ['mysql:5.7', mariadb]
prefix: ['', flarum_]
@@ -21,16 +21,16 @@ jobs:
prefixStr: (prefix)
exclude:
- php: 7.2
- php: 7.1
service: 'mysql:5.7'
prefix: flarum_
- php: 7.2
- php: 7.1
service: mariadb
prefix: flarum_
- php: 7.3
- php: 7.2
service: 'mysql:5.7'
prefix: flarum_
- php: 7.3
- php: 7.2
service: mariadb
prefix: flarum_
@@ -43,17 +43,30 @@ jobs:
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
steps:
- uses: actions/checkout@master
- name: Checkout repository
uses: actions/checkout@v1
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v1
id: cache
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Select PHP version
run: sudo update-alternatives --set php $(which php${{ matrix.php }})
- name: Create MySQL Database
run: |
sudo systemctl start mysql
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
run: mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
- name: Install Composer dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: composer install
- name: Setup Composer tests

View File

@@ -1,62 +1,5 @@
# Changelog
## [0.1.0-beta.12](https://github.com/flarum/core/compare/v0.1.0-beta.11.1...v0.1.0-beta.12)
### Added
- Full support for PHP 7.4 (#1980)
- Mail settings: Configure region for the Mailgun driver (#1834, #1850)
- Mail settings: Alert admins about incomplete settings (#1763, #1921)
- New permission that allows users to post without throttling (#1255, #1938)
- Basic transliteration of discussion "slugs" / pretty URLs (#194, #1975)
- User profiles: Render basic content on server side (#1901)
- New extender for configuring middleware (#1919, #1952, #1957, #1971)
- New extender for configuring error handling (#1781, #1970)
- Automated tests for PHP extenders to guarantee their backwards compatibility
### Changed
- Profile URLs for non-existing users properly return HTTP 404 (#1846, #1901)
- Confirmation email subject no longer contains the forum title (#1613)
- Improved error handling during Flarum's early boot phase (#1607)
- Updated deprecated "Zend" libraries to their new "Laminas" equivalents (#1963)
### Fixed
- Update page did not work when installed in subdirectories (#1947)
- Avatar upload did not work in IE11 / Edge (#1125, #1570)
- Translation fallback was ignored for client-rendered pages (#1774, #1961)
- The success alert when posting replies was invisible (#1976)
## [0.1.0-beta.11.1](https://github.com/flarum/core/compare/v0.1.0-beta.11...v0.1.0-beta.11.1)
### Fixed
- Saving custom css in admin failed (#1946)
## [0.1.0-beta.11](https://github.com/flarum/core/compare/v0.1.0-beta.10...v0.1.0-beta.11)
### Added
- Comments have an additional class `Post--by-actor` when posted by the user (#1927)
### Changed
- Improved support for URL identification during installation (#1861)
- KeyboardNavigatable now has a callback ability (#1922)
- Links are no longer opened with target `_blank` but in the same window (#859)
- Links now have `nofollow ugc` by default as their `rel` attribute (#859, #1884)
- Improved performance of the full text gambit when searching for users (#1877)
- The Queue implementation is now available under its Illuminate contract
### Fixed
- No error handling was possible in the console/cli (#1789)
- Enable scrollbars in log in modals so it fits for GitHub (#1716)
- Reduce log in modal for SSO so it fits for Facebook (#1727)
- Deleting discussions permanently did not delete its posts (#1909)
- Fixed the queue:restart command (#1932)
- Deleted posts were visible to all visitors (#1827)
- Old avatars weren't being deleted when replaced (#1918)
- The search performance regression was reverted (#1764)
- No profile background could be set for remote images (#445)
- Back button sends to home even though it could actually go back (#1942)
- Debug button no longer visible (#1687)
- Modals on smaller screens use the whole width of the page
## [0.1.0-beta.10](https://github.com/flarum/core/compare/v0.1.0-beta.9...v0.1.0-beta.10)
### Added

View File

@@ -1,7 +1,6 @@
The MIT License (MIT)
Copyright (c) 2019-2020 Stichting Flarum (Flarum Foundation)
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
Copyright (c) Toby Zerner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -27,7 +27,7 @@ Thank you for considering contributing to Flarum! Please read the **[Contributin
## Security Vulnerabilities
If you discover a security vulnerability within Flarum, please send an e-mail to [security@flarum.org](mailto:security@flarum.org). All security vulnerabilities will be promptly addressed. More details can be found in our [security policy](https://github.com/flarum/core/security/policy).
If you discover a security vulnerability within Flarum, please send an e-mail to [security@flarum.org](mailto:security@flarum.org). All security vulnerabilities will be promptly addressed.
## License

View File

@@ -5,28 +5,17 @@
"homepage": "https://flarum.org/",
"license": "MIT",
"authors": [
{
"name": "Toby Zerner",
"email": "toby.zerner@gmail.com"
},
{
"name": "Franz Liedke",
"email": "franz@develophp.org"
},
{
"name": "Daniel Klabbers",
"email": "daniel@klabbers.email",
"homepage": "https://luceos.com"
},
{
"name": "David Sevilla Martin",
"email": "me+flarum@datitisev.me",
"homepage": "https://datitisev.me"
},
{
"name": "Clark Winkelmann",
"email": "clark.winkelmann@gmail.com",
"homepage": "https://clarkwinkelmann.com"
},
{
"name": "Matthew Kilgore",
"email": "matthew@kilgore.dev"
"name": "Daniel Klabbers",
"email": "daniel@klabbers.email"
}
],
"support": {
@@ -35,7 +24,7 @@
"docs": "https://flarum.org/docs/"
},
"require": {
"php": ">=7.2",
"php": ">=7.1",
"axy/sourcemap": "^0.1.4",
"components/font-awesome": "5.9.*",
"dflydev/fig-cookies": "^1.0.2",
@@ -56,10 +45,7 @@
"illuminate/support": "5.7.*",
"illuminate/validation": "5.7.*",
"illuminate/view": "5.7.*",
"intervention/image": "^2.5.0",
"laminas/laminas-diactoros": "^1.8.4",
"laminas/laminas-httphandlerrunner": "^1.0",
"laminas/laminas-stratigility": "^3.0",
"intervention/image": "^2.3.0",
"league/flysystem": "^1.0.11",
"matthiasmullie/minify": "^1.3",
"middlewares/base-path": "^1.1",
@@ -67,17 +53,20 @@
"middlewares/request-handler": "^1.2",
"monolog/monolog": "^1.16.0",
"nikic/fast-route": "^0.6",
"oyejorge/less.php": "^1.7",
"psr/http-message": "^1.0",
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"s9e/text-formatter": "^2.3.6",
"s9e/text-formatter": "^1.2.0",
"symfony/config": "^3.3",
"symfony/console": "^4.2",
"symfony/event-dispatcher": "^4.3.2",
"symfony/translation": "^3.3",
"symfony/yaml": "^3.3",
"tobscure/json-api": "^0.3.0",
"wikimedia/less.php": "^3.0"
"zendframework/zend-diactoros": "^1.8.4",
"zendframework/zend-httphandlerrunner": "^1.0",
"zendframework/zend-stratigility": "^3.0"
},
"require-dev": {
"mockery/mockery": "^1.0",

View File

@@ -1,6 +0,0 @@
{
"printWidth": 150,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}

20
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

22
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1468
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,23 +15,12 @@
"moment": "^2.22.2",
"punycode": "^2.1.1",
"spin.js": "^3.1.0",
"webpack": "^4.41.2",
"webpack": "^4.26.0",
"webpack-cli": "^3.1.2",
"webpack-merge": "^4.1.4"
},
"devDependencies": {
"husky": "^4.2.5",
"prettier": "2.0.2"
},
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production",
"format": "prettier --write src",
"format-check": "prettier --check src"
},
"husky": {
"hooks": {
"pre-commit": "npm run format"
}
"build": "webpack --mode production"
}
}

View File

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

View File

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

View File

@@ -22,10 +22,8 @@ export default class AddExtensionModal extends Modal {
return (
<div className="Modal-body">
<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.developer_text', { a: <a href="http://flarum.org/docs/extend" 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>
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,14 +11,10 @@ export default class EditCustomCssModal extends SettingsModal {
form() {
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">
<textarea className="FormControl" rows="30" bidi={this.setting('custom_less')} />
</div>,
<textarea className="FormControl" rows="30" bidi={this.setting('custom_less')}/>
</div>
];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,45 +10,38 @@ export default class MailPage extends Page {
init() {
super.init();
this.saving = false;
this.refresh();
}
refresh() {
this.loading = true;
this.saving = false;
this.driverFields = {};
this.fields = ['mail_driver', 'mail_from'];
this.values = {};
this.status = { sending: false, errors: {} };
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({
method: 'GET',
url: app.forum.attribute('apiUrl') + '/mail-settings',
})
.then((response) => {
this.driverFields = response['data']['attributes']['fields'];
this.status.sending = response['data']['attributes']['sending'];
this.status.errors = response['data']['attributes']['errors'];
app.request({
method: 'GET',
url: app.forum.attribute('apiUrl') + '/mail-drivers'
}).then(response => {
this.driverFields = response['data'].reduce(
(hash, driver) => ({...hash, [driver['id']]: driver['attributes']['fields']}),
{}
);
for (const driver in this.driverFields) {
for (const field in this.driverFields[driver]) {
this.fields.push(field);
this.values[field] = m.prop(settings[field]);
}
Object.keys(this.driverFields).flatMap(key => this.driverFields[key]).forEach(
key => {
this.fields.push(key);
this.values[key] = m.prop(settings[key]);
}
this.loading = false;
m.redraw();
});
);
this.loading = false;
m.redraw();
});
}
view() {
if (this.loading || this.saving) {
if (this.loading) {
return (
<div className="MailPage">
<div className="container">
@@ -58,27 +51,24 @@ export default class MailPage extends Page {
);
}
const fields = this.driverFields[this.values.mail_driver()];
const fieldKeys = Object.keys(fields);
return (
<div className="MailPage">
<div className="container">
<form onsubmit={this.onsubmit.bind(this)}>
<h2>{app.translator.trans('core.admin.email.heading')}</h2>
<div className="helpText">{app.translator.trans('core.admin.email.text')}</div>
<div className="helpText">
{app.translator.trans('core.admin.email.text')}
</div>
{FieldSet.component({
label: app.translator.trans('core.admin.email.addresses_heading'),
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
<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)} />
</label>
</div>,
],
<label>{app.translator.trans('core.admin.email.from_label')}</label>
<input className="FormControl" value={this.values.mail_from() || ''} oninput={m.withAttr('value', this.values.mail_from)} />
</div>
]
})}
{FieldSet.component({
@@ -86,46 +76,31 @@ export default class MailPage extends Page {
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
<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}
/>
</label>
</div>,
],
<label>{app.translator.trans('core.admin.email.driver_label')}</label>
<Select value={this.values.mail_driver()} options={Object.keys(this.driverFields).reduce((memo, val) => ({...memo, [val]: val}), {})} onchange={this.values.mail_driver} />
</div>
]
})}
{this.status.sending ||
Alert.component({
children: app.translator.trans('core.admin.email.not_sending_message'),
dismissible: false,
})}
{fieldKeys.length > 0 &&
FieldSet.component({
label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`),
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
{fieldKeys.map((field) => [
<label>
{app.translator.trans(`core.admin.email.${field}_label`)}
{this.renderField(field)}
</label>,
this.status.errors[field] && <p className="ValidationError">{this.status.errors[field]}</p>,
])}
</div>,
],
})}
{Object.keys(this.driverFields[this.values.mail_driver()]).length > 0 && FieldSet.component({
label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`),
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
{this.driverFields[this.values.mail_driver()].flatMap(field => [
<label>{app.translator.trans(`core.admin.email.${field}_label`)}</label>,
<input className="FormControl" value={this.values[field]() || ''} oninput={m.withAttr('value', this.values[field])} />
])}
</div>
]
})}
{Button.component({
type: 'submit',
className: 'Button Button--primary',
children: app.translator.trans('core.admin.email.submit_button'),
disabled: !this.changed(),
loading: this.saving,
disabled: !this.changed()
})}
</form>
</div>
@@ -133,20 +108,8 @@ export default class MailPage extends Page {
);
}
renderField(name) {
const driver = this.values.mail_driver();
const field = this.driverFields[driver][name];
const prop = this.values[name];
if (typeof field === 'string') {
return <input className="FormControl" value={prop() || ''} oninput={m.withAttr('value', prop)} />;
} else {
return <Select value={prop()} options={field} onchange={prop} />;
}
}
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) {
@@ -159,16 +122,16 @@ export default class MailPage extends Page {
const settings = {};
this.fields.forEach((key) => (settings[key] = this.values[key]()));
this.fields.forEach(key => settings[key] = this.values[key]());
saveSettings(settings)
.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(() => {})
.then(() => {
this.saving = false;
this.refresh();
m.redraw();
});
}
}

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,10 @@ export default class SessionDropdown extends Dropdown {
getButtonContent() {
const user = app.session.user;
return [avatar(user), ' ', <span className="Button-label">{username(user)}</span>];
return [
avatar(user), ' ',
<span className="Button-label">{username(user)}</span>
];
}
/**
@@ -37,12 +40,11 @@ export default class SessionDropdown extends Dropdown {
items() {
const items = new ItemList();
items.add(
'logOut',
items.add('logOut',
Button.component({
icon: 'fas fa-sign-out-alt',
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
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,7 @@
import ItemList from './utils/ItemList';
import Alert from './components/Alert';
import Button from './components/Button';
import ModalManager from './components/ModalManager';
import AlertManager from './components/AlertManager';
import RequestErrorModal from './components/RequestErrorModal';
import Translator from './Translator';
import Store from './Store';
import Session from './Session';
@@ -86,7 +84,7 @@ export default class Application {
discussions: Discussion,
posts: Post,
groups: Group,
notifications: Notification,
notifications: Notification
});
/**
@@ -126,21 +124,24 @@ export default class Application {
}
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.session = new Session(this.store.getById('users', this.data.session.userId), this.data.session.csrfToken);
this.session = new Session(
this.store.getById('users', this.data.session.userId),
this.data.session.csrfToken
);
this.mount();
}
bootExtensions(extensions) {
Object.keys(extensions).forEach((name) => {
Object.keys(extensions).forEach(name => {
const extension = extensions[name];
const extenders = flattenDeep(extension.extend);
for (const extender of extenders) {
@@ -150,20 +151,26 @@ export default class Application {
}
mount(basePath = '') {
this.modal = m.mount(document.getElementById('modal'), <ModalManager />);
this.alerts = m.mount(document.getElementById('alerts'), <AlertManager />);
this.modal = m.mount(document.getElementById('modal'), <ModalManager/>);
this.alerts = m.mount(document.getElementById('alerts'), <AlertManager/>);
this.drawer = new Drawer();
m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath));
m.route(
document.getElementById('content'),
basePath + '/',
mapRoutes(this.routes, basePath)
);
// Add a class to the body which indicates that the page has been scrolled
// down.
new ScrollListener((top) => {
new ScrollListener(top => {
const $app = $('#app');
const offset = $app.offset().top;
$app.toggleClass('affix', top >= offset).toggleClass('scrolled', top > offset);
$app
.toggleClass('affix', top >= offset)
.toggleClass('scrolled', top > offset);
}).start();
$(() => {
@@ -211,7 +218,9 @@ export default class Application {
}
updateTitle() {
document.title = (this.titleCount ? `(${this.titleCount}) ` : '') + (this.title ? this.title + ' - ' : '') + this.forum.attribute('title');
document.title = (this.titleCount ? `(${this.titleCount}) ` : '') +
(this.title ? this.title + ' - ' : '') +
this.forum.attribute('title');
}
/**
@@ -245,19 +254,17 @@ export default class Application {
// 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
// error message to the user instead.
options.deserialize = options.deserialize || ((responseText) => responseText);
options.deserialize = options.deserialize || (responseText => responseText);
options.errorHandler =
options.errorHandler ||
((error) => {
throw error;
});
options.errorHandler = options.errorHandler || (error => {
throw error;
});
// When extracting the data from the response, we can check the server
// response code and show an error message to the user if something's gone
// awry.
const original = options.extract;
options.extract = (xhr) => {
options.extract = xhr => {
let responseText;
if (original) {
@@ -290,74 +297,54 @@ export default class Application {
// returned and show an alert containing its contents.
const deferred = m.deferred();
m.request(options).then(
(response) => deferred.resolve(response),
(error) => {
this.requestError = error;
m.request(options).then(response => deferred.resolve(response), error => {
this.requestError = error;
let children;
let children;
switch (error.status) {
case 422:
children = error.response.errors
.map((error) => [error.detail, <br />])
.reduce((a, b) => a.concat(b), [])
.slice(0, -1);
break;
switch (error.status) {
case 422:
children = error.response.errors
.map(error => [error.detail, <br/>])
.reduce((a, b) => a.concat(b), [])
.slice(0, -1);
break;
case 401:
case 403:
children = app.translator.trans('core.lib.error.permission_denied_message');
break;
case 401:
case 403:
children = app.translator.trans('core.lib.error.permission_denied_message');
break;
case 404:
case 410:
children = app.translator.trans('core.lib.error.not_found_message');
break;
case 404:
case 410:
children = app.translator.trans('core.lib.error.not_found_message');
break;
case 429:
children = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
break;
case 429:
children = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
break;
default:
children = app.translator.trans('core.lib.error.generic_message');
}
const isDebug = app.forum.attribute('debug');
error.alert = new Alert({
type: 'error',
children,
controls: isDebug && [
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error)}>
Debug
</Button>,
],
});
try {
options.errorHandler(error);
} catch (error) {
this.alerts.show(error.alert);
}
deferred.reject(error);
default:
children = app.translator.trans('core.lib.error.generic_message');
}
);
error.alert = new Alert({
type: 'error',
children
});
try {
options.errorHandler(error);
} catch (error) {
this.alerts.show(error.alert);
}
deferred.reject(error);
});
return deferred.promise;
}
/**
* @param {RequestError} error
* @private
*/
showDebug(error) {
this.alerts.dismiss(this.requestError.alert);
this.modal.show(new RequestErrorModal({ error }));
}
/**
* Construct a URL to the route with the given name.
*

View File

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

View File

@@ -88,7 +88,7 @@ export default class Model {
// relationship data object.
for (const innerKey in data[key]) {
if (data[key][innerKey] instanceof Model) {
data[key][innerKey] = { data: Model.getIdentifier(data[key][innerKey]) };
data[key][innerKey] = {data: Model.getIdentifier(data[key][innerKey])};
}
this.data[key][innerKey] = data[key][innerKey];
}
@@ -109,7 +109,7 @@ export default class Model {
* @public
*/
pushAttributes(attributes) {
this.pushData({ attributes });
this.pushData({attributes});
}
/**
@@ -125,7 +125,7 @@ export default class Model {
const data = {
type: this.data.type,
id: this.data.id,
attributes,
attributes
};
// If a 'relationships' key exists, extract it from the attributes hash and
@@ -138,7 +138,9 @@ export default class Model {
const model = attributes.relationships[key];
data.relationships[key] = {
data: model instanceof Array ? model.map(Model.getIdentifier) : Model.getIdentifier(model),
data: model instanceof Array
? model.map(Model.getIdentifier)
: Model.getIdentifier(model)
};
}
@@ -152,38 +154,31 @@ export default class Model {
this.pushData(data);
const request = { data };
const request = {data};
if (options.meta) request.meta = options.meta;
return app
.request(
Object.assign(
{
method: this.exists ? 'PATCH' : 'POST',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data: request,
},
options
)
)
.then(
// 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
// the API returned into the store.
(payload) => {
this.store.data[payload.data.type] = this.store.data[payload.data.type] || {};
this.store.data[payload.data.type][payload.data.id] = this;
return this.store.pushPayload(payload);
},
return app.request(Object.assign({
method: this.exists ? 'PATCH' : 'POST',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data: request
}, options)).then(
// 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
// the API returned into the store.
payload => {
this.store.data[payload.data.type] = this.store.data[payload.data.type] || {};
this.store.data[payload.data.type][payload.data.id] = this;
return this.store.pushPayload(payload);
},
// 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.
(response) => {
this.pushData(oldData);
m.lazyRedraw();
throw response;
}
);
// 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.
response => {
this.pushData(oldData);
m.lazyRedraw();
throw response;
}
);
}
/**
@@ -197,21 +192,14 @@ export default class Model {
delete(data, options = {}) {
if (!this.exists) return m.deferred().resolve().promise;
return app
.request(
Object.assign(
{
method: 'DELETE',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data,
},
options
)
)
.then(() => {
this.exists = false;
this.store.remove(this);
});
return app.request(Object.assign({
method: 'DELETE',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data
}, options)).then(() => {
this.exists = false;
this.store.remove(this);
});
}
/**
@@ -237,7 +225,7 @@ export default class Model {
* @public
*/
static attribute(name, transform) {
return function () {
return function() {
const value = this.data.attributes && this.data.attributes[name];
return transform ? transform(value) : value;
@@ -255,7 +243,7 @@ export default class Model {
* @public
*/
static hasOne(name) {
return function () {
return function() {
if (this.data.relationships) {
const relationship = this.data.relationships[name];
@@ -279,12 +267,12 @@ export default class Model {
* @public
*/
static hasMany(name) {
return function () {
return function() {
if (this.data.relationships) {
const relationship = this.data.relationships[name];
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));
}
}
@@ -313,7 +301,7 @@ export default class Model {
static getIdentifier(model) {
return {
type: model.data.type,
id: model.data.id,
id: model.data.id
};
}
}

View File

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

View File

@@ -34,7 +34,9 @@ export default class Store {
pushPayload(payload) {
if (payload.included) payload.included.map(this.pushObject.bind(this));
const result = payload.data instanceof Array ? payload.data.map(this.pushObject.bind(this)) : this.pushObject(payload.data);
const result = payload.data instanceof Array
? payload.data.map(this.pushObject.bind(this))
: this.pushObject(payload.data);
// Attach the original payload to the model that we give back. This is
// useful to consumers as it allows them to access meta information
@@ -56,7 +58,7 @@ export default class Store {
pushObject(data) {
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]) {
type[data.id].pushData(data);
@@ -93,18 +95,11 @@ export default class Store {
url += '/' + id;
}
return app
.request(
Object.assign(
{
method: 'GET',
url,
data,
},
options
)
)
.then(this.pushPayload.bind(this));
return app.request(Object.assign({
method: 'GET',
url,
data
}, options)).then(this.pushPayload.bind(this));
}
/**
@@ -129,7 +124,7 @@ export default class Store {
* @public
*/
getBy(type, key, value) {
return this.all(type).filter((model) => model[key]() === value)[0];
return this.all(type).filter(model => model[key]() === value)[0];
}
/**
@@ -142,7 +137,7 @@ export default class Store {
all(type) {
const records = this.data[type];
return records ? Object.keys(records).map((id) => records[id]) : [];
return records ? Object.keys(records).map(id => records[id]) : [];
}
/**
@@ -165,6 +160,6 @@ export default class Store {
createRecord(type, data = {}) {
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 open = [hydrated];
translation.forEach((part) => {
translation.forEach(part => {
const match = part.match(new RegExp('{([a-z0-9_]+)}|<(/?)([a-z0-9_]+)>', 'i'));
if (match) {
@@ -77,7 +77,7 @@ export default class Translator {
if (match[2]) {
open.shift();
} else {
let tag = input[match[3]] || { tag: match[3], children: [] };
let tag = input[match[3]] || {tag: match[3], children: []};
open[0].push(tag);
open.unshift(tag.children || tag);
}
@@ -87,7 +87,7 @@ export default class Translator {
}
});
return hydrated.filter((part) => part);
return hydrated.filter(part => part);
}
pluralize(translation, number) {
@@ -97,7 +97,7 @@ export default class Translator {
standardRules = [],
explicitRules = [];
translation.split('|').forEach((part) => {
translation.split('|').forEach(part => {
if (cPluralRegex.test(part)) {
const matches = part.match(cPluralRegex);
explicitRules[matches[0]] = matches[matches.length - 1];
@@ -122,13 +122,11 @@ export default class Translator {
}
}
} else {
var leftNumber = this.convertNumber(matches[4]);
var leftNumber = this.convertNumber(matches[4]);
var rightNumber = this.convertNumber(matches[5]);
if (
('[' === matches[3] ? number >= leftNumber : number > leftNumber) &&
(']' === matches[6] ? number <= rightNumber : number < rightNumber)
) {
if (('[' === matches[3] ? number >= leftNumber : number > leftNumber) &&
(']' === matches[6] ? number <= rightNumber : number < rightNumber)) {
return explicitRules[e];
}
}
@@ -225,7 +223,7 @@ export default class Translator {
case 'tr':
case 'ur':
case 'zu':
return number == 1 ? 0 : 1;
return (number == 1) ? 0 : 1;
case 'am':
case 'bh':
@@ -239,7 +237,7 @@ export default class Translator {
case 'xbr':
case 'ti':
case 'wa':
return number === 0 || number == 1 ? 0 : 1;
return ((number === 0) || (number == 1)) ? 0 : 1;
case 'be':
case 'bs':
@@ -247,41 +245,41 @@ export default class Translator {
case 'ru':
case 'sr':
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 'sk':
return number == 1 ? 0 : number >= 2 && number <= 4 ? 1 : 2;
return (number == 1) ? 0 : (((number >= 2) && (number <= 4)) ? 1 : 2);
case 'ga':
return number == 1 ? 0 : number == 2 ? 1 : 2;
return (number == 1) ? 0 : ((number == 2) ? 1 : 2);
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':
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':
return number % 10 == 1 ? 0 : 1;
return (number % 10 == 1) ? 0 : 1;
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':
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':
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':
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':
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':
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:
return 0;

View File

@@ -37,7 +37,6 @@ import Placeholder from './components/Placeholder';
import Separator from './components/Separator';
import Dropdown from './components/Dropdown';
import SplitDropdown from './components/SplitDropdown';
import RequestErrorModal from './components/RequestErrorModal';
import FieldSet from './components/FieldSet';
import Select from './components/Select';
import Navigation from './components/Navigation';
@@ -62,9 +61,9 @@ import userOnline from './helpers/userOnline';
import listItems from './helpers/listItems';
export default {
extend: extend,
Session: Session,
Store: Store,
'extend': extend,
'Session': Session,
'Store': Store,
'utils/evented': evented,
'utils/liveHumanTimes': liveHumanTimes,
'utils/ItemList': ItemList,
@@ -91,8 +90,8 @@ export default {
'models/Discussion': Discussion,
'models/Group': Group,
'models/Forum': Forum,
Component: Component,
Translator: Translator,
'Component': Component,
'Translator': Translator,
'components/AlertManager': AlertManager,
'components/Switch': Switch,
'components/Badge': Badge,
@@ -101,7 +100,6 @@ export default {
'components/Separator': Separator,
'components/Dropdown': Dropdown,
'components/SplitDropdown': SplitDropdown,
'components/RequestErrorModal': RequestErrorModal,
'components/FieldSet': FieldSet,
'components/Select': Select,
'components/Navigation': Navigation,
@@ -113,8 +111,8 @@ export default {
'components/Button': Button,
'components/Modal': Modal,
'components/GroupBadge': GroupBadge,
Model: Model,
Application: Application,
'Model': Model,
'Application': Application,
'helpers/fullTime': fullTime,
'helpers/avatar': avatar,
'helpers/icon': icon,
@@ -123,5 +121,5 @@ export default {
'helpers/highlight': highlight,
'helpers/username': username,
'helpers/userOnline': userOnline,
'helpers/listItems': listItems,
'helpers/listItems': listItems
};

View File

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

View File

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

View File

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

View File

@@ -62,9 +62,9 @@ export default class Button extends Component {
const iconName = this.props.icon;
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.loading ? LoadingIndicator.component({ size: 'tiny', className: 'LoadingIndicator--inline' }) : '',
this.props.loading ? LoadingIndicator.component({size: 'tiny', className: 'LoadingIndicator--inline'}) : ''
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,32 +0,0 @@
import Modal from './Modal';
export default class RequestErrorModal extends Modal {
className() {
return 'RequestErrorModal Modal--large';
}
title() {
return this.props.error.xhr ? this.props.error.xhr.status + ' ' + this.props.error.xhr.statusText : '';
}
content() {
let responseText;
try {
responseText = JSON.stringify(JSON.parse(this.props.error.responseText), null, 2);
} catch (e) {
responseText = this.props.error.responseText;
}
return (
<div className="Modal-body">
<pre>
{this.props.error.options.method} {this.props.error.options.url}
<br />
<br />
{responseText}
</pre>
</div>
);
}
}

View File

@@ -8,25 +8,17 @@ import icon from '../helpers/icon';
* - `options` A map of option values to labels.
* - `onchange` A callback to run when the selected value is changed.
* - `value` The value of the selected option.
* - `disabled` Disabled state for the input.
*/
export default class Select extends Component {
view() {
const { options, onchange, value, disabled } = this.props;
const {options, onchange, value} = this.props;
return (
<span className="Select">
<select
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 className="Select-input FormControl" onchange={onchange ? m.withAttr('value', onchange.bind(this)) : undefined} value={value}>
{Object.keys(options).map(key => <option value={key}>{options[key]}</option>)}
</select>
{icon('fas fa-sort', { className: 'Select-caret' })}
{icon('fas fa-sort', {className: 'Select-caret'})}
</span>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,4 +10,4 @@ export default class PostTypes {
extend(app, extension) {
Object.assign(app.postComponents, this.postComponents);
}
}
}

View File

@@ -10,4 +10,4 @@ export default class Routes {
extend(app, extension) {
Object.assign(app.routes, this.routes);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ export default function punctuateSeries(items) {
if (items.length === 2) {
return app.translator.trans('core.lib.series.two_text', {
first: items[0],
second: items[1],
second: items[1]
});
} else if (items.length >= 3) {
// 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', {
first: items[0],
second,
third: items[items.length - 1],
third: items[items.length - 1]
});
}

View File

@@ -7,7 +7,7 @@ import icon from './icon';
* @return {Object}
*/
export default function userOnline(user) {
if (user.lastSeenAt() && user.isOnline()) {
return <span className="UserOnline">{icon('fas fa-circle')}</span>;
}
if (user.lastSeenAt() && user.isOnline()) {
return <span className="UserOnline">{icon('fas fa-circle')}</span>;
}
}

View File

@@ -19,18 +19,18 @@ Object.assign(Discussion.prototype, {
lastPostNumber: Model.attribute('lastPostNumber'),
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'),
mostRelevantPost: Model.hasOne('mostRelevantPost'),
lastReadAt: Model.attribute('lastReadAt', Model.transformDate),
lastReadPostNumber: Model.attribute('lastReadPostNumber'),
isUnread: computed('unreadCount', (unreadCount) => !!unreadCount),
isRead: computed('unreadCount', (unreadCount) => app.session.user && !unreadCount),
isUnread: computed('unreadCount', unreadCount => !!unreadCount),
isRead: computed('unreadCount', unreadCount => app.session.user && !unreadCount),
hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
hiddenUser: Model.hasOne('hiddenUser'),
isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt),
isHidden: computed('hiddenAt', hiddenAt => !!hiddenAt),
canReply: Model.attribute('canReply'),
canRename: Model.attribute('canRename'),
@@ -84,7 +84,7 @@ Object.assign(Discussion.prototype, {
const items = new ItemList();
if (this.isHidden()) {
items.add('hidden', <Badge type="hidden" icon="fas fa-trash" label={app.translator.trans('core.lib.badge.hidden_tooltip')} />);
items.add('hidden', <Badge type="hidden" icon="fas fa-trash" label={app.translator.trans('core.lib.badge.hidden_tooltip')}/>);
}
return items;
@@ -99,6 +99,6 @@ Object.assign(Discussion.prototype, {
postIds() {
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'),
namePlural: Model.attribute('namePlural'),
color: Model.attribute('color'),
icon: Model.attribute('icon'),
icon: Model.attribute('icon')
});
Group.ADMINISTRATOR_ID = '1';

View File

@@ -11,5 +11,5 @@ Object.assign(Notification.prototype, {
user: Model.hasOne('user'),
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),
editedUser: Model.hasOne('editedUser'),
isEdited: computed('editedAt', (editedAt) => !!editedAt),
isEdited: computed('editedAt', editedAt => !!editedAt),
hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
hiddenUser: Model.hasOne('hiddenUser'),
isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt),
isHidden: computed('hiddenAt', hiddenAt => !!hiddenAt),
canEdit: Model.attribute('canEdit'),
canHide: Model.attribute('canHide'),
canDelete: Model.attribute('canDelete'),
canDelete: Model.attribute('canDelete')
});

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
const later =
window.requestAnimationFrame ||
const later = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
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
@@ -58,7 +57,10 @@ export default class ScrollListener {
*/
start() {
if (!this.active) {
window.addEventListener('scroll', (this.active = this.loop.bind(this)));
window.addEventListener(
'scroll',
this.active = this.loop.bind(this)
);
}
}

View File

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

View File

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

View File

@@ -14,12 +14,12 @@ export default function computed(...dependentKeys) {
const dependentValues = {};
let computedValue;
return function () {
return function() {
let recompute = false;
// Read all of the dependent values. If any of them have changed since last
// 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];
if (dependentValues[key] !== value) {
@@ -29,10 +29,7 @@ export default function computed(...dependentKeys) {
});
if (recompute) {
computedValue = compute.apply(
this,
keys.map((key) => dependentValues[key])
);
computedValue = compute.apply(this, keys.map(key => dependentValues[key]));
}
return computedValue;

View File

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

View File

@@ -6,7 +6,7 @@
*/
export default function extractText(vdom) {
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) {
return extractText(vdom.children);
} else {

View File

@@ -26,11 +26,11 @@ export default function humanTime(time) {
if (m.year() === moment().year()) {
ago = m.format('D MMM');
} else {
ago = m.format('ll');
ago = m.format('MMM \'YY');
}
} else {
ago = m.fromNow();
}
return ago;
}
};

View File

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

View File

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

View File

@@ -3,11 +3,11 @@ import Component from '../Component';
export default function patchMithril(global) {
const mo = global.m;
const m = function (comp, ...args) {
const m = function(comp, ...args) {
if (comp.prototype && comp.prototype instanceof Component) {
let children = args.slice(1);
if (children.length === 1 && Array.isArray(children[0])) {
children = children[0];
children = children[0]
}
return comp.component(args[0], children);
@@ -29,14 +29,14 @@ export default function patchMithril(global) {
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).
*
* @return {void}
*/
m.lazyRedraw = function () {
m.lazyRedraw = function() {
m.startComputation();
m.endComputation();
};

View File

@@ -7,23 +7,20 @@
* @return {String}
*/
export function truncate(string, length, start = 0) {
return (start > 0 ? '...' : '') + string.substring(start, start + length) + (string.length > start + length ? '...' : '');
return (start > 0 ? '...' : '') +
string.substring(start, start + length) +
(string.length > start + length ? '...' : '');
}
/**
* Create a slug out of the given string. Non-alphanumeric characters are
* converted to hyphens.
*
* NOTE: This method does not use the comparably sophisticated transliteration
* mechanism that is employed in the backend. Therefore, it should only be used
* to *suggest* slugs that can be overridden by the user.
*
* @param {String} string
* @return {String}
*/
export function slug(string) {
return string
.toLowerCase()
return string.toLowerCase()
.replace(/[^a-z0-9]/gi, '-')
.replace(/-+/g, '-')
.replace(/-$|^-/g, '');
@@ -37,7 +34,9 @@ export function slug(string) {
* @return {String}
*/
export function getPlainContent(string) {
const html = string.replace(/(<\/p>|<br>)/g, '$1 &nbsp;').replace(/<img\b[^>]*>/gi, ' ');
const html = string
.replace(/(<\/p>|<br>)/g, '$1 &nbsp;')
.replace(/<img\b[^>]*>/ig, ' ');
const dom = $('<div/>').html(html);

View File

@@ -10,42 +10,18 @@ function hsvToRgb(h, s, v) {
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0:
r = v;
g = t;
b = p;
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;
case 0: r = v; g = t; b = p; 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 {
r: Math.floor(r * 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}
*/
notificationComponents = {
discussionRenamed: DiscussionRenamedNotification,
discussionRenamed: DiscussionRenamedNotification
};
/**
* A map of post types to their components.
@@ -31,7 +31,7 @@ export default class ForumApplication extends Application {
*/
postComponents = {
comment: CommentPost,
discussionRenamed: DiscussionRenamedPost,
discussionRenamed: DiscussionRenamedPost
};
/**
@@ -87,7 +87,7 @@ export default class ForumApplication extends Application {
this.routes[defaultAction].path = '/';
this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/');
m.mount(document.getElementById('app-navigation'), Navigation.component({ className: 'App-backControl', drawer: true }));
m.mount(document.getElementById('app-navigation'), Navigation.component({className: 'App-backControl', drawer: true}));
m.mount(document.getElementById('header-navigation'), Navigation.component());
m.mount(document.getElementById('header-primary'), HeaderPrimary.component());
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
@@ -102,7 +102,7 @@ export default class ForumApplication extends Application {
// Route the home link back home when clicked. We do not want it to register
// 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;
e.preventDefault();
app.history.home();
@@ -123,11 +123,9 @@ export default class ForumApplication extends Application {
* @return {Boolean}
*/
composingReplyTo(discussion) {
return (
this.composer.component instanceof ReplyComposer &&
return this.composer.component instanceof ReplyComposer &&
this.composer.component.props.discussion === discussion &&
this.composer.position !== Composer.PositionEnum.HIDDEN
);
this.composer.position !== Composer.PositionEnum.HIDDEN;
}
/**
@@ -137,7 +135,8 @@ export default class ForumApplication extends Application {
* @return {Boolean}
*/
viewingDiscussion(discussion) {
return this.current instanceof DiscussionPage && this.current.discussion === discussion;
return this.current instanceof DiscussionPage &&
this.current.discussion === discussion;
}
/**

View File

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

View File

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

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