From 924815b6e1f01cf709212d58ded1badd3e03779c Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Tue, 16 Nov 2021 15:49:42 -0500 Subject: [PATCH] Extension permission typings, fix glitch with extension permissions grid --- ...ionGrid.js => ExtensionPermissionGrid.tsx} | 32 +-- .../{PermissionGrid.js => PermissionGrid.tsx} | 63 ++++-- .../admin/resolvers/ExtensionPageResolver.ts | 2 +- js/src/admin/utils/ExtensionData.js | 177 ----------------- js/src/admin/utils/ExtensionData.ts | 186 ++++++++++++++++++ 5 files changed, 257 insertions(+), 203 deletions(-) rename js/src/admin/components/{ExtensionPermissionGrid.js => ExtensionPermissionGrid.tsx} (59%) rename js/src/admin/components/{PermissionGrid.js => PermissionGrid.tsx} (85%) delete mode 100644 js/src/admin/utils/ExtensionData.js create mode 100644 js/src/admin/utils/ExtensionData.ts diff --git a/js/src/admin/components/ExtensionPermissionGrid.js b/js/src/admin/components/ExtensionPermissionGrid.tsx similarity index 59% rename from js/src/admin/components/ExtensionPermissionGrid.js rename to js/src/admin/components/ExtensionPermissionGrid.tsx index 104a16dfa..a1fb0b184 100644 --- a/js/src/admin/components/ExtensionPermissionGrid.js +++ b/js/src/admin/components/ExtensionPermissionGrid.tsx @@ -1,26 +1,36 @@ import app from '../../admin/app'; -import PermissionGrid from './PermissionGrid'; +import PermissionGrid, { PermissionGridEntry } from './PermissionGrid'; import Button from '../../common/components/Button'; import ItemList from '../../common/utils/ItemList'; +import Mithril from 'mithril'; -export default class ExtensionPermissionGrid extends PermissionGrid { - oninit(vnode) { +export interface IExtensionPermissionGridAttrs { + extensionId: string; +} + +export default class ExtensionPermissionGrid< + CustomAttrs extends IExtensionPermissionGridAttrs = IExtensionPermissionGridAttrs +> extends PermissionGrid { + protected extensionId!: string; + + oninit(vnode: Mithril.Vnode) { super.oninit(vnode); this.extensionId = this.attrs.extensionId; } permissionItems() { - const permissionCategories = super.permissionItems(); + const items = new ItemList<{ label: Mithril.Children; children: PermissionGridEntry[] }>(); - permissionCategories.items = Object.entries(permissionCategories.items) - .filter(([category, info]) => info.content.children.length > 0) - .reduce((obj, [category, info]) => { - obj[category] = info; - return obj; - }, {}); + super + .permissionItems() + .toArray() + .filter((item) => item.children.length > 0) + .forEach((item) => { + items.add(item.itemName, item); + }); - return permissionCategories; + return items; } viewItems() { diff --git a/js/src/admin/components/PermissionGrid.js b/js/src/admin/components/PermissionGrid.tsx similarity index 85% rename from js/src/admin/components/PermissionGrid.js rename to js/src/admin/components/PermissionGrid.tsx index a0c10da4b..6044576b0 100644 --- a/js/src/admin/components/PermissionGrid.js +++ b/js/src/admin/components/PermissionGrid.tsx @@ -1,17 +1,49 @@ import app from '../../admin/app'; -import Component from '../../common/Component'; +import Component, { ComponentAttrs } from '../../common/Component'; import PermissionDropdown from './PermissionDropdown'; import SettingDropdown from './SettingDropdown'; import Button from '../../common/components/Button'; import ItemList from '../../common/utils/ItemList'; import icon from '../../common/helpers/icon'; +import type Mithril from 'mithril'; -export default class PermissionGrid extends Component { - view() { +export interface PermissionConfig { + permission: string; + icon: string; + label: Mithril.Children; + allowGuest?: boolean; +} + +export interface PermissionSetting { + setting: () => Mithril.Children; + icon: string; + label: Mithril.Children; +} + +export type PermissionGridEntry = PermissionConfig | PermissionSetting; + +export type PermissionType = 'view' | 'start' | 'reply' | 'moderate'; + +export interface ScopeItem { + label: Mithril.Children; + render: (permission: PermissionGridEntry) => Mithril.Children; + onremove?: () => void; +} + +export interface IPermissionGridAttrs extends ComponentAttrs {} + +export default class PermissionGrid extends Component { + view(vnode: Mithril.Vnode) { const scopes = this.scopeItems().toArray(); - const permissionCells = (permission) => { - return scopes.map((scope) => {scope.render(permission)}); + const permissionCells = (permission: PermissionGridEntry | { children: PermissionGridEntry[] }) => { + return scopes.map((scope) => { + if ('children' in permission) { + return ; + } + + return scope.render(permission); + }); }; return ( @@ -56,7 +88,10 @@ export default class PermissionGrid extends Component { } permissionItems() { - const items = new ItemList(); + const items = new ItemList<{ + label: Mithril.Children; + children: PermissionGridEntry[]; + }>(); items.add( 'view', @@ -98,7 +133,7 @@ export default class PermissionGrid extends Component { } viewItems() { - const items = new ItemList(); + const items = new ItemList(); items.add( 'viewForum', @@ -162,7 +197,7 @@ export default class PermissionGrid extends Component { } startItems() { - const items = new ItemList(); + const items = new ItemList(); items.add( 'start', @@ -205,7 +240,7 @@ export default class PermissionGrid extends Component { } replyItems() { - const items = new ItemList(); + const items = new ItemList(); items.add( 'reply', @@ -247,7 +282,7 @@ export default class PermissionGrid extends Component { } moderateItems() { - const items = new ItemList(); + const items = new ItemList(); items.add( 'viewIpsPosts', @@ -365,16 +400,16 @@ export default class PermissionGrid extends Component { } scopeItems() { - const items = new ItemList(); + const items = new ItemList(); items.add( 'global', { label: app.translator.trans('core.admin.permissions.global_heading'), - render: (item) => { - if (item.setting) { + render: (item: PermissionGridEntry) => { + if ('setting' in item) { return item.setting(); - } else if (item.permission) { + } else if ('permission' in item) { return PermissionDropdown.component({ permission: item.permission, allowGuest: item.allowGuest, diff --git a/js/src/admin/resolvers/ExtensionPageResolver.ts b/js/src/admin/resolvers/ExtensionPageResolver.ts index 899e97130..90277ed55 100644 --- a/js/src/admin/resolvers/ExtensionPageResolver.ts +++ b/js/src/admin/resolvers/ExtensionPageResolver.ts @@ -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(args.id); + const extensionPage = app.extensionData.getPage(args.id); if (extensionPage) { return extensionPage; diff --git a/js/src/admin/utils/ExtensionData.js b/js/src/admin/utils/ExtensionData.js deleted file mode 100644 index 043cc612d..000000000 --- a/js/src/admin/utils/ExtensionData.js +++ /dev/null @@ -1,177 +0,0 @@ -import ItemList from '../../common/utils/ItemList'; - -export default class ExtensionData { - constructor() { - this.data = {}; - this.currentExtension = null; - } - - /** - * This function simply takes the extension id - * - * @example - * app.extensionData.load('flarum-tags') - * - * flarum/flags -> flarum-flags | acme/extension -> acme-extension - * - * @param extension - */ - for(extension) { - this.currentExtension = extension; - this.data[extension] = this.data[extension] || {}; - - return this; - } - - /** - * This function registers your settings with Flarum - * - * It takes either a settings object or a callback. - * - * @example - * - * .registerSetting({ - * setting: 'flarum-flags.guidelines_url', - * type: 'text', // This will be inputted into the input tag for the setting (text/number/etc) - * label: app.translator.trans('flarum-flags.admin.settings.guidelines_url_label') - * }, 15) // priority is optional (ItemList) - * - * - * @param content - * @param priority - * @returns {ExtensionData} - */ - registerSetting(content, priority = 0) { - this.data[this.currentExtension].settings = this.data[this.currentExtension].settings || new ItemList(); - - // Callbacks can be passed in instead of settings to display custom content. - // By default, they will be added with the `null` key, since they don't have a `.setting` attr. - // To support multiple such items for one extension, we assign a random ID. - // 36 is arbitrary length, but makes collisions very unlikely. - if (typeof content === 'function') { - content.setting = Math.random().toString(36); - } - - this.data[this.currentExtension].settings.add(content.setting, content, priority); - - return this; - } - - /** - * This function registers your permission with Flarum - * - * @example - * - * .registerPermission('permissions', { - * icon: 'fas fa-flag', - * label: app.translator.trans('flarum-flags.admin.permissions.view_flags_label'), - * permission: 'discussion.viewFlags' - * }, 'moderate', 65) - * - * @param content - * @param permissionType - * @param priority - * @returns {ExtensionData} - */ - registerPermission(content, permissionType = null, priority = 0) { - this.data[this.currentExtension].permissions = this.data[this.currentExtension].permissions || {}; - - if (!this.data[this.currentExtension].permissions[permissionType]) { - this.data[this.currentExtension].permissions[permissionType] = new ItemList(); - } - - this.data[this.currentExtension].permissions[permissionType].add(content.permission, content, priority); - - return this; - } - - /** - * Replace the default extension page with a custom component. - * This component would typically extend ExtensionPage - * - * @param component - * @returns {ExtensionData} - */ - registerPage(component) { - this.data[this.currentExtension].page = component; - - return this; - } - - /** - * Get an extension's registered settings - * - * @param extensionId - * @returns {boolean|*} - */ - getSettings(extensionId) { - if (this.data[extensionId] && this.data[extensionId].settings) { - return this.data[extensionId].settings.toArray(); - } - - return false; - } - - /** - * - * Get an ItemList of all extensions' registered permissions - * - * @param extension - * @param type - * @returns {ItemList} - */ - getAllExtensionPermissions(type) { - const items = new ItemList(); - - Object.keys(this.data).map((extension) => { - if (this.extensionHasPermissions(extension) && this.data[extension].permissions[type]) { - items.merge(this.data[extension].permissions[type]); - } - }); - - return items; - } - - /** - * Get a singular extension's registered permissions - * - * @param extension - * @param type - * @returns {boolean|*} - */ - getExtensionPermissions(extension, type) { - if (this.extensionHasPermissions(extension) && this.data[extension].permissions[type]) { - return this.data[extension].permissions[type]; - } - - return new ItemList(); - } - - /** - * Checks whether a given extension has registered permissions. - * - * @param extension - * @returns {boolean} - */ - extensionHasPermissions(extension) { - if (this.data[extension] && this.data[extension].permissions) { - return true; - } - - return false; - } - - /** - * Returns an extension's custom page component if it exists. - * - * @param extension - * @returns {boolean|*} - */ - getPage(extension) { - if (this.data[extension]) { - return this.data[extension].page; - } - - return false; - } -} diff --git a/js/src/admin/utils/ExtensionData.ts b/js/src/admin/utils/ExtensionData.ts new file mode 100644 index 000000000..b6414508b --- /dev/null +++ b/js/src/admin/utils/ExtensionData.ts @@ -0,0 +1,186 @@ +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'; + +type SettingConfigInput = SettingsComponentOptions | (() => Mithril.Children); + +type SettingConfigInternal = SettingsComponentOptions | ((() => Mithril.Children) & { setting: string }); + +export type CustomExtensionPage = new () => ExtensionPage; + +type ExtensionConfig = { + settings?: ItemList; + permissions?: { + view?: ItemList; + start?: ItemList; + reply?: ItemList; + moderate?: ItemList; + }; + page?: CustomExtensionPage; +}; + +type InnerDataNoActiveExtension = { + currentExtension: null; + data: { + [key: string]: ExtensionConfig | undefined; + }; +}; + +type InnerDataActiveExtension = { + currentExtension: string; + data: { + [key: string]: ExtensionConfig; + }; +}; + +const noActiveExtensionErrorMessage = 'You must select an active extension via `.for()` before using extensionData.'; + +export default class ExtensionData { + protected state: InnerDataActiveExtension | InnerDataNoActiveExtension = { + currentExtension: null, + data: {}, + }; + + /** + * This function simply takes the extension id + * + * @example + * app.extensionData.for('flarum-tags') + * + * flarum/flags -> flarum-flags | acme/extension -> acme-extension + */ + for(extension: string) { + this.state.currentExtension = extension; + this.state.data[extension] = this.state.data[extension] || {}; + + return this; + } + + /** + * This function registers your settings with Flarum + * + * It takes either a settings object or a callback. + * + * @example + * + * .registerSetting({ + * setting: 'flarum-flags.guidelines_url', + * type: 'text', // This will be inputted into the input tag for the setting (text/number/etc) + * label: app.translator.trans('flarum-flags.admin.settings.guidelines_url_label') + * }, 15) // priority is optional (ItemList) + */ + registerSetting(content: SettingConfigInput, priority = 0): this { + if (this.state.currentExtension === null) { + throw new Error(noActiveExtensionErrorMessage); + } + + const tmpContent = content as SettingConfigInternal; + + // Callbacks can be passed in instead of settings to display custom content. + // By default, they will be added with the `null` key, since they don't have a `.setting` attr. + // 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); + } + + const settings = this.state.data[this.state.currentExtension].settings || new ItemList(); + settings.add(tmpContent.setting, tmpContent, priority); + + this.state.data[this.state.currentExtension].settings = settings; + + return this; + } + + /** + * This function registers your permission with Flarum + * + * @example + * + * .registerPermission('permissions', { + * icon: 'fas fa-flag', + * label: app.translator.trans('flarum-flags.admin.permissions.view_flags_label'), + * permission: 'discussion.viewFlags' + * }, 'moderate', 65) + */ + registerPermission(content: PermissionConfig, permissionType: PermissionType, priority = 0): 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(); + + permissionsForType.add(content.permission, content, priority); + + this.state.data[this.state.currentExtension].permissions = { ...permissions, [permissionType]: permissionsForType }; + + return this; + } + + /** + * Replace the default extension page with a custom component. + * This component would typically extend ExtensionPage + */ + registerPage(component: CustomExtensionPage): this { + if (this.state.currentExtension === null) { + throw new Error(noActiveExtensionErrorMessage); + } + + this.state.data[this.state.currentExtension].page = component; + + return this; + } + + /** + * Get an extension's registered settings + */ + getSettings(extensionId: string): SettingConfigInternal[] | undefined { + return this.state.data[extensionId]?.settings?.toArray(); + } + + /** + * Get an ItemList of all extensions' registered permissions + */ + getAllExtensionPermissions(type: PermissionType): ItemList { + const items = new ItemList(); + + Object.keys(this.state.data).map((extension) => { + const extPerms = this.state.data[extension]?.permissions?.[type]; + if (this.extensionHasPermissions(extension) && extPerms !== undefined) { + items.merge(extPerms); + } + }); + + return items; + } + + /** + * Get a singular extension's registered permissions + */ + getExtensionPermissions(extension: string, type: PermissionType): ItemList { + const extPerms = this.state.data[extension]?.permissions?.[type]; + if (this.extensionHasPermissions(extension) && extPerms != null) { + return extPerms; + } + + return new ItemList(); + } + + /** + * Checks whether a given extension has registered permissions. + */ + extensionHasPermissions(extension: string) { + return this.state.data[extension]?.permissions !== undefined; + } + + /** + * Returns an extension's custom page component if it exists. + */ + getPage(extension: string): CustomExtensionPage | undefined { + return this.state.data[extension]?.page as CustomExtensionPage | undefined; + } +}