Skip to content

Commit

Permalink
chore: extract buildSettingComponent method into a FormGroup comp…
Browse files Browse the repository at this point in the history
…onent
  • Loading branch information
SychO9 committed Nov 10, 2023
1 parent d01c0e5 commit 2f11f15
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 221 deletions.
225 changes: 5 additions & 220 deletions framework/core/js/src/admin/components/AdminPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 './FormGroup';

export interface AdminHeaderOptions {
title: Mithril.Children;
Expand All @@ -27,115 +21,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
Expand Down Expand Up @@ -205,41 +93,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
Expand Down Expand Up @@ -283,77 +136,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} />;
}

/**
Expand Down
Loading

0 comments on commit 2f11f15

Please sign in to comment.