mirror of
https://github.com/flarum/core.git
synced 2025-08-06 08:27:42 +02:00
feat: advanced admin registry extenders (#4209)
* feat: advanced admin registry extenders * fix: admin extender execution order
This commit is contained in:
@@ -129,12 +129,14 @@ export default class AdminApplication extends Application {
|
|||||||
this.route = (Object.getPrototypeOf(Object.getPrototypeOf(this)) as Application).route.bind(this);
|
this.route = (Object.getPrototypeOf(Object.getPrototypeOf(this)) as Application).route.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected beforeMount(): void {
|
protected runBeforeMount(): void {
|
||||||
BasicsPage.register();
|
BasicsPage.register();
|
||||||
AppearancePage.register();
|
AppearancePage.register();
|
||||||
MailPage.register();
|
MailPage.register();
|
||||||
AdvancedPage.register();
|
AdvancedPage.register();
|
||||||
PermissionsPage.register();
|
PermissionsPage.register();
|
||||||
|
|
||||||
|
super.runBeforeMount();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -9,7 +9,7 @@ import Icon from '../../common/components/Icon';
|
|||||||
import PermissionGrid from './PermissionGrid';
|
import PermissionGrid from './PermissionGrid';
|
||||||
import escapeRegExp from '../../common/utils/escapeRegExp';
|
import escapeRegExp from '../../common/utils/escapeRegExp';
|
||||||
import { GeneralIndexData, GeneralIndexItem } from '../states/GeneralSearchIndex';
|
import { GeneralIndexData, GeneralIndexItem } from '../states/GeneralSearchIndex';
|
||||||
import { ExtensionConfig, SettingConfigInternal } from '../utils/AdminRegistry';
|
import { ExtensionConfig, SettingConfigInput } from '../utils/AdminRegistry';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
|
||||||
export class GeneralSearchResult {
|
export class GeneralSearchResult {
|
||||||
@@ -94,7 +94,7 @@ export default class GeneralSearchSource implements GlobalSearchSource {
|
|||||||
for (const extensionId in data) {
|
for (const extensionId in data) {
|
||||||
// settings
|
// settings
|
||||||
const settings = data[extensionId]!.settings;
|
const settings = data[extensionId]!.settings;
|
||||||
let normalizedSettings: GeneralIndexItem[] | SettingConfigInternal[] = [];
|
let normalizedSettings: GeneralIndexItem[] | SettingConfigInput[] = [];
|
||||||
|
|
||||||
if (settings instanceof ItemList) {
|
if (settings instanceof ItemList) {
|
||||||
normalizedSettings = settings?.toArray();
|
normalizedSettings = settings?.toArray();
|
||||||
@@ -113,7 +113,7 @@ export default class GeneralSearchSource implements GlobalSearchSource {
|
|||||||
const group = app.generalIndex.getGroup(extensionId);
|
const group = app.generalIndex.getGroup(extensionId);
|
||||||
|
|
||||||
if (this.itemHasQuery(label, query) || this.itemHasQuery(help, query)) {
|
if (this.itemHasQuery(label, query) || this.itemHasQuery(help, query)) {
|
||||||
const id = extensionId + '-' + ('setting' in setting ? setting : setting.id);
|
const id = extensionId + '-' + ('setting' in setting ? setting : 'id' in setting ? setting.id : '');
|
||||||
|
|
||||||
results.push(
|
results.push(
|
||||||
new GeneralSearchResult(
|
new GeneralSearchResult(
|
||||||
|
@@ -71,7 +71,7 @@ export default class AdminRegistry {
|
|||||||
* label: app.translator.trans('flarum-flags.admin.settings.guidelines_url_label')
|
* label: app.translator.trans('flarum-flags.admin.settings.guidelines_url_label')
|
||||||
* }, 15) // priority is optional (ItemList)
|
* }, 15) // priority is optional (ItemList)
|
||||||
*/
|
*/
|
||||||
registerSetting(content: SettingConfigInput, priority = 0): this {
|
registerSetting(content: SettingConfigInput, priority = 0, key: string | null = null): this {
|
||||||
if (this.state.currentExtension === null) {
|
if (this.state.currentExtension === null) {
|
||||||
throw new Error(noActiveExtensionErrorMessage);
|
throw new Error(noActiveExtensionErrorMessage);
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ export default class AdminRegistry {
|
|||||||
// To support multiple such items for one extension, we assign a random ID.
|
// To support multiple such items for one extension, we assign a random ID.
|
||||||
// 36 is arbitrary length, but makes collisions very unlikely.
|
// 36 is arbitrary length, but makes collisions very unlikely.
|
||||||
if (tmpContent instanceof Function) {
|
if (tmpContent instanceof Function) {
|
||||||
tmpContent.setting = Math.random().toString(36);
|
tmpContent.setting = key || Math.random().toString(36);
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings = this.state.data[this.state.currentExtension].settings || new ItemList();
|
const settings = this.state.data[this.state.currentExtension].settings || new ItemList();
|
||||||
@@ -94,6 +94,62 @@ export default class AdminRegistry {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function allows you to change the configuration of a setting.
|
||||||
|
*/
|
||||||
|
setSetting(key: string, content: SettingConfigInput | ((original: SettingConfigInput) => SettingConfigInput)): this {
|
||||||
|
if (this.state.currentExtension === null) {
|
||||||
|
throw new Error(noActiveExtensionErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = this.state.data[this.state.currentExtension].settings || new ItemList();
|
||||||
|
|
||||||
|
if (settings.has(key)) {
|
||||||
|
if (content instanceof Function) {
|
||||||
|
const original = settings.get(key);
|
||||||
|
content = content(original) as SettingConfigInternal;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.setContent(key, content as SettingConfigInternal);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function allows you to change the priority of a setting.
|
||||||
|
*/
|
||||||
|
setSettingPriority(key: string, priority: number): this {
|
||||||
|
if (this.state.currentExtension === null) {
|
||||||
|
throw new Error(noActiveExtensionErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = this.state.data[this.state.currentExtension].settings || new ItemList();
|
||||||
|
|
||||||
|
if (settings.has(key)) {
|
||||||
|
settings.setPriority(key, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function allows you to remove a setting.
|
||||||
|
*/
|
||||||
|
removeSetting(key: string): this {
|
||||||
|
if (this.state.currentExtension === null) {
|
||||||
|
throw new Error(noActiveExtensionErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = this.state.data[this.state.currentExtension].settings || new ItemList();
|
||||||
|
|
||||||
|
if (settings.has(key)) {
|
||||||
|
settings.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function registers your permission with Flarum
|
* This function registers your permission with Flarum
|
||||||
*
|
*
|
||||||
@@ -125,6 +181,65 @@ export default class AdminRegistry {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function allows you to change the configuration of a permission.
|
||||||
|
*/
|
||||||
|
setPermission(key: string, content: PermissionConfig | ((original: PermissionConfig) => PermissionConfig), permissionType: PermissionType): this {
|
||||||
|
if (this.state.currentExtension === null) {
|
||||||
|
throw new Error(noActiveExtensionErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = this.state.data[this.state.currentExtension].permissions || {};
|
||||||
|
const permissionsForType = permissions[permissionType] || new ItemList();
|
||||||
|
|
||||||
|
if (permissionsForType.has(key)) {
|
||||||
|
if (content instanceof Function) {
|
||||||
|
const original = permissionsForType.get(key);
|
||||||
|
content = content(original) as PermissionConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
permissionsForType.setContent(key, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function allows you to change the priority of a permission.
|
||||||
|
*/
|
||||||
|
setPermissionPriority(key: string, permissionType: PermissionType, priority: number): this {
|
||||||
|
if (this.state.currentExtension === null) {
|
||||||
|
throw new Error(noActiveExtensionErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = this.state.data[this.state.currentExtension].permissions;
|
||||||
|
const permissionsForType = permissions?.[permissionType] || new ItemList();
|
||||||
|
|
||||||
|
if (permissionsForType.has(key)) {
|
||||||
|
permissionsForType.setPriority(key, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function allows you to remove a permission.
|
||||||
|
*/
|
||||||
|
removePermission(key: string, permissionType: PermissionType): this {
|
||||||
|
if (this.state.currentExtension === null) {
|
||||||
|
throw new Error(noActiveExtensionErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = this.state.data[this.state.currentExtension].permissions;
|
||||||
|
const permissionsForType = permissions?.[permissionType] || new ItemList();
|
||||||
|
|
||||||
|
if (permissionsForType.has(key)) {
|
||||||
|
permissionsForType.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace the default extension page with a custom component.
|
* Replace the default extension page with a custom component.
|
||||||
* This component would typically extend ExtensionPage
|
* This component would typically extend ExtensionPage
|
||||||
|
@@ -286,6 +286,8 @@ export default class Application {
|
|||||||
|
|
||||||
private handledErrors: { extension: null | string; errorId: string; error: any }[] = [];
|
private handledErrors: { extension: null | string; errorId: string; error: any }[] = [];
|
||||||
|
|
||||||
|
private beforeMounts: (() => void)[] = [];
|
||||||
|
|
||||||
public load(payload: Application['data']) {
|
public load(payload: Application['data']) {
|
||||||
this.data = payload;
|
this.data = payload;
|
||||||
this.translator.setLocale(payload.locale);
|
this.translator.setLocale(payload.locale);
|
||||||
@@ -326,7 +328,7 @@ export default class Application {
|
|||||||
|
|
||||||
this.session = new Session(this.store.getById<User>('users', String(this.data.session.userId)) ?? null, this.data.session.csrfToken);
|
this.session = new Session(this.store.getById<User>('users', String(this.data.session.userId)) ?? null, this.data.session.csrfToken);
|
||||||
|
|
||||||
this.beforeMount();
|
this.runBeforeMount();
|
||||||
|
|
||||||
this.mount();
|
this.mount();
|
||||||
|
|
||||||
@@ -335,8 +337,13 @@ export default class Application {
|
|||||||
caughtInitializationErrors.forEach((handler) => handler());
|
caughtInitializationErrors.forEach((handler) => handler());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected beforeMount(): void {
|
public beforeMount(callback: () => void) {
|
||||||
// ...
|
this.beforeMounts.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected runBeforeMount(): void {
|
||||||
|
this.beforeMounts.forEach((callback) => callback());
|
||||||
|
this.beforeMounts = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public bootExtensions(extensions: Record<string, { extend?: IExtender[] }>) {
|
public bootExtensions(extensions: Record<string, { extend?: IExtender[] }>) {
|
||||||
|
@@ -1,25 +1,65 @@
|
|||||||
import IExtender, { IExtensionModule } from './IExtender';
|
import IExtender, { IExtensionModule } from './IExtender';
|
||||||
import type AdminApplication from '../../admin/AdminApplication';
|
import type AdminApplication from '../../admin/AdminApplication';
|
||||||
import type { CustomExtensionPage, SettingConfigInternal } from '../../admin/utils/AdminRegistry';
|
import type { CustomExtensionPage, SettingConfigInput } from '../../admin/utils/AdminRegistry';
|
||||||
import type { PermissionConfig, PermissionType } from '../../admin/components/PermissionGrid';
|
import type { PermissionConfig, PermissionType } from '../../admin/components/PermissionGrid';
|
||||||
import type Mithril from 'mithril';
|
import type Mithril from 'mithril';
|
||||||
import type { GeneralIndexItem } from '../../admin/states/GeneralSearchIndex';
|
import type { GeneralIndexItem } from '../../admin/states/GeneralSearchIndex';
|
||||||
|
|
||||||
export default class Admin implements IExtender<AdminApplication> {
|
export default class Admin implements IExtender<AdminApplication> {
|
||||||
protected settings: { setting?: () => SettingConfigInternal | null; customSetting?: () => Mithril.Children; priority: number }[] = [];
|
protected context: string | null;
|
||||||
|
|
||||||
|
protected settings: { setting?: () => SettingConfigInput | null; customSetting?: () => Mithril.Children; priority: number }[] = [];
|
||||||
|
protected settingReplacements: { setting: string; replacement: (original: SettingConfigInput) => SettingConfigInput }[] = [];
|
||||||
|
protected settingPriorityChanges: { setting: string; priority: number }[] = [];
|
||||||
|
protected settingRemovals: string[] = [];
|
||||||
protected permissions: { permission: () => PermissionConfig | null; type: PermissionType; priority: number }[] = [];
|
protected permissions: { permission: () => PermissionConfig | null; type: PermissionType; priority: number }[] = [];
|
||||||
|
protected permissionsReplacements: { permission: string; type: PermissionType; replacement: (original: PermissionConfig) => PermissionConfig }[] =
|
||||||
|
[];
|
||||||
|
protected permissionsPriorityChanges: { permission: string; type: PermissionType; priority: number }[] = [];
|
||||||
|
protected permissionsRemovals: { permission: string; type: PermissionType }[] = [];
|
||||||
protected customPage: CustomExtensionPage | null = null;
|
protected customPage: CustomExtensionPage | null = null;
|
||||||
protected generalIndexes: { settings?: () => GeneralIndexItem[]; permissions?: () => GeneralIndexItem[] } = {};
|
protected generalIndexes: { settings?: () => GeneralIndexItem[]; permissions?: () => GeneralIndexItem[] } = {};
|
||||||
|
|
||||||
|
constructor(context: string | null = null) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a setting to be shown on the extension's settings page.
|
* Register a setting to be shown on the extension's settings page.
|
||||||
*/
|
*/
|
||||||
setting(setting: () => SettingConfigInternal | null, priority = 0) {
|
setting(setting: () => SettingConfigInput | null, priority = 0) {
|
||||||
this.settings.push({ setting, priority });
|
this.settings.push({ setting, priority });
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace an existing setting's configuration.
|
||||||
|
*/
|
||||||
|
replaceSetting(setting: string, replacement: (original: SettingConfigInput) => SettingConfigInput) {
|
||||||
|
this.settingReplacements.push({ setting, replacement });
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the priority of an existing setting.
|
||||||
|
*/
|
||||||
|
setSettingPriority(setting: string, priority: number) {
|
||||||
|
this.settingPriorityChanges.push({ setting, priority });
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a setting from the extension's settings page.
|
||||||
|
*/
|
||||||
|
removeSetting(setting: string) {
|
||||||
|
this.settingRemovals.push(setting);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a custom setting to be shown on the extension's settings page.
|
* Register a custom setting to be shown on the extension's settings page.
|
||||||
*/
|
*/
|
||||||
@@ -38,6 +78,33 @@ export default class Admin implements IExtender<AdminApplication> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace an existing permission's configuration.
|
||||||
|
*/
|
||||||
|
replacePermission(permission: string, replacement: (original: PermissionConfig) => PermissionConfig, type: PermissionType) {
|
||||||
|
this.permissionsReplacements.push({ permission, type, replacement });
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the priority of an existing permission.
|
||||||
|
*/
|
||||||
|
setPermissionPriority(permission: string, type: PermissionType, priority: number) {
|
||||||
|
this.permissionsPriorityChanges.push({ permission, type, priority });
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a permission from the extension's permissions page.
|
||||||
|
*/
|
||||||
|
removePermission(permission: string, type: PermissionType) {
|
||||||
|
this.permissionsRemovals.push({ permission, type });
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a custom page to be shown in the admin interface.
|
* Register a custom page to be shown in the admin interface.
|
||||||
*/
|
*/
|
||||||
@@ -57,38 +124,64 @@ export default class Admin implements IExtender<AdminApplication> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extend(app: AdminApplication, extension: IExtensionModule) {
|
extend(app: AdminApplication, extension: IExtensionModule) {
|
||||||
app.registry.for(extension.name);
|
app.beforeMount(() => {
|
||||||
|
app.registry.for(this.context || extension.name);
|
||||||
|
|
||||||
this.settings.forEach(({ setting, customSetting, priority }) => {
|
this.settings.forEach(({ setting, customSetting, priority }) => {
|
||||||
const settingConfig = setting ? setting() : customSetting!;
|
const settingConfig = setting ? setting() : customSetting!;
|
||||||
|
|
||||||
if (settingConfig) {
|
if (settingConfig) {
|
||||||
app.registry.registerSetting(settingConfig, priority);
|
app.registry.registerSetting(settingConfig, priority);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settingReplacements.forEach(({ setting, replacement }) => {
|
||||||
|
app.registry.setSetting(setting, replacement);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settingPriorityChanges.forEach(({ setting, priority }) => {
|
||||||
|
app.registry.setSettingPriority(setting, priority);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settingRemovals.forEach((setting) => {
|
||||||
|
app.registry.removeSetting(setting);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.permissions.forEach(({ permission, type, priority }) => {
|
||||||
|
const permissionConfig = permission();
|
||||||
|
|
||||||
|
if (permissionConfig) {
|
||||||
|
app.registry.registerPermission(permissionConfig, type, priority);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.permissionsReplacements.forEach(({ permission, type, replacement }) => {
|
||||||
|
app.registry.setPermission(permission, replacement, type);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.permissionsPriorityChanges.forEach(({ permission, type, priority }) => {
|
||||||
|
app.registry.setPermissionPriority(permission, type, priority);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.permissionsRemovals.forEach(({ permission, type }) => {
|
||||||
|
app.registry.removePermission(permission, type);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.customPage) {
|
||||||
|
app.registry.registerPage(this.customPage);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
this.permissions.forEach(({ permission, type, priority }) => {
|
app.generalIndex.for(extension.name);
|
||||||
const permissionConfig = permission();
|
|
||||||
|
|
||||||
if (permissionConfig) {
|
Object.keys(this.generalIndexes).forEach((key) => {
|
||||||
app.registry.registerPermission(permissionConfig, type, priority);
|
if (key !== 'settings' && key !== 'permissions') return;
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.customPage) {
|
const callback = this.generalIndexes[key];
|
||||||
app.registry.registerPage(this.customPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.generalIndex.for(extension.name);
|
if (callback) {
|
||||||
|
app.generalIndex.add(key, callback());
|
||||||
Object.keys(this.generalIndexes).forEach((key) => {
|
}
|
||||||
if (key !== 'settings' && key !== 'permissions') return;
|
});
|
||||||
|
|
||||||
const callback = this.generalIndexes[key];
|
|
||||||
|
|
||||||
if (callback) {
|
|
||||||
app.generalIndex.add(key, callback());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user