mirror of
https://github.com/flarum/core.git
synced 2025-08-06 08:27:42 +02:00
feat: admin search UI (#4022)
This commit is contained in:
26
extensions/akismet/js/src/admin/extend.tsx
Normal file
26
extensions/akismet/js/src/admin/extend.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export default [
|
||||
new Extend.Admin()
|
||||
.setting(() => ({
|
||||
setting: 'flarum-akismet.api_key',
|
||||
type: 'text',
|
||||
label: app.translator.trans('flarum-akismet.admin.akismet_settings.api_key_label'),
|
||||
}))
|
||||
.setting(() => ({
|
||||
// https://blog.akismet.com/2014/04/23/theres-a-ninja-in-your-akismet/
|
||||
setting: 'flarum-akismet.delete_blatant_spam',
|
||||
type: 'boolean',
|
||||
label: app.translator.trans('flarum-akismet.admin.akismet_settings.delete_blatant_spam_label'),
|
||||
help: app.translator.trans('flarum-akismet.admin.akismet_settings.delete_blatant_spam_help'),
|
||||
}))
|
||||
.permission(
|
||||
() => ({
|
||||
icon: 'fas fa-vote-yea',
|
||||
label: app.translator.trans('flarum-akismet.admin.permissions.bypass_akismet'),
|
||||
permission: 'bypassAkismet',
|
||||
}),
|
||||
'start'
|
||||
),
|
||||
];
|
@@ -1,26 +1,7 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-akismet', () => {
|
||||
app.extensionData
|
||||
.for('flarum-akismet')
|
||||
.registerSetting({
|
||||
setting: 'flarum-akismet.api_key',
|
||||
type: 'text',
|
||||
label: app.translator.trans('flarum-akismet.admin.akismet_settings.api_key_label'),
|
||||
})
|
||||
.registerSetting({
|
||||
//https://blog.akismet.com/2014/04/23/theres-a-ninja-in-your-akismet/
|
||||
setting: 'flarum-akismet.delete_blatant_spam',
|
||||
type: 'boolean',
|
||||
label: app.translator.trans('flarum-akismet.admin.akismet_settings.delete_blatant_spam_label'),
|
||||
help: app.translator.trans('flarum-akismet.admin.akismet_settings.delete_blatant_spam_help'),
|
||||
})
|
||||
.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-vote-yea',
|
||||
label: app.translator.trans('flarum-akismet.admin.permissions.bypass_akismet'),
|
||||
permission: 'bypassAkismet',
|
||||
},
|
||||
'start'
|
||||
);
|
||||
// ...
|
||||
});
|
||||
|
@@ -1,43 +1,33 @@
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
app.initializers.add('flarum-approval', () => {
|
||||
extend(app, 'getRequiredPermissions', function (required, permission) {
|
||||
if (permission === 'discussion.startWithoutApproval') {
|
||||
required.push('startDiscussion');
|
||||
}
|
||||
if (permission === 'discussion.replyWithoutApproval') {
|
||||
required.push('discussion.reply');
|
||||
}
|
||||
});
|
||||
|
||||
app.extensionData
|
||||
.for('flarum-approval')
|
||||
.registerPermission(
|
||||
{
|
||||
export default [
|
||||
new Extend.Admin()
|
||||
.permission(
|
||||
() => ({
|
||||
icon: 'fas fa-check',
|
||||
label: app.translator.trans('flarum-approval.admin.permissions.start_discussions_without_approval_label'),
|
||||
permission: 'discussion.startWithoutApproval',
|
||||
},
|
||||
}),
|
||||
'start',
|
||||
95
|
||||
)
|
||||
.registerPermission(
|
||||
{
|
||||
.permission(
|
||||
() => ({
|
||||
icon: 'fas fa-check',
|
||||
label: app.translator.trans('flarum-approval.admin.permissions.reply_without_approval_label'),
|
||||
permission: 'discussion.replyWithoutApproval',
|
||||
},
|
||||
}),
|
||||
'reply',
|
||||
95
|
||||
)
|
||||
.registerPermission(
|
||||
{
|
||||
.permission(
|
||||
() => ({
|
||||
icon: 'fas fa-check',
|
||||
label: app.translator.trans('flarum-approval.admin.permissions.approve_posts_label'),
|
||||
permission: 'discussion.approvePosts',
|
||||
},
|
||||
}),
|
||||
'moderate',
|
||||
65
|
||||
);
|
||||
});
|
||||
),
|
||||
];
|
15
extensions/approval/js/src/admin/index.ts
Normal file
15
extensions/approval/js/src/admin/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-approval', () => {
|
||||
extend(app, 'getRequiredPermissions', function (required, permission) {
|
||||
if (permission === 'discussion.startWithoutApproval') {
|
||||
required.push('startDiscussion');
|
||||
}
|
||||
if (permission === 'discussion.replyWithoutApproval') {
|
||||
required.push('discussion.reply');
|
||||
}
|
||||
});
|
||||
});
|
15
extensions/approval/js/tsconfig.json
Normal file
15
extensions/approval/js/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use Flarum's tsconfig as a starting point
|
||||
"extends": "flarum-tsconfig",
|
||||
// This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder
|
||||
// and also tells your Typescript server to read core's global typings for
|
||||
// access to `dayjs` and `$` in the global namespace.
|
||||
"include": ["src/**/*", "../../../*/*/js/dist-typings/@types/**/*", "@types/**/*"],
|
||||
"compilerOptions": {
|
||||
// This will output typings to `dist-typings`
|
||||
"declarationDir": "./dist-typings",
|
||||
"paths": {
|
||||
"flarum/*": ["../../../framework/core/js/dist-typings/*"]
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,13 +1,14 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import app from 'flarum/admin/app';
|
||||
import { version } from '../common/cdn';
|
||||
|
||||
app.initializers.add('flarum-emoji', () => {
|
||||
app.extensionData.for('flarum-emoji').registerSetting({
|
||||
export default [
|
||||
new Extend.Admin().setting(() => ({
|
||||
setting: 'flarum-emoji.cdn',
|
||||
type: 'text',
|
||||
label: app.translator.trans('flarum-emoji.admin.settings.cdn_label'),
|
||||
help: app.translator.trans('flarum-emoji.admin.settings.cdn_help', {
|
||||
version: version,
|
||||
}),
|
||||
});
|
||||
});
|
||||
})),
|
||||
];
|
7
extensions/emoji/js/src/admin/index.ts
Normal file
7
extensions/emoji/js/src/admin/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-emoji', () => {
|
||||
// ...
|
||||
});
|
15
extensions/emoji/js/tsconfig.json
Normal file
15
extensions/emoji/js/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use Flarum's tsconfig as a starting point
|
||||
"extends": "flarum-tsconfig",
|
||||
// This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder
|
||||
// and also tells your Typescript server to read core's global typings for
|
||||
// access to `dayjs` and `$` in the global namespace.
|
||||
"include": ["src/**/*", "../../../*/*/js/dist-typings/@types/**/*", "@types/**/*"],
|
||||
"compilerOptions": {
|
||||
// This will output typings to `dist-typings`
|
||||
"declarationDir": "./dist-typings",
|
||||
"paths": {
|
||||
"flarum/*": ["../../../framework/core/js/dist-typings/*"]
|
||||
}
|
||||
}
|
||||
}
|
37
extensions/flags/js/src/admin/extend.tsx
Normal file
37
extensions/flags/js/src/admin/extend.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export default [
|
||||
new Extend.Admin()
|
||||
.setting(
|
||||
() => ({
|
||||
setting: 'flarum-flags.guidelines_url',
|
||||
type: 'text',
|
||||
label: app.translator.trans('flarum-flags.admin.settings.guidelines_url_label'),
|
||||
}),
|
||||
15
|
||||
)
|
||||
.setting(() => ({
|
||||
setting: 'flarum-flags.can_flag_own',
|
||||
type: 'boolean',
|
||||
label: app.translator.trans('flarum-flags.admin.settings.flag_own_posts_label'),
|
||||
}))
|
||||
.permission(
|
||||
() => ({
|
||||
icon: 'fas fa-flag',
|
||||
label: app.translator.trans('flarum-flags.admin.permissions.view_flags_label'),
|
||||
permission: 'discussion.viewFlags',
|
||||
}),
|
||||
'moderate',
|
||||
65
|
||||
)
|
||||
.permission(
|
||||
() => ({
|
||||
icon: 'fas fa-flag',
|
||||
label: app.translator.trans('flarum-flags.admin.permissions.flag_posts_label'),
|
||||
permission: 'discussion.flagPosts',
|
||||
}),
|
||||
'reply',
|
||||
65
|
||||
),
|
||||
];
|
@@ -1,38 +1,7 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
app.initializers.add('flarum-flags', () => {
|
||||
app.extensionData
|
||||
.for('flarum-flags')
|
||||
.registerSetting(
|
||||
{
|
||||
setting: 'flarum-flags.guidelines_url',
|
||||
type: 'text',
|
||||
label: app.translator.trans('flarum-flags.admin.settings.guidelines_url_label'),
|
||||
},
|
||||
15
|
||||
)
|
||||
.registerSetting({
|
||||
setting: 'flarum-flags.can_flag_own',
|
||||
type: 'boolean',
|
||||
label: app.translator.trans('flarum-flags.admin.settings.flag_own_posts_label'),
|
||||
})
|
||||
.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-flag',
|
||||
label: app.translator.trans('flarum-flags.admin.permissions.view_flags_label'),
|
||||
permission: 'discussion.viewFlags',
|
||||
},
|
||||
'moderate',
|
||||
65
|
||||
)
|
||||
export { default as extend } from './extend';
|
||||
|
||||
.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-flag',
|
||||
label: app.translator.trans('flarum-flags.admin.permissions.flag_posts_label'),
|
||||
permission: 'discussion.flagPosts',
|
||||
},
|
||||
'reply',
|
||||
65
|
||||
);
|
||||
app.initializers.add('flarum-flags', () => {
|
||||
// ...
|
||||
});
|
||||
|
@@ -1,20 +1,23 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import app from 'flarum/admin/app';
|
||||
import commonExtend from '../common/extend';
|
||||
|
||||
app.initializers.add('flarum-likes', () => {
|
||||
app.extensionData
|
||||
.for('flarum-likes')
|
||||
.registerPermission(
|
||||
{
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
new Extend.Admin()
|
||||
.permission(
|
||||
() => ({
|
||||
icon: 'far fa-thumbs-up',
|
||||
label: app.translator.trans('flarum-likes.admin.permissions.like_posts_label'),
|
||||
permission: 'discussion.likePosts',
|
||||
},
|
||||
}),
|
||||
'reply'
|
||||
)
|
||||
.registerSetting({
|
||||
.setting(() => ({
|
||||
setting: 'flarum-likes.like_own_post',
|
||||
type: 'bool',
|
||||
label: app.translator.trans('flarum-likes.admin.settings.like_own_posts_label'),
|
||||
help: app.translator.trans('flarum-likes.admin.settings.like_own_posts_help'),
|
||||
});
|
||||
});
|
||||
})),
|
||||
];
|
7
extensions/likes/js/src/admin/index.tsx
Normal file
7
extensions/likes/js/src/admin/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-likes', () => {
|
||||
// ...
|
||||
});
|
@@ -1 +0,0 @@
|
||||
export { default as default } from '../common/extend';
|
@@ -1,15 +1,17 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import app from 'flarum/admin/app';
|
||||
import commonExtend from '../common/extend';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
app.initializers.add('flarum-lock', () => {
|
||||
app.extensionData.for('flarum-lock').registerPermission(
|
||||
{
|
||||
new Extend.Admin().permission(
|
||||
() => ({
|
||||
icon: 'fas fa-lock',
|
||||
label: app.translator.trans('flarum-lock.admin.permissions.lock_discussions_label'),
|
||||
permission: 'discussion.lock',
|
||||
},
|
||||
}),
|
||||
'moderate',
|
||||
95
|
||||
);
|
||||
});
|
||||
),
|
||||
];
|
7
extensions/lock/js/src/admin/index.tsx
Normal file
7
extensions/lock/js/src/admin/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-lock', () => {
|
||||
// ...
|
||||
});
|
@@ -1,3 +1,23 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import app from 'flarum/admin/app';
|
||||
import commonExtend from '../common/extend';
|
||||
|
||||
export default [...commonExtend];
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
new Extend.Admin()
|
||||
.setting(() => ({
|
||||
setting: 'flarum-mentions.allow_username_format',
|
||||
type: 'boolean',
|
||||
label: app.translator.trans('flarum-mentions.admin.settings.allow_username_format_label'),
|
||||
help: app.translator.trans('flarum-mentions.admin.settings.allow_username_format_text'),
|
||||
}))
|
||||
.permission(
|
||||
() => ({
|
||||
permission: 'mentionGroups',
|
||||
label: app.translator.trans('flarum-mentions.admin.permissions.mention_groups_label'),
|
||||
icon: 'fas fa-at',
|
||||
}),
|
||||
'start'
|
||||
),
|
||||
];
|
||||
|
@@ -1,22 +0,0 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-mentions', () => {
|
||||
app.extensionData
|
||||
.for('flarum-mentions')
|
||||
.registerSetting({
|
||||
setting: 'flarum-mentions.allow_username_format',
|
||||
type: 'boolean',
|
||||
label: app.translator.trans('flarum-mentions.admin.settings.allow_username_format_label'),
|
||||
help: app.translator.trans('flarum-mentions.admin.settings.allow_username_format_text'),
|
||||
})
|
||||
.registerPermission(
|
||||
{
|
||||
permission: 'mentionGroups',
|
||||
label: app.translator.trans('flarum-mentions.admin.permissions.mention_groups_label'),
|
||||
icon: 'fas fa-at',
|
||||
},
|
||||
'start'
|
||||
);
|
||||
});
|
7
extensions/mentions/js/src/admin/index.tsx
Normal file
7
extensions/mentions/js/src/admin/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-mentions', () => {
|
||||
// ...
|
||||
});
|
58
extensions/nicknames/js/src/admin/extend.tsx
Normal file
58
extensions/nicknames/js/src/admin/extend.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import app from 'flarum/admin/app';
|
||||
import Alert from 'flarum/common/components/Alert';
|
||||
import Link from 'flarum/common/components/Link';
|
||||
|
||||
export default [
|
||||
new Extend.Admin()
|
||||
.customSetting(function () {
|
||||
if (app.data.settings.display_name_driver === 'nickname') return;
|
||||
|
||||
return (
|
||||
<div className="Form-group">
|
||||
<Alert dismissible={false}>
|
||||
{app.translator.trans('flarum-nicknames.admin.wrong_driver', { a: <Link href={app.route('basics')}></Link> })}
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.setting(() => ({
|
||||
setting: 'flarum-nicknames.set_on_registration',
|
||||
type: 'boolean',
|
||||
label: app.translator.trans('flarum-nicknames.admin.settings.set_on_registration_label'),
|
||||
}))
|
||||
.setting(() => ({
|
||||
setting: 'flarum-nicknames.random_username',
|
||||
type: 'boolean',
|
||||
label: app.translator.trans('flarum-nicknames.admin.settings.random_username_label'),
|
||||
help: app.translator.trans('flarum-nicknames.admin.settings.random_username_help'),
|
||||
}))
|
||||
.setting(() => ({
|
||||
setting: 'flarum-nicknames.unique',
|
||||
type: 'boolean',
|
||||
label: app.translator.trans('flarum-nicknames.admin.settings.unique_label'),
|
||||
}))
|
||||
.setting(() => ({
|
||||
setting: 'flarum-nicknames.regex',
|
||||
type: 'text',
|
||||
label: app.translator.trans('flarum-nicknames.admin.settings.regex_label'),
|
||||
}))
|
||||
.setting(() => ({
|
||||
setting: 'flarum-nicknames.min',
|
||||
type: 'number',
|
||||
label: app.translator.trans('flarum-nicknames.admin.settings.min_label'),
|
||||
}))
|
||||
.setting(() => ({
|
||||
setting: 'flarum-nicknames.max',
|
||||
type: 'number',
|
||||
label: app.translator.trans('flarum-nicknames.admin.settings.max_label'),
|
||||
}))
|
||||
.permission(
|
||||
() => ({
|
||||
icon: 'fas fa-user-tag',
|
||||
label: app.translator.trans('flarum-nicknames.admin.permissions.edit_own_nickname_label'),
|
||||
permission: 'user.editOwnNickname',
|
||||
}),
|
||||
'start'
|
||||
),
|
||||
];
|
@@ -1,64 +1,11 @@
|
||||
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';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-nicknames', () => {
|
||||
app.extensionData
|
||||
.for('flarum-nicknames')
|
||||
.registerSetting(function () {
|
||||
if (app.data.settings.display_name_driver === 'nickname') return;
|
||||
|
||||
return (
|
||||
<div className="Form-group">
|
||||
<Alert dismissible={false}>
|
||||
{app.translator.trans('flarum-nicknames.admin.wrong_driver', { a: <Link href={app.route('basics')}></Link> })}
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.registerSetting({
|
||||
setting: 'flarum-nicknames.set_on_registration',
|
||||
type: 'boolean',
|
||||
label: app.translator.trans('flarum-nicknames.admin.settings.set_on_registration_label'),
|
||||
})
|
||||
.registerSetting({
|
||||
setting: 'flarum-nicknames.random_username',
|
||||
type: 'boolean',
|
||||
label: app.translator.trans('flarum-nicknames.admin.settings.random_username_label'),
|
||||
help: app.translator.trans('flarum-nicknames.admin.settings.random_username_help'),
|
||||
})
|
||||
.registerSetting({
|
||||
setting: 'flarum-nicknames.unique',
|
||||
type: 'boolean',
|
||||
label: app.translator.trans('flarum-nicknames.admin.settings.unique_label'),
|
||||
})
|
||||
.registerSetting({
|
||||
setting: 'flarum-nicknames.regex',
|
||||
type: 'text',
|
||||
label: app.translator.trans('flarum-nicknames.admin.settings.regex_label'),
|
||||
})
|
||||
.registerSetting({
|
||||
setting: 'flarum-nicknames.min',
|
||||
type: 'number',
|
||||
label: app.translator.trans('flarum-nicknames.admin.settings.min_label'),
|
||||
})
|
||||
.registerSetting({
|
||||
setting: 'flarum-nicknames.max',
|
||||
type: 'number',
|
||||
label: app.translator.trans('flarum-nicknames.admin.settings.max_label'),
|
||||
})
|
||||
.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-user-tag',
|
||||
label: app.translator.trans('flarum-nicknames.admin.permissions.edit_own_nickname_label'),
|
||||
permission: 'user.editOwnNickname',
|
||||
},
|
||||
'start'
|
||||
);
|
||||
|
||||
extend(BasicsPage.prototype, 'driverLocale', function (locale) {
|
||||
locale.display_name['nickname'] = extractText(app.translator.trans('flarum-nicknames.admin.basics.display_name_driver_options.nickname'));
|
||||
});
|
||||
|
@@ -12,7 +12,7 @@ import ConfigureAuth from './ConfigureAuth';
|
||||
|
||||
export default class SettingsPage extends ExtensionPage {
|
||||
content() {
|
||||
const settings = app.extensionData.getSettings(this.extension.id);
|
||||
const settings = app.registry.getSettings(this.extension.id);
|
||||
|
||||
const warnings = [app.translator.trans('flarum-extension-manager.admin.settings.access_warning')];
|
||||
|
||||
|
31
extensions/package-manager/js/src/admin/extend.tsx
Normal file
31
extensions/package-manager/js/src/admin/extend.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import app from 'flarum/admin/app';
|
||||
import extractText from 'flarum/common/utils/extractText';
|
||||
import SettingsPage from './components/SettingsPage';
|
||||
|
||||
export default [
|
||||
new Extend.Admin()
|
||||
.setting(() => ({
|
||||
setting: 'flarum-extension-manager.queue_jobs',
|
||||
label: app.translator.trans('flarum-extension-manager.admin.settings.queue_jobs'),
|
||||
help: m.trust(
|
||||
extractText(
|
||||
app.translator.trans('flarum-extension-manager.admin.settings.queue_jobs_help', {
|
||||
basic_impl_link: 'https://discuss.flarum.org/d/28151-database-queue-the-simplest-queue-even-for-shared-hosting',
|
||||
adv_impl_link: 'https://discuss.flarum.org/d/21873-redis-sessions-cache-queues',
|
||||
php_version: `<strong>${app.data.phpVersion}</strong>`,
|
||||
folder_perms_link: 'https://docs.flarum.org/install#folder-ownership',
|
||||
})
|
||||
)
|
||||
),
|
||||
type: 'boolean',
|
||||
disabled: app.data['flarum-extension-manager.using_sync_queue'],
|
||||
}))
|
||||
.setting(() => ({
|
||||
setting: 'flarum-extension-manager.task_retention_days',
|
||||
label: app.translator.trans('flarum-extension-manager.admin.settings.task_retention_days'),
|
||||
help: app.translator.trans('flarum-extension-manager.admin.settings.task_retention_days_help'),
|
||||
type: 'number',
|
||||
}))
|
||||
.page(SettingsPage),
|
||||
];
|
@@ -4,13 +4,13 @@ import ExtensionPage from 'flarum/admin/components/ExtensionPage';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import LoadingModal from 'flarum/admin/components/LoadingModal';
|
||||
import isExtensionEnabled from 'flarum/admin/utils/isExtensionEnabled';
|
||||
import SettingsPage from './components/SettingsPage';
|
||||
import Task from './models/Task';
|
||||
import jumpToQueue from './utils/jumpToQueue';
|
||||
import extractText from 'flarum/common/utils/extractText';
|
||||
import { AsyncBackendResponse } from './shims';
|
||||
import ExtensionManagerState from './states/ExtensionManagerState';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-extension-manager', (app) => {
|
||||
app.store.models['extension-manager-tasks'] = Task;
|
||||
|
||||
@@ -20,32 +20,6 @@ app.initializers.add('flarum-extension-manager', (app) => {
|
||||
app.data.settings['flarum-extension-manager.queue_jobs'] = '0';
|
||||
}
|
||||
|
||||
app.extensionData
|
||||
.for('flarum-extension-manager')
|
||||
.registerSetting({
|
||||
setting: 'flarum-extension-manager.queue_jobs',
|
||||
label: app.translator.trans('flarum-extension-manager.admin.settings.queue_jobs'),
|
||||
help: m.trust(
|
||||
extractText(
|
||||
app.translator.trans('flarum-extension-manager.admin.settings.queue_jobs_help', {
|
||||
basic_impl_link: 'https://discuss.flarum.org/d/28151-database-queue-the-simplest-queue-even-for-shared-hosting',
|
||||
adv_impl_link: 'https://discuss.flarum.org/d/21873-redis-sessions-cache-queues',
|
||||
php_version: `<strong>${app.data.phpVersion}</strong>`,
|
||||
folder_perms_link: 'https://docs.flarum.org/install#folder-ownership',
|
||||
})
|
||||
)
|
||||
),
|
||||
type: 'boolean',
|
||||
disabled: app.data['flarum-extension-manager.using_sync_queue'],
|
||||
})
|
||||
.registerSetting({
|
||||
setting: 'flarum-extension-manager.task_retention_days',
|
||||
label: app.translator.trans('flarum-extension-manager.admin.settings.task_retention_days'),
|
||||
help: app.translator.trans('flarum-extension-manager.admin.settings.task_retention_days_help'),
|
||||
type: 'number',
|
||||
})
|
||||
.registerPage(SettingsPage);
|
||||
|
||||
extend(ExtensionPage.prototype, 'topItems', function (items) {
|
||||
if (this.extension.id === 'flarum-extension-manager' || isExtensionEnabled(this.extension.id)) {
|
||||
return;
|
||||
|
38
extensions/pusher/js/src/admin/extend.tsx
Normal file
38
extensions/pusher/js/src/admin/extend.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export default [
|
||||
new Extend.Admin()
|
||||
.setting(
|
||||
() => ({
|
||||
setting: 'flarum-pusher.app_id',
|
||||
label: app.translator.trans('flarum-pusher.admin.pusher_settings.app_id_label'),
|
||||
type: 'text',
|
||||
}),
|
||||
30
|
||||
)
|
||||
.setting(
|
||||
() => ({
|
||||
setting: 'flarum-pusher.app_key',
|
||||
label: app.translator.trans('flarum-pusher.admin.pusher_settings.app_key_label'),
|
||||
type: 'text',
|
||||
}),
|
||||
20
|
||||
)
|
||||
.setting(
|
||||
() => ({
|
||||
setting: 'flarum-pusher.app_secret',
|
||||
label: app.translator.trans('flarum-pusher.admin.pusher_settings.app_secret_label'),
|
||||
type: 'text',
|
||||
}),
|
||||
10
|
||||
)
|
||||
.setting(
|
||||
() => ({
|
||||
setting: 'flarum-pusher.app_cluster',
|
||||
label: app.translator.trans('flarum-pusher.admin.pusher_settings.app_cluster_label'),
|
||||
type: 'text',
|
||||
}),
|
||||
0
|
||||
),
|
||||
];
|
@@ -1,38 +1,7 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-pusher', () => {
|
||||
app.extensionData
|
||||
.for('flarum-pusher')
|
||||
.registerSetting(
|
||||
{
|
||||
setting: 'flarum-pusher.app_id',
|
||||
label: app.translator.trans('flarum-pusher.admin.pusher_settings.app_id_label'),
|
||||
type: 'text',
|
||||
},
|
||||
30
|
||||
)
|
||||
.registerSetting(
|
||||
{
|
||||
setting: 'flarum-pusher.app_key',
|
||||
label: app.translator.trans('flarum-pusher.admin.pusher_settings.app_key_label'),
|
||||
type: 'text',
|
||||
},
|
||||
20
|
||||
)
|
||||
.registerSetting(
|
||||
{
|
||||
setting: 'flarum-pusher.app_secret',
|
||||
label: app.translator.trans('flarum-pusher.admin.pusher_settings.app_secret_label'),
|
||||
type: 'text',
|
||||
},
|
||||
10
|
||||
)
|
||||
.registerSetting(
|
||||
{
|
||||
setting: 'flarum-pusher.app_cluster',
|
||||
label: app.translator.trans('flarum-pusher.admin.pusher_settings.app_cluster_label'),
|
||||
type: 'text',
|
||||
},
|
||||
0
|
||||
);
|
||||
// ...
|
||||
});
|
||||
|
4
extensions/statistics/js/src/admin/extend.tsx
Normal file
4
extensions/statistics/js/src/admin/extend.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import StatisticsPage from './components/StatisticsPage';
|
||||
|
||||
export default [new Extend.Admin().page(StatisticsPage)];
|
@@ -1,15 +1,13 @@
|
||||
import app from 'flarum/admin/app';
|
||||
import { extend } from 'flarum/common/extend';
|
||||
|
||||
import DashboardPage from 'flarum/admin/components/DashboardPage';
|
||||
|
||||
import MiniStatisticsWidget from './components/MiniStatisticsWidget';
|
||||
import StatisticsPage from './components/StatisticsPage';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-statistics', () => {
|
||||
extend(DashboardPage.prototype, 'availableWidgets', function (widgets) {
|
||||
widgets.add('statistics', <MiniStatisticsWidget />, 20);
|
||||
});
|
||||
|
||||
app.extensionData.for('flarum-statistics').registerPage(StatisticsPage);
|
||||
});
|
||||
|
@@ -1 +0,0 @@
|
||||
export { default as default } from '../common/extend';
|
@@ -1,15 +1,17 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import app from 'flarum/admin/app';
|
||||
import commonExtend from '../common/extend';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
app.initializers.add('flarum-sticky', () => {
|
||||
app.extensionData.for('flarum-sticky').registerPermission(
|
||||
{
|
||||
new Extend.Admin().permission(
|
||||
() => ({
|
||||
icon: 'fas fa-thumbtack',
|
||||
label: app.translator.trans('flarum-sticky.admin.permissions.sticky_discussions_label'),
|
||||
permission: 'discussion.sticky',
|
||||
},
|
||||
}),
|
||||
'moderate',
|
||||
95
|
||||
);
|
||||
});
|
||||
),
|
||||
];
|
7
extensions/sticky/js/src/admin/index.tsx
Normal file
7
extensions/sticky/js/src/admin/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-sticky', () => {
|
||||
// ...
|
||||
});
|
@@ -1 +0,0 @@
|
||||
export { default as default } from '../common/extend';
|
@@ -1,14 +1,16 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import app from 'flarum/admin/app';
|
||||
import commonExtend from '../common/extend';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
app.initializers.add('flarum-suspend', () => {
|
||||
app.extensionData.for('flarum-suspend').registerPermission(
|
||||
{
|
||||
new Extend.Admin().permission(
|
||||
() => ({
|
||||
icon: 'fas fa-ban',
|
||||
label: app.translator.trans('flarum-suspend.admin.permissions.suspend_users_label'),
|
||||
permission: 'user.suspend',
|
||||
},
|
||||
}),
|
||||
'moderate'
|
||||
);
|
||||
});
|
||||
),
|
||||
];
|
7
extensions/suspend/js/src/admin/index.tsx
Normal file
7
extensions/suspend/js/src/admin/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-suspend', () => {
|
||||
// ...
|
||||
});
|
@@ -2,7 +2,7 @@ import { extend } from 'flarum/common/extend';
|
||||
import BasicsPage from 'flarum/admin/components/BasicsPage';
|
||||
|
||||
export default function () {
|
||||
extend(BasicsPage.prototype, 'homePageItems', (items) => {
|
||||
extend(BasicsPage, 'homePageItems', (items) => {
|
||||
items.add('tags', {
|
||||
path: '/tags',
|
||||
label: app.translator.trans('flarum-tags.admin.basics.tags_label'),
|
||||
|
@@ -5,5 +5,4 @@ import './components/EditTagModal';
|
||||
|
||||
import './addTagsHomePageOption';
|
||||
import './addTagChangePermission';
|
||||
import './addTagPermission';
|
||||
import './addTagsPermissionScope';
|
||||
|
@@ -1,22 +1,29 @@
|
||||
export default function () {
|
||||
app.extensionData
|
||||
.for('flarum-tags')
|
||||
.registerPermission(
|
||||
{
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import commonExtend from '../common/extend';
|
||||
import app from 'flarum/admin/app';
|
||||
import TagsPage from './components/TagsPage';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
new Extend.Admin()
|
||||
.page(TagsPage)
|
||||
.permission(
|
||||
() => ({
|
||||
icon: 'fas fa-tag',
|
||||
label: app.translator.trans('flarum-tags.admin.permissions.tag_discussions_label'),
|
||||
permission: 'discussion.tag',
|
||||
},
|
||||
}),
|
||||
'moderate',
|
||||
95
|
||||
)
|
||||
.registerPermission(
|
||||
{
|
||||
.permission(
|
||||
() => ({
|
||||
icon: 'fas fa-tags',
|
||||
label: app.translator.trans('flarum-tags.admin.permissions.bypass_tag_counts_label'),
|
||||
permission: 'bypassTagCounts',
|
||||
},
|
||||
}),
|
||||
'start',
|
||||
89
|
||||
);
|
||||
}
|
||||
),
|
||||
];
|
@@ -1,21 +1,16 @@
|
||||
import app from 'flarum/admin/app';
|
||||
import addTagsPermissionScope from './addTagsPermissionScope';
|
||||
import addTagPermission from './addTagPermission';
|
||||
import addTagsHomePageOption from './addTagsHomePageOption';
|
||||
import addTagChangePermission from './addTagChangePermission';
|
||||
import addTagSelectionSettingComponent from './addTagSelectionSettingComponent';
|
||||
import TagsPage from './components/TagsPage';
|
||||
import TagListState from '../common/states/TagListState';
|
||||
|
||||
export { default as extend } from '../common/extend';
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-tags', (app) => {
|
||||
app.tagList = new TagListState();
|
||||
|
||||
app.extensionData.for('flarum-tags').registerPage(TagsPage);
|
||||
|
||||
addTagsPermissionScope();
|
||||
addTagPermission();
|
||||
addTagsHomePageOption();
|
||||
addTagChangePermission();
|
||||
addTagSelectionSettingComponent();
|
||||
|
@@ -4,10 +4,17 @@ import routes, { AdminRoutes } from './routes';
|
||||
import Application, { ApplicationData } from '../common/Application';
|
||||
import Navigation from '../common/components/Navigation';
|
||||
import AdminNav from './components/AdminNav';
|
||||
import ExtensionData from './utils/ExtensionData';
|
||||
import AdminRegistry from './utils/AdminRegistry';
|
||||
import IHistory from '../common/IHistory';
|
||||
import SearchManager from '../common/SearchManager';
|
||||
import SearchState from '../common/states/SearchState';
|
||||
import app from './app';
|
||||
import BasicsPage from './components/BasicsPage';
|
||||
import GeneralSearchIndex from './states/GeneralSearchIndex';
|
||||
import AppearancePage from './components/AppearancePage';
|
||||
import MailPage from './components/MailPage';
|
||||
import AdvancedPage from './components/AdvancedPage';
|
||||
import PermissionsPage from './components/PermissionsPage';
|
||||
|
||||
export type Extension = {
|
||||
id: string;
|
||||
@@ -32,6 +39,7 @@ export type Extension = {
|
||||
extra: {
|
||||
'flarum-extension': {
|
||||
title: string;
|
||||
category?: string;
|
||||
'database-support'?: string[];
|
||||
};
|
||||
};
|
||||
@@ -66,7 +74,13 @@ export interface AdminApplicationData extends ApplicationData {
|
||||
}
|
||||
|
||||
export default class AdminApplication extends Application {
|
||||
extensionData = new ExtensionData();
|
||||
/**
|
||||
* Stores the available settings, permissions, and custom pages of the app.
|
||||
* Allows the global search to find these items.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
registry = new AdminRegistry();
|
||||
|
||||
extensionCategories = {
|
||||
feature: 30,
|
||||
@@ -88,6 +102,12 @@ export default class AdminApplication extends Application {
|
||||
|
||||
search: SearchManager<SearchState> = new SearchManager(new SearchState());
|
||||
|
||||
/**
|
||||
* Custom settings and custom permissions do not go through the registry.
|
||||
* The general index is used to manually add these items to be picked up by the search.
|
||||
*/
|
||||
generalIndex: GeneralSearchIndex = new GeneralSearchIndex();
|
||||
|
||||
/**
|
||||
* Settings are serialized to the admin dashboard as strings.
|
||||
* Additional encoding/decoding is possible, but must take
|
||||
@@ -108,6 +128,14 @@ export default class AdminApplication extends Application {
|
||||
this.route = (Object.getPrototypeOf(Object.getPrototypeOf(this)) as Application).route.bind(this);
|
||||
}
|
||||
|
||||
protected beforeMount(): void {
|
||||
BasicsPage.register();
|
||||
AppearancePage.register();
|
||||
MailPage.register();
|
||||
AdvancedPage.register();
|
||||
PermissionsPage.register();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import '../common/common';
|
||||
|
||||
import './utils/saveSettings';
|
||||
import './utils/ExtensionData';
|
||||
import './utils/AdminRegistry';
|
||||
import './utils/isExtensionEnabled';
|
||||
import './utils/getCategorizedExtensions';
|
||||
|
||||
|
@@ -168,7 +168,7 @@ export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAt
|
||||
this.refreshAfterSaving.push(setting);
|
||||
}
|
||||
|
||||
return <FormGroup stream={bidi} {...attrs} />;
|
||||
return <FormGroup stream={bidi} getSetting={this.setting.bind(this)} {...attrs} />;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,7 +243,7 @@ export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAt
|
||||
.catch(this.onsavefailed.bind(this));
|
||||
}
|
||||
|
||||
modelLocale(): Record<string, string> {
|
||||
static modelLocale(): Record<string, string> {
|
||||
return {
|
||||
'Flarum\\Discussion\\Discussion': extractText(app.translator.trans('core.admin.models.discussions')),
|
||||
'Flarum\\User\\User': extractText(app.translator.trans('core.admin.models.users')),
|
||||
|
@@ -86,7 +86,7 @@ export default class AdvancedPage<CustomAttrs extends IPageAttrs = IPageAttrs> e
|
||||
<Form>
|
||||
{Object.keys(this.searchDriverOptions).map((model) => {
|
||||
const options = this.searchDriverOptions[model];
|
||||
const modelLocale = this.modelLocale()[model] || model;
|
||||
const modelLocale = AdminPage.modelLocale()[model] || model;
|
||||
|
||||
if (Object.keys(options).length > 1) {
|
||||
return this.buildSettingComponent({
|
||||
@@ -208,4 +208,32 @@ export default class AdvancedPage<CustomAttrs extends IPageAttrs = IPageAttrs> e
|
||||
</FormSection>
|
||||
);
|
||||
}
|
||||
|
||||
static register() {
|
||||
app.generalIndex.group('core-advanced', {
|
||||
label: app.translator.trans('core.admin.advanced.title', {}, true),
|
||||
icon: {
|
||||
name: 'fas fa-cog',
|
||||
},
|
||||
link: app.route('advanced'),
|
||||
});
|
||||
|
||||
app.generalIndex.for('core-advanced').add('settings', [
|
||||
{
|
||||
id: 'maintenance_mode',
|
||||
label: app.translator.trans('core.admin.advanced.maintenance.section_label', {}, true),
|
||||
help: app.translator.trans('core.admin.advanced.maintenance.help', {}, true),
|
||||
},
|
||||
{
|
||||
id: 'safe_mode_extensions',
|
||||
label: app.translator.trans('core.admin.advanced.maintenance.safe_mode_extensions', {}, true),
|
||||
visible: () => app.data.maintenanceMode === MaintenanceMode.SAFE_MODE,
|
||||
},
|
||||
{
|
||||
id: 'extension_bisect',
|
||||
label: app.translator.trans('core.admin.advanced.maintenance.bisect.label', {}, true),
|
||||
help: app.translator.trans('core.admin.advanced.maintenance.bisect.help', {}, true),
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@@ -147,4 +147,55 @@ export default class AppearancePage extends AdminPage {
|
||||
onsaved() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
static register() {
|
||||
app.generalIndex.group('core-appearance', {
|
||||
label: app.translator.trans('core.admin.appearance.title', {}, true),
|
||||
icon: {
|
||||
name: 'fas fa-paint-brush',
|
||||
},
|
||||
link: app.route('appearance'),
|
||||
});
|
||||
|
||||
app.generalIndex.for('core-appearance').add('settings', [
|
||||
{
|
||||
id: 'colors_heading',
|
||||
label: app.translator.trans('core.admin.appearance.colors_heading', {}, true),
|
||||
help: app.translator.trans('core.admin.appearance.colors_text', {}, true),
|
||||
},
|
||||
{
|
||||
id: 'color_scheme',
|
||||
label: app.translator.trans('core.admin.appearance.color_scheme_label', {}, true),
|
||||
},
|
||||
{
|
||||
id: 'colored_header',
|
||||
label: app.translator.trans('core.admin.appearance.colored_header_label', {}, true),
|
||||
},
|
||||
{
|
||||
id: 'logo_heading',
|
||||
label: app.translator.trans('core.admin.appearance.logo_heading', {}, true),
|
||||
help: app.translator.trans('core.admin.appearance.logo_text', {}, true),
|
||||
},
|
||||
{
|
||||
id: 'favicon_heading',
|
||||
label: app.translator.trans('core.admin.appearance.favicon_heading', {}, true),
|
||||
help: app.translator.trans('core.admin.appearance.favicon_text', {}, true),
|
||||
},
|
||||
{
|
||||
id: 'custom_header_heading',
|
||||
label: app.translator.trans('core.admin.appearance.custom_header_heading', {}, true),
|
||||
help: app.translator.trans('core.admin.appearance.custom_header_text', {}, true),
|
||||
},
|
||||
{
|
||||
id: 'custom_footer_heading',
|
||||
label: app.translator.trans('core.admin.appearance.custom_footer_heading', {}, true),
|
||||
help: app.translator.trans('core.admin.appearance.custom_footer_text', {}, true),
|
||||
},
|
||||
{
|
||||
id: 'custom_styles_heading',
|
||||
label: app.translator.trans('core.admin.appearance.custom_styles_heading', {}, true),
|
||||
help: app.translator.trans('core.admin.appearance.custom_styles_text', {}, true),
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@@ -14,30 +14,8 @@ export type DriverLocale = {
|
||||
};
|
||||
|
||||
export default class BasicsPage<CustomAttrs extends IPageAttrs = IPageAttrs> extends AdminPage<CustomAttrs> {
|
||||
localeOptions: Record<string, string> = {};
|
||||
displayNameOptions: Record<string, string> = {};
|
||||
slugDriverOptions: Record<string, Record<string, string>> = {};
|
||||
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
Object.keys(app.data.locales).forEach((i) => {
|
||||
this.localeOptions[i] = `${app.data.locales[i]} (${i})`;
|
||||
});
|
||||
|
||||
const driverLocale = this.driverLocale();
|
||||
|
||||
app.data.displayNameDrivers.forEach((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] = (driverLocale.slug[model] && driverLocale.slug[model][option]) || option;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
headerInfo() {
|
||||
@@ -50,86 +28,11 @@ export default class BasicsPage<CustomAttrs extends IPageAttrs = IPageAttrs> ext
|
||||
}
|
||||
|
||||
content() {
|
||||
const settings = app.registry.getSettings('core-basics');
|
||||
|
||||
return [
|
||||
<Form>
|
||||
{this.buildSettingComponent({
|
||||
type: 'text',
|
||||
setting: 'forum_title',
|
||||
label: app.translator.trans('core.admin.basics.forum_title_heading'),
|
||||
})}
|
||||
{this.buildSettingComponent({
|
||||
type: 'text',
|
||||
setting: 'forum_description',
|
||||
label: app.translator.trans('core.admin.basics.forum_description_heading'),
|
||||
help: app.translator.trans('core.admin.basics.forum_description_text'),
|
||||
})}
|
||||
|
||||
{Object.keys(this.localeOptions).length > 1 && (
|
||||
<>
|
||||
{this.buildSettingComponent({
|
||||
type: 'select',
|
||||
setting: 'default_locale',
|
||||
options: this.localeOptions,
|
||||
label: app.translator.trans('core.admin.basics.default_language_heading'),
|
||||
})}
|
||||
{this.buildSettingComponent({
|
||||
type: 'switch',
|
||||
setting: 'show_language_selector',
|
||||
label: app.translator.trans('core.admin.basics.show_language_selector_label'),
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
<FieldSet
|
||||
className="BasicsPage-homePage Form-group"
|
||||
label={app.translator.trans('core.admin.basics.home_page_heading')}
|
||||
description={app.translator.trans('core.admin.basics.home_page_text')}
|
||||
>
|
||||
{this.homePageItems()
|
||||
.toArray()
|
||||
.map(({ path, label }) => (
|
||||
<label className="checkbox">
|
||||
<input type="radio" name="homePage" value={path} bidi={this.setting('default_route')} />
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
</FieldSet>
|
||||
|
||||
<div className="Form-group BasicsPage-welcomeBanner-input">
|
||||
<label>{app.translator.trans('core.admin.basics.welcome_banner_heading')}</label>
|
||||
<div className="helpText">{app.translator.trans('core.admin.basics.welcome_banner_text')}</div>
|
||||
<div className="StackedFormControl">
|
||||
<input type="text" className="FormControl" bidi={this.setting('welcome_title')} />
|
||||
<textarea className="FormControl" bidi={this.setting('welcome_message')} cols={80} rows={6} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Object.keys(this.displayNameOptions).length > 1 &&
|
||||
this.buildSettingComponent({
|
||||
type: 'select',
|
||||
setting: 'display_name_driver',
|
||||
options: this.displayNameOptions,
|
||||
label: app.translator.trans('core.admin.basics.display_name_heading'),
|
||||
help: app.translator.trans('core.admin.basics.display_name_text'),
|
||||
})}
|
||||
|
||||
{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: modelLocale }),
|
||||
help: app.translator.trans('core.admin.basics.slug_driver_text', { model: modelLocale }),
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
{settings?.map(this.buildSettingComponent.bind(this))}
|
||||
<div className="Form-group Form-controls">{this.submitButton()}</div>
|
||||
</Form>,
|
||||
];
|
||||
@@ -139,7 +42,7 @@ export default class BasicsPage<CustomAttrs extends IPageAttrs = IPageAttrs> ext
|
||||
* Build a list of options for the default homepage. Each option must be an
|
||||
* object with `path` and `label` properties.
|
||||
*/
|
||||
homePageItems() {
|
||||
static homePageItems() {
|
||||
const items = new ItemList<HomePageItem>();
|
||||
|
||||
items.add('allDiscussions', {
|
||||
@@ -150,7 +53,7 @@ export default class BasicsPage<CustomAttrs extends IPageAttrs = IPageAttrs> ext
|
||||
return items;
|
||||
}
|
||||
|
||||
driverLocale(): DriverLocale {
|
||||
static driverLocale(): DriverLocale {
|
||||
return {
|
||||
display_name: {
|
||||
username: extractText(app.translator.trans('core.admin.basics.display_name_driver_options.username')),
|
||||
@@ -167,4 +70,118 @@ export default class BasicsPage<CustomAttrs extends IPageAttrs = IPageAttrs> ext
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static register() {
|
||||
app.generalIndex.group('core-basics', {
|
||||
label: app.translator.trans('core.admin.basics.title', {}, true),
|
||||
icon: {
|
||||
name: 'fas fa-pencil-alt',
|
||||
},
|
||||
link: app.route('basics'),
|
||||
});
|
||||
|
||||
const localeOptions: Record<string, string> = {};
|
||||
const displayNameOptions: Record<string, string> = {};
|
||||
const slugDriverOptions: Record<string, Record<string, string>> = {};
|
||||
|
||||
const driverLocale = BasicsPage.driverLocale();
|
||||
|
||||
Object.keys(app.data.locales).forEach((i) => {
|
||||
localeOptions[i] = `${app.data.locales[i]} (${i})`;
|
||||
});
|
||||
|
||||
app.data.displayNameDrivers.forEach((identifier) => {
|
||||
displayNameOptions[identifier] = driverLocale.display_name[identifier] || identifier;
|
||||
});
|
||||
|
||||
Object.keys(app.data.slugDrivers).forEach((model) => {
|
||||
slugDriverOptions[model] = {};
|
||||
|
||||
app.data.slugDrivers[model].forEach((option) => {
|
||||
slugDriverOptions[model][option] = (driverLocale.slug[model] && driverLocale.slug[model][option]) || option;
|
||||
});
|
||||
});
|
||||
|
||||
app.registry.for('core-basics');
|
||||
|
||||
app.registry
|
||||
.registerSetting({
|
||||
type: 'text',
|
||||
setting: 'forum_title',
|
||||
label: app.translator.trans('core.admin.basics.forum_title_heading'),
|
||||
})
|
||||
.registerSetting({
|
||||
type: 'text',
|
||||
setting: 'forum_description',
|
||||
label: app.translator.trans('core.admin.basics.forum_description_heading'),
|
||||
help: app.translator.trans('core.admin.basics.forum_description_text'),
|
||||
});
|
||||
|
||||
if (Object.keys(localeOptions).length > 1) {
|
||||
app.registry
|
||||
.registerSetting({
|
||||
type: 'select',
|
||||
setting: 'default_locale',
|
||||
options: localeOptions,
|
||||
label: app.translator.trans('core.admin.basics.default_language_heading'),
|
||||
})
|
||||
.registerSetting({
|
||||
type: 'switch',
|
||||
setting: 'show_language_selector',
|
||||
label: app.translator.trans('core.admin.basics.show_language_selector_label'),
|
||||
});
|
||||
}
|
||||
|
||||
app.registry
|
||||
.registerSetting({
|
||||
type: 'radio',
|
||||
setting: 'default_route',
|
||||
options: BasicsPage.homePageItems()
|
||||
.toArray()
|
||||
.map((item: HomePageItem) => ({
|
||||
...item,
|
||||
value: item.path,
|
||||
})),
|
||||
label: app.translator.trans('core.admin.basics.home_page_heading', {}, true),
|
||||
help: app.translator.trans('core.admin.basics.home_page_text', {}, true),
|
||||
containerClassName: 'BasicsPage-homePage',
|
||||
})
|
||||
.registerSetting({
|
||||
type: 'stacked-text',
|
||||
setting: 'welcome_title',
|
||||
textArea: {
|
||||
setting: 'welcome_message',
|
||||
cols: 80,
|
||||
rows: 6,
|
||||
},
|
||||
label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
|
||||
help: app.translator.trans('core.admin.basics.welcome_banner_text'),
|
||||
containerClassName: 'BasicsPage-welcomeBanner-input',
|
||||
});
|
||||
|
||||
if (Object.keys(displayNameOptions).length > 1) {
|
||||
app.registry.registerSetting({
|
||||
type: 'select',
|
||||
setting: 'display_name_driver',
|
||||
options: displayNameOptions,
|
||||
label: app.translator.trans('core.admin.basics.display_name_heading'),
|
||||
help: app.translator.trans('core.admin.basics.display_name_text'),
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(slugDriverOptions).forEach((model) => {
|
||||
const options = slugDriverOptions[model];
|
||||
const modelLocale = AdminPage.modelLocale()[model] || model;
|
||||
|
||||
if (Object.keys(options).length > 1) {
|
||||
app.registry.registerSetting({
|
||||
type: 'select',
|
||||
setting: `slug_driver_${model}`,
|
||||
options,
|
||||
label: app.translator.trans('core.admin.basics.slug_driver_heading', { model: modelLocale }),
|
||||
help: app.translator.trans('core.admin.basics.slug_driver_text', { model: modelLocale }),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -149,7 +149,7 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
|
||||
</div>
|
||||
</div>
|
||||
<div className="container">
|
||||
{app.extensionData.extensionHasPermissions(this.extension.id) ? (
|
||||
{app.registry.extensionHasPermissions(this.extension.id) ? (
|
||||
<ExtensionPermissionGrid extensionId={this.extension.id} />
|
||||
) : (
|
||||
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_permissions')}</h3>
|
||||
@@ -162,7 +162,7 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
|
||||
}
|
||||
|
||||
content(vnode: Mithril.VnodeDOM<Attrs, this>) {
|
||||
const settings = app.extensionData.getSettings(this.extension.id);
|
||||
const settings = app.registry.getSettings(this.extension.id);
|
||||
|
||||
return (
|
||||
<div className="ExtensionPage-settings">
|
||||
|
@@ -34,19 +34,19 @@ export default class ExtensionPermissionGrid<
|
||||
}
|
||||
|
||||
viewItems() {
|
||||
return app.extensionData.getExtensionPermissions(this.extensionId, 'view') || new ItemList();
|
||||
return app.registry.getExtensionPermissions(this.extensionId, 'view') || new ItemList();
|
||||
}
|
||||
|
||||
startItems() {
|
||||
return app.extensionData.getExtensionPermissions(this.extensionId, 'start') || new ItemList();
|
||||
return app.registry.getExtensionPermissions(this.extensionId, 'start') || new ItemList();
|
||||
}
|
||||
|
||||
replyItems() {
|
||||
return app.extensionData.getExtensionPermissions(this.extensionId, 'reply') || new ItemList();
|
||||
return app.registry.getExtensionPermissions(this.extensionId, 'reply') || new ItemList();
|
||||
}
|
||||
|
||||
moderateItems() {
|
||||
return app.extensionData.getExtensionPermissions(this.extensionId, 'moderate') || new ItemList();
|
||||
return app.registry.getExtensionPermissions(this.extensionId, 'moderate') || new ItemList();
|
||||
}
|
||||
|
||||
scopeControlItems() {
|
||||
|
242
framework/core/js/src/admin/components/GeneralSearchSource.tsx
Normal file
242
framework/core/js/src/admin/components/GeneralSearchSource.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
import app from '../app';
|
||||
import highlight from '../../common/helpers/highlight';
|
||||
import type { SearchSource } from './Search';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import Link from '../../common/components/Link';
|
||||
import Icon from '../../common/components/Icon';
|
||||
import PermissionGrid from './PermissionGrid';
|
||||
import escapeRegExp from '../../common/utils/escapeRegExp';
|
||||
import { GeneralIndexData, GeneralIndexItem } from '../states/GeneralSearchIndex';
|
||||
import { ExtensionConfig, SettingConfigInternal } from '../utils/AdminRegistry';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
|
||||
export class GeneralSearchResult {
|
||||
constructor(
|
||||
public id: string,
|
||||
public category: string,
|
||||
public icon: { name: string; [key: string]: any },
|
||||
public tree: string[],
|
||||
public link: string,
|
||||
public help?: string
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and displays settings, permissions and installed extensions (i.e. general search results) in the search dropdown.
|
||||
*/
|
||||
export default class GeneralSearchSource implements SearchSource {
|
||||
protected results = new Map<string, GeneralSearchResult[]>();
|
||||
|
||||
public resource: string = 'general';
|
||||
|
||||
title(): string {
|
||||
return extractText(app.translator.trans('core.admin.search_source.general.heading'));
|
||||
}
|
||||
|
||||
isCached(query: string): boolean {
|
||||
return this.results.has(query.toLowerCase());
|
||||
}
|
||||
|
||||
async search(query: string, limit: number): Promise<void> {
|
||||
query = query.toLowerCase();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const results: GeneralSearchResult[] = [];
|
||||
|
||||
// extensions.
|
||||
for (const extensionId in app.data.extensions) {
|
||||
const extension = app.data.extensions[extensionId];
|
||||
const title = extension.extra['flarum-extension'].title || extensionId.replace('core-', '');
|
||||
const icon = extension.icon || { name: 'fas fa-cog' };
|
||||
const category = extension.extra['flarum-extension'].category || 'other';
|
||||
|
||||
if (this.itemHasQuery(title, query)) {
|
||||
results.push(
|
||||
new GeneralSearchResult(
|
||||
extensionId,
|
||||
app.translator.trans('core.admin.nav.categories.' + category, {}, true),
|
||||
icon,
|
||||
[title],
|
||||
app.route('extension', { id: extensionId })
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// extension registered settings && permissions
|
||||
results.push(...this.lookup(app.registry.getData(), query));
|
||||
|
||||
// manually registered settings && permissions into the search index.
|
||||
results.push(...this.lookup(app.generalIndex.getData(), query));
|
||||
|
||||
this.results.set(query, results);
|
||||
m.redraw();
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
protected lookup(
|
||||
data:
|
||||
| GeneralIndexData
|
||||
| {
|
||||
[key: string]: ExtensionConfig | undefined;
|
||||
},
|
||||
query: string
|
||||
): GeneralSearchResult[] {
|
||||
const extensions = app.data.extensions;
|
||||
const permissionItems = PermissionGrid.prototype.permissionItems();
|
||||
|
||||
const results: GeneralSearchResult[] = [];
|
||||
|
||||
for (const extensionId in data) {
|
||||
// settings
|
||||
const settings = data[extensionId]!.settings;
|
||||
let normalizedSettings: GeneralIndexItem[] | SettingConfigInternal[] = [];
|
||||
|
||||
if (settings instanceof ItemList) {
|
||||
normalizedSettings = settings?.toArray();
|
||||
} else if (settings) {
|
||||
normalizedSettings = settings;
|
||||
}
|
||||
|
||||
for (const setting of normalizedSettings) {
|
||||
if ('visible' in setting && !setting.visible()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = 'label' in setting ? extractText(setting.label) : '';
|
||||
const help = 'help' in setting ? extractText(setting.help) : '';
|
||||
const corePage = !extensions[extensionId] ? extensionId.replace('core-', '') : null;
|
||||
const group = app.generalIndex.getGroup(extensionId);
|
||||
|
||||
if (this.itemHasQuery(label, query) || this.itemHasQuery(help, query)) {
|
||||
const id = extensionId + '-' + ('setting' in setting ? setting : setting.id);
|
||||
|
||||
results.push(
|
||||
new GeneralSearchResult(
|
||||
id,
|
||||
group?.label ||
|
||||
extensions[extensionId]?.extra['flarum-extension'].title ||
|
||||
app.translator.trans('core.admin.' + corePage + '.title', {}, true),
|
||||
group?.icon || extensions[extensionId]?.icon || { name: 'fas fa-cog' },
|
||||
'tree' in setting && setting.tree ? setting.tree.map(extractText).push(label) : [label],
|
||||
group?.link || (corePage ? app.route(corePage) : app.route('extension', { id: extensionId })),
|
||||
help
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// permissions
|
||||
const permissions = data[extensionId]!.permissions || {};
|
||||
|
||||
for (const permissionType in permissions) {
|
||||
// @ts-ignore
|
||||
const permissionList = (permissions[permissionType] || []).toArray();
|
||||
|
||||
for (const permission of permissionList) {
|
||||
const label = extractText(permission.label);
|
||||
|
||||
if (this.itemHasQuery(label, query)) {
|
||||
const id = extensionId + '-' + permissionType + '-' + permission.permission;
|
||||
const corePage = !extensions[extensionId] ? extensionId.replace('core-', '') : null;
|
||||
const group = app.generalIndex.getGroup(extensionId);
|
||||
|
||||
results.push(
|
||||
new GeneralSearchResult(
|
||||
id,
|
||||
group?.label ||
|
||||
extensions[extensionId]?.extra['flarum-extension'].title ||
|
||||
app.translator.trans('core.admin.' + corePage + '.title', {}, true),
|
||||
group?.icon || extensions[extensionId]?.icon || { name: 'fas fa-key' },
|
||||
[
|
||||
app.translator.trans('core.admin.permissions.title', {}, true),
|
||||
extractText(permissionItems.get(permissionType)?.label) || permissionType,
|
||||
label,
|
||||
],
|
||||
group?.link || (corePage ? app.route(corePage) : app.route('extension', { id: extensionId }))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
protected itemHasQuery(item: string, query: string): boolean {
|
||||
return query.split(' ').every((part) => item.toLowerCase().includes(part));
|
||||
}
|
||||
|
||||
view(query: string): Array<Mithril.Vnode> {
|
||||
const results = (this.results.get(query) || []).slice(0, 30);
|
||||
const categories = Array.from(new Set([...results.map((r) => r.category)]));
|
||||
|
||||
if (!categories.length) return [];
|
||||
|
||||
return categories.map((category) => {
|
||||
return (
|
||||
<>
|
||||
<li className="GeneralSearchResult-group Dropdown-header">{category}</li>
|
||||
{results
|
||||
.filter((r) => r.category === category)
|
||||
.map((result) => {
|
||||
const phrase = escapeRegExp(query);
|
||||
const highlightRegExp = new RegExp(phrase + '|' + phrase.trim().replace(/\s+/g, '|'), 'gi');
|
||||
|
||||
const tree: any[] = result.tree.map((part) => {
|
||||
return highlight(part, highlightRegExp, undefined, true);
|
||||
});
|
||||
|
||||
let help: any = result.help;
|
||||
|
||||
if (help) {
|
||||
help = highlight(help, highlightRegExp, 100, true);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="GeneralSearchResult" data-index={'general-' + result.id} data-id={result.id}>
|
||||
<Link href={result.link}>
|
||||
<div className="ExtensionIcon" style={result.icon}>
|
||||
<Icon name={result.icon.name} />
|
||||
</div>
|
||||
<div className="GeneralSearchResult-info">
|
||||
<div className="GeneralSearchResult-tree">
|
||||
{tree.map((part, i) => {
|
||||
return [
|
||||
<span>{part}</span>,
|
||||
i < tree.length - 1 ? (
|
||||
<span className="GeneralSearchResult-tree-separator">
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</span>
|
||||
) : null,
|
||||
];
|
||||
})}
|
||||
</div>
|
||||
{help ? <div className="GeneralSearchResult-help">{help}</div> : null}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
customGrouping(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
fullPage(query: string): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
gotoItem(id: string): string | null {
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -1,9 +1,11 @@
|
||||
import app from '../../admin/app';
|
||||
import app from '../app';
|
||||
import Component from '../../common/Component';
|
||||
import LinkButton from '../../common/components/LinkButton';
|
||||
import SessionDropdown from './SessionDropdown';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import type Mithril from 'mithril';
|
||||
import Search from './Search';
|
||||
|
||||
/**
|
||||
* The `HeaderSecondary` component displays secondary header controls.
|
||||
@@ -15,11 +17,11 @@ export default class HeaderSecondary extends Component {
|
||||
|
||||
/**
|
||||
* Build an item list for the controls.
|
||||
*
|
||||
* @return {ItemList<import('mithril').Children>}
|
||||
*/
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
items.add('search', <Search state={app.search.state} />, 30);
|
||||
|
||||
items.add(
|
||||
'help',
|
@@ -193,4 +193,35 @@ export default class MailPage<CustomAttrs extends IPageAttrs = IPageAttrs> exten
|
||||
saveSettings(e: SaveSubmitEvent) {
|
||||
return super.saveSettings(e).then(() => this.refresh());
|
||||
}
|
||||
|
||||
static register() {
|
||||
app.generalIndex.group('core-mail', {
|
||||
label: app.translator.trans('core.admin.email.title', {}, true),
|
||||
icon: {
|
||||
name: 'fas fa-envelope',
|
||||
},
|
||||
link: app.route('mail'),
|
||||
});
|
||||
|
||||
app.generalIndex.for('core-mail').add('settings', [
|
||||
{
|
||||
id: 'mail_from',
|
||||
label: app.translator.trans('core.admin.email.addresses_heading', {}, true),
|
||||
},
|
||||
{
|
||||
id: 'mail_format',
|
||||
label: app.translator.trans('core.admin.email.format_heading', {}, true),
|
||||
help: app.translator.trans('core.admin.email.format_help', {}, true),
|
||||
},
|
||||
{
|
||||
id: 'mail_driver',
|
||||
label: app.translator.trans('core.admin.email.driver_heading', {}, true),
|
||||
},
|
||||
{
|
||||
id: 'send_test_mail_heading',
|
||||
label: app.translator.trans('core.admin.email.send_test_mail_heading', {}, true),
|
||||
help: app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user!.email() }, true),
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@@ -8,9 +8,11 @@ import type Mithril from 'mithril';
|
||||
import Icon from '../../common/components/Icon';
|
||||
|
||||
export interface PermissionConfig {
|
||||
permission: string;
|
||||
permission?: string;
|
||||
icon: string;
|
||||
label: Mithril.Children;
|
||||
id?: string;
|
||||
setting?: () => Mithril.Children;
|
||||
allowGuest?: boolean;
|
||||
}
|
||||
|
||||
@@ -137,64 +139,7 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
|
||||
viewItems() {
|
||||
const items = new ItemList<PermissionGridEntry>();
|
||||
|
||||
items.add(
|
||||
'viewForum',
|
||||
{
|
||||
icon: 'fas fa-eye',
|
||||
label: app.translator.trans('core.admin.permissions.view_forum_label'),
|
||||
permission: 'viewForum',
|
||||
allowGuest: true,
|
||||
},
|
||||
100
|
||||
);
|
||||
|
||||
items.add(
|
||||
'viewHiddenGroups',
|
||||
{
|
||||
icon: 'fas fa-users',
|
||||
label: app.translator.trans('core.admin.permissions.view_hidden_groups_label'),
|
||||
permission: 'viewHiddenGroups',
|
||||
},
|
||||
100
|
||||
);
|
||||
|
||||
items.add(
|
||||
'searchUsers',
|
||||
{
|
||||
icon: 'fas fa-users',
|
||||
label: app.translator.trans('core.admin.permissions.search_users_label'),
|
||||
permission: 'searchUsers',
|
||||
allowGuest: true,
|
||||
},
|
||||
100
|
||||
);
|
||||
|
||||
items.add(
|
||||
'signUp',
|
||||
{
|
||||
icon: 'fas fa-user-plus',
|
||||
label: app.translator.trans('core.admin.permissions.sign_up_label'),
|
||||
setting: () => (
|
||||
<SettingDropdown
|
||||
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') },
|
||||
]}
|
||||
lazyDraw
|
||||
/>
|
||||
),
|
||||
},
|
||||
90
|
||||
);
|
||||
|
||||
items.add('viewLastSeenAt', {
|
||||
icon: 'far fa-clock',
|
||||
label: app.translator.trans('core.admin.permissions.view_last_seen_at_label'),
|
||||
permission: 'user.viewLastSeenAt',
|
||||
});
|
||||
|
||||
items.merge(app.extensionData.getAllExtensionPermissions('view'));
|
||||
items.merge(app.registry.getAllPermissions('view'));
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -202,56 +147,7 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
|
||||
startItems() {
|
||||
const items = new ItemList<PermissionGridEntry>();
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
<SettingDropdown
|
||||
defaultLabel={
|
||||
minutes
|
||||
? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { 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') },
|
||||
]}
|
||||
lazyDraw
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
90
|
||||
);
|
||||
|
||||
items.add(
|
||||
'createAccessToken',
|
||||
{
|
||||
icon: 'fas fa-key',
|
||||
label: app.translator.trans('core.admin.permissions.create_access_token_label'),
|
||||
permission: 'createAccessToken',
|
||||
},
|
||||
80
|
||||
);
|
||||
|
||||
items.merge(app.extensionData.getAllExtensionPermissions('start'));
|
||||
items.merge(app.registry.getAllPermissions('start'));
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -259,70 +155,7 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
|
||||
replyItems() {
|
||||
const items = new ItemList<PermissionGridEntry>();
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
<SettingDropdown
|
||||
defaultLabel={
|
||||
minutes
|
||||
? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { 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
|
||||
);
|
||||
|
||||
items.add(
|
||||
'hideOwnPosts',
|
||||
{
|
||||
icon: 'far fa-trash-alt',
|
||||
label: app.translator.trans('core.admin.permissions.allow_hide_own_posts_label'),
|
||||
setting: () => {
|
||||
const minutes = parseInt(app.data.settings.allow_hide_own_posts, 10);
|
||||
|
||||
return SettingDropdown.component({
|
||||
defaultLabel: minutes
|
||||
? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { count: minutes })
|
||||
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
|
||||
key: 'allow_hide_own_posts',
|
||||
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') },
|
||||
{ value: '0', label: app.translator.trans('core.admin.permissions_controls.allow_never_button') },
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
80
|
||||
);
|
||||
|
||||
items.merge(app.extensionData.getAllExtensionPermissions('reply'));
|
||||
items.merge(app.registry.getAllPermissions('reply'));
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -330,127 +163,7 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
|
||||
moderateItems() {
|
||||
const items = new ItemList<PermissionGridEntry>();
|
||||
|
||||
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(
|
||||
'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(
|
||||
'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(
|
||||
'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(
|
||||
'userEditCredentials',
|
||||
{
|
||||
icon: 'fas fa-user-cog',
|
||||
label: app.translator.trans('core.admin.permissions.edit_users_credentials_label'),
|
||||
permission: 'user.editCredentials',
|
||||
},
|
||||
60
|
||||
);
|
||||
|
||||
items.add(
|
||||
'userEditGroups',
|
||||
{
|
||||
icon: 'fas fa-users-cog',
|
||||
label: app.translator.trans('core.admin.permissions.edit_users_groups_label'),
|
||||
permission: 'user.editGroups',
|
||||
},
|
||||
60
|
||||
);
|
||||
|
||||
items.add(
|
||||
'userEdit',
|
||||
{
|
||||
icon: 'fas fa-address-card',
|
||||
label: app.translator.trans('core.admin.permissions.edit_users_label'),
|
||||
permission: 'user.edit',
|
||||
},
|
||||
60
|
||||
);
|
||||
|
||||
items.add(
|
||||
'moderateAccessTokens',
|
||||
{
|
||||
icon: 'fas fa-key',
|
||||
label: app.translator.trans('core.admin.permissions.moderate_access_tokens_label'),
|
||||
permission: 'moderateAccessTokens',
|
||||
},
|
||||
60
|
||||
);
|
||||
|
||||
items.merge(app.extensionData.getAllExtensionPermissions('moderate'));
|
||||
items.merge(app.registry.getAllPermissions('moderate'));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import Group from '../../common/models/Group';
|
||||
import PermissionGrid from './PermissionGrid';
|
||||
import AdminPage from './AdminPage';
|
||||
import Icon from '../../common/components/Icon';
|
||||
import SettingDropdown from './SettingDropdown';
|
||||
|
||||
export default class PermissionsPage extends AdminPage {
|
||||
headerInfo() {
|
||||
@@ -41,4 +42,325 @@ export default class PermissionsPage extends AdminPage {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
static register() {
|
||||
app.generalIndex.group('core-permissions', {
|
||||
label: app.translator.trans('core.admin.permissions.title', {}, true),
|
||||
icon: {
|
||||
name: 'fas fa-key',
|
||||
},
|
||||
link: app.route('permissions'),
|
||||
});
|
||||
|
||||
app.registry.for('core-permissions');
|
||||
|
||||
PermissionsPage.registerViewPermissions();
|
||||
PermissionsPage.registerStartPermissions();
|
||||
PermissionsPage.registerReplyPermissions();
|
||||
PermissionsPage.registerModeratePermissions();
|
||||
}
|
||||
|
||||
static registerViewPermissions() {
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-eye',
|
||||
label: app.translator.trans('core.admin.permissions.view_forum_label'),
|
||||
permission: 'viewForum',
|
||||
allowGuest: true,
|
||||
},
|
||||
'view',
|
||||
100
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-users',
|
||||
label: app.translator.trans('core.admin.permissions.view_hidden_groups_label'),
|
||||
permission: 'viewHiddenGroups',
|
||||
},
|
||||
'view',
|
||||
100
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-users',
|
||||
label: app.translator.trans('core.admin.permissions.search_users_label'),
|
||||
permission: 'searchUsers',
|
||||
allowGuest: true,
|
||||
},
|
||||
'view',
|
||||
100
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-user-plus',
|
||||
label: app.translator.trans('core.admin.permissions.sign_up_label'),
|
||||
id: 'allow_sign_up',
|
||||
setting: () => (
|
||||
<SettingDropdown
|
||||
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') },
|
||||
]}
|
||||
lazyDraw
|
||||
/>
|
||||
),
|
||||
},
|
||||
'view',
|
||||
90
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'far fa-clock',
|
||||
label: app.translator.trans('core.admin.permissions.view_last_seen_at_label'),
|
||||
permission: 'user.viewLastSeenAt',
|
||||
},
|
||||
'view'
|
||||
);
|
||||
}
|
||||
|
||||
static registerStartPermissions() {
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-edit',
|
||||
label: app.translator.trans('core.admin.permissions.start_discussions_label'),
|
||||
permission: 'startDiscussion',
|
||||
},
|
||||
'start',
|
||||
100
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-i-cursor',
|
||||
label: app.translator.trans('core.admin.permissions.allow_renaming_label'),
|
||||
id: 'allow_renaming',
|
||||
setting: () => {
|
||||
const minutes = parseInt(app.data.settings.allow_renaming, 10);
|
||||
|
||||
return (
|
||||
<SettingDropdown
|
||||
defaultLabel={
|
||||
minutes
|
||||
? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { 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') },
|
||||
]}
|
||||
lazyDraw
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
'start',
|
||||
90
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-key',
|
||||
label: app.translator.trans('core.admin.permissions.create_access_token_label'),
|
||||
permission: 'createAccessToken',
|
||||
},
|
||||
'start',
|
||||
80
|
||||
);
|
||||
}
|
||||
|
||||
static registerReplyPermissions() {
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-reply',
|
||||
label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'),
|
||||
permission: 'discussion.reply',
|
||||
},
|
||||
'reply',
|
||||
100
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-pencil-alt',
|
||||
label: app.translator.trans('core.admin.permissions.allow_post_editing_label'),
|
||||
id: 'allow_post_editing',
|
||||
setting: () => {
|
||||
const minutes = parseInt(app.data.settings.allow_post_editing, 10);
|
||||
|
||||
return (
|
||||
<SettingDropdown
|
||||
defaultLabel={
|
||||
minutes
|
||||
? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { 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') },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
'reply',
|
||||
90
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'far fa-trash-alt',
|
||||
label: app.translator.trans('core.admin.permissions.allow_hide_own_posts_label'),
|
||||
id: 'allow_hide_own_posts',
|
||||
setting: () => {
|
||||
const minutes = parseInt(app.data.settings.allow_hide_own_posts, 10);
|
||||
|
||||
return SettingDropdown.component({
|
||||
defaultLabel: minutes
|
||||
? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { count: minutes })
|
||||
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
|
||||
key: 'allow_hide_own_posts',
|
||||
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') },
|
||||
{ value: '0', label: app.translator.trans('core.admin.permissions_controls.allow_never_button') },
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
'reply',
|
||||
80
|
||||
);
|
||||
}
|
||||
|
||||
static registerModeratePermissions() {
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-bullseye',
|
||||
label: app.translator.trans('core.admin.permissions.view_post_ips_label'),
|
||||
permission: 'discussion.viewIpsPosts',
|
||||
},
|
||||
'moderate',
|
||||
110
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-i-cursor',
|
||||
label: app.translator.trans('core.admin.permissions.rename_discussions_label'),
|
||||
permission: 'discussion.rename',
|
||||
},
|
||||
'moderate',
|
||||
100
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'far fa-trash-alt',
|
||||
label: app.translator.trans('core.admin.permissions.delete_discussions_label'),
|
||||
permission: 'discussion.hide',
|
||||
},
|
||||
'moderate',
|
||||
90
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-times',
|
||||
label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'),
|
||||
permission: 'discussion.delete',
|
||||
},
|
||||
'moderate',
|
||||
80
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-swimmer',
|
||||
label: app.translator.trans('core.admin.permissions.post_without_throttle_label'),
|
||||
permission: 'postWithoutThrottle',
|
||||
},
|
||||
'moderate',
|
||||
70
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-pencil-alt',
|
||||
label: app.translator.trans('core.admin.permissions.edit_posts_label'),
|
||||
permission: 'discussion.editPosts',
|
||||
},
|
||||
'moderate',
|
||||
70
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'far fa-trash-alt',
|
||||
label: app.translator.trans('core.admin.permissions.delete_posts_label'),
|
||||
permission: 'discussion.hidePosts',
|
||||
},
|
||||
'moderate',
|
||||
60
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-times',
|
||||
label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'),
|
||||
permission: 'discussion.deletePosts',
|
||||
},
|
||||
'moderate',
|
||||
60
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-user-cog',
|
||||
label: app.translator.trans('core.admin.permissions.edit_users_credentials_label'),
|
||||
permission: 'user.editCredentials',
|
||||
},
|
||||
'moderate',
|
||||
60
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-users-cog',
|
||||
label: app.translator.trans('core.admin.permissions.edit_users_groups_label'),
|
||||
permission: 'user.editGroups',
|
||||
},
|
||||
'moderate',
|
||||
60
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-address-card',
|
||||
label: app.translator.trans('core.admin.permissions.edit_users_label'),
|
||||
permission: 'user.edit',
|
||||
},
|
||||
'moderate',
|
||||
60
|
||||
);
|
||||
|
||||
app.registry.registerPermission(
|
||||
{
|
||||
icon: 'fas fa-key',
|
||||
label: app.translator.trans('core.admin.permissions.moderate_access_tokens_label'),
|
||||
permission: 'moderateAccessTokens',
|
||||
},
|
||||
'moderate',
|
||||
60
|
||||
);
|
||||
}
|
||||
}
|
||||
|
21
framework/core/js/src/admin/components/Search.tsx
Normal file
21
framework/core/js/src/admin/components/Search.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import AbstractSearch, { type SearchAttrs, type SearchSource as BaseSearchSource } from '../../common/components/AbstractSearch';
|
||||
import GeneralSearchSource from './GeneralSearchSource';
|
||||
import app from '../app';
|
||||
|
||||
export interface SearchSource extends BaseSearchSource {}
|
||||
|
||||
export default class Search extends AbstractSearch {
|
||||
static initAttrs(attrs: SearchAttrs) {
|
||||
attrs.label = app.translator.trans('core.admin.header.search_placeholder', {}, true);
|
||||
attrs.a11yRoleLabel = app.translator.trans('core.admin.header.search_role_label', {}, true);
|
||||
}
|
||||
|
||||
sourceItems(): ItemList<SearchSource> {
|
||||
const items = new ItemList<SearchSource>();
|
||||
|
||||
items.add('general', new GeneralSearchSource());
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
@@ -13,7 +13,7 @@ export default class ExtensionPageResolver<
|
||||
static extension: string | null = null;
|
||||
|
||||
onmatch(args: Attrs & RouteArgs, requestedPath: string, route: string) {
|
||||
const extensionPage = app.extensionData.getPage<Attrs>(args.id);
|
||||
const extensionPage = app.registry.getPage<Attrs>(args.id);
|
||||
|
||||
if (extensionPage) {
|
||||
return Promise.resolve(extensionPage);
|
||||
|
53
framework/core/js/src/admin/states/GeneralSearchIndex.ts
Normal file
53
framework/core/js/src/admin/states/GeneralSearchIndex.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export type GeneralIndexItem = {
|
||||
id: string;
|
||||
tree?: string[];
|
||||
label: string;
|
||||
help?: string;
|
||||
link?: string;
|
||||
visible?: () => boolean;
|
||||
};
|
||||
|
||||
export type GeneralIndexData = Record<string, Record<'settings' | 'permissions', GeneralIndexItem[]>>;
|
||||
|
||||
export type GeneralIndexGroup = {
|
||||
label: string;
|
||||
icon: {
|
||||
name: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
link: string;
|
||||
};
|
||||
|
||||
export default class GeneralSearchIndex {
|
||||
protected currentId: string = '';
|
||||
protected data: GeneralIndexData = {};
|
||||
protected groups: Record<string, GeneralIndexGroup> = {};
|
||||
|
||||
public group(id: string, data: GeneralIndexGroup) {
|
||||
this.groups[id] = data;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public for(id: string) {
|
||||
this.currentId = id;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public add(type: 'settings' | 'permissions', items: GeneralIndexItem[]) {
|
||||
this.data[this.currentId] ||= {
|
||||
settings: [],
|
||||
permissions: [],
|
||||
};
|
||||
this.data[this.currentId][type].push(...items);
|
||||
}
|
||||
|
||||
public getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public getGroup(id: string): null | GeneralIndexGroup {
|
||||
return this.groups[id] || null;
|
||||
}
|
||||
}
|
@@ -2,15 +2,15 @@ import type Mithril from 'mithril';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import { SettingsComponentOptions } from '../components/AdminPage';
|
||||
import ExtensionPage, { ExtensionPageAttrs } from '../components/ExtensionPage';
|
||||
import { PermissionConfig, PermissionType } from '../components/PermissionGrid';
|
||||
import type { PermissionConfig, PermissionType } from '../components/PermissionGrid';
|
||||
|
||||
type SettingConfigInput = SettingsComponentOptions | (() => Mithril.Children);
|
||||
export type SettingConfigInput = SettingsComponentOptions | (() => Mithril.Children);
|
||||
|
||||
type SettingConfigInternal = SettingsComponentOptions | ((() => Mithril.Children) & { setting: string });
|
||||
export type SettingConfigInternal = SettingsComponentOptions | ((() => Mithril.Children) & { setting: string });
|
||||
|
||||
export type CustomExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionPageAttrs> = new () => ExtensionPage<Attrs>;
|
||||
|
||||
type ExtensionConfig = {
|
||||
export type ExtensionConfig = {
|
||||
settings?: ItemList<SettingConfigInternal>;
|
||||
permissions?: {
|
||||
view?: ItemList<PermissionConfig>;
|
||||
@@ -37,7 +37,7 @@ type InnerDataActiveExtension = {
|
||||
|
||||
const noActiveExtensionErrorMessage = 'You must select an active extension via `.for()` before using extensionData.';
|
||||
|
||||
export default class ExtensionData {
|
||||
export default class AdminRegistry {
|
||||
protected state: InnerDataActiveExtension | InnerDataNoActiveExtension = {
|
||||
currentExtension: null,
|
||||
data: {},
|
||||
@@ -47,7 +47,7 @@ export default class ExtensionData {
|
||||
* This function simply takes the extension id
|
||||
*
|
||||
* @example
|
||||
* app.extensionData.for('flarum-tags')
|
||||
* app.registry.for('flarum-tags')
|
||||
*
|
||||
* flarum/flags -> flarum-flags | acme/extension -> acme-extension
|
||||
*/
|
||||
@@ -114,7 +114,11 @@ export default class ExtensionData {
|
||||
|
||||
const permissionsForType = permissions[permissionType] || new ItemList();
|
||||
|
||||
permissionsForType.add(content.permission, content, priority);
|
||||
if (!content.permission && !content.id) {
|
||||
throw new Error('Permission definition must have either a permission or id attribute.');
|
||||
}
|
||||
|
||||
permissionsForType.add(content.permission || content.id!, content, priority);
|
||||
|
||||
this.state.data[this.state.currentExtension].permissions = { ...permissions, [permissionType]: permissionsForType };
|
||||
|
||||
@@ -145,7 +149,7 @@ export default class ExtensionData {
|
||||
/**
|
||||
* Get an ItemList of all extensions' registered permissions
|
||||
*/
|
||||
getAllExtensionPermissions(type: PermissionType): ItemList<PermissionConfig> {
|
||||
getAllPermissions(type: PermissionType): ItemList<PermissionConfig> {
|
||||
const items = new ItemList<PermissionConfig>();
|
||||
|
||||
Object.keys(this.state.data).map((extension) => {
|
||||
@@ -183,4 +187,8 @@ export default class ExtensionData {
|
||||
getPage<Attrs extends ExtensionPageAttrs = ExtensionPageAttrs>(extension: string): CustomExtensionPage<Attrs> | undefined {
|
||||
return this.state.data[extension]?.page as CustomExtensionPage<Attrs> | undefined;
|
||||
}
|
||||
|
||||
getData() {
|
||||
return this.state.data;
|
||||
}
|
||||
}
|
@@ -280,7 +280,7 @@ export default class Application {
|
||||
this.translator.setLocale(payload.locale);
|
||||
}
|
||||
|
||||
public boot() {
|
||||
protected initialize(): CallableFunction[] {
|
||||
const caughtInitializationErrors: CallableFunction[] = [];
|
||||
|
||||
this.initializers.toArray().forEach((initializer) => {
|
||||
@@ -301,12 +301,20 @@ export default class Application {
|
||||
}
|
||||
});
|
||||
|
||||
return caughtInitializationErrors;
|
||||
}
|
||||
|
||||
public boot() {
|
||||
const caughtInitializationErrors: CallableFunction[] = this.initialize();
|
||||
|
||||
this.store.pushPayload({ data: this.data.resources });
|
||||
|
||||
this.forum = this.store.getById('forums', '1')!;
|
||||
|
||||
this.session = new Session(this.store.getById<User>('users', String(this.data.session.userId)) ?? null, this.data.session.csrfToken);
|
||||
|
||||
this.beforeMount();
|
||||
|
||||
this.mount();
|
||||
|
||||
this.initialRoute = window.location.href;
|
||||
@@ -314,6 +322,10 @@ export default class Application {
|
||||
caughtInitializationErrors.forEach((handler) => handler());
|
||||
}
|
||||
|
||||
protected beforeMount(): void {
|
||||
// ...
|
||||
}
|
||||
|
||||
public bootExtensions(extensions: Record<string, { extend?: IExtender[] }>) {
|
||||
Object.keys(extensions).forEach((name) => {
|
||||
const extension = extensions[name];
|
||||
|
133
framework/core/js/src/common/components/AbstractSearch.tsx
Normal file
133
framework/core/js/src/common/components/AbstractSearch.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import app from '../app';
|
||||
import Component, { ComponentAttrs } from '../Component';
|
||||
import SearchState from '../states/SearchState';
|
||||
import extractText from '../utils/extractText';
|
||||
import ItemList from '../utils/ItemList';
|
||||
import Input from './Input';
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
export interface SearchAttrs extends ComponentAttrs {
|
||||
/** The type of alert this is. Will be used to give the alert a class name of `Alert--{type}`. */
|
||||
state: SearchState;
|
||||
label: string;
|
||||
a11yRoleLabel: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `SearchSource` interface defines a section of search results in the
|
||||
* search dropdown.
|
||||
*
|
||||
* Search sources should be registered with the `Search` component class
|
||||
* by extending the `sourceItems` method. When the user types a
|
||||
* query, each search source will be prompted to load search results via the
|
||||
* `search` method. When the dropdown is redrawn, it will be constructed by
|
||||
* putting together the output from the `view` method of each source.
|
||||
*/
|
||||
export interface SearchSource {
|
||||
/**
|
||||
* The resource type that this search source is responsible for.
|
||||
*/
|
||||
resource: string;
|
||||
|
||||
/**
|
||||
* Get the title for this search source.
|
||||
*/
|
||||
title(): string;
|
||||
|
||||
/**
|
||||
* Check if a query has been cached for this search source.
|
||||
*/
|
||||
isCached(query: string): boolean;
|
||||
|
||||
/**
|
||||
* Make a request to get results for the given query.
|
||||
* The results will be updated internally in the search source, not exposed.
|
||||
*/
|
||||
search(query: string, limit: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get an array of virtual <li>s that list the search results for the given
|
||||
* query.
|
||||
*/
|
||||
view(query: string): Array<Mithril.Vnode>;
|
||||
|
||||
/**
|
||||
* Whether the search results view uses custom grouping of the results.
|
||||
* Prevents the `Search Preview` default group from display.
|
||||
*/
|
||||
customGrouping(): boolean;
|
||||
|
||||
/**
|
||||
* Get a list item for the full search results page.
|
||||
*/
|
||||
fullPage(query: string): Mithril.Vnode | null;
|
||||
|
||||
/**
|
||||
* Get to the result item page. Only called if each list item has a data-id.
|
||||
*/
|
||||
gotoItem(id: string): string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `Search` component displays a primary search input at the top of the frontend (forum or admin).
|
||||
* When clicked, it opens an advanced search modal with results from various sources.
|
||||
*
|
||||
* Must be extended and the abstract methods implemented per-frontend.
|
||||
*/
|
||||
export default abstract class AbstractSearch<T extends SearchAttrs = SearchAttrs> extends Component<T, SearchState> {
|
||||
/**
|
||||
* The instance of `SearchState` for this component.
|
||||
*/
|
||||
protected searchState!: SearchState;
|
||||
|
||||
oninit(vnode: Mithril.Vnode<T, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.searchState = this.attrs.state;
|
||||
}
|
||||
|
||||
view() {
|
||||
// Hide the search view if no sources were loaded
|
||||
if (this.sourceItems().isEmpty()) return <div></div>;
|
||||
|
||||
const openSearchModal = () => {
|
||||
this.$('input').blur() &&
|
||||
app.modal.show(() => import('../../common/components/SearchModal'), { searchState: this.searchState, sources: this.sourceItems().toArray() });
|
||||
};
|
||||
|
||||
return (
|
||||
<div role="search" className="Search" aria-label={this.attrs.a11yRoleLabel}>
|
||||
<Input
|
||||
type="search"
|
||||
className="Search-input"
|
||||
clearable={this.searchState.getValue()}
|
||||
clearLabel={app.translator.trans('core.lib.search.search_clear_button_accessible_label')}
|
||||
prefixIcon="fas fa-search"
|
||||
aria-label={this.attrs.label}
|
||||
readonly={true}
|
||||
placeholder={this.attrs.label}
|
||||
value={this.searchState.getValue()}
|
||||
onchange={(value: string) => {
|
||||
if (!value) this.searchState.clear();
|
||||
else this.searchState.setValue(value);
|
||||
}}
|
||||
inputAttrs={{
|
||||
onclick: () => setTimeout(() => openSearchModal(), 150),
|
||||
// for keyboard navigation, click event would be triggered on keydown
|
||||
onkeydown: (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.$('input').blur() && openSearchModal();
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of search sources that can be used to query for search results.
|
||||
*/
|
||||
abstract sourceItems(): ItemList<SearchSource>;
|
||||
}
|
@@ -11,6 +11,7 @@ import type { IUploadImageButtonAttrs } from './UploadImageButton';
|
||||
import type { ComponentAttrs } from '../Component';
|
||||
import type Mithril from 'mithril';
|
||||
import MultiSelect from './MultiSelect';
|
||||
import FieldSet from './FieldSet';
|
||||
|
||||
/**
|
||||
* A type that matches any valid value for the `type` attribute on an HTML `<input>` element.
|
||||
@@ -63,9 +64,11 @@ export interface HTMLInputFieldComponentOptions extends CommonFieldOptions {
|
||||
|
||||
const BooleanSettingTypes = ['bool', 'checkbox', 'switch', 'boolean'] as const;
|
||||
const SelectSettingTypes = ['select', 'dropdown', 'selectdropdown'] as const;
|
||||
const RadioSettingTypes = ['radio'] as const;
|
||||
const TextareaSettingTypes = ['textarea'] as const;
|
||||
const ColorPreviewSettingType = 'color-preview' as const;
|
||||
const ImageUploadSettingType = 'image-upload' as const;
|
||||
const StackedFormControlType = 'stacked-text' as const;
|
||||
|
||||
/**
|
||||
* Valid options for the setting component builder to generate a Switch.
|
||||
@@ -78,7 +81,7 @@ export interface SwitchFieldComponentOptions extends CommonFieldOptions {
|
||||
* Valid options for the setting component builder to generate a Select dropdown.
|
||||
*/
|
||||
export interface SelectFieldComponentOptions extends CommonFieldOptions {
|
||||
type: typeof SelectSettingTypes[number];
|
||||
type: typeof SelectSettingTypes[number] | typeof RadioSettingTypes[number];
|
||||
/**
|
||||
* Map of values to their labels
|
||||
*/
|
||||
@@ -112,6 +115,14 @@ export interface ImageUploadFieldComponentOptions extends CommonFieldOptions, IU
|
||||
type: typeof ImageUploadSettingType;
|
||||
}
|
||||
|
||||
export interface StackedFormControlFieldComponentOptions extends CommonFieldOptions {
|
||||
type: typeof StackedFormControlType;
|
||||
textArea: {
|
||||
setting: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CustomFieldComponentOptions extends CommonFieldOptions {
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
@@ -127,11 +138,13 @@ export type FieldComponentOptions =
|
||||
| TextareaFieldComponentOptions
|
||||
| ColorPreviewFieldComponentOptions
|
||||
| ImageUploadFieldComponentOptions
|
||||
| StackedFormControlFieldComponentOptions
|
||||
| CustomFieldComponentOptions;
|
||||
|
||||
export type IFormGroupAttrs = ComponentAttrs &
|
||||
FieldComponentOptions & {
|
||||
stream?: Stream<any>;
|
||||
getSetting?: (key: string) => Stream<any>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -166,7 +179,7 @@ export default class FormGroup<CustomAttrs extends IFormGroupAttrs = IFormGroupA
|
||||
view(vnode: Mithril.Vnode<CustomAttrs, this>): Mithril.Children {
|
||||
const customFieldComponents = this.customFieldComponents();
|
||||
|
||||
const { help, type, label, stream, ...componentAttrs } = this.attrs;
|
||||
const { help, type, label, stream, getSetting, containerClassName, ...componentAttrs } = this.attrs;
|
||||
|
||||
// TypeScript being TypeScript
|
||||
const attrs = componentAttrs as unknown as Omit<IFormGroupAttrs, 'stream' | 'label' | 'help' | 'type'>;
|
||||
@@ -183,7 +196,7 @@ export default class FormGroup<CustomAttrs extends IFormGroupAttrs = IFormGroupA
|
||||
return (
|
||||
// TODO: Add aria-describedby for switch help text.
|
||||
//? Requires changes to Checkbox component to allow providing attrs directly for the element(s).
|
||||
<div className="Form-group">
|
||||
<div className={classList('Form-group', containerClassName)}>
|
||||
<Switch state={!!value && value !== '0'} onchange={stream} {...attrs}>
|
||||
{label}
|
||||
</Switch>
|
||||
@@ -198,6 +211,29 @@ export default class FormGroup<CustomAttrs extends IFormGroupAttrs = IFormGroupA
|
||||
settingElement = (
|
||||
<Tag id={inputId} aria-describedby={helpTextId} value={value || defaultValue} options={options} onchange={stream} {...otherAttrs} />
|
||||
);
|
||||
} else if ((RadioSettingTypes as readonly string[]).includes(type)) {
|
||||
const { default: defaultValue, options, multiple, ...otherAttrs } = attrs;
|
||||
|
||||
settingElement = (
|
||||
<FieldSet {...otherAttrs}>
|
||||
{options.map(({ value, label }: { value: string; label: string }) => (
|
||||
<label className="checkbox">
|
||||
<input type="radio" name="homePage" value={value} bidi={stream} />
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
</FieldSet>
|
||||
);
|
||||
} else if (type === StackedFormControlType) {
|
||||
const { textArea, ...otherAttrs } = attrs;
|
||||
const { setting: textAreaSetting, ...textAreaAttrs } = textArea;
|
||||
|
||||
settingElement = (
|
||||
<div className="StackedFormControl" {...otherAttrs}>
|
||||
<input type="text" className="FormControl" bidi={stream} />
|
||||
<textarea className="FormControl" bidi={getSetting?.(textAreaSetting)} {...textAreaAttrs} />
|
||||
</div>
|
||||
);
|
||||
} else if (type === ImageUploadSettingType) {
|
||||
const { value, ...otherAttrs } = attrs;
|
||||
|
||||
@@ -223,7 +259,7 @@ export default class FormGroup<CustomAttrs extends IFormGroupAttrs = IFormGroupA
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Form-group">
|
||||
<div className={classList('Form-group', containerClassName)}>
|
||||
{label && <label for={inputId}>{label}</label>}
|
||||
{help && (
|
||||
<div id={helpTextId} className="helpText">
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import app from '../../forum/app';
|
||||
import app from '../../common/app';
|
||||
import Component from '../Component';
|
||||
import Icon from './Icon';
|
||||
import LoadingIndicator from './LoadingIndicator';
|
||||
|
@@ -1,20 +1,20 @@
|
||||
import app from '../app';
|
||||
import type { IFormModalAttrs } from '../../common/components/FormModal';
|
||||
import FormModal from '../../common/components/FormModal';
|
||||
import type { IFormModalAttrs } from './FormModal';
|
||||
import FormModal from './FormModal';
|
||||
import type Mithril from 'mithril';
|
||||
import type SearchState from '../../common/states/SearchState';
|
||||
import KeyboardNavigatable from '../../common/utils/KeyboardNavigatable';
|
||||
import SearchManager from '../../common/SearchManager';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import Input from '../../common/components/Input';
|
||||
import Button from '../../common/components/Button';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
import InfoTile from '../../common/components/InfoTile';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import type { SearchSource } from './Search';
|
||||
import type IGambit from '../../common/query/IGambit';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import GambitsAutocomplete from '../../common/utils/GambitsAutocomplete';
|
||||
import type SearchState from '../states/SearchState';
|
||||
import KeyboardNavigatable from '../utils/KeyboardNavigatable';
|
||||
import SearchManager from '../SearchManager';
|
||||
import extractText from '../utils/extractText';
|
||||
import Input from './Input';
|
||||
import Button from './Button';
|
||||
import Stream from '../utils/Stream';
|
||||
import InfoTile from './InfoTile';
|
||||
import LoadingIndicator from './LoadingIndicator';
|
||||
import type IGambit from '../query/IGambit';
|
||||
import ItemList from '../utils/ItemList';
|
||||
import GambitsAutocomplete from '../utils/GambitsAutocomplete';
|
||||
import type { SearchSource } from './AbstractSearch';
|
||||
|
||||
export interface ISearchModalAttrs extends IFormModalAttrs {
|
||||
onchange: (value: string) => void;
|
||||
@@ -72,7 +72,7 @@ export default class SearchModal<CustomAttrs extends ISearchModalAttrs = ISearch
|
||||
}
|
||||
|
||||
title(): Mithril.Children {
|
||||
return app.translator.trans('core.forum.search.title');
|
||||
return app.translator.trans('core.lib.search.title');
|
||||
}
|
||||
|
||||
className(): string {
|
||||
@@ -87,7 +87,7 @@ export default class SearchModal<CustomAttrs extends ISearchModalAttrs = ISearch
|
||||
(value: string) => this.search(value)
|
||||
);
|
||||
|
||||
const searchLabel = extractText(app.translator.trans('core.forum.search.placeholder'));
|
||||
const searchLabel = extractText(app.translator.trans('core.lib.search.placeholder'));
|
||||
|
||||
return (
|
||||
<div className="Modal-body SearchModal-body">
|
||||
@@ -97,7 +97,7 @@ export default class SearchModal<CustomAttrs extends ISearchModalAttrs = ISearch
|
||||
type="search"
|
||||
loading={!!this.loadingSources.length}
|
||||
clearable={true}
|
||||
clearLabel={app.translator.trans('core.forum.header.search_clear_button_accessible_label')}
|
||||
clearLabel={app.translator.trans('core.lib.header.search_clear_button_accessible_label')}
|
||||
prefixIcon="fas fa-search"
|
||||
aria-label={searchLabel}
|
||||
placeholder={searchLabel}
|
||||
@@ -157,6 +157,7 @@ export default class SearchModal<CustomAttrs extends ISearchModalAttrs = ISearch
|
||||
const gambits = this.gambits();
|
||||
const fullPageLink = this.activeSource().fullPage(this.query());
|
||||
const results = this.activeSource()?.view(this.query());
|
||||
const customGrouping = this.activeSource().customGrouping();
|
||||
|
||||
if (shouldShowResults && fullPageLink) {
|
||||
items.add(
|
||||
@@ -175,7 +176,7 @@ export default class SearchModal<CustomAttrs extends ISearchModalAttrs = ISearch
|
||||
<div className="SearchModal-section">
|
||||
<hr className="Modal-divider" />
|
||||
<ul className="Dropdown-menu SearchModal-options" aria-live={gambits.length ? 'polite' : undefined}>
|
||||
<li className="Dropdown-header">{app.translator.trans('core.forum.search.options_heading')}</li>
|
||||
<li className="Dropdown-header">{app.translator.trans('core.lib.search.options_heading')}</li>
|
||||
{gambits}
|
||||
</ul>
|
||||
</div>,
|
||||
@@ -188,16 +189,16 @@ export default class SearchModal<CustomAttrs extends ISearchModalAttrs = ISearch
|
||||
<div className="SearchModal-section">
|
||||
<hr className="Modal-divider" />
|
||||
<ul className="Dropdown-menu SearchModal-results" aria-live={shouldShowResults ? 'polite' : undefined}>
|
||||
<li className="Dropdown-header">{app.translator.trans('core.forum.search.preview_heading')}</li>
|
||||
{!customGrouping && <li className="Dropdown-header">{app.translator.trans('core.lib.search.preview_heading')}</li>}
|
||||
{!shouldShowResults && (
|
||||
<li className="Dropdown-message">
|
||||
<InfoTile icon="fas fa-search">{app.translator.trans('core.forum.search.no_search_text')}</InfoTile>
|
||||
<InfoTile icon="fas fa-search">{app.translator.trans('core.lib.search.no_search_text')}</InfoTile>
|
||||
</li>
|
||||
)}
|
||||
{shouldShowResults && results}
|
||||
{shouldShowResults && !results?.length && (
|
||||
<li className="Dropdown-message">
|
||||
<InfoTile icon="far fa-tired">{app.translator.trans('core.forum.search.no_results_text')}</InfoTile>
|
||||
<InfoTile icon="far fa-tired">{app.translator.trans('core.lib.search.no_results_text')}</InfoTile>
|
||||
</li>
|
||||
)}
|
||||
{loading && (
|
63
framework/core/js/src/common/extenders/Admin.ts
Normal file
63
framework/core/js/src/common/extenders/Admin.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import IExtender, { IExtensionModule } from './IExtender';
|
||||
import type AdminApplication from '../../admin/AdminApplication';
|
||||
import type { CustomExtensionPage, SettingConfigInternal } from '../../admin/utils/AdminRegistry';
|
||||
import type { PermissionConfig, PermissionType } from '../../admin/components/PermissionGrid';
|
||||
import Mithril from 'mithril';
|
||||
|
||||
export default class Admin implements IExtender<AdminApplication> {
|
||||
protected settings: { setting?: () => SettingConfigInternal; customSetting?: () => Mithril.Children; priority: number }[] = [];
|
||||
protected permissions: { permission: () => PermissionConfig; type: PermissionType; priority: number }[] = [];
|
||||
protected customPage: CustomExtensionPage | null = null;
|
||||
|
||||
/**
|
||||
* Register a setting to be shown on the extension's settings page.
|
||||
*/
|
||||
setting(setting: () => SettingConfigInternal, priority = 0) {
|
||||
this.settings.push({ setting, priority });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom setting to be shown on the extension's settings page.
|
||||
*/
|
||||
customSetting(setting: () => Mithril.Children, priority = 0) {
|
||||
this.settings.push({ customSetting: setting, priority });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a permission to be shown on the extension's permissions page.
|
||||
*/
|
||||
permission(permission: () => PermissionConfig, type: PermissionType, priority = 0) {
|
||||
this.permissions.push({ permission, type, priority });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom page to be shown in the admin interface.
|
||||
*/
|
||||
page(page: CustomExtensionPage) {
|
||||
this.customPage = page;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
extend(app: AdminApplication, extension: IExtensionModule) {
|
||||
app.registry.for(extension.name);
|
||||
|
||||
this.settings.forEach(({ setting, customSetting, priority }) => {
|
||||
app.registry.registerSetting(setting ? setting() : customSetting!, priority);
|
||||
});
|
||||
|
||||
this.permissions.forEach(({ permission, type, priority }) => {
|
||||
app.registry.registerPermission(permission(), type, priority);
|
||||
});
|
||||
|
||||
if (this.customPage) {
|
||||
app.registry.registerPage(this.customPage);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,10 +1,9 @@
|
||||
import IExtender, { IExtensionModule } from './IExtender';
|
||||
import type Component from '../Component';
|
||||
import ForumApplication from '../../forum/ForumApplication';
|
||||
import type Application from '../Application';
|
||||
import type ForumApplication from '../../forum/ForumApplication';
|
||||
import type { NewComponent } from '../Application';
|
||||
|
||||
export default class Notification implements IExtender {
|
||||
export default class Notification implements IExtender<ForumApplication> {
|
||||
private notificationComponents: Record<string, new () => Component> = {};
|
||||
|
||||
/**
|
||||
@@ -19,7 +18,7 @@ export default class Notification implements IExtender {
|
||||
return this;
|
||||
}
|
||||
|
||||
extend(app: Application, extension: IExtensionModule): void {
|
||||
Object.assign((app as unknown as ForumApplication).notificationComponents, this.notificationComponents);
|
||||
extend(app: ForumApplication, extension: IExtensionModule): void {
|
||||
Object.assign(app.notificationComponents, this.notificationComponents);
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import IExtender, { IExtensionModule } from './IExtender';
|
||||
import Application from '../Application';
|
||||
import ForumApplication from '../../forum/ForumApplication';
|
||||
import type ForumApplication from '../../forum/ForumApplication';
|
||||
|
||||
export default class PostTypes implements IExtender {
|
||||
export default class PostTypes implements IExtender<ForumApplication> {
|
||||
private postComponents: Record<string, any> = {};
|
||||
|
||||
/**
|
||||
@@ -18,7 +17,7 @@ export default class PostTypes implements IExtender {
|
||||
return this;
|
||||
}
|
||||
|
||||
extend(app: Application, extension: IExtensionModule): void {
|
||||
Object.assign((app as unknown as ForumApplication).postComponents, this.postComponents);
|
||||
extend(app: ForumApplication, extension: IExtensionModule): void {
|
||||
Object.assign(app.postComponents, this.postComponents);
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import Store from './Store';
|
||||
import Search from './Search';
|
||||
import Notification from './Notification';
|
||||
import ThemeMode from './ThemeMode';
|
||||
import Admin from './Admin';
|
||||
|
||||
const extenders = {
|
||||
Model,
|
||||
@@ -14,6 +15,7 @@ const extenders = {
|
||||
Search,
|
||||
Notification,
|
||||
ThemeMode,
|
||||
Admin,
|
||||
};
|
||||
|
||||
export default extenders;
|
||||
|
@@ -52,6 +52,10 @@ export default class DiscussionsSearchSource implements SearchSource {
|
||||
}) as Array<Mithril.Vnode>;
|
||||
}
|
||||
|
||||
customGrouping(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
fullPage(query: string): Mithril.Vnode {
|
||||
const filter = app.search.gambits.apply('discussions', { q: query });
|
||||
const q = filter.q || null;
|
||||
|
@@ -52,6 +52,10 @@ export default class PostsSearchSource implements SearchSource {
|
||||
}) as Array<Mithril.Vnode>;
|
||||
}
|
||||
|
||||
customGrouping(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
fullPage(query: string): Mithril.Vnode {
|
||||
const filter = app.search.gambits.apply('posts', { q: query });
|
||||
const q = filter.q || null;
|
||||
|
@@ -1,130 +1,18 @@
|
||||
import app from '../../forum/app';
|
||||
import Component, { ComponentAttrs } from '../../common/Component';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import Input from '../../common/components/Input';
|
||||
import SearchState from '../../common/states/SearchState';
|
||||
import SearchModal from './SearchModal';
|
||||
import type Mithril from 'mithril';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import DiscussionsSearchSource from './DiscussionsSearchSource';
|
||||
import UsersSearchSource from './UsersSearchSource';
|
||||
import PostsSearchSource from './PostsSearchSource';
|
||||
import AbstractSearch, { type SearchAttrs, type SearchSource as BaseSearchSource } from '../../common/components/AbstractSearch';
|
||||
|
||||
export interface SearchAttrs extends ComponentAttrs {
|
||||
/** The type of alert this is. Will be used to give the alert a class name of `Alert--{type}`. */
|
||||
state: SearchState;
|
||||
}
|
||||
export interface SearchSource extends BaseSearchSource {}
|
||||
|
||||
/**
|
||||
* The `SearchSource` interface defines a section of search results in the
|
||||
* search dropdown.
|
||||
*
|
||||
* Search sources should be registered with the `Search` component class
|
||||
* by extending the `sourceItems` method. When the user types a
|
||||
* query, each search source will be prompted to load search results via the
|
||||
* `search` method. When the dropdown is redrawn, it will be constructed by
|
||||
* putting together the output from the `view` method of each source.
|
||||
*/
|
||||
export interface SearchSource {
|
||||
/**
|
||||
* The resource type that this search source is responsible for.
|
||||
*/
|
||||
resource: string;
|
||||
|
||||
/**
|
||||
* Get the title for this search source.
|
||||
*/
|
||||
title(): string;
|
||||
|
||||
/**
|
||||
* Check if a query has been cached for this search source.
|
||||
*/
|
||||
isCached(query: string): boolean;
|
||||
|
||||
/**
|
||||
* Make a request to get results for the given query.
|
||||
* The results will be updated internally in the search source, not exposed.
|
||||
*/
|
||||
search(query: string, limit: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get an array of virtual <li>s that list the search results for the given
|
||||
* query.
|
||||
*/
|
||||
view(query: string): Array<Mithril.Vnode>;
|
||||
|
||||
/**
|
||||
* Get a list item for the full search results page.
|
||||
*/
|
||||
fullPage(query: string): Mithril.Vnode | null;
|
||||
|
||||
/**
|
||||
* Get to the result item page. Only called if each list item has a data-id.
|
||||
*/
|
||||
gotoItem(id: string): string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `Search` component displays a menu of as-you-type results from a variety
|
||||
* of sources.
|
||||
*
|
||||
* The search box will be 'activated' if the app's search state's
|
||||
* getInitialSearch() value is a truthy value. If this is the case, an 'x'
|
||||
* button will be shown next to the search field, and clicking it will clear the search.
|
||||
*
|
||||
* ATTRS:
|
||||
*
|
||||
* - state: SearchState instance.
|
||||
*/
|
||||
export default class Search<T extends SearchAttrs = SearchAttrs> extends Component<T, SearchState> {
|
||||
/**
|
||||
* The instance of `SearchState` for this component.
|
||||
*/
|
||||
protected searchState!: SearchState;
|
||||
|
||||
oninit(vnode: Mithril.Vnode<T, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.searchState = this.attrs.state;
|
||||
export default class Search extends AbstractSearch {
|
||||
static initAttrs(attrs: SearchAttrs) {
|
||||
attrs.label = app.translator.trans('core.forum.header.search_placeholder', {}, true);
|
||||
attrs.a11yRoleLabel = app.translator.trans('core.forum.header.search_role_label', {}, true);
|
||||
}
|
||||
|
||||
view() {
|
||||
// Hide the search view if no sources were loaded
|
||||
if (this.sourceItems().isEmpty()) return <div></div>;
|
||||
|
||||
const searchLabel = extractText(app.translator.trans('core.forum.header.search_placeholder'));
|
||||
|
||||
return (
|
||||
<div role="search" className="Search" aria-label={app.translator.trans('core.forum.header.search_role_label')}>
|
||||
<Input
|
||||
type="search"
|
||||
className="Search-input"
|
||||
clearable={this.searchState.getValue()}
|
||||
clearLabel={app.translator.trans('core.forum.header.search_clear_button_accessible_label')}
|
||||
prefixIcon="fas fa-search"
|
||||
aria-label={searchLabel}
|
||||
readonly={true}
|
||||
placeholder={searchLabel}
|
||||
value={this.searchState.getValue()}
|
||||
onchange={(value: string) => {
|
||||
if (!value) this.searchState.clear();
|
||||
else this.searchState.setValue(value);
|
||||
}}
|
||||
inputAttrs={{
|
||||
onfocus: () =>
|
||||
setTimeout(() => {
|
||||
this.$('input').blur() &&
|
||||
app.modal.show(() => import('./SearchModal'), { searchState: this.searchState, sources: this.sourceItems().toArray() });
|
||||
}, 150),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of search sources that can be used to query for search results.
|
||||
*/
|
||||
sourceItems(): ItemList<SearchSource> {
|
||||
const items = new ItemList<SearchSource>();
|
||||
|
||||
|
@@ -70,6 +70,10 @@ export default class UsersSearchSource implements SearchSource {
|
||||
});
|
||||
}
|
||||
|
||||
customGrouping(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
fullPage(query: string): null {
|
||||
return null;
|
||||
}
|
||||
|
@@ -16,3 +16,4 @@
|
||||
@import "admin/MailPage";
|
||||
@import "admin/NoJs";
|
||||
@import "admin/UsersListPage";
|
||||
@import "admin/GeneralSearchResult";
|
||||
|
35
framework/core/less/admin/GeneralSearchResult.less
Normal file
35
framework/core/less/admin/GeneralSearchResult.less
Normal file
@@ -0,0 +1,35 @@
|
||||
.GeneralSearchResult {
|
||||
.ExtensionIcon {
|
||||
--size: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-group {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&-tree {
|
||||
font-weight: bold;
|
||||
|
||||
&-separator {
|
||||
color: var(--muted-more-color);
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&-help {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--muted-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-info {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: var(--highlight-color);
|
||||
}
|
||||
}
|
@@ -66,6 +66,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.FieldSet-label:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.FieldSet--min .FieldSet-items > * {
|
||||
width: auto;
|
||||
}
|
||||
|
@@ -213,6 +213,8 @@
|
||||
--hero-bg: var(--control-bg);
|
||||
--hero-color: var(--control-color);
|
||||
|
||||
--highlight-color: #FFE300;
|
||||
|
||||
.light-contents-vars();
|
||||
|
||||
.Button--color-vars(@control-success-color, @control-success-bg, 'control-success');
|
||||
|
@@ -74,7 +74,7 @@ p {
|
||||
}
|
||||
|
||||
mark {
|
||||
background: #FFE300;
|
||||
background: var(--highlight-color);
|
||||
padding: 1px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
|
||||
|
@@ -246,6 +246,8 @@ core:
|
||||
header:
|
||||
get_help: Get Help
|
||||
log_out_button: => core.ref.log_out
|
||||
search_placeholder: Search...
|
||||
search_role_label: Search
|
||||
|
||||
# These translations are used in the modal dialog displayed when loading extensions.
|
||||
loading:
|
||||
@@ -331,6 +333,11 @@ core:
|
||||
signup_closed_button: Closed
|
||||
signup_open_button: Open
|
||||
|
||||
# These translations are used by the admin-specific search sources.
|
||||
search_source:
|
||||
general:
|
||||
heading: General
|
||||
|
||||
# These translations are used generically in setting fields.
|
||||
settings:
|
||||
saved_message: Your changes were saved.
|
||||
@@ -480,7 +487,6 @@ core:
|
||||
log_in_link: => core.ref.log_in
|
||||
log_out_button: => core.ref.log_out
|
||||
profile_button: Profile
|
||||
search_clear_button_accessible_label: Clear search query
|
||||
search_placeholder: Search Forum
|
||||
search_role_label: Search Forum
|
||||
session_dropdown_accessible_label: Toggle session options dropdown menu
|
||||
@@ -583,17 +589,6 @@ core:
|
||||
submit_button: => core.ref.rename
|
||||
title: Rename Discussion
|
||||
|
||||
# These translations are used by the search modal.
|
||||
search:
|
||||
gambit_plus_button_a11y_label: Add a positive filter
|
||||
gambit_minus_button_a11y_label: Add a negative filter
|
||||
title: Search
|
||||
no_results_text: It looks like there are no results here.
|
||||
no_search_text: You have not searched for anything yet.
|
||||
options_heading: Search options
|
||||
placeholder: Search...
|
||||
preview_heading: Search preview
|
||||
|
||||
# These translations are used in the Security page.
|
||||
security:
|
||||
browser_on_operating_system: "{browser} on {os}"
|
||||
@@ -744,6 +739,32 @@ core:
|
||||
rate_limit_exceeded_message: You're going a little too quickly. Please try again in a few seconds.
|
||||
render_failed_message: Sorry, we encountered an error while displaying this content. If you're a user, please try again later. If you're an administrator, take a look in your Flarum log files for more information.
|
||||
|
||||
# These translations are used by gambits. Gambit keys must be in snake_case, no spaces.
|
||||
gambits:
|
||||
boolean_key: is
|
||||
discussions:
|
||||
author:
|
||||
key: author
|
||||
hint: username or comma-separated list of usernames
|
||||
created:
|
||||
key: created
|
||||
hint: 2020-12-31 or 2020-12-31..2021-09-30
|
||||
hidden:
|
||||
key: hidden
|
||||
unread:
|
||||
key: unread
|
||||
posts:
|
||||
discussion:
|
||||
key: discussion
|
||||
hint: the ID of the discussion
|
||||
users:
|
||||
email:
|
||||
key: email
|
||||
hint: example@machine.local
|
||||
group:
|
||||
key: group
|
||||
hint: singular or plural group names
|
||||
|
||||
# These translations are used in the input component.
|
||||
input:
|
||||
clear_button: Clear input
|
||||
@@ -781,6 +802,18 @@ core:
|
||||
kilo_text: K
|
||||
mega_text: M
|
||||
|
||||
# These translations are used by the abstract search component and search modal.
|
||||
search:
|
||||
gambit_plus_button_a11y_label: Add a positive filter
|
||||
gambit_minus_button_a11y_label: Add a negative filter
|
||||
title: Search
|
||||
no_results_text: It looks like there are no results here.
|
||||
no_search_text: You have not searched for anything yet.
|
||||
options_heading: Search options
|
||||
placeholder: Search...
|
||||
preview_heading: Search preview
|
||||
search_clear_button_accessible_label: Clear search query
|
||||
|
||||
# These translations are used by search sources.
|
||||
search_source:
|
||||
discussions:
|
||||
@@ -792,32 +825,6 @@ core:
|
||||
users:
|
||||
heading: => core.ref.users
|
||||
|
||||
# These translations are used by gambits. Gambit keys must be in snake_case, no spaces.
|
||||
gambits:
|
||||
boolean_key: is
|
||||
discussions:
|
||||
author:
|
||||
key: author
|
||||
hint: username or comma-separated list of usernames
|
||||
created:
|
||||
key: created
|
||||
hint: 2020-12-31 or 2020-12-31..2021-09-30
|
||||
hidden:
|
||||
key: hidden
|
||||
unread:
|
||||
key: unread
|
||||
posts:
|
||||
discussion:
|
||||
key: discussion
|
||||
hint: the ID of the discussion
|
||||
users:
|
||||
email:
|
||||
key: email
|
||||
hint: example@machine.local
|
||||
group:
|
||||
key: group
|
||||
hint: singular or plural group names
|
||||
|
||||
# These translations are used to punctuate a series of items.
|
||||
series:
|
||||
glue_text: ", "
|
||||
|
Reference in New Issue
Block a user