diff --git a/framework/core/js/src/admin/AdminApplication.tsx b/framework/core/js/src/admin/AdminApplication.tsx index 2e46f3e73..b85dd3aec 100644 --- a/framework/core/js/src/admin/AdminApplication.tsx +++ b/framework/core/js/src/admin/AdminApplication.tsx @@ -129,12 +129,14 @@ export default class AdminApplication extends Application { this.route = (Object.getPrototypeOf(Object.getPrototypeOf(this)) as Application).route.bind(this); } - protected beforeMount(): void { + protected runBeforeMount(): void { BasicsPage.register(); AppearancePage.register(); MailPage.register(); AdvancedPage.register(); PermissionsPage.register(); + + super.runBeforeMount(); } /** diff --git a/framework/core/js/src/admin/components/GeneralSearchSource.tsx b/framework/core/js/src/admin/components/GeneralSearchSource.tsx index 3b416cc19..f972c47bd 100644 --- a/framework/core/js/src/admin/components/GeneralSearchSource.tsx +++ b/framework/core/js/src/admin/components/GeneralSearchSource.tsx @@ -9,7 +9,7 @@ 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 { ExtensionConfig, SettingConfigInput } from '../utils/AdminRegistry'; import ItemList from '../../common/utils/ItemList'; export class GeneralSearchResult { @@ -94,7 +94,7 @@ export default class GeneralSearchSource implements GlobalSearchSource { for (const extensionId in data) { // settings const settings = data[extensionId]!.settings; - let normalizedSettings: GeneralIndexItem[] | SettingConfigInternal[] = []; + let normalizedSettings: GeneralIndexItem[] | SettingConfigInput[] = []; if (settings instanceof ItemList) { normalizedSettings = settings?.toArray(); @@ -113,7 +113,7 @@ export default class GeneralSearchSource implements GlobalSearchSource { const group = app.generalIndex.getGroup(extensionId); 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( new GeneralSearchResult( diff --git a/framework/core/js/src/admin/utils/AdminRegistry.ts b/framework/core/js/src/admin/utils/AdminRegistry.ts index 59d7b6eb1..65fc61249 100644 --- a/framework/core/js/src/admin/utils/AdminRegistry.ts +++ b/framework/core/js/src/admin/utils/AdminRegistry.ts @@ -71,7 +71,7 @@ export default class AdminRegistry { * label: app.translator.trans('flarum-flags.admin.settings.guidelines_url_label') * }, 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) { 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. // 36 is arbitrary length, but makes collisions very unlikely. 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(); @@ -94,6 +94,62 @@ export default class AdminRegistry { 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 * @@ -125,6 +181,65 @@ export default class AdminRegistry { 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. * This component would typically extend ExtensionPage diff --git a/framework/core/js/src/common/Application.tsx b/framework/core/js/src/common/Application.tsx index 4db332ce3..1c49f71f5 100644 --- a/framework/core/js/src/common/Application.tsx +++ b/framework/core/js/src/common/Application.tsx @@ -286,6 +286,8 @@ export default class Application { private handledErrors: { extension: null | string; errorId: string; error: any }[] = []; + private beforeMounts: (() => void)[] = []; + public load(payload: Application['data']) { this.data = payload; this.translator.setLocale(payload.locale); @@ -326,7 +328,7 @@ export default class Application { this.session = new Session(this.store.getById('users', String(this.data.session.userId)) ?? null, this.data.session.csrfToken); - this.beforeMount(); + this.runBeforeMount(); this.mount(); @@ -335,8 +337,13 @@ export default class Application { 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) { diff --git a/framework/core/js/src/common/extenders/Admin.ts b/framework/core/js/src/common/extenders/Admin.ts index 49881cda3..b875c3464 100644 --- a/framework/core/js/src/common/extenders/Admin.ts +++ b/framework/core/js/src/common/extenders/Admin.ts @@ -1,25 +1,65 @@ import IExtender, { IExtensionModule } from './IExtender'; 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 Mithril from 'mithril'; import type { GeneralIndexItem } from '../../admin/states/GeneralSearchIndex'; export default class Admin implements IExtender { - 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 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 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. */ - setting(setting: () => SettingConfigInternal | null, priority = 0) { + setting(setting: () => SettingConfigInput | null, priority = 0) { this.settings.push({ setting, priority }); 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. */ @@ -38,6 +78,33 @@ export default class Admin implements IExtender { 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. */ @@ -57,38 +124,64 @@ export default class Admin implements IExtender { } 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 }) => { - const settingConfig = setting ? setting() : customSetting!; + this.settings.forEach(({ setting, customSetting, priority }) => { + const settingConfig = setting ? setting() : customSetting!; - if (settingConfig) { - app.registry.registerSetting(settingConfig, priority); + if (settingConfig) { + 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 }) => { - const permissionConfig = permission(); + app.generalIndex.for(extension.name); - if (permissionConfig) { - app.registry.registerPermission(permissionConfig, type, priority); - } - }); + Object.keys(this.generalIndexes).forEach((key) => { + if (key !== 'settings' && key !== 'permissions') return; - if (this.customPage) { - app.registry.registerPage(this.customPage); - } + const callback = this.generalIndexes[key]; - app.generalIndex.for(extension.name); - - Object.keys(this.generalIndexes).forEach((key) => { - if (key !== 'settings' && key !== 'permissions') return; - - const callback = this.generalIndexes[key]; - - if (callback) { - app.generalIndex.add(key, callback()); - } + if (callback) { + app.generalIndex.add(key, callback()); + } + }); }); } }