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 (#3927)

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

* chore: typings

* feat: move to common
  • Loading branch information
SychO9 authored Apr 6, 2024
1 parent e771b90 commit bf523b2
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 228 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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} />;
});
});
}
2 changes: 0 additions & 2 deletions framework/core/js/src/admin/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down
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 '../../common/components/FormGroup';
import extractText from '../../common/utils/extractText';

export interface AdminHeaderOptions {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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} />;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion framework/core/js/src/admin/components/AppearancePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions framework/core/js/src/common/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down
Loading

0 comments on commit bf523b2

Please sign in to comment.