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

fix: Assorted Typing Fixes (#3348)

With all the commits below, we resolve all outstanding typing issues in the repo, and CI jobs run green.

* fix: Convert DashboardPage and DashboardWidget to TypeScript

* fix: fix type errors in package manager ext

* fix: Convert Post component to TypeScript

* fix: avatar typings should accept null user

* fix: convert Notification component to TypeScript

* fix: properly use `typeof` in ForumApplication

* feat: make Notification content attr generic

* chore: format Notification component

* fix: Convert DiscussionRenamedNotification to TypeScript

* fix(pusher) move shims to a location where they get applied

* fix(pusher): fix some typing errors

* fix(akismet): fix some typing issues

* chore: update core dist typings

* chore(pusher): format

* fix: anchorScroll should accept string selectors

* fix: more accurately represent ApiQueryParamsPlural

* fix: convert PostStreamState to TypeScript

* chore(core): rebuild typings

* feat: allow extending app.routes

* fix: more flexible typings for highlight.ts

* fix: use primitive `number` type for Discussion typings

* fix: convert DiscussionListItem to TypeScript

* chore: rebuild core typings

* fix: final pusher type fixes

* feat: start tags TypeScript conversion

* fix: require-dev tags in pusher for CI TypeScript purposes.

* chore(core): format

* chore(tags): build dist typings

* feat(pusher): use dist types from tags.

* feat: convert flags to TypeScript

* chore(flags): generate dist typings

* fix(akismet): last type errors

* chore: update .yarn-integrity

* chore: partially run flarum-cli audit infra --fix

The tsconfig changes from that command are ignored, since we don't yet support "replacable sections" that would let us add custom config.

* chore: use type imports

* fix: broader gitattributes

* chore: run flarum-cli audit infra --monorepo --fix

* feat: make `app.data` typings extensible

* chore(core): format

* chore: boost tags TypeScript coverage

* fix(tags): further increase type coverage.
This commit is contained in:
Alexander Skvortsov
2022-03-23 11:43:14 -04:00
committed by GitHub
parent 4ecd9a9b2f
commit a595665bfb
255 changed files with 1578 additions and 1218 deletions

View File

@ -1,4 +1,5 @@
import Application from '../common/Application';
import { AdminRoutes } from './routes';
import Application, { ApplicationData } from '../common/Application';
import ExtensionData from './utils/ExtensionData';
export declare type Extension = {
id: string;
@ -25,6 +26,13 @@ export declare type Extension = {
};
};
};
export interface AdminApplicationData extends ApplicationData {
extensions: Record<string, Extension>;
settings: Record<string, string>;
modelStatistics: Record<string, {
total: number;
}>;
}
export default class AdminApplication extends Application {
extensionData: ExtensionData;
extensionCategories: {
@ -45,13 +53,8 @@ export default class AdminApplication extends Application {
*
* @inheritdoc
*/
data: Application['data'] & {
extensions: Record<string, Extension>;
settings: Record<string, string>;
modelStatistics: Record<string, {
total: number;
}>;
};
data: AdminApplicationData;
route: typeof Application.prototype.route & AdminRoutes;
constructor();
/**
* @inheritdoc

View File

@ -1,4 +1,5 @@
export default class AdminHeader extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(vnode: any): JSX.Element[];
}
import Component from "../../common/Component";

View File

@ -1,6 +1,10 @@
export default class AdminNav extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
oninit(vnode: any): void;
query: Stream<string> | undefined;
view(): JSX.Element;
oncreate(vnode: any): void;
onupdate(vnode: any): void;
scrollToActive(): void;
/**
* Build an item list of main links to show in the admin navigation.

View File

@ -1,5 +1,13 @@
/// <reference path="../../@types/translator-icu-rich.d.ts" />
export default class AppearancePage extends AdminPage<import("../../common/components/Page").IPageAttrs> {
constructor();
headerInfo(): {
className: string;
icon: string;
title: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
description: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
};
content(): JSX.Element[];
colorItems(): ItemList<any>;
}
import AdminPage from "./AdminPage";

View File

@ -1,8 +1,17 @@
/// <reference path="../../@types/translator-icu-rich.d.ts" />
export default class BasicsPage extends AdminPage<import("../../common/components/Page").IPageAttrs> {
constructor();
oninit(vnode: any): void;
localeOptions: {} | undefined;
displayNameOptions: {} | undefined;
slugDriverOptions: {} | undefined;
headerInfo(): {
className: string;
icon: string;
title: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
description: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
};
content(): JSX.Element[];
/**
* Build a list of options for the default homepage. Each option must be an
* object with `path` and `label` properties.

View File

@ -1,6 +1,16 @@
export default class DashboardPage extends AdminPage<import("../../common/components/Page").IPageAttrs> {
constructor();
availableWidgets(): ItemList<any>;
/// <reference path="../../@types/translator-icu-rich.d.ts" />
import ItemList from '../../common/utils/ItemList';
import AdminPage from './AdminPage';
import { Children } from 'mithril';
export default class DashboardPage extends AdminPage {
headerInfo(): {
className: string;
icon: string;
title: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
description: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
};
content(): (Children & {
itemName: string;
})[];
availableWidgets(): ItemList<Children>;
}
import AdminPage from "./AdminPage";
import ItemList from "../../common/utils/ItemList";

View File

@ -1,16 +1,9 @@
export default class DashboardWidget extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
/**
* Get the class name to apply to the widget.
*
* @return {string}
*/
className(): string;
/**
* Get the content of the widget.
*
* @return {import('mithril').Children}
*/
content(): import('mithril').Children;
import { Children, Vnode } from 'mithril';
import Component, { ComponentAttrs } from '../../common/Component';
export interface IDashboardWidgetAttrs extends ComponentAttrs {
}
export default class DashboardWidget<CustomAttrs extends IDashboardWidgetAttrs = IDashboardWidgetAttrs> extends Component<CustomAttrs> {
view(vnode: Vnode<CustomAttrs, this>): Children;
className(): string;
content(): Children;
}
import Component from "../../common/Component";

View File

@ -1,3 +1,6 @@
/// <reference path="../../@types/translator-icu-rich.d.ts" />
export default class EditCustomCssModal extends SettingsModal {
title(): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
form(): JSX.Element[];
}
import SettingsModal from "./SettingsModal";

View File

@ -1,3 +1,6 @@
/// <reference path="../../@types/translator-icu-rich.d.ts" />
export default class EditCustomFooterModal extends SettingsModal {
title(): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
form(): JSX.Element[];
}
import SettingsModal from "./SettingsModal";

View File

@ -1,3 +1,6 @@
/// <reference path="../../@types/translator-icu-rich.d.ts" />
export default class EditCustomHeaderModal extends SettingsModal {
title(): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
form(): JSX.Element[];
}
import SettingsModal from "./SettingsModal";

View File

@ -4,12 +4,15 @@
*/
export default class EditGroupModal extends Modal<import("../../common/components/Modal").IInternalModalAttrs> {
constructor();
oninit(vnode: any): void;
group: any;
nameSingular: Stream<any> | undefined;
namePlural: Stream<any> | undefined;
icon: Stream<any> | undefined;
color: Stream<any> | undefined;
isHidden: Stream<any> | undefined;
title(): any[];
content(): JSX.Element;
fields(): ItemList<any>;
submitData(): {
nameSingular: any;
@ -18,6 +21,7 @@ export default class EditGroupModal extends Modal<import("../../common/component
icon: any;
isHidden: any;
};
onsubmit(e: any): void;
deleteGroup(): void;
}
import Modal from "../../common/components/Modal";

View File

@ -1,4 +1,5 @@
export default class ExtensionLinkButton extends LinkButton {
getButtonContent(children: any): import("mithril").ChildArray;
statusItems(name: any): ItemList<any>;
}
import LinkButton from "../../common/components/LinkButton";

View File

@ -1,5 +1,8 @@
export default class ExtensionsWidget extends DashboardWidget {
export default class ExtensionsWidget extends DashboardWidget<import("./DashboardWidget").IDashboardWidgetAttrs> {
constructor();
oninit(vnode: any): void;
categorizedExtensions: {} | undefined;
content(): JSX.Element;
extensionCategory(category: any): JSX.Element;
extensionWidget(extension: any): JSX.Element;
}

View File

@ -4,6 +4,7 @@
*/
export default class HeaderPrimary extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
config(isInitialized: any, context: any): void;
/**
* Build an item list for the controls.

View File

@ -3,6 +3,7 @@
*/
export default class HeaderSecondary extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
/**
* Build an item list for the controls.
*

View File

@ -1,13 +1,23 @@
/// <reference path="../../@types/translator-icu-rich.d.ts" />
export default class MailPage extends AdminPage<import("../../common/components/Page").IPageAttrs> {
constructor();
oninit(vnode: any): void;
sendingTest: boolean | undefined;
headerInfo(): {
className: string;
icon: string;
title: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
description: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
};
refresh(): void;
status: {
sending: boolean;
errors: {};
} | undefined;
driverFields: any;
content(): JSX.Element;
sendTestEmail(): void;
testEmailSuccessAlert: number | undefined;
saveSettings(e: any): void;
}
import AdminPage from "./AdminPage";

View File

@ -1,4 +1,12 @@
/// <reference path="../../@types/translator-icu-rich.d.ts" />
export default class PermissionsPage extends AdminPage<import("../../common/components/Page").IPageAttrs> {
constructor();
headerInfo(): {
className: string;
icon: string;
title: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
description: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
};
content(): JSX.Element[];
}
import AdminPage from "./AdminPage";

View File

@ -3,6 +3,7 @@
* avatar/name, with a dropdown of session controls.
*/
export default class SessionDropdown extends Dropdown {
getButtonContent(): (string | JSX.Element)[];
/**
* Build an item list for the contents of the dropdown menu.
*

View File

@ -1,11 +1,14 @@
export default class SettingsModal extends Modal<import("../../common/components/Modal").IInternalModalAttrs> {
constructor();
oninit(vnode: any): void;
settings: {} | undefined;
form(): string;
content(): JSX.Element;
submitButton(): JSX.Element;
setting(key: any, fallback?: string): any;
dirty(): {};
changed(): number;
onsubmit(e: any): void;
onsaved(): void;
}
import Modal from "../../common/components/Modal";

View File

@ -1,4 +1,6 @@
export default class StatusWidget extends DashboardWidget {
export default class StatusWidget extends DashboardWidget<import("./DashboardWidget").IDashboardWidgetAttrs> {
constructor();
content(): JSX.Element;
items(): ItemList<any>;
toolsItems(): ItemList<any>;
handleClearCache(e: any): void;

View File

@ -1,6 +1,7 @@
export default class UploadImageButton extends Button<import("../../common/components/Button").IButtonAttrs> {
constructor();
loading: boolean;
view(vnode: any): JSX.Element;
/**
* Prompt the user to upload an image.
*/

View File

@ -1,4 +1,9 @@
import AdminApplication from './AdminApplication';
/**
* Helper functions to generate URLs to admin pages.
*/
export interface AdminRoutes {
}
/**
* The `routes` initializer defines the forum app's routes.
*/

View File

@ -91,6 +91,17 @@ export interface RouteResolver<Attrs extends ComponentAttrs, Comp extends Compon
*/
render?(this: this, vnode: Mithril.Vnode<Attrs, Comp>): Mithril.Children;
}
export interface ApplicationData {
apiDocument: ApiPayload | null;
locale: string;
locales: Record<string, string>;
resources: SavedModelData[];
session: {
userId: number;
csrfToken: string;
};
[key: string]: unknown;
}
/**
* The `App` class provides a container for an application, as well as various
* utilities for the rest of the app to use.
@ -166,17 +177,7 @@ export default class Application {
* An object that manages the state of the navigation drawer.
*/
drawer: Drawer;
data: {
apiDocument: ApiPayload | null;
locale: string;
locales: Record<string, string>;
resources: SavedModelData[];
session: {
userId: number;
csrfToken: string;
};
[key: string]: unknown;
};
data: ApplicationData;
private _title;
private _titleCount;
private set title(value);

View File

@ -14,9 +14,9 @@ export interface ApiQueryParamsPlural {
include?: string;
filter?: {
q: string;
[key: string]: string;
};
} | Record<string, string>;
page?: {
near?: number;
offset?: number;
number?: number;
limit?: number;

View File

@ -4,5 +4,8 @@
*/
export default class AlertManager extends Component<import("../Component").ComponentAttrs, undefined> {
constructor();
oninit(vnode: any): void;
state: any;
view(): JSX.Element;
}
import Component from "../Component";

View File

@ -13,5 +13,6 @@
*/
export default class Badge extends Component<import("../Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
}
import Component from "../Component";

View File

@ -12,6 +12,7 @@
*/
export default class Checkbox extends Component<import("../Component").ComponentAttrs, undefined> {
constructor();
view(vnode: any): JSX.Element;
/**
* Get the template for the checkbox's display (tick/cross icon).
*

View File

@ -17,6 +17,9 @@
export default class ConfirmDocumentUnload extends Component<import("../Component").ComponentAttrs, undefined> {
constructor();
handler(): any;
oncreate(vnode: any): void;
boundHandler: (() => any) | undefined;
onremove(vnode: any): void;
view(vnode: any): any;
}
import Component from "../Component";

View File

@ -18,7 +18,10 @@
export default class Dropdown extends Component<import("../Component").ComponentAttrs, undefined> {
static initAttrs(attrs: any): void;
constructor();
oninit(vnode: any): void;
showing: boolean | undefined;
view(vnode: any): JSX.Element;
oncreate(vnode: any): void;
/**
* Get the template for the button.
*

View File

@ -9,5 +9,6 @@
*/
export default class FieldSet extends Component<import("../Component").ComponentAttrs, undefined> {
constructor();
view(vnode: any): JSX.Element;
}
import Component from "../Component";

View File

@ -8,5 +8,6 @@
*/
export default class Link extends Component<import("../Component").ComponentAttrs, undefined> {
constructor();
view(vnode: any): JSX.Element;
}
import Component from "../Component";

View File

@ -21,5 +21,6 @@ export default class LinkButton extends Button<import("./Button").IButtonAttrs>
*/
static isActive(attrs: object): boolean;
constructor();
view(vnode: any): JSX.Element;
}
import Button from "./Button";

View File

@ -15,6 +15,7 @@
*/
export default class Navigation extends Component<import("../Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
/**
* Get the back button.
*

View File

@ -8,5 +8,6 @@
*/
export default class Placeholder extends Component<import("../Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
}
import Component from "../Component";

View File

@ -12,5 +12,6 @@
*/
export default class Select extends Component<import("../Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
}
import Component from "../Component";

View File

@ -9,5 +9,6 @@
* - `defaultLabel`
*/
export default class SelectDropdown extends Dropdown {
getButtonContent(children: any): JSX.Element[];
}
import Dropdown from "./Dropdown";

View File

@ -4,6 +4,7 @@ export default Separator;
*/
declare class Separator extends Component<import("../Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
}
declare namespace Separator {
const isListItem: boolean;

View File

@ -3,6 +3,7 @@
* is displayed as its own button prior to the toggle button.
*/
export default class SplitDropdown extends Dropdown {
getButton(children: any): JSX.Element[];
/**
* Get the first child. If the first child is an array, the first item in that
* array will be returned.

View File

@ -13,6 +13,7 @@
*/
export default class TextEditor extends Component<import("../Component").ComponentAttrs, undefined> {
constructor();
oninit(vnode: any): void;
/**
* The value of the editor.
*
@ -23,6 +24,9 @@ export default class TextEditor extends Component<import("../Component").Compone
* Whether the editor is disabled.
*/
disabled: any;
view(): JSX.Element;
oncreate(vnode: any): void;
onupdate(vnode: any): void;
buildEditorParams(): {
classNames: string[];
disabled: any;

View File

@ -10,5 +10,6 @@
export default class TextEditorButton extends Button<import("./Button").IButtonAttrs> {
static initAttrs(attrs: any): void;
constructor();
view(vnode: any): JSX.Element;
}
import Button from "./Button";

View File

@ -9,4 +9,4 @@ export interface AvatarAttrs extends ComponentAttrs {
* @param user
* @param attrs Attributes to apply to the avatar element
*/
export default function avatar(user: User, attrs?: ComponentAttrs): Mithril.Vnode;
export default function avatar(user: User | null, attrs?: ComponentAttrs): Mithril.Vnode;

View File

@ -8,4 +8,4 @@ import type Mithril from 'mithril';
* @param [length] The number of characters to truncate the string to.
* The string will be truncated surrounding the first match.
*/
export default function highlight(string: string, phrase: string | RegExp, length?: number): Mithril.Vnode<any, any> | string;
export default function highlight(string: string, phrase?: string | RegExp, length?: number): Mithril.Vnode<any, any> | string;

View File

@ -14,7 +14,7 @@ export default class Discussion extends Model {
lastPost(): false | Post | null;
lastPostNumber(): number | null | undefined;
commentCount(): number | undefined;
replyCount(): Number;
replyCount(): number;
posts(): false | (Post | undefined)[];
mostRelevantPost(): false | Post | null;
lastReadAt(): Date | null | undefined;

View File

@ -2,7 +2,7 @@ import Model from '../Model';
import User from './User';
export default class Notification extends Model {
contentType(): string;
content(): string;
content<T = unknown>(): T;
createdAt(): Date;
isRead(): boolean;
user(): false | User;

View File

@ -8,7 +8,7 @@
* position can be anchor to an element that is in or below the viewport, so
* the content in the viewport will stay the same.
*
* @param {HTMLElement | SVGElement | Element} element The element to anchor the scroll position to.
* @param {string | HTMLElement | SVGElement | Element} element The element to anchor the scroll position to.
* @param {() => void} callback The callback to run that will change page content.
*/
export default function anchorScroll(element: HTMLElement | SVGElement | Element, callback: () => void): void;
export default function anchorScroll(element: string | HTMLElement | SVGElement | Element, callback: () => void): void;

View File

@ -1,23 +1,35 @@
import History from './utils/History';
import Pane from './utils/Pane';
import { makeRouteHelpers } from './routes';
import Application from '../common/Application';
import { ForumRoutes } from './routes';
import Application, { ApplicationData } from '../common/Application';
import NotificationListState from './states/NotificationListState';
import GlobalSearchState from './states/GlobalSearchState';
import DiscussionListState from './states/DiscussionListState';
import ComposerState from './states/ComposerState';
import type Notification from './components/Notification';
import type Post from './components/Post';
import Discussion from '../common/models/Discussion';
import type Discussion from '../common/models/Discussion';
import type NotificationModel from '../common/models/Notification';
import type PostModel from '../common/models/Post';
export interface ForumApplicationData extends ApplicationData {
}
export default class ForumApplication extends Application {
/**
* A map of notification types to their components.
*/
notificationComponents: Record<string, typeof Notification>;
notificationComponents: Record<string, ComponentClass<{
notification: NotificationModel;
}, Notification<{
notification: NotificationModel;
}>>>;
/**
* A map of post types to their components.
*/
postComponents: Record<string, typeof Post>;
postComponents: Record<string, ComponentClass<{
post: PostModel;
}, Post<{
post: PostModel;
}>>>;
/**
* An object which controls the state of the page's side pane.
*/
@ -45,7 +57,8 @@ export default class ForumApplication extends Application {
* is used in the index page and the slideout pane.
*/
discussions: DiscussionListState;
route: typeof Application.prototype.route & ReturnType<typeof makeRouteHelpers>;
route: typeof Application.prototype.route & ForumRoutes;
data: ForumApplicationData;
constructor();
/**
* @inheritdoc

View File

@ -11,7 +11,10 @@
*/
export default class AffixedSidebar extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(vnode: any): any;
oncreate(vnode: any): void;
boundOnresize: (() => void) | undefined;
onremove(vnode: any): void;
onresize(): void;
bottom: number | undefined;
}

View File

@ -9,6 +9,7 @@
*/
export default class AvatarEditor extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
oninit(vnode: any): void;
/**
* Whether or not an avatar upload is in progress.
*
@ -21,6 +22,7 @@ export default class AvatarEditor extends Component<import("../../common/Compone
* @type {Boolean}
*/
isDraggedOver: boolean | undefined;
view(): JSX.Element;
/**
* Get the items in the edit avatar dropdown menu.
*

View File

@ -1,9 +1,11 @@
/// <reference path="../../@types/translator-icu-rich.d.ts" />
/**
* The `ChangeEmailModal` component shows a modal dialog which allows the user
* to change their email address.
*/
export default class ChangeEmailModal extends Modal<import("../../common/components/Modal").IInternalModalAttrs> {
constructor();
oninit(vnode: any): void;
/**
* Whether or not the email has been changed successfully.
*
@ -22,5 +24,9 @@ export default class ChangeEmailModal extends Modal<import("../../common/compone
* @type {function}
*/
password: Function | undefined;
title(): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
content(): JSX.Element;
onsubmit(e: any): void;
onerror(error: any): void;
}
import Modal from "../../common/components/Modal";

View File

@ -1,8 +1,12 @@
/// <reference path="../../@types/translator-icu-rich.d.ts" />
/**
* The `ChangePasswordModal` component shows a modal dialog which allows the
* user to send themself a password reset email.
*/
export default class ChangePasswordModal extends Modal<import("../../common/components/Modal").IInternalModalAttrs> {
constructor();
title(): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
content(): JSX.Element;
onsubmit(e: any): void;
}
import Modal from "../../common/components/Modal";

View File

@ -7,7 +7,9 @@
*
* - `post`
*/
export default class CommentPost extends Post {
export default class CommentPost extends Post<import("./Post").IPostAttrs> {
constructor();
oninit(vnode: any): void;
/**
* If the post has been hidden, then this flag determines whether or not its
* content has been expanded.
@ -22,8 +24,11 @@ export default class CommentPost extends Post {
* @type {Boolean}
*/
cardVisible: boolean | undefined;
content(): any;
refreshContent(): void;
contentHtml: any;
oncreate(vnode: any): void;
onupdate(vnode: any): void;
isEditing(): boolean;
/**
* Toggle the visibility of a hidden post's content.

View File

@ -5,6 +5,13 @@
*/
export default class Composer extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
oninit(vnode: any): void;
/**
* The composer's "state".
*
* @type {ComposerState}
*/
state: ComposerState | undefined;
/**
* Whether or not the composer currently has focus.
*
@ -12,7 +19,11 @@ export default class Composer extends Component<import("../../common/Component")
*/
active: boolean | undefined;
prevPosition: any;
view(): JSX.Element;
onupdate(vnode: any): void;
oncreate(vnode: any): void;
handlers: {} | undefined;
onremove(vnode: any): void;
/**
* Add the necessary event handlers to the composer's handle so that it can
* be used to resize the composer.
@ -102,4 +113,5 @@ export default class Composer extends Component<import("../../common/Component")
changeHeight(height: number): void;
}
import Component from "../../common/Component";
import ComposerState from "../states/ComposerState";
import ItemList from "../../common/utils/ItemList";

View File

@ -17,6 +17,7 @@
*/
export default class ComposerBody extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
oninit(vnode: any): void;
composer: any;
/**
* Whether or not the component is loading.
@ -24,6 +25,7 @@ export default class ComposerBody extends Component<import("../../common/Compone
* @type {Boolean}
*/
loading: boolean | undefined;
view(): JSX.Element;
/**
* Check if there is any unsaved data.
*

View File

@ -13,6 +13,9 @@
export default class ComposerPostPreview extends Component<import("../../common/Component").ComponentAttrs, undefined> {
static initAttrs(attrs: any): void;
constructor();
view(): JSX.Element;
oncreate(vnode: any): void;
updateInterval: NodeJS.Timer | undefined;
onremove(vnode: any): void;
}
import Component from "../../common/Component";

View File

@ -24,6 +24,7 @@ export default class DiscussionComposer extends ComposerBody {
* @param {KeyboardEvent} e
*/
onkeydown(e: KeyboardEvent): void;
hasChanges(): any;
/**
* Get the data to submit to the server when the discussion is saved.
*

View File

@ -7,6 +7,7 @@
*/
export default class DiscussionHero extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
/**
* Build an item list for the contents of the discussion hero.
*

View File

@ -7,5 +7,6 @@
*/
export default class DiscussionList extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
}
import Component from "../../common/Component";

View File

@ -1,37 +1,39 @@
import Component, { ComponentAttrs } from '../../common/Component';
import ItemList from '../../common/utils/ItemList';
import SubtreeRetainer from '../../common/utils/SubtreeRetainer';
import Discussion from '../../common/models/Discussion';
import Mithril from 'mithril';
import { DiscussionListParams } from '../states/DiscussionListState';
export interface IDiscussionListItemAttrs extends ComponentAttrs {
discussion: Discussion;
params: DiscussionListParams;
}
/**
* The `DiscussionListItem` component shows a single discussion in the
* discussion list.
*
* ### Attrs
*
* - `discussion`
* - `params`
*/
export default class DiscussionListItem extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemAttrs = IDiscussionListItemAttrs> extends Component<CustomAttrs> {
/**
* Set up a subtree retainer so that the discussion will not be redrawn
* Ensures that the discussion will not be redrawn
* unless new data comes in.
*
* @type {SubtreeRetainer}
*/
subtree: SubtreeRetainer | undefined;
subtree: SubtreeRetainer;
highlightRegExp?: RegExp;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
elementAttrs(): {
className: string;
};
highlightRegExp: RegExp | undefined;
view(): JSX.Element;
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>): void;
onbeforeupdate(vnode: Mithril.VnodeDOM<CustomAttrs, this>): boolean;
/**
* Determine whether or not the discussion is currently being viewed.
*
* @return {boolean}
*/
active(): boolean;
/**
* Determine whether or not information about who started the discussion
* should be displayed instead of information about the most recent reply to
* the discussion.
*
* @return {boolean}
*/
showFirstPost(): boolean;
/**
@ -48,12 +50,7 @@ export default class DiscussionListItem extends Component<import("../../common/C
/**
* Build an item list of info for a discussion listing. By default this is
* just the first/last post indicator.
*
* @return {ItemList<import('mithril').Children>}
*/
infoItems(): ItemList<import('mithril').Children>;
infoItems(): ItemList<Mithril.Children>;
replyCountItem(): JSX.Element;
}
import Component from "../../common/Component";
import SubtreeRetainer from "../../common/utils/SubtreeRetainer";
import ItemList from "../../common/utils/ItemList";

View File

@ -9,6 +9,9 @@
*/
export default class DiscussionListPane extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element | undefined;
oncreate(vnode: any): void;
onremove(vnode: any): void;
/**
* Are we on a device that's larger than we consider "mobile"?
*

View File

@ -1,11 +1,12 @@
/// <reference path="../../@types/translator-icu-rich.d.ts" />
import Notification from './Notification';
/**
* The `DiscussionRenamedNotification` component displays a notification which
* indicates that a discussion has had its title changed.
*
* ### Attrs
*
* - All of the attrs for Notification
*/
export default class DiscussionRenamedNotification extends Notification {
icon(): string;
href(): string;
content(): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
excerpt(): string;
}
import Notification from "./Notification";

View File

@ -7,5 +7,10 @@
* - All of the attrs for EventPost
*/
export default class DiscussionRenamedPost extends EventPost {
description(data: any): JSX.Element;
descriptionData(): {
old: string;
new: JSX.Element;
};
}
import EventPost from "./EventPost";

View File

@ -3,5 +3,14 @@
* page.
*/
export default class DiscussionsUserPage extends UserPage {
show(user: any): void;
state: DiscussionListState<{
filter: {
author: any;
};
sort: string;
}>;
content(): JSX.Element;
}
import UserPage from "./UserPage";
import DiscussionListState from "../states/DiscussionListState";

View File

@ -9,7 +9,9 @@
*
* @abstract
*/
export default class EventPost extends Post {
export default class EventPost extends Post<import("./Post").IPostAttrs> {
constructor();
content(): any;
/**
* Get the name of the event icon.
*

View File

@ -4,6 +4,7 @@
*/
export default class HeaderPrimary extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
/**
* Build an item list for the controls.
*

View File

@ -5,6 +5,7 @@
*/
export default class HeaderSecondary extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
/**
* Build an item list for the controls.
*

View File

@ -5,8 +5,13 @@
export default class IndexPage extends Page<import("../../common/components/Page").IPageAttrs> {
static providesInitialSearch: boolean;
constructor();
oninit(vnode: any): void;
lastDiscussion: any;
view(): JSX.Element;
setTitle(): void;
oncreate(vnode: any): void;
onbeforeremove(vnode: any): void;
onremove(vnode: any): void;
/**
* Get the component to display as the hero.
*

View File

@ -4,5 +4,6 @@
*/
export default class LoadingPost extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
}
import Component from "../../common/Component";

View File

@ -3,6 +3,7 @@
*/
export default class LogInButtons extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
/**
* Build a list of LogInButton components.
*

View File

@ -1,46 +1,33 @@
import NotificationModel from '../../common/models/Notification';
import Component, { ComponentAttrs } from '../../common/Component';
import Mithril from 'mithril';
export interface INotificationAttrs extends ComponentAttrs {
notification: NotificationModel;
}
/**
* The `Notification` component abstract displays a single notification.
* Subclasses should implement the `icon`, `href`, and `content` methods.
*
* ### Attrs
*
* - `notification`
*
* @abstract
*/
export default class Notification extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
export default abstract class Notification<CustomAttrs extends INotificationAttrs = INotificationAttrs> extends Component<CustomAttrs> {
view(vnode: Mithril.Vnode<CustomAttrs, this>): JSX.Element;
/**
* Get the name of the icon that should be displayed in the notification.
*
* @return {string}
* @abstract
*/
icon(): string;
abstract icon(): string;
/**
* Get the URL that the notification should link to.
*
* @return {string}
* @abstract
*/
href(): string;
abstract href(): string;
/**
* Get the content of the notification.
*
* @return {import('mithril').Children}
* @abstract
*/
content(): import('mithril').Children;
abstract content(): Mithril.Children;
/**
* Get the excerpt of the notification.
*
* @return {import('mithril').Children}
* @abstract
*/
excerpt(): import('mithril').Children;
abstract excerpt(): Mithril.Children;
/**
* Mark the notification as read.
*/
markAsRead(): void;
}
import Component from "../../common/Component";

View File

@ -8,6 +8,7 @@
*/
export default class NotificationGrid extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
oninit(vnode: any): void;
/**
* Information about the available notification methods.
*
@ -34,6 +35,8 @@ export default class NotificationGrid extends Component<import("../../common/Com
icon: string;
label: import('mithril').Children;
}[] | undefined;
view(): JSX.Element;
oncreate(vnode: any): void;
/**
* Toggle the state of the given preferences, based on the value of the first
* one.

View File

@ -4,11 +4,14 @@
*/
export default class NotificationList extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
controlItems(): ItemList<any>;
content(state: any): any;
oncreate(vnode: any): void;
$notifications: JQuery<HTMLElement> | undefined;
$scrollParent: JQuery<HTMLElement> | JQuery<Window & typeof globalThis> | undefined;
boundScrollHandler: (() => void) | undefined;
onremove(vnode: any): void;
scrollHandler(): void;
/**
* If the NotificationList component isn't in a panel (e.g. on NotificationPage when mobile),

View File

@ -1,4 +1,7 @@
export default class NotificationsDropdown extends Dropdown {
getButton(): import("mithril").Children;
getButtonContent(): (false | JSX.Element)[];
getMenu(): JSX.Element;
onclick(): void;
goToRoute(): void;
getUnreadCount(): number | undefined;

View File

@ -4,5 +4,7 @@
*/
export default class NotificationsPage extends Page<import("../../common/components/Page").IPageAttrs> {
constructor();
oninit(vnode: any): void;
view(): JSX.Element;
}
import Page from "../../common/components/Page";

View File

@ -1,59 +1,48 @@
import Component, { ComponentAttrs } from '../../common/Component';
import SubtreeRetainer from '../../common/utils/SubtreeRetainer';
import ItemList from '../../common/utils/ItemList';
import PostModel from '../../common/models/Post';
import Mithril from 'mithril';
export interface IPostAttrs extends ComponentAttrs {
post: PostModel;
}
/**
* The `Post` component displays a single post. The basic post template just
* includes a controls dropdown; subclasses must implement `content` and `attrs`
* methods.
*
* ### Attrs
*
* - `post`
*
* @abstract
*/
export default class Post extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
export default abstract class Post<CustomAttrs extends IPostAttrs = IPostAttrs> extends Component<CustomAttrs> {
/**
* May be set by subclasses.
*/
loading: boolean | undefined;
loading: boolean;
/**
* Set up a subtree retainer so that the post will not be redrawn
* Ensures that the post will not be redrawn
* unless new data comes in.
*
* @type {SubtreeRetainer}
*/
subtree: SubtreeRetainer | undefined;
subtree: SubtreeRetainer;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
view(vnode: Mithril.Vnode<CustomAttrs, this>): JSX.Element;
onbeforeupdate(vnode: Mithril.VnodeDOM<CustomAttrs, this>): boolean;
onupdate(vnode: Mithril.VnodeDOM<CustomAttrs, this>): void;
/**
* Get attributes for the post element.
*
* @return {Record<string, unknown>}
*/
elementAttrs(): Record<string, unknown>;
/**
* Get the post's content.
*
* @return {import('mithril').Children}
*/
content(): import('mithril').Children;
content(): Mithril.Children;
/**
* Get the post's classes.
*
* @param {string} existing
* @returns {string[]}
*/
classes(existing: string): string[];
classes(existing?: string): string[];
/**
* Build an item list for the post's actions.
*
* @return {ItemList<import('mithril').Children>}
*/
actionItems(): ItemList<import('mithril').Children>;
actionItems(): ItemList<Mithril.Children>;
/**
* Build an item list for the post's footer.
*
* @return {ItemList<import('mithril').Children>}
*/
footerItems(): ItemList<import('mithril').Children>;
footerItems(): ItemList<Mithril.Children>;
}
import Component from "../../common/Component";
import SubtreeRetainer from "../../common/utils/SubtreeRetainer";
import ItemList from "../../common/utils/ItemList";

View File

@ -8,5 +8,8 @@
*/
export default class PostEdited extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
oninit(vnode: any): void;
view(): JSX.Element;
oncreate(vnode: any): void;
}
import Component from "../../common/Component";

View File

@ -9,6 +9,7 @@
*/
export default class PostMeta extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
/**
* Get the permalink for the given post.
*

View File

@ -8,5 +8,6 @@
*/
export default class PostPreview extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
}
import Component from "../../common/Component";

View File

@ -11,9 +11,14 @@
*/
export default class PostStream extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
oninit(vnode: any): void;
discussion: any;
stream: any;
scrollListener: ScrollListener | undefined;
view(): JSX.Element;
onupdate(vnode: any): void;
oncreate(vnode: any): void;
onremove(vnode: any): void;
/**
* Start scrolling, if appropriate, to a newly-targeted post.
*/

View File

@ -9,12 +9,17 @@
*/
export default class PostStreamScrubber extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
oninit(vnode: any): void;
stream: any;
handlers: {} | undefined;
scrollListener: ScrollListener | undefined;
view(): JSX.Element;
onupdate(vnode: any): void;
oncreate(vnode: any): void;
dragging: boolean | undefined;
mouseStart: any;
indexStart: any;
onremove(vnode: any): void;
/**
* Update the scrollbar's position to reflect the current values of the
* index/visible properties.

View File

@ -7,6 +7,8 @@
*/
export default class PostUser extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
oncreate(vnode: any): void;
/**
* Show the user card.
*/

View File

@ -27,6 +27,12 @@ export default class PostsUserPage extends UserPage {
* @type {number}
*/
loadLimit: number | undefined;
content(): JSX.Element;
/**
* Initialize the component with a user, and trigger the loading of their
* activity feed.
*/
show(user: any): void;
/**
* Clear and reload the user's activity feed.
*/

View File

@ -1,11 +1,16 @@
/// <reference path="../../@types/translator-icu-rich.d.ts" />
/**
* The 'RenameDiscussionModal' displays a modal dialog with an input to rename a discussion
*/
export default class RenameDiscussionModal extends Modal<import("../../common/components/Modal").IInternalModalAttrs> {
constructor();
oninit(vnode: any): void;
discussion: any;
currentTitle: any;
newTitle: Stream<any> | undefined;
title(): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
content(): JSX.Element;
onsubmit(e: any): any;
}
import Modal from "../../common/components/Modal";
import Stream from "../../common/utils/Stream";

View File

@ -8,6 +8,7 @@
*/
export default class ReplyPlaceholder extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
anchorPreview(preview: any): void;
}
import Component from "../../common/Component";

View File

@ -3,6 +3,7 @@
* avatar/name, with a dropdown of session controls.
*/
export default class SessionDropdown extends Dropdown {
getButtonContent(): (string | JSX.Element)[];
/**
* Build an item list for the contents of the dropdown menu.
*

View File

@ -3,6 +3,7 @@
* the context of their user profile.
*/
export default class SettingsPage extends UserPage {
content(): JSX.Element;
/**
* Build an item list for the user's settings controls.
*

View File

@ -8,5 +8,6 @@
*/
export default class TerminalPost extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
}
import Component from "../../common/Component";

View File

@ -12,6 +12,7 @@
*/
export default class UserCard extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
/**
* Build an item list of tidbits of info to show on this user's profile.
*

View File

@ -7,12 +7,19 @@
*/
export default class UserPage extends Page<import("../../common/components/Page").IPageAttrs> {
constructor();
oninit(vnode: any): void;
/**
* The user this page is for.
*
* @type {User}
*/
user: any;
/**
* Base view template for the user page.
*
* @return {import('mithril').Children}
*/
view(): import('mithril').Children;
/**
* Get the content to display in the user page.
*

View File

@ -2,6 +2,14 @@ import ForumApplication from './ForumApplication';
import Discussion from '../common/models/Discussion';
import Post from '../common/models/Post';
import User from '../common/models/User';
/**
* Helper functions to generate URLs to form pages.
*/
export interface ForumRoutes {
discussion: (discussion: Discussion, near?: number) => string;
post: (post: Post) => string;
user: (user: User) => string;
}
/**
* The `routes` initializer defines the forum app's routes.
*/

View File

@ -1,33 +1,44 @@
export default PostStreamState;
declare class PostStreamState {
constructor(discussion: any, includedPosts?: any[]);
/// <reference types="node" />
import Discussion from '../../common/models/Discussion';
import Post from '../../common/models/Post';
export default class PostStreamState {
/**
* @see https://github.com/Microsoft/TypeScript/issues/3841#issuecomment-337560146
*/
['constructor']: typeof PostStreamState;
/**
* The number of posts to load per page.
*/
static loadCount: number;
/**
* The discussion to display the post stream for.
*
* @type {Discussion}
*/
discussion: Discussion;
/**
* Whether or not the infinite-scrolling auto-load functionality is
* disabled.
*
* @type {Boolean}
*/
paused: boolean;
loadPageTimeouts: {};
loadPageTimeouts: Record<number, NodeJS.Timeout>;
pagesLoading: number;
index: number;
number: number;
/**
* The number of posts that are currently visible in the viewport.
*
* @type {Number}
*/
visible: number;
visibleStart: number;
visibleEnd: number;
animateScroll: boolean;
needsScroll: boolean;
targetPost: {
number: number;
} | {
index: number;
reply?: boolean;
} | null;
/**
* The description to render on the scrubber.
*
* @type {String}
*/
description: string;
/**
@ -38,147 +49,93 @@ declare class PostStreamState {
* onupdate to update itself, but only when needed, as indicated by this
* property.
*
* @type {Boolean}
*/
forceUpdateScrubber: boolean;
loadNext: throttle<() => void>;
loadPrevious: throttle<() => void>;
loadPromise: Promise<void> | null;
loadNext: () => void;
loadPrevious: () => void;
constructor(discussion: Discussion, includedPosts?: Post[]);
/**
* Update the stream so that it loads and includes the latest posts in the
* discussion, if the end is being viewed.
*/
update(): Promise<void>;
visibleEnd: any;
update(): Promise<void> | Promise<Post[]>;
/**
* Load and scroll up to the first post in the discussion.
*
* @return {Promise<void>}
*/
goToFirst(): Promise<void>;
/**
* Load and scroll down to the last post in the discussion.
*
* @return {Promise<void>}
*/
goToLast(): Promise<void>;
/**
* Load and scroll to a post with a certain number.
*
* @param {number | string} number The post number to go to. If 'reply', go to the last post and scroll the reply preview into view.
* @param {boolean} [noAnimation]
* @return {Promise<void>}
* @param number The post number to go to. If 'reply', go to the last post and scroll the reply preview into view.
*/
goToNumber(number: number | string, noAnimation?: boolean | undefined): Promise<void>;
loadPromise: Promise<void> | undefined;
needsScroll: boolean | undefined;
targetPost: {
number: string | number;
index?: undefined;
} | {
index: number;
number?: undefined;
} | undefined;
animateScroll: boolean | undefined;
goToNumber(number: number | 'reply', noAnimation?: boolean): Promise<void>;
/**
* Load and scroll to a certain index within the discussion.
*
* @param {number} index
* @param {boolean} [noAnimation]
* @return {Promise<void>}
*/
goToIndex(index: number, noAnimation?: boolean | undefined): Promise<void>;
goToIndex(index: number, noAnimation?: boolean): Promise<void>;
/**
* Clear the stream and load posts near a certain number. Returns a promise.
* If the post with the given number is already loaded, the promise will be
* resolved immediately.
*
* @param {number} number
* @return {Promise<void>}
*/
loadNearNumber(number: number): Promise<void>;
/**
* Clear the stream and load posts near a certain index. A page of posts
* surrounding the given index will be loaded. Returns a promise. If the given
* index is already loaded, the promise will be resolved immediately.
*
* @param {number} index
* @return {Promise<void>}
*/
loadNearIndex(index: number): Promise<void>;
/**
* Load the next page of posts.
*/
_loadNext(): void;
visibleStart: any;
/**
* Load the previous page of posts.
*/
_loadPrevious(): void;
/**
* Load a page of posts into the stream and redraw.
*
* @param {number} start
* @param {number} end
* @param {boolean} backwards
*/
loadPage(start: number, end: number, backwards?: boolean): void;
/**
* Load and inject the specified range of posts into the stream, without
* clearing it.
*
* @param {number} start
* @param {number} end
* @return {Promise<void>}
*/
loadRange(start: number, end: number): Promise<void>;
loadRange(start: number, end: number): Promise<Post[]>;
/**
* Set up the stream with the given array of posts.
*
* @param {import('../../common/models/Post').default[]} posts
*/
show(posts: import('../../common/models/Post').default[]): void;
show(posts: Post[]): void;
/**
* Reset the stream so that a specific range of posts is displayed. If a range
* is not specified, the first page of posts will be displayed.
*
* @param {number} [start]
* @param {number} [end]
*/
reset(start?: number | undefined, end?: number | undefined): void;
reset(start?: number, end?: number): void;
/**
* Get the visible page of posts.
*
* @return {Post[]}
*/
posts(): Post[];
posts(): (Post | null)[];
/**
* Get the total number of posts in the discussion.
*
* @return {number}
*/
count(): number;
/**
* Check whether or not the scrubber should be disabled, i.e. if all of the
* posts are visible in the viewport.
*
* @return {boolean}
*/
disabled(): boolean;
/**
* Are we currently viewing the end of the discussion?
*
* @return {boolean}
*/
viewingEnd(): boolean;
/**
* Make sure that the given index is not outside of the possible range of
* indexes in the discussion.
*
* @param {number} index
*/
sanitizeIndex(index: number): number;
}
declare namespace PostStreamState {
const loadCount: number;
}
import { throttle } from "throttle-debounce";

View File

@ -44,7 +44,7 @@
"format": "prettier --write src",
"format-check": "prettier --check src",
"clean-typings": "npx rimraf dist-typings && mkdir dist-typings",
"build-typings": "yarn run clean-typings && [ -e src/@types ] && cp -r src/@types dist-typings/@types && tsc && yarn run post-build-typings",
"build-typings": "yarn run clean-typings && ([ -e src/@types ] && cp -r src/@types dist-typings/@types || true) && tsc && yarn run post-build-typings",
"post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'",
"check-typings": "tsc --noEmit --emitDeclarationOnly false",
"check-typings-coverage": "typescript-coverage-report"

View File

@ -1,7 +1,7 @@
import HeaderPrimary from './components/HeaderPrimary';
import HeaderSecondary from './components/HeaderSecondary';
import routes from './routes';
import Application from '../common/Application';
import routes, { AdminRoutes } from './routes';
import Application, { ApplicationData } from '../common/Application';
import Navigation from '../common/components/Navigation';
import AdminNav from './components/AdminNav';
import ExtensionData from './utils/ExtensionData';
@ -32,6 +32,12 @@ export type Extension = {
};
};
export interface AdminApplicationData extends ApplicationData {
extensions: Record<string, Extension>;
settings: Record<string, string>;
modelStatistics: Record<string, { total: number }>;
}
export default class AdminApplication extends Application {
extensionData = new ExtensionData();
@ -58,16 +64,16 @@ export default class AdminApplication extends Application {
* @inheritdoc
*/
data!: Application['data'] & {
extensions: Record<string, Extension>;
settings: Record<string, string>;
modelStatistics: Record<string, { total: number }>;
};
data!: AdminApplicationData;
route: typeof Application.prototype.route & AdminRoutes;
constructor() {
super();
routes(this);
this.route = (Object.getPrototypeOf(Object.getPrototypeOf(this)) as Application).route.bind(this);
}
/**

View File

@ -3,6 +3,7 @@ import StatusWidget from './StatusWidget';
import ExtensionsWidget from './ExtensionsWidget';
import ItemList from '../../common/utils/ItemList';
import AdminPage from './AdminPage';
import type { Children } from 'mithril';
export default class DashboardPage extends AdminPage {
headerInfo() {
@ -18,8 +19,8 @@ export default class DashboardPage extends AdminPage {
return this.availableWidgets().toArray();
}
availableWidgets() {
const items = new ItemList();
availableWidgets(): ItemList<Children> {
const items = new ItemList<Children>();
items.add('status', <StatusWidget />, 30);

View File

@ -1,25 +0,0 @@
import Component from '../../common/Component';
export default class DashboardWidget extends Component {
view() {
return <div className={'DashboardWidget Widget ' + this.className()}>{this.content()}</div>;
}
/**
* Get the class name to apply to the widget.
*
* @return {string}
*/
className() {
return '';
}
/**
* Get the content of the widget.
*
* @return {import('mithril').Children}
*/
content() {
return null;
}
}

View File

@ -0,0 +1,18 @@
import type { Children, Vnode } from 'mithril';
import Component, { ComponentAttrs } from '../../common/Component';
export interface IDashboardWidgetAttrs extends ComponentAttrs {}
export default class DashboardWidget<CustomAttrs extends IDashboardWidgetAttrs = IDashboardWidgetAttrs> extends Component<CustomAttrs> {
view(vnode: Vnode<CustomAttrs, this>): Children {
return <div className={'DashboardWidget Widget ' + this.className()}>{this.content()}</div>;
}
className() {
return '';
}
content(): Children {
return null;
}
}

View File

@ -8,6 +8,11 @@ import UserListPage from './components/UserListPage';
import ExtensionPage from './components/ExtensionPage';
import ExtensionPageResolver from './resolvers/ExtensionPageResolver';
/**
* Helper functions to generate URLs to admin pages.
*/
export interface AdminRoutes {}
/**
* The `routes` initializer defines the forum app's routes.
*/

View File

@ -123,6 +123,15 @@ export interface RouteResolver<
render?(this: this, vnode: Mithril.Vnode<Attrs, Comp>): Mithril.Children;
}
export interface ApplicationData {
apiDocument: ApiPayload | null;
locale: string;
locales: Record<string, string>;
resources: SavedModelData[];
session: { userId: number; csrfToken: string };
[key: string]: unknown;
}
/**
* The `App` class provides a container for an application, as well as various
* utilities for the rest of the app to use.
@ -218,14 +227,7 @@ export default class Application {
*/
drawer!: Drawer;
data!: {
apiDocument: ApiPayload | null;
locale: string;
locales: Record<string, string>;
resources: SavedModelData[];
session: { userId: number; csrfToken: string };
[key: string]: unknown;
};
data!: ApplicationData;
private _title: string = '';
private _titleCount: number = 0;

View File

@ -34,7 +34,7 @@ export interface SavedModelData {
export type ModelData = UnsavedModelData | SavedModelData;
export interface SaveRelationships {
[relationship: string]: Model | Model[];
[relationship: string]: null | Model | Model[];
}
export interface SaveAttributes {
@ -137,6 +137,12 @@ export default abstract class Model {
for (const r in data.relationships) {
const relationship = data.relationships[r];
if (relationship === null) {
delete relationships[r];
delete data.relationships[r];
continue;
}
let identifier: ModelRelationships[string];
if (relationship instanceof Model) {
identifier = { data: Model.getIdentifier(relationship) };
@ -197,6 +203,8 @@ export default abstract class Model {
for (const key in attributes.relationships) {
const model = attributes.relationships[key];
if (model === null) continue;
data.relationships[key] = {
data: model instanceof Array ? model.map(Model.getIdentifier) : Model.getIdentifier(model),
};

View File

@ -17,11 +17,13 @@ export interface ApiQueryParamsSingle {
export interface ApiQueryParamsPlural {
fields?: string[];
include?: string;
filter?: {
q: string;
[key: string]: string;
};
filter?:
| {
q: string;
}
| Record<string, string>;
page?: {
near?: number;
offset?: number;
number?: number;
limit?: number;
@ -145,8 +147,8 @@ export default class Store {
/**
* Make a request to the API to find record(s) of a specific type.
*/
async find<M extends Model>(type: string, params: ApiQueryParamsSingle): Promise<ApiResponseSingle<M>>;
async find<Ms extends Model[]>(type: string, params: ApiQueryParamsPlural): Promise<ApiResponsePlural<Ms[number]>>;
async find<M extends Model>(type: string, params?: ApiQueryParamsSingle): Promise<ApiResponseSingle<M>>;
async find<Ms extends Model[]>(type: string, params?: ApiQueryParamsPlural): Promise<ApiResponsePlural<Ms[number]>>;
async find<M extends Model>(
type: string,
id: string,
@ -161,7 +163,7 @@ export default class Store {
): Promise<ApiResponsePlural<Ms[number]>>;
async find<M extends Model | Model[]>(
type: string,
idOrParams: string | string[] | ApiQueryParams,
idOrParams: undefined | string | string[] | ApiQueryParams,
query: ApiQueryParams = {},
options: ApiQueryRequestOptions<M extends Array<infer _T> ? ApiPayloadPlural : ApiPayloadSingle> = {}
): Promise<ApiResponse<FlatArray<M, 1>>> {

View File

@ -10,7 +10,7 @@ export interface AvatarAttrs extends ComponentAttrs {}
* @param user
* @param attrs Attributes to apply to the avatar element
*/
export default function avatar(user: User, attrs: ComponentAttrs = {}): Mithril.Vnode {
export default function avatar(user: User | null, attrs: ComponentAttrs = {}): Mithril.Vnode {
attrs.className = 'Avatar ' + (attrs.className || '');
let content: string = '';

Some files were not shown because too many files have changed in this diff Show More