chore: Usa `mithril-utilities` per Component, Form e Request
This commit is contained in:
parent
18ba65ec38
commit
37162d5e79
|
@ -1,174 +0,0 @@
|
|||
import classnames, {Argument as ClassNames} from 'classnames';
|
||||
import collect, {Collection} from 'collect.js';
|
||||
import Mithril from 'mithril';
|
||||
import type {
|
||||
Children,
|
||||
ClassComponent,
|
||||
Vnode,
|
||||
VnodeDOM
|
||||
} from 'mithril';
|
||||
|
||||
export interface Attributes {
|
||||
}
|
||||
|
||||
interface AttributesCollection<T extends Attributes> extends Collection<T> {
|
||||
addClassNames(...classNames: ClassNames[]): void;
|
||||
}
|
||||
|
||||
// noinspection SpellCheckingInspection,JSUnusedGlobalSymbols,JSUnusedLocalSymbols
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
*
|
||||
* The `Component` class defines a user interface 'building block'. A component
|
||||
* generates a virtual DOM to be rendered on each redraw.
|
||||
*
|
||||
* Essentially, this is a wrapper for Mithril's components that adds several useful features:
|
||||
*
|
||||
* — In the `oninit` and `onbeforeupdate` lifecycle hooks, we store vnode attrs in `this.attrs.
|
||||
* This allows us to use attrs across components without having to pass the vnode to every single
|
||||
* method.
|
||||
* — The static `initAttrs` method allows a convenient way to provide defaults (or to otherwise
|
||||
* modify) the attrs that have been passed into a component.
|
||||
* — When the component is created in the DOM, we store its DOM element under `this.element`;
|
||||
* this lets us use Cash to modify child DOM state from internal methods via the `this.$()`
|
||||
* method.
|
||||
* — A convenience `component` method, which serves as an alternative to hyperscript and JSX.
|
||||
*
|
||||
* As with other Mithril components, components extending Component can be initialized
|
||||
* and nested using JSX. The `component` method can also
|
||||
* be used.
|
||||
*
|
||||
* @example
|
||||
* return m('div', <MyComponent foo="bar"><p>Hello World</p></MyComponent>);
|
||||
*
|
||||
* @see https://mithril.js.org/components.html
|
||||
*/
|
||||
|
||||
export abstract class Component<
|
||||
A extends Attributes = Attributes,
|
||||
S = undefined
|
||||
> implements ClassComponent<A> {
|
||||
/**
|
||||
* The root DOM element for the component.
|
||||
*/
|
||||
element!: Element;
|
||||
|
||||
/**
|
||||
* The attributes passed into the component.
|
||||
*
|
||||
* @see https://mithril.js.org/components.html#passing-data-to-components
|
||||
* @see initAttrs
|
||||
*/
|
||||
attrs!: AttributesCollection<A>;
|
||||
|
||||
/**
|
||||
* Class component state that is persisted between redraws.
|
||||
*
|
||||
* Updating this will **not** automatically trigger a redraw, unlike
|
||||
* other frameworks.
|
||||
*
|
||||
* This is different to Vnode state, which is always an instance of your
|
||||
* class component.
|
||||
*
|
||||
* This is `undefined` by default.
|
||||
*/
|
||||
state!: S;
|
||||
|
||||
/**
|
||||
* Used for attribute code completion in JSX.
|
||||
* @private
|
||||
*/
|
||||
private __attrs!: A;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
abstract view(vnode: Vnode<A, this>): Children;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
oninit(vnode: Vnode<A, this>) {
|
||||
this.setAttrs(vnode.attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
oncreate(vnode: VnodeDOM<A, this>) {
|
||||
this.element = vnode.dom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
onbeforeupdate(vnode: VnodeDOM<A, this>) {
|
||||
this.setAttrs(vnode.attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
onupdate(vnode: VnodeDOM<A, this>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
onbeforeremove(vnode: VnodeDOM<A, this>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
onremove(vnode: VnodeDOM<A, this>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a reference to the vnode attrs after running them through initAttrs,
|
||||
* and checking for common issues.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
setAttrs(attributes: A): void {
|
||||
this.initAttrs(attributes);
|
||||
|
||||
if (attributes) {
|
||||
if ('children' in attributes) {
|
||||
// noinspection JSUnresolvedVariable
|
||||
throw new Error(
|
||||
`[${this.constructor.name}] The "children" attribute of attrs should never be used. Either pass children in as
|
||||
the vnode children or rename the attribute`
|
||||
);
|
||||
}
|
||||
|
||||
if ('tag' in attributes) {
|
||||
// noinspection JSUnresolvedVariable
|
||||
throw new Error(
|
||||
`[${this.constructor.name}] You cannot use the "tag" attribute name with Mithril 2.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const attributesCollection = collect<A>(attributes);
|
||||
attributesCollection.macro('addClassNames', (...classNames: ClassNames[]) => {
|
||||
attributesCollection.put(
|
||||
'className',
|
||||
classnames(attributesCollection.get('className') as ClassNames, ...classNames)
|
||||
);
|
||||
});
|
||||
this.attrs = attributesCollection as AttributesCollection<A>;
|
||||
}
|
||||
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
/**
|
||||
* Initialize the component's attrs.
|
||||
*
|
||||
* This can be used to assign default values for missing, optional attrs.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
initAttrs(attributes: A): void {
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ import {
|
|||
import {
|
||||
Attributes,
|
||||
Component
|
||||
} from '~/Components/Component';
|
||||
} from 'mithril-utilities';
|
||||
import MdIcon from '~/Components/MdIcon';
|
||||
|
||||
export interface DataTableAttributes extends Attributes {
|
||||
|
|
|
@ -10,7 +10,7 @@ import {Vnode} from 'mithril';
|
|||
import {
|
||||
Attributes,
|
||||
Component
|
||||
} from '~/Components/Component';
|
||||
} from 'mithril-utilities';
|
||||
import MdIcon from '~/Components/MdIcon';
|
||||
|
||||
export interface DataTableColumnAttributes extends Attributes, Partial<JSX.IntrinsicElements['md-data-table-column']> {
|
||||
|
|
|
@ -5,12 +5,12 @@ import {
|
|||
Vnode,
|
||||
VnodeDOM
|
||||
} from 'mithril';
|
||||
|
||||
import {Form} from 'mithril-utilities';
|
||||
import Stream from 'mithril/stream';
|
||||
import {Class} from 'type-fest';
|
||||
|
||||
import Form from '~/Components/Form';
|
||||
import MdIcon from '~/Components/MdIcon';
|
||||
import RecordDialog, {RecordDialogAttributes} from '~/Components/Dialogs/RecordDialog';
|
||||
import MdIcon from '~/Components/MdIcon';
|
||||
import Model from '~/Models/Model';
|
||||
import {
|
||||
VnodeCollection,
|
||||
|
|
|
@ -2,10 +2,10 @@ import {
|
|||
Children,
|
||||
Vnode
|
||||
} from 'mithril';
|
||||
import {RequestError} from 'mithril-utilities';
|
||||
|
||||
import Model from '~/Models/Model';
|
||||
import {showSnackbar} from '~/utils/misc';
|
||||
import {RequestError} from '~/utils/Request';
|
||||
|
||||
import RecordDialog, {RecordDialogAttributes} from './RecordDialog';
|
||||
|
||||
|
|
|
@ -6,12 +6,12 @@ import {
|
|||
Vnode,
|
||||
VnodeDOM
|
||||
} from 'mithril';
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
import {
|
||||
Attributes,
|
||||
Component
|
||||
} from '../Component';
|
||||
} from 'mithril-utilities';
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
export interface DialogAttributes extends Attributes, Partial<Pick<MDDialog,
|
||||
'fullscreen' | 'fullscreenBreakpoint' | 'footerHidden' | 'stacked' | 'defaultAction' |
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
import {
|
||||
ChildArray,
|
||||
Children,
|
||||
Vnode,
|
||||
VnodeDOM
|
||||
} from 'mithril';
|
||||
import Stream from 'mithril/stream';
|
||||
import {Without} from 'type-fest/source/merge-exclusive';
|
||||
|
||||
import {isVnode} from '~/utils/misc';
|
||||
|
||||
import {Component} from './Component';
|
||||
|
||||
export type FormSubmitEvent = SubmitEvent & {data: FormData};
|
||||
|
||||
export interface FormAttributes extends Partial<Omit<HTMLElementTagNameMap['form'], 'style' | 'onsubmit'>> {
|
||||
onsubmit?: (event: FormSubmitEvent) => void,
|
||||
state?: Record<string, Stream<string | any>> | Map<string, Stream<string | any>>
|
||||
}
|
||||
|
||||
export default class Form<A extends FormAttributes = FormAttributes> extends Component<A, Record<string, Stream<string | any>> | Map<string, Stream<string | any>>> {
|
||||
element!: HTMLFormElement;
|
||||
onsubmitFunction?: A['onsubmit'];
|
||||
// TODO: Change all states to Map?
|
||||
state!: NonNullable<A['state']>;
|
||||
|
||||
oninit(vnode: Vnode<A, this>) {
|
||||
super.oninit(vnode);
|
||||
this.onsubmitFunction = vnode.attrs.onsubmit;
|
||||
this.state = vnode.attrs.state ?? {};
|
||||
delete vnode.attrs.state;
|
||||
}
|
||||
|
||||
view(vnode: Vnode<A>) {
|
||||
const attributes = this.attrs.except(['onsubmit', 'state']);
|
||||
return (
|
||||
<form {...attributes.all()} onsubmit={this.onsubmit.bind(this)}>
|
||||
{(vnode.children as ChildArray).map(this.attachStreamToInput.bind(this))}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
attachStreamToInput(child: Children) {
|
||||
// Check if child is a Vnode
|
||||
if (isVnode<{name?: string, id: string, value: unknown, oninput?: (event: Event) => void, state?: Stream<any>}>(child)) {
|
||||
const stream = child.attrs.state ?? this.getState(child.attrs.name ?? child.attrs.id);
|
||||
if (stream) {
|
||||
child.attrs.value = stream();
|
||||
|
||||
const originalOninput = child.attrs.oninput;
|
||||
// This ESLint rule is disabled because it doesn't recognize that the `oninput` attribute is being set and Mithril uses it instead of adding an event listener
|
||||
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||
child.attrs.oninput = (event: Event) => {
|
||||
stream((event.target as HTMLInputElement).value);
|
||||
if (originalOninput) {
|
||||
originalOninput(event);
|
||||
}
|
||||
};
|
||||
|
||||
delete child.attrs.state;
|
||||
}
|
||||
|
||||
// Check if `child` has children and recursively call this function on them.
|
||||
if (Array.isArray(child.children)) {
|
||||
child.children = child.children.map(this.attachStreamToInput.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
oncreate(vnode: VnodeDOM<A, this>) {
|
||||
super.oncreate(vnode);
|
||||
const submitter = this.element.querySelector<HTMLElement>('[type="submit"]');
|
||||
if (submitter) {
|
||||
submitter.addEventListener('click', () => {
|
||||
// TODO: Add submitter when https://github.com/material-components/material-web/issues/3941 is completed
|
||||
this.element.requestSubmit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onsubmit(e: FormSubmitEvent) {
|
||||
e.preventDefault();
|
||||
e.data = new FormData(e.target as HTMLFormElement);
|
||||
if (this.onsubmitFunction) {
|
||||
this.onsubmitFunction(e);
|
||||
}
|
||||
}
|
||||
|
||||
private getState(key: string) {
|
||||
if (this.state instanceof Map) {
|
||||
return this.state.get(key);
|
||||
}
|
||||
return this.state[key];
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ import '@material/web/icon/icon.js';
|
|||
|
||||
import type MaterialIcons from '@mdi/js';
|
||||
|
||||
import {Component} from './Component';
|
||||
import {Component} from 'mithril-utilities';
|
||||
|
||||
export interface Attributes extends Partial<SVGElement> {
|
||||
icon: typeof MaterialIcons | string;
|
||||
|
|
|
@ -6,14 +6,14 @@ import {
|
|||
Vnode,
|
||||
VnodeDOM
|
||||
} from 'mithril';
|
||||
import {
|
||||
Attributes,
|
||||
Component
|
||||
} from 'mithril-utilities';
|
||||
|
||||
import {Footer} from '~/Components/layout/Footer';
|
||||
|
||||
import logoUrl from '../../images/logo_completo.png';
|
||||
import {
|
||||
Attributes,
|
||||
Component
|
||||
} from './Component';
|
||||
import TopAppBar from './layout/TopAppBar';
|
||||
|
||||
export interface PageAttributes<A extends Record<string, any> & {external?: boolean} = Record<string, any>> extends Attributes, Required<ComponentAttributes<A>> {
|
||||
|
|
|
@ -10,12 +10,12 @@ import {
|
|||
Children,
|
||||
Vnode
|
||||
} from 'mithril';
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
import {
|
||||
Attributes,
|
||||
Component
|
||||
} from '~/Components/Component';
|
||||
} from 'mithril-utilities';
|
||||
import Stream from 'mithril/stream';
|
||||
import MdIcon from '~/Components/MdIcon';
|
||||
import {VnodeCollectionItem} from '~/typings/jsx';
|
||||
import {isMobile} from '~/utils/misc';
|
||||
|
|
|
@ -4,12 +4,12 @@ import {ListItemLink} from '@material/web/list/lib/listitemlink/list-item-link';
|
|||
import '@material/web/list/list-item-link.js';
|
||||
import type * as MaterialIcons from '@mdi/js';
|
||||
import {Vnode} from 'mithril';
|
||||
import {ValueOf} from 'type-fest';
|
||||
|
||||
import {
|
||||
Attributes,
|
||||
Component
|
||||
} from '~/Components/Component';
|
||||
} from 'mithril-utilities';
|
||||
import {ValueOf} from 'type-fest';
|
||||
import MdIcon from '~/Components/MdIcon';
|
||||
|
||||
type Icons = ValueOf<typeof MaterialIcons>;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {Component} from '../Component';
|
||||
import {Component} from 'mithril-utilities';
|
||||
|
||||
export class Footer extends Component {
|
||||
view() {
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
import '@material/web/iconbutton/standard-icon-button.js';
|
||||
import '~/WebComponents/TopAppBar';
|
||||
|
||||
import {IconButton} from '@material/web/iconbutton/lib/icon-button';
|
||||
import {Menu} from '@material/web/menu/lib/menu';
|
||||
import {
|
||||
mdiMenu,
|
||||
mdiMenuOpen
|
||||
|
@ -12,13 +8,13 @@ import {
|
|||
Vnode,
|
||||
VnodeDOM
|
||||
} from 'mithril';
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
import logo from '~/../images/logo.png';
|
||||
import {
|
||||
Attributes,
|
||||
Component
|
||||
} from '~/Components/Component';
|
||||
} from 'mithril-utilities';
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
import logo from '~/../images/logo.png';
|
||||
import Drawer from '~/Components/layout/Drawer';
|
||||
import NotificationsAction from '~/Components/layout/topappbar_actions/NotificationsAction';
|
||||
import PeriodSwitcherAction from '~/Components/layout/topappbar_actions/PeriodSwitcherAction';
|
||||
|
@ -30,6 +26,7 @@ import {
|
|||
isMobile,
|
||||
mobileMediaQuery
|
||||
} from '~/utils/misc';
|
||||
import '~/WebComponents/TopAppBar';
|
||||
|
||||
export default class TopAppBar extends Component {
|
||||
drawerOpenState = Stream(!isMobile());
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
Vnode
|
||||
} from 'mithril';
|
||||
|
||||
import {Component} from '~/Components/Component';
|
||||
import {Component} from 'mithril-utilities';
|
||||
import MdIcon, {Attributes as MdIconAttributes} from '~/Components/MdIcon';
|
||||
|
||||
export default abstract class TopAppBarAction extends Component {
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import {router} from '@maicol07/inertia-mithril';
|
||||
import '@material/web/button/outlined-button.js';
|
||||
import '@material/web/button/text-button.js';
|
||||
|
||||
import {router} from '@maicol07/inertia-mithril';
|
||||
import {
|
||||
mdiAccountCircleOutline,
|
||||
mdiAccountOutline,
|
||||
mdiLogoutVariant
|
||||
} from '@mdi/js';
|
||||
import {Vnode} from 'mithril';
|
||||
import {Request} from 'mithril-utilities';
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
import Dialog from '~/Components/Dialogs/Dialog';
|
||||
import MdIcon from '~/Components/MdIcon';
|
||||
import Request from '~/utils/Request';
|
||||
|
||||
import TopAppBarAction from './TopAppBarAction';
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import RequestHttpClientPromise from './RequestHttpClientPromise';
|
||||
import Request from '~/utils/Request';
|
||||
import type {
|
||||
HttpClient,
|
||||
HttpClientPromise
|
||||
} from 'coloquent';
|
||||
import {Request} from 'mithril-utilities';
|
||||
|
||||
import RequestHttpClientPromise from './RequestHttpClientPromise';
|
||||
|
||||
/**
|
||||
* @class RequestHttpClient
|
||||
|
|
|
@ -3,7 +3,6 @@ import '@material/web/button/filled-button.js';
|
|||
import '@material/web/button/text-button.js';
|
||||
import '@material/web/checkbox/checkbox.js';
|
||||
import '@material/web/dialog/dialog.js';
|
||||
import '~/Components/m3/FilledTextField';
|
||||
|
||||
import {Dialog} from '@material/web/dialog/lib/dialog';
|
||||
import {
|
||||
|
@ -18,18 +17,19 @@ import type {
|
|||
Vnode,
|
||||
VnodeDOM
|
||||
} from 'mithril';
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
import Form, {FormSubmitEvent} from '~/Components/Form';
|
||||
import {
|
||||
Form,
|
||||
FormSubmitEvent,
|
||||
Request,
|
||||
RequestError
|
||||
} from 'mithril-utilities';
|
||||
import Stream from 'mithril/stream';
|
||||
import '~/Components/m3/FilledTextField';
|
||||
import MdIcon from '~/Components/MdIcon';
|
||||
import Page, {
|
||||
PageAttributes
|
||||
} from '~/Components/Page';
|
||||
import Page, {PageAttributes} from '~/Components/Page';
|
||||
import {VnodeCollectionItem} from '~/typings/jsx';
|
||||
import {showSnackbar} from '~/utils/misc';
|
||||
import Request, {
|
||||
RequestError
|
||||
} from '~/utils/Request';
|
||||
|
||||
export default class LoginPage extends Page {
|
||||
form = {
|
||||
|
|
|
@ -1,30 +1,25 @@
|
|||
import {router} from '@maicol07/inertia-mithril';
|
||||
import '@maicol07/material-web-additions/card/elevated-card.js';
|
||||
import '@material/web/button/filled-button.js';
|
||||
import '~/Components/m3/FilledTextField';
|
||||
|
||||
import {router} from '@maicol07/inertia-mithril';
|
||||
import {
|
||||
mdiAccountOutline,
|
||||
mdiLockCheckOutline,
|
||||
mdiLockOutline
|
||||
} from '@mdi/js';
|
||||
import logoUrl from '~/../images/logo_completo.png';
|
||||
import collect from 'collect.js';
|
||||
import type {
|
||||
Vnode,
|
||||
VnodeDOM
|
||||
} from 'mithril';
|
||||
import type {Vnode} from 'mithril';
|
||||
import {
|
||||
Form,
|
||||
FormSubmitEvent,
|
||||
Request,
|
||||
RequestError
|
||||
} from 'mithril-utilities';
|
||||
import Stream from 'mithril/stream';
|
||||
import Form, {FormSubmitEvent} from '~/Components/Form';
|
||||
import '~/Components/m3/FilledTextField';
|
||||
import MdIcon from '~/Components/MdIcon';
|
||||
import Page, {
|
||||
PageAttributes
|
||||
} from '~/Components/Page';
|
||||
import Page, {PageAttributes} from '~/Components/Page';
|
||||
import {VnodeCollectionItem} from '~/typings/jsx';
|
||||
import {showSnackbar} from '~/utils/misc';
|
||||
import Request, {
|
||||
RequestError
|
||||
} from '~/utils/Request';
|
||||
|
||||
export default class ResetPasswordPage extends Page {
|
||||
form = {
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import '@maicol07/material-web-additions/card/elevated-card.js';
|
||||
|
||||
import {router} from '@maicol07/inertia-mithril';
|
||||
import type {
|
||||
Vnode
|
||||
} from 'mithril';
|
||||
import '@maicol07/material-web-additions/card/elevated-card.js';
|
||||
import type {Vnode} from 'mithril';
|
||||
import {
|
||||
Request,
|
||||
RequestError
|
||||
} from 'mithril-utilities';
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
import Page, {
|
||||
PageAttributes
|
||||
} from '~/Components/Page';
|
||||
import Page, {PageAttributes} from '~/Components/Page';
|
||||
import {showSnackbar} from '~/utils/misc';
|
||||
import Request, {
|
||||
RequestError
|
||||
} from '~/utils/Request';
|
||||
import AdminUserStep from '~/Views/Setup/Steps/AdminUserStep';
|
||||
import DatabaseStep from '~/Views/Setup/Steps/DatabaseStep';
|
||||
import RegionalSettings from '~/Views/Setup/Steps/RegionalSettings';
|
||||
|
|
|
@ -9,9 +9,12 @@ import {
|
|||
} from '@mdi/js';
|
||||
import collect from 'collect.js';
|
||||
import {Vnode} from 'mithril';
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
import Form, {FormSubmitEvent} from '~/Components/Form';
|
||||
import {
|
||||
Form,
|
||||
FormSubmitEvent
|
||||
} from 'mithril-utilities';
|
||||
import Stream from 'mithril/stream';
|
||||
import MdIcon from '~/Components/MdIcon';
|
||||
import {VnodeCollectionItem} from '~/typings/jsx';
|
||||
import {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import '@maicol07/material-web-additions/layout-grid/layout-grid.js';
|
||||
import '~/Components/m3/FilledTextField';
|
||||
|
||||
import {
|
||||
mdiAccountOutline,
|
||||
|
@ -14,15 +13,14 @@ import {
|
|||
Children,
|
||||
Vnode
|
||||
} from 'mithril';
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
import Form from '~/Components/Form';
|
||||
import Form from 'mithril-utilities';
|
||||
import Request, {RequestError} from 'mithril-utilities';
|
||||
import Stream from 'mithril/stream';
|
||||
import '~/Components/m3/FilledTextField';
|
||||
import MdIcon from '~/Components/MdIcon';
|
||||
import {VnodeCollectionItem} from '~/typings/jsx';
|
||||
import {showSnackbar} from '~/utils/misc';
|
||||
import Request, {
|
||||
RequestError
|
||||
} from '~/utils/Request';
|
||||
|
||||
import {
|
||||
SetupStep,
|
||||
|
|
|
@ -5,9 +5,9 @@ import {
|
|||
} from '@mdi/js';
|
||||
import collect from 'collect.js';
|
||||
import dayjs from 'dayjs';
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
import Form from '~/Components/Form';
|
||||
import {Form} from 'mithril-utilities';
|
||||
import Stream from 'mithril/stream';
|
||||
import MdIcon from '~/Components/MdIcon';
|
||||
import {VnodeCollectionItem} from '~/typings/jsx';
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
import {
|
||||
Attributes,
|
||||
Component
|
||||
} from '~/Components/Component';
|
||||
} from 'mithril-utilities';
|
||||
import MdIcon from '~/Components/MdIcon';
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type {MdCheckbox} from '@material/web/checkbox/checkbox';
|
||||
import '@material/web/checkbox/checkbox.js';
|
||||
import '@material/web/field/outlined-field.js';
|
||||
|
||||
import type {MdCheckbox} from '@material/web/checkbox/checkbox';
|
||||
import {mdiLicense} from '@mdi/js';
|
||||
import {
|
||||
Vnode,
|
||||
|
@ -14,13 +13,7 @@ import {
|
|||
getFlag,
|
||||
getLocaleDisplayName
|
||||
} from '~/utils/i18n';
|
||||
import {
|
||||
capitalize,
|
||||
showSnackbar
|
||||
} from '~/utils/misc';
|
||||
import Request, {
|
||||
RequestError
|
||||
} from '~/utils/Request';
|
||||
import {capitalize} from '~/utils/misc';
|
||||
|
||||
import {
|
||||
SetupStep,
|
||||
|
|
|
@ -1,142 +0,0 @@
|
|||
import {RequestOptions as MithrilRequestOptions} from 'mithril';
|
||||
import {Cookies} from 'typescript-cookie';
|
||||
|
||||
export type RequestMethods = 'get' | 'head' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | 'patch' | string;
|
||||
|
||||
export interface RequestOptions<R = any> extends MithrilRequestOptions<R> {
|
||||
url?: string,
|
||||
renewCSRF?: boolean,
|
||||
renewCSRFOnFailure?: boolean,
|
||||
method?: RequestMethods
|
||||
beforeRequest?: (options: RequestOptionsWithUrl) => Promise<void>
|
||||
afterRequest?: (response: Promise<R>, options: RequestOptionsWithUrl) => void,
|
||||
xsrfCookieName?: string,
|
||||
xsrfHeaderName?: string
|
||||
}
|
||||
|
||||
export interface RequestOptionsWithUrl<R = any> extends RequestOptions<R> {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface RequestError<T = {message: string}> extends Error {
|
||||
code: number;
|
||||
response: T;
|
||||
}
|
||||
|
||||
export default class Request<R> {
|
||||
options: RequestOptionsWithUrl<R> = {
|
||||
url: '',
|
||||
headers: {},
|
||||
renewCSRF: false,
|
||||
renewCSRFOnFailure: false, // Renew CSRF token if request fails with 419 status code (CSRF token expired; risky since it can cause an infinite loop)
|
||||
withCredentials: true,
|
||||
xsrfCookieName: 'XSRF-TOKEN',
|
||||
xsrfHeaderName: 'X-XSRF-TOKEN'
|
||||
};
|
||||
|
||||
constructor(options: RequestOptions) {
|
||||
this.options = {...this.options, ...options};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request
|
||||
*
|
||||
* @throws {RequestError} If request has an error
|
||||
*/
|
||||
public async send() {
|
||||
await this.beforeSendingRequest();
|
||||
const response = m.request<R>(this.options);
|
||||
this.afterSendingRequest(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions to perform before sending the request
|
||||
* @private
|
||||
*/
|
||||
private async beforeSendingRequest() {
|
||||
this.phpWorkaround();
|
||||
this.xsfrAutoHeader();
|
||||
|
||||
await this.options.beforeRequest?.(this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for PHP issue with PUT/PATCH/DELETE requests and FormData
|
||||
*
|
||||
* @see https://bugs.php.net/bug.php?id=55815
|
||||
* @private
|
||||
*/
|
||||
private phpWorkaround() {
|
||||
if (this.options.method && !['get', 'post'].includes(this.options.method) && this.options.body instanceof FormData) {
|
||||
this.options.body.append('_method', this.options.method);
|
||||
this.options.method = 'post';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically set the XSRF header if the cookie is set
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private xsfrAutoHeader() {
|
||||
const token = Cookies.get(this.options.xsrfCookieName) as string | undefined;
|
||||
if (token && this.options.xsrfHeaderName) {
|
||||
this.options.headers![this.options.xsrfHeaderName] = decodeURIComponent(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions to perform after sending the request
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private afterSendingRequest(response: Promise<R>) {
|
||||
this.options.afterRequest?.(response, this.options);
|
||||
}
|
||||
|
||||
static get<R>(url: string, parameters?: RequestOptions['params'], options?: RequestOptions) {
|
||||
return this.sendRequest<R>('get', url, parameters, options);
|
||||
}
|
||||
|
||||
static post<R>(url: string, data?: RequestOptions['body'], options?: RequestOptions) {
|
||||
return this.sendRequest<R>('post', url, data, options);
|
||||
}
|
||||
|
||||
static put<R>(url: string, data?: RequestOptions['body'], options?: RequestOptions) {
|
||||
return this.sendRequest<R>('put', url, data, options);
|
||||
}
|
||||
|
||||
static patch<R>(url: string, data?: RequestOptions['body'], options?: RequestOptions) {
|
||||
return this.sendRequest<R>('patch', url, data, options);
|
||||
}
|
||||
|
||||
static delete<R>(url: string, options?: RequestOptions) {
|
||||
return this.sendRequest<R>('delete', url, undefined, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request
|
||||
* @param method The method of the request (get, post, put, patch, delete)
|
||||
* @param url The URL of the request
|
||||
* @param data The data to send
|
||||
* @param options The options of the request
|
||||
* @private
|
||||
*/
|
||||
static sendRequest<R>(
|
||||
method: RequestMethods,
|
||||
url: string,
|
||||
data?: RequestOptions['params'] | RequestOptions['body'],
|
||||
options: RequestOptions = {}
|
||||
) {
|
||||
if (method === 'get') {
|
||||
options.params = data as RequestOptions['params'];
|
||||
} else {
|
||||
options.body = data;
|
||||
}
|
||||
options.url ??= url;
|
||||
|
||||
return (new Request<R>({method, ...options})).send();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue