-
-
{app.translator.trans('flarum-tags.admin.tag_settings.required_primary_text')}
-
-
-
-
{app.translator.trans('flarum-tags.admin.tag_settings.required_secondary_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 [
+ ,
+ ];
+ }
+
+ 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',