1
0
mirror of https://github.com/flarum/core.git synced 2025-08-04 07:27:39 +02:00

chore: extract buildSettingComponent method into a FormGroup component (#3927)

* chore: extract `buildSettingComponent` method into a `FormGroup` component

* chore: typings

* feat: move to common
This commit is contained in:
Sami Mazouz
2024-04-06 14:52:13 +01:00
committed by GitHub
parent e771b908d5
commit bf523b2325
9 changed files with 274 additions and 228 deletions

View File

@@ -1,6 +1,6 @@
import app from 'flarum/admin/app';
import ItemList from 'flarum/common/utils/ItemList';
import generateElementId from 'flarum/admin/utils/generateElementId';
import generateElementId from 'flarum/common/utils/generateElementId';
import FormModal, { IFormModalAttrs } from 'flarum/common/components/FormModal';
import Mithril from 'mithril';

View File

@@ -1,11 +1,12 @@
import { extend } from 'flarum/common/extend';
import AdminPage from 'flarum/admin/components/AdminPage';
import SelectTagsSettingComponent from './components/SelectTagsSettingComponent';
import FormGroup from 'flarum/common/components/FormGroup';
import type { IFormGroupAttrs } from 'flarum/common/components/FormGroup';
export default function () {
extend(AdminPage.prototype, 'customSettingComponents', function (items) {
items.add('flarum-tags.select-tags', (attrs) => {
return <SelectTagsSettingComponent {...attrs} settingValue={this.settings[attrs.setting]} />;
extend(FormGroup.prototype, 'customFieldComponents', function (items) {
items.add('flarum-tags.select-tags', (attrs: IFormGroupAttrs) => {
return <SelectTagsSettingComponent {...attrs} settingValue={attrs.bidi} />;
});
});
}

View File

@@ -4,7 +4,6 @@ import './utils/saveSettings';
import './utils/ExtensionData';
import './utils/isExtensionEnabled';
import './utils/getCategorizedExtensions';
import './utils/generateElementId';
import './components/SettingDropdown';
import './components/EditCustomFooterModal';
@@ -22,7 +21,6 @@ import './components/ExtensionLinkButton';
import './components/PermissionGrid';
import './components/ExtensionPermissionGrid';
import './components/MailPage';
import './components/UploadImageButton';
import './components/LoadingModal';
import './components/DashboardPage';
import './components/BasicsPage';

View File

@@ -3,17 +3,11 @@ import type Mithril from 'mithril';
import app from '../app';
import Page, { IPageAttrs } from '../../common/components/Page';
import Button from '../../common/components/Button';
import Switch from '../../common/components/Switch';
import Select from '../../common/components/Select';
import classList from '../../common/utils/classList';
import Stream from '../../common/utils/Stream';
import saveSettings from '../utils/saveSettings';
import AdminHeader from './AdminHeader';
import generateElementId from '../utils/generateElementId';
import ColorPreviewInput from '../../common/components/ColorPreviewInput';
import ItemList from '../../common/utils/ItemList';
import type { IUploadImageButtonAttrs } from './UploadImageButton';
import UploadImageButton from './UploadImageButton';
import FormGroup, { FieldComponentOptions } from '../../common/components/FormGroup';
import extractText from '../../common/utils/extractText';
export interface AdminHeaderOptions {
@@ -28,115 +22,9 @@ export interface AdminHeaderOptions {
className: string;
}
/**
* A type that matches any valid value for the `type` attribute on an HTML `<input>` element.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-type
*
* Note: this will be exported from a different location in the future.
*
* @see https://github.com/flarum/core/issues/3039
*/
export type HTMLInputTypes =
| 'button'
| 'checkbox'
| 'color'
| 'date'
| 'datetime-local'
| 'email'
| 'file'
| 'hidden'
| 'image'
| 'month'
| 'number'
| 'password'
| 'radio'
| 'range'
| 'reset'
| 'search'
| 'submit'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week';
export interface CommonSettingsItemOptions extends Mithril.Attributes {
export type SettingsComponentOptions = FieldComponentOptions & {
setting: string;
label?: Mithril.Children;
help?: Mithril.Children;
className?: string;
}
/**
* Valid options for the setting component builder to generate an HTML input element.
*/
export interface HTMLInputSettingsComponentOptions extends CommonSettingsItemOptions {
/**
* Any valid HTML input `type` value.
*/
type: HTMLInputTypes;
}
const BooleanSettingTypes = ['bool', 'checkbox', 'switch', 'boolean'] as const;
const SelectSettingTypes = ['select', 'dropdown', 'selectdropdown'] as const;
const TextareaSettingTypes = ['textarea'] as const;
const ColorPreviewSettingType = 'color-preview' as const;
const ImageUploadSettingType = 'image-upload' as const;
/**
* Valid options for the setting component builder to generate a Switch.
*/
export interface SwitchSettingComponentOptions extends CommonSettingsItemOptions {
type: typeof BooleanSettingTypes[number];
}
/**
* Valid options for the setting component builder to generate a Select dropdown.
*/
export interface SelectSettingComponentOptions extends CommonSettingsItemOptions {
type: typeof SelectSettingTypes[number];
/**
* Map of values to their labels
*/
options: { [value: string]: Mithril.Children };
default: string;
}
/**
* Valid options for the setting component builder to generate a Textarea.
*/
export interface TextareaSettingComponentOptions extends CommonSettingsItemOptions {
type: typeof TextareaSettingTypes[number];
}
/**
* Valid options for the setting component builder to generate a ColorPreviewInput.
*/
export interface ColorPreviewSettingComponentOptions extends CommonSettingsItemOptions {
type: typeof ColorPreviewSettingType;
}
export interface ImageUploadSettingComponentOptions extends CommonSettingsItemOptions, IUploadImageButtonAttrs {
type: typeof ImageUploadSettingType;
}
export interface CustomSettingComponentOptions extends CommonSettingsItemOptions {
type: string;
[key: string]: unknown;
}
/**
* All valid options for the setting component builder.
*/
export type SettingsComponentOptions =
| HTMLInputSettingsComponentOptions
| SwitchSettingComponentOptions
| SelectSettingComponentOptions
| TextareaSettingComponentOptions
| ColorPreviewSettingComponentOptions
| ImageUploadSettingComponentOptions
| CustomSettingComponentOptions;
};
/**
* Valid attrs that can be returned by the `headerInfo` function
@@ -206,41 +94,6 @@ export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAt
};
}
/**
* A list of extension-defined custom setting components to be available through
* {@link AdminPage.buildSettingComponent}.
*
* The ItemList key represents the value for `type` to be provided when calling
* {@link AdminPage.buildSettingComponent}. Other attributes passed are provided
* as arguments to the function added to the ItemList.
*
* ItemList priority has no effect here.
*
* @example
* ```tsx
* extend(AdminPage.prototype, 'customSettingComponents', function (items) {
* // You can access the AdminPage instance with `this` to access its `settings` property.
*
* // Prefixing the key with your extension ID is recommended to avoid collisions.
* items.add('my-ext.setting-component', (attrs) => {
* return (
* <div className={attrs.className}>
* <label>{attrs.label}</label>
* {attrs.help && <p className="helpText">{attrs.help}</p>}
*
* My setting component!
* </div>
* );
* })
* })
* ```
*/
customSettingComponents(): ItemList<(attributes: CommonSettingsItemOptions) => Mithril.Children> {
const items = new ItemList<(attributes: CommonSettingsItemOptions) => Mithril.Children>();
return items;
}
/**
* `buildSettingComponent` takes a settings object and turns it into a component.
* Depending on the type of input, you can set the type to 'bool', 'select', or
@@ -284,77 +137,9 @@ export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAt
return entry.call(this);
}
const customSettingComponents = this.customSettingComponents();
const { setting, ...attrs } = entry;
const { setting, help, type, label, ...componentAttrs } = entry;
const value = this.setting(setting)();
const [inputId, helpTextId] = [generateElementId(), generateElementId()];
let settingElement: Mithril.Children;
// Typescript being Typescript
// https://github.com/microsoft/TypeScript/issues/14520
if ((BooleanSettingTypes as readonly string[]).includes(type)) {
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">
<Switch state={!!value && value !== '0'} onchange={this.settings[setting]} {...componentAttrs}>
{label}
</Switch>
{help ? <div className="helpText">{help}</div> : null}
</div>
);
} else if ((SelectSettingTypes as readonly string[]).includes(type)) {
const { default: defaultValue, options, ...otherAttrs } = componentAttrs;
settingElement = (
<Select
id={inputId}
aria-describedby={helpTextId}
value={value || defaultValue}
options={options}
onchange={this.settings[setting]}
{...otherAttrs}
/>
);
} else if (type === ImageUploadSettingType) {
const { value, ...otherAttrs } = componentAttrs;
settingElement = <UploadImageButton value={this.settings[setting]} {...otherAttrs} />;
} else if (customSettingComponents.has(type)) {
return customSettingComponents.get(type)({ setting, help, label, ...componentAttrs });
} else {
componentAttrs.className = classList('FormControl', componentAttrs.className);
if ((TextareaSettingTypes as readonly string[]).includes(type)) {
settingElement = <textarea id={inputId} aria-describedby={helpTextId} bidi={this.setting(setting)} {...componentAttrs} />;
} else {
let Tag: VnodeElementTag = 'input';
if (type === ColorPreviewSettingType) {
Tag = ColorPreviewInput;
} else {
componentAttrs.type = type;
}
settingElement = <Tag id={inputId} aria-describedby={helpTextId} bidi={this.setting(setting)} {...componentAttrs} />;
}
}
return (
<div className="Form-group">
{label && <label for={inputId}>{label}</label>}
{help && (
<div id={helpTextId} className="helpText">
{help}
</div>
)}
{settingElement}
</div>
);
return <FormGroup bidi={this.setting(setting)} {...attrs} />;
}
/**

View File

@@ -3,7 +3,7 @@ import Button from '../../common/components/Button';
import EditCustomCssModal from './EditCustomCssModal';
import EditCustomHeaderModal from './EditCustomHeaderModal';
import EditCustomFooterModal from './EditCustomFooterModal';
import UploadImageButton from './UploadImageButton';
import UploadImageButton from '../../common/components/UploadImageButton';
import AdminPage from './AdminPage';
import ItemList from '../../common/utils/ItemList';
import type Mithril from 'mithril';

View File

@@ -36,6 +36,7 @@ import './utils/withAttr';
import './utils/focusTrap';
import './utils/isDark';
import './utils/KeyboardNavigatable';
import './utils/generateElementId';
import './models/Notification';
import './models/User';
@@ -76,6 +77,8 @@ import './components/TextEditorButton';
import './components/Tooltip';
import './components/AutocompleteDropdown';
import './components/GambitsAutocompleteDropdown';
import './components/UploadImageButton';
import './components/FormGroup';
import './helpers/fullTime';
import './components/Avatar';

View File

@@ -0,0 +1,259 @@
import Component from '../Component';
import generateElementId from '../utils/generateElementId';
import Switch from './Switch';
import Select from './Select';
import UploadImageButton from './UploadImageButton';
import classList from '../utils/classList';
import ColorPreviewInput from './ColorPreviewInput';
import Stream from '../utils/Stream';
import ItemList from '../utils/ItemList';
import type { IUploadImageButtonAttrs } from './UploadImageButton';
import type { ComponentAttrs } from '../Component';
import type Mithril from 'mithril';
/**
* A type that matches any valid value for the `type` attribute on an HTML `<input>` element.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-type
*
* Note: this will be exported from a different location in the future.
*
* @see https://github.com/flarum/core/issues/3039
*/
export type HTMLInputTypes =
| 'button'
| 'checkbox'
| 'color'
| 'date'
| 'datetime-local'
| 'email'
| 'file'
| 'hidden'
| 'image'
| 'month'
| 'number'
| 'password'
| 'radio'
| 'range'
| 'reset'
| 'search'
| 'submit'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week';
export interface CommonFieldOptions extends Mithril.Attributes {
label?: Mithril.Children;
help?: Mithril.Children;
className?: string;
}
/**
* Valid options for the setting component builder to generate an HTML input element.
*/
export interface HTMLInputFieldComponentOptions extends CommonFieldOptions {
/**
* Any valid HTML input `type` value.
*/
type: HTMLInputTypes;
}
const BooleanSettingTypes = ['bool', 'checkbox', 'switch', 'boolean'] as const;
const SelectSettingTypes = ['select', 'dropdown', 'selectdropdown'] as const;
const TextareaSettingTypes = ['textarea'] as const;
const ColorPreviewSettingType = 'color-preview' as const;
const ImageUploadSettingType = 'image-upload' as const;
/**
* Valid options for the setting component builder to generate a Switch.
*/
export interface SwitchFieldComponentOptions extends CommonFieldOptions {
type: typeof BooleanSettingTypes[number];
}
/**
* Valid options for the setting component builder to generate a Select dropdown.
*/
export interface SelectFieldComponentOptions extends CommonFieldOptions {
type: typeof SelectSettingTypes[number];
/**
* Map of values to their labels
*/
options: { [value: string]: Mithril.Children };
default: string;
}
/**
* Valid options for the setting component builder to generate a Textarea.
*/
export interface TextareaFieldComponentOptions extends CommonFieldOptions {
type: typeof TextareaSettingTypes[number];
}
/**
* Valid options for the setting component builder to generate a ColorPreviewInput.
*/
export interface ColorPreviewFieldComponentOptions extends CommonFieldOptions {
type: typeof ColorPreviewSettingType;
}
export interface ImageUploadFieldComponentOptions extends CommonFieldOptions, IUploadImageButtonAttrs {
type: typeof ImageUploadSettingType;
}
export interface CustomFieldComponentOptions extends CommonFieldOptions {
type: string;
[key: string]: unknown;
}
/**
* All valid options for the setting component builder.
*/
export type FieldComponentOptions =
| HTMLInputFieldComponentOptions
| SwitchFieldComponentOptions
| SelectFieldComponentOptions
| TextareaFieldComponentOptions
| ColorPreviewFieldComponentOptions
| ImageUploadFieldComponentOptions
| CustomFieldComponentOptions;
export type IFormGroupAttrs = ComponentAttrs &
FieldComponentOptions & {
bidi?: Stream<any>;
};
/**
* Builds a field component based on the provided attributes.
* Depending on the type of input, you can set the type to 'bool', 'select', or
* any standard <input> type. Any values inside the 'extra' object will be added
* to the component as an attribute.
*
* Alternatively, you can pass a callback that will be executed in ExtensionPage's
* context to include custom JSX elements.
*
* @example
*
* <FormGroup key="acme.checkbox"
* label={app.translator.trans('acme.admin.setting_label')}
* type="bool"
* help={app.translator.trans('acme.admin.setting_help')}
* className="Setting-item" />
*
* @example
*
* <FormGroup key="acme.select"
* label={app.translator.trans('acme.admin.setting_label')}
* type="select"
* options={{
* 'option1': 'Option 1 label',
* 'option2': 'Option 2 label',
* }}
* default="option1" />
*/
export default class FormGroup<CustomAttrs extends IFormGroupAttrs = IFormGroupAttrs> extends Component<CustomAttrs> {
view(vnode: Mithril.Vnode<CustomAttrs, this>): Mithril.Children {
const customFieldComponents = this.customFieldComponents();
const { help, type, label, bidi, ...componentAttrs } = this.attrs;
// TypeScript being TypeScript
const attrs = componentAttrs as unknown as Omit<IFormGroupAttrs, 'bidi' | 'label' | 'help' | 'type'>;
const value = bidi ? bidi() : null;
const [inputId, helpTextId] = [generateElementId(), generateElementId()];
let settingElement: Mithril.Children;
// Typescript being Typescript
// https://github.com/microsoft/TypeScript/issues/14520
if ((BooleanSettingTypes as readonly string[]).includes(type)) {
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">
<Switch state={!!value && value !== '0'} onchange={bidi} {...attrs}>
{label}
</Switch>
{help ? <div className="helpText">{help}</div> : null}
</div>
);
} else if ((SelectSettingTypes as readonly string[]).includes(type)) {
const { default: defaultValue, options, ...otherAttrs } = attrs;
settingElement = (
<Select id={inputId} aria-describedby={helpTextId} value={value || defaultValue} options={options} onchange={bidi} {...otherAttrs} />
);
} else if (type === ImageUploadSettingType) {
const { value, ...otherAttrs } = attrs;
settingElement = <UploadImageButton value={bidi} {...otherAttrs} />;
} else if (customFieldComponents.has(type)) {
return customFieldComponents.get(type)(this.attrs);
} else {
attrs.className = classList('FormControl', attrs.className);
if ((TextareaSettingTypes as readonly string[]).includes(type)) {
settingElement = <textarea id={inputId} aria-describedby={helpTextId} bidi={bidi} {...attrs} />;
} else {
let Tag: VnodeElementTag = 'input';
if (type === ColorPreviewSettingType) {
Tag = ColorPreviewInput;
} else {
attrs.type = type;
}
settingElement = <Tag id={inputId} aria-describedby={helpTextId} bidi={bidi} {...attrs} />;
}
}
return (
<div className="Form-group">
{label && <label for={inputId}>{label}</label>}
{help && (
<div id={helpTextId} className="helpText">
{help}
</div>
)}
{settingElement}
</div>
);
}
/**
* A list of extension-defined custom setting components to be available.
*
* The ItemList key represents the value for the `type` attribute.
* All attributes passed are provided as arguments to the function added to the ItemList.
*
* ItemList priority has no effect here.
*
* @example
* ```tsx
* extend(AdminPage.prototype, 'customFieldComponents', function (items) {
* // You can access the AdminPage instance with `this` to access its `settings` property.
*
* // Prefixing the key with your extension ID is recommended to avoid collisions.
* items.add('my-ext.setting-component', (attrs) => {
* return (
* <div className={attrs.className}>
* <label>{attrs.label}</label>
* {attrs.help && <p className="helpText">{attrs.help}</p>}
*
* My setting component!
* </div>
* );
* })
* })
* ```
*/
customFieldComponents(): ItemList<(attributes: CustomAttrs) => Mithril.Children> {
const items = new ItemList<(attributes: CustomAttrs) => Mithril.Children>();
return items;
}
}