From 30694985db21bf5fbf24d1c1ea0134ce01b82155 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sat, 21 Oct 2023 17:47:17 +0100 Subject: [PATCH] feat: advanced page and localized driver settings (#3905) --- extensions/nicknames/js/src/admin/index.js | 7 ++ extensions/nicknames/locale/en.yml | 3 + .../tags/js/src/admin/components/TagsPage.js | 80 +++++++++-------- extensions/tags/less/admin/TagsPage.less | 88 +++++-------------- .../core/js/src/admin/AdminApplication.tsx | 2 + .../core/js/src/admin/components/AdminNav.js | 10 +++ .../js/src/admin/components/AdminPage.tsx | 9 ++ .../js/src/admin/components/AdvancedPage.tsx | 73 +++++++++++++++ .../js/src/admin/components/BasicsPage.tsx | 34 ++++++- .../src/admin/components/FormSectionGroup.tsx | 35 ++++++++ .../js/src/admin/components/StatusWidget.js | 20 +++++ framework/core/js/src/admin/routes.ts | 2 + framework/core/less/admin.less | 1 + .../core/less/admin/FormSectionGroup.less | 24 +++++ framework/core/locale/core.yml | 29 ++++++ .../core/src/Admin/Content/AdminPayload.php | 26 ++++++ .../core/src/Install/Steps/WriteSettings.php | 2 +- .../src/Settings/SettingsServiceProvider.php | 2 +- 18 files changed, 337 insertions(+), 110 deletions(-) create mode 100644 framework/core/js/src/admin/components/AdvancedPage.tsx create mode 100644 framework/core/js/src/admin/components/FormSectionGroup.tsx create mode 100644 framework/core/less/admin/FormSectionGroup.less diff --git a/extensions/nicknames/js/src/admin/index.js b/extensions/nicknames/js/src/admin/index.js index 929bce3513..196821b7a2 100644 --- a/extensions/nicknames/js/src/admin/index.js +++ b/extensions/nicknames/js/src/admin/index.js @@ -1,6 +1,9 @@ import app from 'flarum/admin/app'; import Alert from 'flarum/common/components/Alert'; import Link from 'flarum/common/components/Link'; +import BasicsPage from 'flarum/admin/components/BasicsPage'; +import extractText from 'flarum/common/utils/extractText'; +import { extend } from 'flarum/common/extend'; app.initializers.add('flarum/nicknames', () => { app.extensionData @@ -55,4 +58,8 @@ app.initializers.add('flarum/nicknames', () => { }, 'start' ); + + extend(BasicsPage.prototype, 'driverLocale', function (locale) { + locale.display_name['nickname'] = extractText(app.translator.trans('flarum-nicknames.admin.basics.display_name_driver_options.nickname')); + }); }); diff --git a/extensions/nicknames/locale/en.yml b/extensions/nicknames/locale/en.yml index dfe15640ad..e969afea07 100644 --- a/extensions/nicknames/locale/en.yml +++ b/extensions/nicknames/locale/en.yml @@ -1,5 +1,8 @@ flarum-nicknames: admin: + basics: + display_name_driver_options: + nickname: Nickname permissions: edit_own_nickname_label: Edit own nickname settings: diff --git a/extensions/tags/js/src/admin/components/TagsPage.js b/extensions/tags/js/src/admin/components/TagsPage.js index 21becc24d9..f48e4a4cdd 100644 --- a/extensions/tags/js/src/admin/components/TagsPage.js +++ b/extensions/tags/js/src/admin/components/TagsPage.js @@ -10,6 +10,7 @@ import Form from 'flarum/common/components/Form'; import EditTagModal from './EditTagModal'; import tagIcon from '../../common/helpers/tagIcon'; import sortTags from '../../common/utils/sortTags'; +import FormSectionGroup, { FormSection } from '@flarum/core/src/admin/components/FormSectionGroup'; function tagItem(tag) { return ( @@ -66,17 +67,15 @@ export default class TagsPage extends ExtensionPage {
-
-
- + +
    {tags.filter((tag) => tag.position() !== null && !tag.isChild()).map(tagItem)}
-
+ -
- +
    {tags .filter((tag) => tag.position() === null) @@ -86,41 +85,44 @@ export default class TagsPage extends ExtensionPage { -
-
-
- -
{app.translator.trans('flarum-tags.admin.tag_settings.required_primary_text')}
-
- - {app.translator.trans('flarum-tags.admin.tag_settings.range_separator_text')} - + + + + +
+ +
{app.translator.trans('flarum-tags.admin.tag_settings.required_primary_text')}
+
+ + {app.translator.trans('flarum-tags.admin.tag_settings.range_separator_text')} + +
-
-
- -
{app.translator.trans('flarum-tags.admin.tag_settings.required_secondary_text')}
-
- - {app.translator.trans('flarum-tags.admin.tag_settings.range_separator_text')} - +
+ +
{app.translator.trans('flarum-tags.admin.tag_settings.required_secondary_text')}
+
+ + {app.translator.trans('flarum-tags.admin.tag_settings.range_separator_text')} + +
-
-
{this.submitButton()}
- -
+
{this.submitButton()}
+ + +

{app.translator.trans('flarum-tags.admin.tags.about_tags_text')}

diff --git a/extensions/tags/less/admin/TagsPage.less b/extensions/tags/less/admin/TagsPage.less index 20c9ca9322..7f19b45545 100644 --- a/extensions/tags/less/admin/TagsPage.less +++ b/extensions/tags/less/admin/TagsPage.less @@ -13,7 +13,6 @@ .TagsContent-list { padding: 20px 0 0; - } .TagList, @@ -22,6 +21,7 @@ padding: 0; color: var(--muted-color); font-size: 13px; + margin-top: 0; >li { display: inline-block; @@ -80,77 +80,35 @@ li:not(.sortable-dragging)>.TagListItem-info:hover>.Button { height: 34px; } -.SettingsGroups { - display: flex; - column-count: 3; - column-gap: 30px; - flex-wrap: wrap; - - @media (@tablet-up) { - .TagGroup--secondary { - max-width: 250px !important; - } +@media (@tablet-up) { + .TagGroup--secondary { + max-width: 250px !important; } +} - .Form { - min-width: 300px; - max-height: 500px; - - >label { - margin-bottom: 10px; - } +.TagList-button { + background: none; + border: 1px dashed var(--control-bg); + height: 40px; + margin: auto auto 0 0; +} - .TagSettings-rangeInput { - input { - width: 80px; - display: inline; - margin: 0 5px; +.TagSettings-rangeInput { + input { + width: 80px; + display: inline; + margin: 0 5px; - &:first-child { - margin-left: 0; - } - } + &:first-child { + margin-left: 0; } } +} - .TagGroup, - .Form { - display: inline-grid; - padding: 10px 20px; - min-height: 20vh; - max-width: 400px; - grid-template-rows: min-content; - border: 1px solid var(--control-bg); - border-radius: var(--border-radius); - flex: 1 1 160px; - - @media (max-width: 1209px) { - margin-bottom: 20px; - } - - >ol { - >li { - margin-top: 8px; - - .Button { - float: right; - visibility: hidden; - margin: -8px -16px -8px 16px; - } - } - } - - .TagList-button { - background: none; - border: 1px dashed var(--control-bg); - height: 40px; - margin: auto auto 0 0; - } - - >label { - float: left; - font-weight: bold; - color: var(--muted-color); +.TagGroup { + ol { + > li:not(:first-child) { + margin-top: 8px; } } } diff --git a/framework/core/js/src/admin/AdminApplication.tsx b/framework/core/js/src/admin/AdminApplication.tsx index 9a16f3381a..751a9a3fb2 100644 --- a/framework/core/js/src/admin/AdminApplication.tsx +++ b/framework/core/js/src/admin/AdminApplication.tsx @@ -40,7 +40,9 @@ export interface AdminApplicationData extends ApplicationData { modelStatistics: Record; displayNameDrivers: string[]; slugDrivers: Record; + searchDrivers: Record; permissions: Record; + advancedPageEmpty: boolean; } export default class AdminApplication extends Application { diff --git a/framework/core/js/src/admin/components/AdminNav.js b/framework/core/js/src/admin/components/AdminNav.js index 1f18a65e5b..3972bc4140 100644 --- a/framework/core/js/src/admin/components/AdminNav.js +++ b/framework/core/js/src/admin/components/AdminNav.js @@ -110,6 +110,16 @@ export default class AdminNav extends Component { 50 ); + if (app.data.settings.show_advanced_settings && !app.data.advancedPageEmpty) { + items.add( + 'advanced', + + {app.translator.trans('core.admin.nav.advanced_button')} + , + 40 + ); + } + items.add( 'search',
diff --git a/framework/core/js/src/admin/components/AdminPage.tsx b/framework/core/js/src/admin/components/AdminPage.tsx index a705c6b047..5c0368414b 100644 --- a/framework/core/js/src/admin/components/AdminPage.tsx +++ b/framework/core/js/src/admin/components/AdminPage.tsx @@ -14,6 +14,7 @@ import ColorPreviewInput from '../../common/components/ColorPreviewInput'; import ItemList from '../../common/utils/ItemList'; import type { IUploadImageButtonAttrs } from './UploadImageButton'; import UploadImageButton from './UploadImageButton'; +import extractText from '../../common/utils/extractText'; export interface AdminHeaderOptions { title: Mithril.Children; @@ -410,4 +411,12 @@ export default abstract class AdminPage { + return { + 'Flarum\\Discussion\\Discussion': extractText(app.translator.trans('core.admin.models.discussions')), + 'Flarum\\User\\User': extractText(app.translator.trans('core.admin.models.users')), + 'Flarum\\Post\\Post': extractText(app.translator.trans('core.admin.models.posts')), + }; + } } diff --git a/framework/core/js/src/admin/components/AdvancedPage.tsx b/framework/core/js/src/admin/components/AdvancedPage.tsx new file mode 100644 index 0000000000..21067aa061 --- /dev/null +++ b/framework/core/js/src/admin/components/AdvancedPage.tsx @@ -0,0 +1,73 @@ +import app from '../../admin/app'; +import AdminPage from './AdminPage'; +import type { IPageAttrs } from '../../common/components/Page'; +import type Mithril from 'mithril'; +import Form from '../../common/components/Form'; +import extractText from '../../common/utils/extractText'; +import FormSectionGroup, { FormSection } from './FormSectionGroup'; + +export default class AdvancedPage extends AdminPage { + searchDriverOptions: Record> = {}; + + oninit(vnode: Mithril.Vnode) { + super.oninit(vnode); + + const locale = this.driverLocale(); + + Object.keys(app.data.searchDrivers).forEach((model) => { + this.searchDriverOptions[model] = {}; + + app.data.searchDrivers[model].forEach((option) => { + this.searchDriverOptions[model][option] = locale.search[option] || option; + }); + }); + } + + headerInfo() { + return { + className: 'AdvancedPage', + icon: 'fas fa-cog', + title: app.translator.trans('core.admin.advanced.title'), + description: app.translator.trans('core.admin.advanced.description'), + }; + } + + content() { + return [ +
+ + + + {Object.keys(this.searchDriverOptions).map((model) => { + const options = this.searchDriverOptions[model]; + const modelLocale = this.modelLocale()[model] || model; + + if (Object.keys(options).length > 1) { + return this.buildSettingComponent({ + type: 'select', + setting: `search_driver_${model}`, + options, + label: app.translator.trans('core.admin.advanced.search.driver_heading', { model: modelLocale }), + help: app.translator.trans('core.admin.advanced.search.driver_text', { model: modelLocale }), + }); + } + + return null; + })} + + +
+ +
{this.submitButton()}
+ , + ]; + } + + driverLocale(): Record> { + return { + search: { + default: extractText(app.translator.trans('core.admin.advanced.search.driver_options.default')), + }, + }; + } +} diff --git a/framework/core/js/src/admin/components/BasicsPage.tsx b/framework/core/js/src/admin/components/BasicsPage.tsx index abfa9f77c8..edd81393e6 100644 --- a/framework/core/js/src/admin/components/BasicsPage.tsx +++ b/framework/core/js/src/admin/components/BasicsPage.tsx @@ -5,8 +5,13 @@ import AdminPage from './AdminPage'; import type { IPageAttrs } from '../../common/components/Page'; import type Mithril from 'mithril'; import Form from '../../common/components/Form'; +import extractText from '../../common/utils/extractText'; export type HomePageItem = { path: string; label: Mithril.Children }; +export type DriverLocale = { + display_name: Record; + slug: Record>; +}; export default class BasicsPage extends AdminPage { localeOptions: Record = {}; @@ -20,15 +25,17 @@ export default class BasicsPage ext this.localeOptions[i] = `${app.data.locales[i]} (${i})`; }); + const driverLocale = this.driverLocale(); + app.data.displayNameDrivers.forEach((identifier) => { - this.displayNameOptions[identifier] = identifier; + this.displayNameOptions[identifier] = driverLocale.display_name[identifier] || identifier; }); Object.keys(app.data.slugDrivers).forEach((model) => { this.slugDriverOptions[model] = {}; app.data.slugDrivers[model].forEach((option) => { - this.slugDriverOptions[model][option] = option; + this.slugDriverOptions[model][option] = (driverLocale.slug[model] && driverLocale.slug[model][option]) || option; }); }); } @@ -108,14 +115,15 @@ export default class BasicsPage ext {Object.keys(this.slugDriverOptions).map((model) => { const options = this.slugDriverOptions[model]; + const modelLocale = this.modelLocale()[model] || model; if (Object.keys(options).length > 1) { return this.buildSettingComponent({ type: 'select', setting: `slug_driver_${model}`, options, - label: app.translator.trans('core.admin.basics.slug_driver_heading', { model }), - help: app.translator.trans('core.admin.basics.slug_driver_text', { model }), + label: app.translator.trans('core.admin.basics.slug_driver_heading', { model: modelLocale }), + help: app.translator.trans('core.admin.basics.slug_driver_text', { model: modelLocale }), }); } @@ -141,4 +149,22 @@ export default class BasicsPage ext return items; } + + driverLocale(): DriverLocale { + return { + display_name: { + username: extractText(app.translator.trans('core.admin.basics.display_name_driver_options.username')), + }, + slug: { + 'Flarum\\Discussion\\Discussion': { + default: extractText(app.translator.trans('core.admin.basics.slug_driver_options.discussions.default')), + utf8: extractText(app.translator.trans('core.admin.basics.slug_driver_options.discussions.utf8')), + }, + 'Flarum\\User\\User': { + default: extractText(app.translator.trans('core.admin.basics.slug_driver_options.users.default')), + id: extractText(app.translator.trans('core.admin.basics.slug_driver_options.users.id')), + }, + }, + }; + } } diff --git a/framework/core/js/src/admin/components/FormSectionGroup.tsx b/framework/core/js/src/admin/components/FormSectionGroup.tsx new file mode 100644 index 0000000000..fa45574152 --- /dev/null +++ b/framework/core/js/src/admin/components/FormSectionGroup.tsx @@ -0,0 +1,35 @@ +import Component from '../../common/Component'; +import type { ComponentAttrs } from '../../common/Component'; +import Mithril from 'mithril'; +import classList from '../../common/utils/classList'; + +export interface IFormSectionGroupAttrs extends ComponentAttrs {} + +export default class FormSectionGroup extends Component { + view(vnode: Mithril.Vnode) { + const { className, ...attrs } = this.attrs; + + return ( +
+ {vnode.children} +
+ ); + } +} + +export interface IFormSectionAttrs extends ComponentAttrs { + label: any; +} + +export class FormSection extends Component { + view(vnode: Mithril.Vnode) { + const { className, ...attrs } = this.attrs; + + return ( +
+ +
{vnode.children}
+
+ ); + } +} diff --git a/framework/core/js/src/admin/components/StatusWidget.js b/framework/core/js/src/admin/components/StatusWidget.js index 4dc042d29c..ee6dc3ebf6 100644 --- a/framework/core/js/src/admin/components/StatusWidget.js +++ b/framework/core/js/src/admin/components/StatusWidget.js @@ -6,6 +6,7 @@ import Dropdown from '../../common/components/Dropdown'; import Button from '../../common/components/Button'; import LoadingModal from './LoadingModal'; import LinkButton from '../../common/components/LinkButton'; +import saveSettings from '../utils/saveSettings.js'; export default class StatusWidget extends DashboardWidget { className() { @@ -71,6 +72,25 @@ export default class StatusWidget extends DashboardWidget { ); + if (!app.data.advancedPageEmpty) { + items.add( + 'toggleAdvancedPage', + + ); + } + return items; } diff --git a/framework/core/js/src/admin/routes.ts b/framework/core/js/src/admin/routes.ts index 87271c88d8..fda289869b 100644 --- a/framework/core/js/src/admin/routes.ts +++ b/framework/core/js/src/admin/routes.ts @@ -7,6 +7,7 @@ import MailPage from './components/MailPage'; import UserListPage from './components/UserListPage'; import ExtensionPage from './components/ExtensionPage'; import ExtensionPageResolver from './resolvers/ExtensionPageResolver'; +import AdvancedPage from './components/AdvancedPage'; /** * Helper functions to generate URLs to admin pages. @@ -24,6 +25,7 @@ export default function (app: AdminApplication) { appearance: { path: '/appearance', component: AppearancePage }, mail: { path: '/mail', component: MailPage }, users: { path: '/users', component: UserListPage }, + advanced: { path: '/advanced', component: AdvancedPage }, extension: { path: '/extension/:id', component: ExtensionPage, resolverClass: ExtensionPageResolver }, }; } diff --git a/framework/core/less/admin.less b/framework/core/less/admin.less index b335ab52ab..6101bcfc65 100644 --- a/framework/core/less/admin.less +++ b/framework/core/less/admin.less @@ -5,6 +5,7 @@ @import "admin/CreateUserModal"; @import "admin/DashboardPage"; @import "admin/DebugWarningWidget"; +@import "admin/FormSectionGroup"; @import "admin/BasicsPage"; @import "admin/PermissionsPage"; @import "admin/EditGroupModal"; diff --git a/framework/core/less/admin/FormSectionGroup.less b/framework/core/less/admin/FormSectionGroup.less new file mode 100644 index 0000000000..4db1c54708 --- /dev/null +++ b/framework/core/less/admin/FormSectionGroup.less @@ -0,0 +1,24 @@ +.FormSectionGroup { + display: flex; + column-gap: 30px; + flex-wrap: wrap; +} + +.FormSection { + --gap: 24px; + display: inline-grid; + padding: 10px 20px 20px; + min-height: 20vh; + min-width: 300px; + max-width: 400px; + grid-template-rows: min-content; + border: 1px solid var(--control-bg); + border-radius: var(--border-radius); + flex: 1 1 160px; + gap: var(--gap); +} + +.FormSection > label { + font-weight: bold; + color: var(--muted-color); +} diff --git a/framework/core/locale/core.yml b/framework/core/locale/core.yml index f11703c49a..015f790862 100644 --- a/framework/core/locale/core.yml +++ b/framework/core/locale/core.yml @@ -7,6 +7,17 @@ core: # Translations in this namespace are used by the admin interface. admin: + # These translations are used in the Advanced page. + advanced: + description: "Configure advanced settings for your forum." + search: + section_label: Search Drivers + driver_heading: "Search Driver: {model}" + driver_text: Select a driver to be used for searching this model. + driver_options: + default: Default database search + title: Advanced + # These translations are used in the Appearance page. appearance: colored_header_label: Colored Header @@ -38,6 +49,8 @@ core: all_discussions_label: => core.ref.all_discussions default_language_heading: Default Language description: "Set your forum title, language, and other basic settings." + display_name_driver_options: + username: Username display_name_heading: User Display Name display_name_text: Select the driver that should be used for users' display names. By default, the username is shown. forum_description_heading: Forum Description @@ -46,6 +59,13 @@ core: home_page_heading: Home Page home_page_text: Choose the page which users will first see when they visit your forum. show_language_selector_label: Show language selector + slug_driver_options: + discussions: + default: ID with slug + utf8: ID with UTF-8 slug + users: + default: Username + id: ID slug_driver_heading: "Slug Driver: {model}" slug_driver_text: Select a driver to be used for slugging this model. title: Basics @@ -78,6 +98,7 @@ core: inactive: Inactive never-run: Never run title: Dashboard + toggle_advanced_page_button: Toggle Advanced Page tools_button: Tools # These translations are used in the debug warning widget. @@ -183,8 +204,16 @@ core: loading: title: Please Wait... + # These translations are used anywhere to localize model names for drivers. + models: + discussions: => core.ref.discussions + posts: => core.ref.posts + users: => core.ref.users + # These translations are used in the navigation bar. nav: + advanced_button: => core.admin.advanced.title + advanced_title: => core.admin.advanced.description appearance_button: => core.admin.appearance.title appearance_title: => core.admin.appearance.description basics_button: => core.admin.basics.title diff --git a/framework/core/src/Admin/Content/AdminPayload.php b/framework/core/src/Admin/Content/AdminPayload.php index d2e8af4681..557115389e 100644 --- a/framework/core/src/Admin/Content/AdminPayload.php +++ b/framework/core/src/Admin/Content/AdminPayload.php @@ -9,11 +9,14 @@ namespace Flarum\Admin\Content; +use Flarum\Database\AbstractModel; use Flarum\Extension\ExtensionManager; use Flarum\Foundation\ApplicationInfoProvider; use Flarum\Foundation\Config; use Flarum\Frontend\Document; use Flarum\Group\Permission; +use Flarum\Search\AbstractDriver; +use Flarum\Search\SearcherInterface; use Flarum\Settings\Event\Deserializing; use Flarum\Settings\SettingsRepositoryInterface; use Flarum\User\User; @@ -52,6 +55,9 @@ public function __invoke(Document $document, Request $request): void $document->payload['slugDrivers'] = array_map(function ($resourceDrivers) { return array_keys($resourceDrivers); }, $this->container->make('flarum.http.slugDrivers')); + $document->payload['searchDrivers'] = $this->getSearchDrivers(); + + $document->payload['advancedPageEmpty'] = $this->checkAdvancedPageEmpty(); $document->payload['phpVersion'] = $this->appInfo->identifyPHPVersion(); $document->payload['mysqlVersion'] = $this->appInfo->identifyDatabaseVersion(); @@ -77,4 +83,24 @@ public function __invoke(Document $document, Request $request): void ] ]; } + + protected function getSearchDrivers(): array + { + $searchDriversPerModel = []; + + foreach ($this->container->make('flarum.search.drivers') as $driverClass => $searcherClasses) { + /** @var array, class-string> $searcherClasses */ + foreach ($searcherClasses as $modelClass => $searcherClass) { + /** @var class-string $driverClass */ + $searchDriversPerModel[$modelClass][] = $driverClass::name(); + } + } + + return $searchDriversPerModel; + } + + protected function checkAdvancedPageEmpty(): bool + { + return count($this->container->make('flarum.search.drivers')) === 1; + } } diff --git a/framework/core/src/Install/Steps/WriteSettings.php b/framework/core/src/Install/Steps/WriteSettings.php index b0320c6032..b62c535157 100644 --- a/framework/core/src/Install/Steps/WriteSettings.php +++ b/framework/core/src/Install/Steps/WriteSettings.php @@ -60,7 +60,7 @@ private function getDefaults(): array 'mail_driver' => 'mail', 'mail_format' => 'multipart', 'mail_from' => 'noreply@localhost', - 'slug_driver_Flarum\User\User' => 'default', // @todo: use a morph map instead `User::class => 'user'` = slug_driver_user (below as well) + 'slug_driver_Flarum\User\User' => 'default', 'theme_colored_header' => '0', 'theme_dark_mode' => '0', 'theme_primary_color' => '#4D698E', diff --git a/framework/core/src/Settings/SettingsServiceProvider.php b/framework/core/src/Settings/SettingsServiceProvider.php index 0bc60a978a..ec7e25672f 100644 --- a/framework/core/src/Settings/SettingsServiceProvider.php +++ b/framework/core/src/Settings/SettingsServiceProvider.php @@ -25,7 +25,7 @@ public function register(): void 'theme_primary_color' => '#4D698E', 'theme_secondary_color' => '#4D698E', 'mail_format' => 'multipart', - 'search_driver_Flarum\User\User' => 'default', // @todo: use a morph map instead `User::class => 'user'` = search_driver_user (below as well) + 'search_driver_Flarum\User\User' => 'default', 'search_driver_Flarum\Discussion\Discussion' => 'default', 'search_driver_Flarum\Group\Group' => 'default', 'search_driver_Flarum\Post\Post' => 'default',