1
0
mirror of https://github.com/flarum/core.git synced 2025-08-05 07:57:46 +02:00

refactor: convert page components to TypeScript (#3538)

* fix(a11y): color preview fields have no aria label
* refactor: convert page components to TypeScript

Co-authored-by: David Wheatley <hi@davwheat.dev>
Signed-off-by: Sami Mazouz <ilyasmazouz@gmail.com>
This commit is contained in:
Sami Mazouz
2022-08-08 22:11:58 +01:00
committed by GitHub
parent 44825f1b94
commit 1948f25151
10 changed files with 95 additions and 63 deletions

View File

@@ -38,6 +38,8 @@ export interface AdminApplicationData extends ApplicationData {
extensions: Record<string, Extension>; extensions: Record<string, Extension>;
settings: Record<string, string>; settings: Record<string, string>;
modelStatistics: Record<string, { total: number }>; modelStatistics: Record<string, { total: number }>;
displayNameDrivers: string[];
slugDrivers: Record<string, string[]>;
} }
export default class AdminApplication extends Application { export default class AdminApplication extends Application {

View File

@@ -60,7 +60,7 @@ export type HTMLInputTypes =
export interface CommonSettingsItemOptions extends Mithril.Attributes { export interface CommonSettingsItemOptions extends Mithril.Attributes {
setting: string; setting: string;
label: Mithril.Children; label?: Mithril.Children;
help?: Mithril.Children; help?: Mithril.Children;
className?: string; className?: string;
} }
@@ -137,6 +137,8 @@ export type AdminHeaderAttrs = AdminHeaderOptions & Partial<Omit<Mithril.Attribu
export type SettingValue = string; export type SettingValue = string;
export type MutableSettings = Record<string, Stream<SettingValue>>; export type MutableSettings = Record<string, Stream<SettingValue>>;
export type SaveSubmitEvent = SubmitEvent & { redraw: boolean };
export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAttrs> extends Page<CustomAttrs> { export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAttrs> extends Page<CustomAttrs> {
settings: MutableSettings = {}; settings: MutableSettings = {};
loading: boolean = false; loading: boolean = false;
@@ -162,7 +164,7 @@ export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAt
* *
* Calls `this.saveSettings` when the button is clicked. * Calls `this.saveSettings` when the button is clicked.
*/ */
submitButton(vnode: Mithril.Vnode<CustomAttrs, this>): Mithril.Children { submitButton(): Mithril.Children {
return ( return (
<Button onclick={this.saveSettings.bind(this)} className="Button Button--primary" loading={this.loading} disabled={!this.isChanged()}> <Button onclick={this.saveSettings.bind(this)} className="Button Button--primary" loading={this.loading} disabled={!this.isChanged()}>
{app.translator.trans('core.admin.settings.submit_button')} {app.translator.trans('core.admin.settings.submit_button')}
@@ -385,7 +387,7 @@ export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAt
/** /**
* Saves the modified settings to the database. * Saves the modified settings to the database.
*/ */
saveSettings(e: SubmitEvent & { redraw: boolean }) { saveSettings(e: SaveSubmitEvent) {
e.preventDefault(); e.preventDefault();
app.alerts.clear(); app.alerts.clear();

View File

@@ -6,6 +6,7 @@ import EditCustomFooterModal from './EditCustomFooterModal';
import UploadImageButton from './UploadImageButton'; import UploadImageButton from './UploadImageButton';
import AdminPage from './AdminPage'; import AdminPage from './AdminPage';
import ItemList from '../../common/utils/ItemList'; import ItemList from '../../common/utils/ItemList';
import type Mithril from 'mithril';
export default class AppearancePage extends AdminPage { export default class AppearancePage extends AdminPage {
headerInfo() { headerInfo() {
@@ -77,7 +78,7 @@ export default class AppearancePage extends AdminPage {
} }
colorItems() { colorItems() {
const items = new ItemList(); const items = new ItemList<Mithril.Children>();
items.add('helpText', <div className="helpText">{app.translator.trans('core.admin.appearance.colors_text')}</div>, 80); items.add('helpText', <div className="helpText">{app.translator.trans('core.admin.appearance.colors_text')}</div>, 80);
@@ -88,11 +89,13 @@ export default class AppearancePage extends AdminPage {
type: 'color-preview', type: 'color-preview',
setting: 'theme_primary_color', setting: 'theme_primary_color',
placeholder: '#aaaaaa', placeholder: '#aaaaaa',
ariaLabel: app.translator.trans('core.admin.appearance.colors_primary_label'),
})} })}
{this.buildSettingComponent({ {this.buildSettingComponent({
type: 'color-preview', type: 'color-preview',
setting: 'theme_secondary_color', setting: 'theme_secondary_color',
placeholder: '#aaaaaa', placeholder: '#aaaaaa',
ariaLabel: app.translator.trans('core.admin.appearance.colors_secondary_label'),
})} })}
</div>, </div>,
70 70

View File

@@ -2,24 +2,27 @@ import app from '../../admin/app';
import FieldSet from '../../common/components/FieldSet'; import FieldSet from '../../common/components/FieldSet';
import ItemList from '../../common/utils/ItemList'; import ItemList from '../../common/utils/ItemList';
import AdminPage from './AdminPage'; import AdminPage from './AdminPage';
import type { IPageAttrs } from '../../common/components/Page';
import type Mithril from 'mithril';
export default class BasicsPage extends AdminPage { export type HomePageItem = { path: string; label: Mithril.Children };
oninit(vnode) {
export default class BasicsPage<CustomAttrs extends IPageAttrs = IPageAttrs> extends AdminPage<CustomAttrs> {
localeOptions: Record<string, string> = {};
displayNameOptions: Record<string, string> = {};
slugDriverOptions: Record<string, Record<string, string>> = {};
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode); super.oninit(vnode);
this.localeOptions = {}; Object.keys(app.data.locales).forEach((i) => {
const locales = app.data.locales; this.localeOptions[i] = `${app.data.locales[i]} (${i})`;
for (const i in locales) { });
this.localeOptions[i] = `${locales[i]} (${i})`;
}
this.displayNameOptions = {}; app.data.displayNameDrivers.forEach((identifier) => {
const displayNameDrivers = app.data.displayNameDrivers;
displayNameDrivers.forEach(function (identifier) {
this.displayNameOptions[identifier] = identifier; this.displayNameOptions[identifier] = identifier;
}, this); });
this.slugDriverOptions = {};
Object.keys(app.data.slugDrivers).forEach((model) => { Object.keys(app.data.slugDrivers).forEach((model) => {
this.slugDriverOptions[model] = {}; this.slugDriverOptions[model] = {};
@@ -100,6 +103,7 @@ export default class BasicsPage extends AdminPage {
{Object.keys(this.slugDriverOptions).map((model) => { {Object.keys(this.slugDriverOptions).map((model) => {
const options = this.slugDriverOptions[model]; const options = this.slugDriverOptions[model];
if (Object.keys(options).length > 1) { if (Object.keys(options).length > 1) {
return this.buildSettingComponent({ return this.buildSettingComponent({
type: 'select', type: 'select',
@@ -109,6 +113,8 @@ export default class BasicsPage extends AdminPage {
help: app.translator.trans('core.admin.basics.slug_driver_text', { model }), help: app.translator.trans('core.admin.basics.slug_driver_text', { model }),
}); });
} }
return null;
})} })}
{this.submitButton()} {this.submitButton()}
@@ -119,11 +125,9 @@ export default class BasicsPage extends AdminPage {
/** /**
* Build a list of options for the default homepage. Each option must be an * Build a list of options for the default homepage. Each option must be an
* object with `path` and `label` properties. * object with `path` and `label` properties.
*
* @return {ItemList<{ path: string, label: import('mithril').Children }>}
*/ */
homePageItems() { homePageItems() {
const items = new ItemList(); const items = new ItemList<HomePageItem>();
items.add('allDiscussions', { items.add('allDiscussions', {
path: '/all', path: '/all',

View File

@@ -140,7 +140,7 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
{settings ? ( {settings ? (
<div className="Form"> <div className="Form">
{settings.map(this.buildSettingComponent.bind(this))} {settings.map(this.buildSettingComponent.bind(this))}
<div className="Form-group">{this.submitButton(vnode)}</div> <div className="Form-group">{this.submitButton()}</div>
</div> </div>
) : ( ) : (
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_settings')}</h3> <h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_settings')}</h3>

View File

@@ -4,12 +4,30 @@ import Button from '../../common/components/Button';
import Alert from '../../common/components/Alert'; import Alert from '../../common/components/Alert';
import LoadingIndicator from '../../common/components/LoadingIndicator'; import LoadingIndicator from '../../common/components/LoadingIndicator';
import AdminPage from './AdminPage'; import AdminPage from './AdminPage';
import type { IPageAttrs } from '../../common/components/Page';
import type { AlertIdentifier } from '../../common/states/AlertManagerState';
import type Mithril from 'mithril';
import type { SaveSubmitEvent } from './AdminPage';
export default class MailPage extends AdminPage { export interface MailSettings {
oninit(vnode) { data: {
attributes: {
fields: Record<string, any>;
sending: boolean;
errors: any[];
};
};
}
export default class MailPage<CustomAttrs extends IPageAttrs = IPageAttrs> extends AdminPage<CustomAttrs> {
sendingTest = false;
status?: { sending: boolean; errors: any };
driverFields?: Record<string, any>;
testEmailSuccessAlert?: AlertIdentifier;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode); super.oninit(vnode);
this.sendingTest = false;
this.refresh(); this.refresh();
} }
@@ -28,14 +46,14 @@ export default class MailPage extends AdminPage {
this.status = { sending: false, errors: {} }; this.status = { sending: false, errors: {} };
app app
.request({ .request<MailSettings>({
method: 'GET', method: 'GET',
url: app.forum.attribute('apiUrl') + '/mail/settings', url: app.forum.attribute('apiUrl') + '/mail/settings',
}) })
.then((response) => { .then((response) => {
this.driverFields = response['data']['attributes']['fields']; this.driverFields = response.data.attributes.fields;
this.status.sending = response['data']['attributes']['sending']; this.status!.sending = response.data.attributes.sending;
this.status.errors = response['data']['attributes']['errors']; this.status!.errors = response.data.attributes.errors;
this.loading = false; this.loading = false;
m.redraw(); m.redraw();
@@ -47,7 +65,7 @@ export default class MailPage extends AdminPage {
return <LoadingIndicator />; return <LoadingIndicator />;
} }
const fields = this.driverFields[this.setting('mail_driver')()]; const fields = this.driverFields![this.setting('mail_driver')()];
const fieldKeys = Object.keys(fields); const fieldKeys = Object.keys(fields);
return ( return (
@@ -60,10 +78,10 @@ export default class MailPage extends AdminPage {
{this.buildSettingComponent({ {this.buildSettingComponent({
type: 'select', type: 'select',
setting: 'mail_driver', setting: 'mail_driver',
options: Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {}), options: Object.keys(this.driverFields!).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
label: app.translator.trans('core.admin.email.driver_heading'), label: app.translator.trans('core.admin.email.driver_heading'),
})} })}
{this.status.sending || {this.status!.sending ||
Alert.component( Alert.component(
{ {
dismissible: false, dismissible: false,
@@ -84,7 +102,7 @@ export default class MailPage extends AdminPage {
setting: field, setting: field,
options: fieldInfo, options: fieldInfo,
}), }),
this.status.errors[field] && <p className="ValidationError">{this.status.errors[field]}</p>, this.status!.errors[field] && <p className="ValidationError">{this.status!.errors[field]}</p>,
]; ];
})} })}
</div> </div>
@@ -93,7 +111,7 @@ export default class MailPage extends AdminPage {
{this.submitButton()} {this.submitButton()}
<FieldSet label={app.translator.trans('core.admin.email.send_test_mail_heading')} className="MailPage-MailSettings"> <FieldSet label={app.translator.trans('core.admin.email.send_test_mail_heading')} className="MailPage-MailSettings">
<div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user.email() })}</div> <div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user!.email() })}</div>
{Button.component( {Button.component(
{ {
className: 'Button Button--primary', className: 'Button Button--primary',
@@ -108,10 +126,11 @@ export default class MailPage extends AdminPage {
} }
sendTestEmail() { sendTestEmail() {
if (this.saving || this.sendingTest) return; if (this.sendingTest) return;
this.sendingTest = true; this.sendingTest = true;
app.alerts.dismiss(this.testEmailSuccessAlert);
if (this.testEmailSuccessAlert) app.alerts.dismiss(this.testEmailSuccessAlert);
app app
.request({ .request({
@@ -129,7 +148,7 @@ export default class MailPage extends AdminPage {
}); });
} }
saveSettings(e) { saveSettings(e: SaveSubmitEvent) {
super.saveSettings(e).then(this.refresh()); return super.saveSettings(e).then(() => this.refresh());
} }
} }

View File

@@ -20,8 +20,8 @@ export default class PermissionsPage extends AdminPage {
return [ return [
<div className="PermissionsPage-groups"> <div className="PermissionsPage-groups">
{app.store {app.store
.all('groups') .all<Group>('groups')
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1) .filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()!) === -1)
.map((group) => ( .map((group) => (
<button className="Button Group" onclick={() => app.modal.show(EditGroupModal, { group })}> <button className="Button Group" onclick={() => app.modal.show(EditGroupModal, { group })}>
{GroupBadge.component({ {GroupBadge.component({

View File

@@ -1,16 +1,18 @@
import app from '../../forum/app'; import app from '../../forum/app';
import Page from '../../common/components/Page'; import Page, { IPageAttrs } from '../../common/components/Page';
import NotificationList from './NotificationList'; import NotificationList from './NotificationList';
import type Mithril from 'mithril';
import extractText from '../../common/utils/extractText';
/** /**
* The `NotificationsPage` component shows the notifications list. It is only * The `NotificationsPage` component shows the notifications list. It is only
* used on mobile devices where the notifications dropdown is within the drawer. * used on mobile devices where the notifications dropdown is within the drawer.
*/ */
export default class NotificationsPage extends Page { export default class NotificationsPage<CustomAttrs extends IPageAttrs = IPageAttrs> extends Page<CustomAttrs> {
oninit(vnode) { oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode); super.oninit(vnode);
app.history.push('notifications'); app.history.push('notifications', extractText(app.translator.trans('core.forum.notifications.title')));
app.notifications.load(); app.notifications.load();

View File

@@ -1,5 +1,5 @@
import app from '../../forum/app'; import app from '../../forum/app';
import UserPage from './UserPage'; import UserPage, { IUserPageAttrs } from './UserPage';
import ItemList from '../../common/utils/ItemList'; import ItemList from '../../common/utils/ItemList';
import Switch from '../../common/components/Switch'; import Switch from '../../common/components/Switch';
import Button from '../../common/components/Button'; import Button from '../../common/components/Button';
@@ -8,18 +8,22 @@ import NotificationGrid from './NotificationGrid';
import ChangePasswordModal from './ChangePasswordModal'; import ChangePasswordModal from './ChangePasswordModal';
import ChangeEmailModal from './ChangeEmailModal'; import ChangeEmailModal from './ChangeEmailModal';
import listItems from '../../common/helpers/listItems'; import listItems from '../../common/helpers/listItems';
import extractText from '../../common/utils/extractText';
import type Mithril from 'mithril';
/** /**
* The `SettingsPage` component displays the user's settings control panel, in * The `SettingsPage` component displays the user's settings control panel, in
* the context of their user profile. * the context of their user profile.
*/ */
export default class SettingsPage extends UserPage { export default class SettingsPage<CustomAttrs extends IUserPageAttrs = IUserPageAttrs> extends UserPage<CustomAttrs> {
oninit(vnode) { discloseOnlineLoading?: boolean;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode); super.oninit(vnode);
this.show(app.session.user); this.show(app.session.user!);
app.setTitle(app.translator.trans('core.forum.settings.title')); app.setTitle(extractText(app.translator.trans('core.forum.settings.title')));
} }
content() { content() {
@@ -32,17 +36,17 @@ export default class SettingsPage extends UserPage {
/** /**
* Build an item list for the user's settings controls. * Build an item list for the user's settings controls.
*
* @return {ItemList<import('mithril').Children>}
*/ */
settingsItems() { settingsItems() {
const items = new ItemList(); const items = new ItemList<Mithril.Children>();
['account', 'notifications', 'privacy'].forEach((section) => { ['account', 'notifications', 'privacy'].forEach((section) => {
const sectionItems = `${section}Items` as 'accountItems' | 'notificationsItems' | 'privacyItems';
items.add( items.add(
section, section,
<FieldSet className={`Settings-${section}`} label={app.translator.trans(`core.forum.settings.${section}_heading`)}> <FieldSet className={`Settings-${section}`} label={app.translator.trans(`core.forum.settings.${section}_heading`)}>
{this[`${section}Items`]().toArray()} {this[sectionItems]().toArray()}
</FieldSet> </FieldSet>
); );
}); });
@@ -52,11 +56,9 @@ export default class SettingsPage extends UserPage {
/** /**
* Build an item list for the user's account settings. * Build an item list for the user's account settings.
*
* @return {ItemList<import('mithril').Children>}
*/ */
accountItems() { accountItems() {
const items = new ItemList(); const items = new ItemList<Mithril.Children>();
items.add( items.add(
'changePassword', 'changePassword',
@@ -77,11 +79,9 @@ export default class SettingsPage extends UserPage {
/** /**
* Build an item list for the user's notification settings. * Build an item list for the user's notification settings.
*
* @return {ItemList<import('mithril').Children>}
*/ */
notificationsItems() { notificationsItems() {
const items = new ItemList(); const items = new ItemList<Mithril.Children>();
items.add('notificationGrid', <NotificationGrid user={this.user} />); items.add('notificationGrid', <NotificationGrid user={this.user} />);
@@ -90,20 +90,18 @@ export default class SettingsPage extends UserPage {
/** /**
* Build an item list for the user's privacy settings. * Build an item list for the user's privacy settings.
*
* @return {ItemList<import('mithril').Children>}
*/ */
privacyItems() { privacyItems() {
const items = new ItemList(); const items = new ItemList<Mithril.Children>();
items.add( items.add(
'discloseOnline', 'discloseOnline',
<Switch <Switch
state={this.user.preferences().discloseOnline} state={this.user!.preferences()?.discloseOnline}
onchange={(value) => { onchange={(value: boolean) => {
this.discloseOnlineLoading = true; this.discloseOnlineLoading = true;
this.user.savePreferences({ discloseOnline: value }).then(() => { this.user!.savePreferences({ discloseOnline: value }).then(() => {
this.discloseOnlineLoading = false; this.discloseOnlineLoading = false;
m.redraw(); m.redraw();
}); });

View File

@@ -11,6 +11,8 @@ core:
appearance: appearance:
colored_header_label: Colored Header colored_header_label: Colored Header
colors_heading: Colors colors_heading: Colors
colors_primary_label: Primary Color
colors_secondary_label: Secondary Color
colors_text: "Choose two colors to theme your forum with. The first will be used as a highlight color, while the second will be used to style background elements." colors_text: "Choose two colors to theme your forum with. The first will be used as a highlight color, while the second will be used to style background elements."
custom_footer_heading: Custom Footer custom_footer_heading: Custom Footer
custom_footer_text: => core.ref.custom_footer_text custom_footer_text: => core.ref.custom_footer_text