diff --git a/framework/core/.github/workflows/js.yml b/framework/core/.github/workflows/js.yml index 3b44e6d7d..cd1e65982 100644 --- a/framework/core/.github/workflows/js.yml +++ b/framework/core/.github/workflows/js.yml @@ -49,7 +49,7 @@ jobs: working-directory: ./js - name: Typecheck - run: yarn run check-typings || true # REMOVE THIS ONCE TYPE SAFETY REACHED + run: yarn run check-typings working-directory: ./js type-coverage: diff --git a/framework/core/js/src/@types/global.d.ts b/framework/core/js/src/@types/global.d.ts index e3ff2fe27..26bc910f3 100644 --- a/framework/core/js/src/@types/global.d.ts +++ b/framework/core/js/src/@types/global.d.ts @@ -21,7 +21,20 @@ declare type KeysOfType = { */ declare type KeyOfType = KeysOfType[keyof Type]; -declare type VnodeElementTag, State = Record> = string | ComponentTypes; +type Component = import('mithril').Component; + +declare type ComponentClass, C extends Component = Component> = { + new (...args: any[]): Component; + prototype: C; +}; + +/** + * Unfortunately, TypeScript only supports strings and classes for JSX tags. + * Therefore, our type definition should only allow for those two types. + * + * @see https://github.com/microsoft/TypeScript/issues/14789#issuecomment-412247771 + */ +declare type VnodeElementTag, C extends Component = Component> = string | ComponentClass; /** * @deprecated Please import `app` from a namespace instead of using it as a global variable. diff --git a/framework/core/js/src/admin/components/AdminPage.tsx b/framework/core/js/src/admin/components/AdminPage.tsx index 981a246cc..8a3b4bc1f 100644 --- a/framework/core/js/src/admin/components/AdminPage.tsx +++ b/framework/core/js/src/admin/components/AdminPage.tsx @@ -13,8 +13,8 @@ import generateElementId from '../utils/generateElementId'; import ColorPreviewInput from '../../common/components/ColorPreviewInput'; export interface AdminHeaderOptions { - title: string; - description: string; + title: Mithril.Children; + description: Mithril.Children; icon: string; /** * Will be used as the class for the AdminPage. diff --git a/framework/core/js/src/admin/components/ExtensionPage.tsx b/framework/core/js/src/admin/components/ExtensionPage.tsx index fa7f70751..fa6acccaa 100644 --- a/framework/core/js/src/admin/components/ExtensionPage.tsx +++ b/framework/core/js/src/admin/components/ExtensionPage.tsx @@ -16,6 +16,7 @@ import RequestError from '../../common/utils/RequestError'; import { Extension } from '../AdminApplication'; import { IPageAttrs } from '../../common/components/Page'; import type Mithril from 'mithril'; +import extractText from '../../common/utils/extractText'; export interface ExtensionPageAttrs extends IPageAttrs { id: string; @@ -156,7 +157,7 @@ export default class ExtensionPage { - if (confirm(app.translator.trans('core.admin.extension.confirm_purge'))) { + if (confirm(extractText(app.translator.trans('core.admin.extension.confirm_purge')))) { app .request({ url: app.forum.attribute('apiUrl') + '/extensions/' + this.extension.id, diff --git a/framework/core/js/src/admin/components/LoadingModal.tsx b/framework/core/js/src/admin/components/LoadingModal.tsx index 2a9555d88..082efd002 100644 --- a/framework/core/js/src/admin/components/LoadingModal.tsx +++ b/framework/core/js/src/admin/components/LoadingModal.tsx @@ -1,7 +1,9 @@ import app from '../../admin/app'; -import Modal from '../../common/components/Modal'; +import Modal, { IInternalModalAttrs } from '../../common/components/Modal'; -export default class LoadingModal extends Modal { +export interface ILoadingModalAttrs extends IInternalModalAttrs {} + +export default class LoadingModal extends Modal { /** * @inheritdoc */ diff --git a/framework/core/js/src/admin/components/ReadmeModal.js b/framework/core/js/src/admin/components/ReadmeModal.tsx similarity index 70% rename from framework/core/js/src/admin/components/ReadmeModal.js rename to framework/core/js/src/admin/components/ReadmeModal.tsx index 425e5aac2..e2cc9f0bc 100644 --- a/framework/core/js/src/admin/components/ReadmeModal.js +++ b/framework/core/js/src/admin/components/ReadmeModal.tsx @@ -1,11 +1,21 @@ import app from '../../admin/app'; -import Modal from '../../common/components/Modal'; +import Modal, { IInternalModalAttrs } from '../../common/components/Modal'; import LoadingIndicator from '../../common/components/LoadingIndicator'; import Placeholder from '../../common/components/Placeholder'; import ExtensionReadme from '../models/ExtensionReadme'; +import type Mithril from 'mithril'; +import type { Extension } from '../AdminApplication'; -export default class ReadmeModal extends Modal { - oninit(vnode) { +export interface IReadmeModalAttrs extends IInternalModalAttrs { + extension: Extension; +} + +export default class ReadmeModal extends Modal { + protected name!: string; + protected extName!: string; + protected readme!: ExtensionReadme; + + oninit(vnode: Mithril.Vnode) { super.oninit(vnode); app.store.models['extension-readmes'] = ExtensionReadme; diff --git a/framework/core/js/src/admin/components/UserListPage.tsx b/framework/core/js/src/admin/components/UserListPage.tsx index 675104aea..3502a24f9 100644 --- a/framework/core/js/src/admin/components/UserListPage.tsx +++ b/framework/core/js/src/admin/components/UserListPage.tsx @@ -21,7 +21,7 @@ type ColumnData = { /** * Column title */ - name: String; + name: Mithril.Children; /** * Component(s) to show for this column. */ diff --git a/framework/core/js/src/common/Model.ts b/framework/core/js/src/common/Model.ts index 1bc29431e..9fce10624 100644 --- a/framework/core/js/src/common/Model.ts +++ b/framework/core/js/src/common/Model.ts @@ -32,11 +32,11 @@ export interface SavedModelData { export type ModelData = UnsavedModelData | SavedModelData; -interface SaveRelationships { +export interface SaveRelationships { [relationship: string]: Model | Model[]; } -interface SaveAttributes { +export interface SaveAttributes { [key: string]: unknown; relationships?: SaveRelationships; } diff --git a/framework/core/js/src/common/Session.ts b/framework/core/js/src/common/Session.ts index 0997bf99f..e4e15a6dc 100644 --- a/framework/core/js/src/common/Session.ts +++ b/framework/core/js/src/common/Session.ts @@ -7,11 +7,8 @@ export type LoginParams = { * The username/email */ identification: string; - - /** - * Password - */ password: string; + remember: boolean; }; /** diff --git a/framework/core/js/src/common/components/EditUserModal.js b/framework/core/js/src/common/components/EditUserModal.tsx similarity index 67% rename from framework/core/js/src/common/components/EditUserModal.js rename to framework/core/js/src/common/components/EditUserModal.tsx index 70b3ca9c5..994d6b8b3 100644 --- a/framework/core/js/src/common/components/EditUserModal.js +++ b/framework/core/js/src/common/components/EditUserModal.tsx @@ -1,17 +1,28 @@ import app from '../../common/app'; -import Modal from './Modal'; +import Modal, { IInternalModalAttrs } from './Modal'; import Button from './Button'; import GroupBadge from './GroupBadge'; import Group from '../models/Group'; import extractText from '../utils/extractText'; import ItemList from '../utils/ItemList'; import Stream from '../utils/Stream'; +import type Mithril from 'mithril'; +import type User from '../models/User'; +import type { SaveAttributes, SaveRelationships } from '../Model'; -/** - * The `EditUserModal` component displays a modal dialog with a login form. - */ -export default class EditUserModal extends Modal { - oninit(vnode) { +export interface IEditUserModalAttrs extends IInternalModalAttrs { + user: User; +} + +export default class EditUserModal extends Modal { + protected username!: Stream; + protected email!: Stream; + protected isEmailConfirmed!: Stream; + protected setPassword!: Stream; + protected password!: Stream; + protected groups: Record> = {}; + + oninit(vnode: Mithril.Vnode) { super.oninit(vnode); const user = this.attrs.user; @@ -19,14 +30,15 @@ export default class EditUserModal extends Modal { this.username = Stream(user.username() || ''); this.email = Stream(user.email() || ''); this.isEmailConfirmed = Stream(user.isEmailConfirmed() || false); - this.setPassword = Stream(false); + this.setPassword = Stream(false as boolean); this.password = Stream(user.password() || ''); - this.groups = {}; + + const userGroups = user.groups() || []; app.store - .all('groups') - .filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1) - .forEach((group) => (this.groups[group.id()] = Stream(user.groups().indexOf(group) !== -1))); + .all('groups') + .filter((group) => ![Group.GUEST_ID, Group.MEMBER_ID].includes(group.id()!)) + .forEach((group) => (this.groups[group.id()!] = Stream(userGroups.includes(group)))); } className() { @@ -49,7 +61,7 @@ export default class EditUserModal extends Modal { fields() { const items = new ItemList(); - if (app.session.user.canEditCredentials()) { + if (app.session.user?.canEditCredentials()) { items.add( 'username',
@@ -103,10 +115,11 @@ export default class EditUserModal extends Modal {