From f96cac605759c98c3bf5cd314009bc625caccd0f Mon Sep 17 00:00:00 2001 From: Toby Zerner <toby.zerner@gmail.com> Date: Wed, 29 Jul 2015 21:00:09 +0930 Subject: [PATCH] Implement basic settings page --- js/admin/src/components/BasicsPage.js | 137 ++++++++++++++++++++++++ js/lib/Model.js | 14 ++- js/lib/components/Button.js | 10 +- js/lib/models/Forum.js | 6 +- less/admin/BasicsPage.less | 33 ++++++ less/forum/Composer.less | 5 - less/lib/Button.less | 16 +++ src/Api/Actions/Forum/ShowAction.php | 11 +- src/Api/Actions/Forum/UpdateAction.php | 53 +++++++++ src/Api/ApiServiceProvider.php | 7 ++ src/Api/Serializers/ForumSerializer.php | 21 +++- src/Locale/LocaleManager.php | 12 +++ src/Locale/LocaleServiceProvider.php | 5 +- 13 files changed, 313 insertions(+), 17 deletions(-) create mode 100644 js/admin/src/components/BasicsPage.js create mode 100644 less/admin/BasicsPage.less create mode 100644 src/Api/Actions/Forum/UpdateAction.php diff --git a/js/admin/src/components/BasicsPage.js b/js/admin/src/components/BasicsPage.js new file mode 100644 index 000000000..a3d8f2f69 --- /dev/null +++ b/js/admin/src/components/BasicsPage.js @@ -0,0 +1,137 @@ +import Component from 'flarum/Component'; +import FieldSet from 'flarum/components/FieldSet'; +import Select from 'flarum/components/Select'; +import Button from 'flarum/components/Button'; + +export default class BasicsPage extends Component { + constructor(...args) { + super(...args); + + this.loading = false; + + this.fields = [ + 'forum_title', + 'forum_description', + 'default_locale', + 'default_route', + 'welcome_title', + 'welcome_message' + ]; + this.values = {}; + + const config = app.forum.attribute('config'); + this.fields.forEach(key => this.values[key] = m.prop(config[key])); + + this.localeOptions = {}; + const locales = app.forum.attribute('availableLocales'); + for (const i in locales) { + this.localeOptions[i] = `${locales[i]} (${i})`; + } + } + + view() { + return ( + <div className="BasicsPage"> + <div className="container"> + <form onsubmit={this.onsubmit.bind(this)}> + {FieldSet.component({ + label: 'Forum Title', + children: [ + <input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)}/> + ] + })} + + {FieldSet.component({ + label: 'Forum Description', + children: [ + <div className="helpText"> + Enter a short sentence or two that describes your community. This will appear in the meta tag and show up in search engines. + </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: 'Default Language', + children: [ + Select.component({ + options: this.localeOptions, + onchange: this.values.default_locale + }) + ] + }) + : ''} + + {FieldSet.component({ + label: 'Home Page', + className: 'BasicsPage-homePage', + children: [ + <div className="helpText"> + Choose the page which users will first see when they visit your forum. If entering a custom value, use the path relative to the forum root. + </div>, + <label className="checkbox"> + <input type="radio" name="homePage" value="/all" checked={this.values.default_route() === '/all'} onclick={m.withAttr('value', this.values.default_route)}/> + All Discussions + </label>, + <label className="checkbox"> + <input type="radio" name="homePage" value="custom" checked={this.values.default_route() !== '/all'} onclick={() => { + this.values.default_route(''); + m.redraw(true); + this.$('.BasicsPage-homePage input').select(); + }}/> + Custom <input className="FormControl" value={this.values.default_route()} onchange={m.withAttr('value', this.values.default_route)} style={this.values.default_route() !== '/all' ? 'margin-top: 5px' : 'display:none'}/> + </label> + ] + })} + + {FieldSet.component({ + label: 'Welcome Banner', + className: 'BasicsPage-welcomeBanner', + children: [ + <div className="helpText"> + Configure the text that displays in the banner on the All Discussions page. Use this to welcome guests to your forum. + </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({ + type: 'submit', + className: 'Button Button--primary', + children: 'Save Changes', + loading: this.loading, + disabled: !this.changed() + })} + </form> + </div> + </div> + ); + } + + changed() { + const config = app.forum.attribute('config'); + + return this.fields.some(key => this.values[key]() !== config[key]); + } + + onsubmit(e) { + e.preventDefault(); + + if (this.loading) return; + + this.loading = true; + + const config = {}; + + this.fields.forEach(key => config[key] = this.values[key]()); + + app.forum.save({config}).finally(() => { + this.loading = false; + m.redraw(); + }); + } +} diff --git a/js/lib/Model.js b/js/lib/Model.js index 55362861b..df16a5e63 100644 --- a/js/lib/Model.js +++ b/js/lib/Model.js @@ -155,7 +155,7 @@ export default class Model { return app.request({ method: this.exists ? 'PATCH' : 'POST', - url: app.forum.attribute('apiUrl') + '/' + this.data.type + (this.exists ? '/' + this.data.id : ''), + url: app.forum.attribute('apiUrl') + this.apiEndpoint(), data: {data} }).then( // If everything went well, we'll make sure the store knows that this @@ -187,13 +187,23 @@ export default class Model { return app.request({ method: 'DELETE', - url: app.forum.attribute('apiUrl') + '/' + this.data.type + '/' + this.data.id, + url: app.forum.attribute('apiUrl') + this.apiEndpoint(), data }).then( () => this.exists = false ); } + /** + * Construct a path to the API endpoint for this resource. + * + * @return {String} + * @protected + */ + apiEndpoint() { + return '/' + this.data.type + (this.exists ? '/' + this.data.id : ''); + } + /** * Generate a function which returns the value of the given attribute. * diff --git a/js/lib/components/Button.js b/js/lib/components/Button.js index f99bda0ce..5991cfe27 100644 --- a/js/lib/components/Button.js +++ b/js/lib/components/Button.js @@ -1,6 +1,7 @@ import Component from 'flarum/Component'; import icon from 'flarum/helpers/icon'; import extract from 'flarum/utils/extract'; +import LoadingIndicator from 'flarum/components/LoadingIndicator'; /** * The `Button` component defines an element which, when clicked, performs an @@ -11,6 +12,7 @@ import extract from 'flarum/utils/extract'; * - `disabled` Whether or not the button is disabled. If truthy, the button * will be given a 'disabled' class name, and any `onclick` handler will be * removed. + * - `loading` Whether or not the button should be in a disabled loading state. * * All other props will be assigned as attributes on the button element. * @@ -28,8 +30,9 @@ export default class Button extends Component { const iconName = extract(attrs, 'icon'); if (iconName) attrs.className += ' hasIcon'; - if (attrs.disabled) { - attrs.className += ' disabled'; + const loading = extract(attrs, 'loading'); + if (attrs.disabled || loading) { + attrs.className += ' disabled' + (loading ? ' loading' : ''); delete attrs.onclick; } @@ -47,7 +50,8 @@ export default class Button extends Component { return [ iconName ? icon(iconName, {className: 'Button-icon'}) : '', ' ', - this.props.children ? <span className="Button-label">{this.props.children}</span> : '' + this.props.children ? <span className="Button-label">{this.props.children}</span> : '', + this.props.loading ? LoadingIndicator.component({size: 'tiny', className: 'LoadingIndicator--inline'}) : '' ]; } } diff --git a/js/lib/models/Forum.js b/js/lib/models/Forum.js index b19a5d4a7..2898b170b 100644 --- a/js/lib/models/Forum.js +++ b/js/lib/models/Forum.js @@ -1,3 +1,7 @@ import Model from 'flarum/Model'; -export default class Forum extends Model {} +export default class Forum extends Model { + apiEndpoint() { + return '/forum'; + } +} diff --git a/less/admin/BasicsPage.less b/less/admin/BasicsPage.less new file mode 100644 index 000000000..52d193961 --- /dev/null +++ b/less/admin/BasicsPage.less @@ -0,0 +1,33 @@ +.BasicsPage { + padding: 20px 0; + + @media @desktop-up { + .container { + max-width: 600px; + margin: 0; + } + } + + fieldset { + margin-bottom: 30px; + + > ul { + list-style: none; + margin: 0; + padding: 0; + } + } +} + +.BasicsPage-welcomeBanner-input { + :first-child { + margin-bottom: 1px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + font-weight: bold; + } + :last-child { + border-top-right-radius: 0; + border-top-left-radius: 0; + } +} diff --git a/less/forum/Composer.less b/less/forum/Composer.less index d55375823..b9c58035a 100644 --- a/less/forum/Composer.less +++ b/less/forum/Composer.less @@ -331,10 +331,5 @@ border-top: 0; padding: 20px 0; } - - & .Button--primary { - padding-left: 20px; - padding-right: 20px; - } } } diff --git a/less/lib/Button.less b/less/lib/Button.less index 80d1906d9..3660c16bf 100755 --- a/less/lib/Button.less +++ b/less/lib/Button.less @@ -86,6 +86,20 @@ pointer-events: none; // Future-proof disabling of clicks on `<a>` elements } } + + .Button-label { + .transition(margin-right 0.1s); + } + .LoadingIndicator { + color: inherit; + margin: 0 -10px 0 -15px; + float: right; + } + &.loading { + .Button-label { + margin-right: 20px; + } + } } .Button--color(@color; @background) { @@ -155,6 +169,8 @@ .Button--primary { .Button--color(@body-bg, @primary-color); font-weight: bold; + padding-left: 20px; + padding-right: 20px; .Button-icon { display: none; diff --git a/src/Api/Actions/Forum/ShowAction.php b/src/Api/Actions/Forum/ShowAction.php index 2125cae5d..b8aebaf34 100644 --- a/src/Api/Actions/Forum/ShowAction.php +++ b/src/Api/Actions/Forum/ShowAction.php @@ -2,6 +2,7 @@ use Flarum\Api\Actions\SerializeResourceAction; use Flarum\Api\JsonApiRequest; +use Flarum\Core\Groups\Group; use Tobscure\JsonApi\Document; class ShowAction extends SerializeResourceAction @@ -14,7 +15,9 @@ class ShowAction extends SerializeResourceAction /** * @inheritdoc */ - public $include = []; + public $include = [ + 'groups' => true + ]; /** * @inheritdoc @@ -51,6 +54,10 @@ class ShowAction extends SerializeResourceAction */ protected function data(JsonApiRequest $request, Document $document) { - return app('flarum.forum'); + $forum = app('flarum.forum'); + + $forum->groups = Group::all(); + + return $forum; } } diff --git a/src/Api/Actions/Forum/UpdateAction.php b/src/Api/Actions/Forum/UpdateAction.php new file mode 100644 index 000000000..0ad33cf04 --- /dev/null +++ b/src/Api/Actions/Forum/UpdateAction.php @@ -0,0 +1,53 @@ +<?php namespace Flarum\Api\Actions\Forum; + +use Flarum\Core\Exceptions\PermissionDeniedException; +use Flarum\Core\Settings\SettingsRepository; +use Flarum\Api\Actions\SerializeResourceAction; +use Flarum\Api\JsonApiRequest; +use Tobscure\JsonApi\Document; + +class UpdateAction extends SerializeResourceAction +{ + /** + * @inheritdoc + */ + public $serializer = 'Flarum\Api\Serializers\ForumSerializer'; + + /** + * @var SettingsRepository + */ + protected $settings; + + /** + * @param SettingsRepository $settings + */ + public function __construct(SettingsRepository $settings) + { + $this->settings = $settings; + } + + /** + * Get the forum, ready to be serialized and assigned to the JsonApi + * response. + * + * @param JsonApiRequest $request + * @param Document $document + * @return \Flarum\Core\Forum + */ + protected function data(JsonApiRequest $request, Document $document) + { + if (! $request->actor->isAdmin()) { + throw new PermissionDeniedException; + } + + $config = $request->get('data.attributes.config'); + + if (is_array($config)) { + foreach ($config as $k => $v) { + $this->settings->set($k, $v); + } + } + + return app('flarum.forum'); + } +} diff --git a/src/Api/ApiServiceProvider.php b/src/Api/ApiServiceProvider.php index c3c9860d8..e08b42b8d 100644 --- a/src/Api/ApiServiceProvider.php +++ b/src/Api/ApiServiceProvider.php @@ -94,6 +94,13 @@ class ApiServiceProvider extends ServiceProvider $this->action('Flarum\Api\Actions\Forum\ShowAction') ); + // Save forum information + $routes->patch( + '/forum', + 'flarum.api.forum.update', + $this->action('Flarum\Api\Actions\Forum\UpdateAction') + ); + // Retrieve authentication token $routes->post( '/token', diff --git a/src/Api/Serializers/ForumSerializer.php b/src/Api/Serializers/ForumSerializer.php index b8b623436..d4915bf33 100644 --- a/src/Api/Serializers/ForumSerializer.php +++ b/src/Api/Serializers/ForumSerializer.php @@ -1,6 +1,7 @@ <?php namespace Flarum\Api\Serializers; use Flarum\Core; +use Flarum\Core\Groups\Permission; class ForumSerializer extends Serializer { @@ -22,13 +23,29 @@ class ForumSerializer extends Serializer */ protected function getDefaultAttributes($forum) { - return [ - 'title' => $forum->title, + $attributes = [ + 'title' => Core::config('forum_title'), 'baseUrl' => Core::config('base_url'), 'apiUrl' => Core::config('api_url'), 'welcomeTitle' => Core::config('welcome_title'), 'welcomeMessage' => Core::config('welcome_message'), 'themePrimaryColor' => Core::config('theme_primary_color') ]; + + if ($this->actor->isAdmin()) { + $attributes['config'] = app('Flarum\Core\Settings\SettingsRepository')->all(); + $attributes['availableLocales'] = app('flarum.localeManager')->getLocales(); + $attributes['permissions'] = Permission::map(); + } + + return $attributes; + } + + /** + * @return callable + */ + protected function groups() + { + return $this->hasMany('Flarum\Api\Serializers\GroupSerializer'); } } diff --git a/src/Locale/LocaleManager.php b/src/Locale/LocaleManager.php index bc9e600e8..c7debf173 100644 --- a/src/Locale/LocaleManager.php +++ b/src/Locale/LocaleManager.php @@ -2,12 +2,24 @@ class LocaleManager { + protected $locales = []; + protected $translations = []; protected $js = []; protected $config = []; + public function addLocale($locale, $name) + { + $this->locales[$locale] = $name; + } + + public function getLocales() + { + return $this->locales; + } + public function addTranslations($locale, $translations) { if (! isset($this->translations[$locale])) { diff --git a/src/Locale/LocaleServiceProvider.php b/src/Locale/LocaleServiceProvider.php index 510e50413..1cc846b2f 100644 --- a/src/Locale/LocaleServiceProvider.php +++ b/src/Locale/LocaleServiceProvider.php @@ -14,15 +14,16 @@ class LocaleServiceProvider extends ServiceProvider { $manager = $this->app->make('flarum.localeManager'); - $this->registerLocale($manager, 'en'); + $this->registerLocale($manager, 'en', 'English'); event(new RegisterLocales($manager)); } - public function registerLocale(LocaleManager $manager, $locale) + public function registerLocale(LocaleManager $manager, $locale, $title) { $path = __DIR__.'/../../locale/'.$locale; + $manager->addLocale($locale, $title); $manager->addTranslations($locale, $path.'.yml'); $manager->addConfig($locale, $path.'.php'); $manager->addJsFile($locale, $path.'.js');