Add typechecks, typescript coverage GH action, fix many type errors (#3136)

This commit is contained in:
Alexander Skvortsov 2021-11-11 14:17:22 -05:00 committed by GitHub
parent 563d40d7da
commit bac0e594ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1079 additions and 324 deletions

View File

@ -29,10 +29,56 @@ jobs:
run: yarn run format-check
working-directory: ./js
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Set up Node
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: "yarn"
cache-dependency-path: js/yarn.lock
- name: Install JS dependencies
run: yarn --frozen-lockfile
working-directory: ./js
- name: Typecheck
run: yarn run check-typings || true # REMOVE THIS ONCE TYPE SAFETY REACHED
working-directory: ./js
type-coverage:
name: Type Coverage
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Set up Node
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: "yarn"
cache-dependency-path: js/yarn.lock
- name: Install JS dependencies
run: yarn --frozen-lockfile
working-directory: ./js
- name: Check type coverage
run: yarn run check-typings-coverage
working-directory: ./js
build-prod:
name: Build and commit
runs-on: ubuntu-latest
needs: [prettier]
needs: [prettier, typecheck, type-coverage]
# Only commit JS on push to master branch
# Remember to change in `build-test` job too
@ -62,7 +108,7 @@ jobs:
build-test:
name: Test build
runs-on: ubuntu-latest
needs: [prettier]
needs: [prettier, typecheck, type-coverage]
# Inverse check of `build-prod`
# Remember to change in `build-prod` job too

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ tests/.phpunit.result.cache
.vagrant
.idea/*
.vscode
js/coverage-ts

View File

@ -30,6 +30,7 @@
"flarum-webpack-config": "^2.0.0",
"prettier": "^2.4.1",
"typescript": "^4.4.4",
"typescript-coverage-report": "^0.6.1",
"webpack": "^5.61.0",
"webpack-cli": "^4.9.1",
"webpack-merge": "^5.8.0"
@ -41,7 +42,9 @@
"format": "prettier --write src",
"format-check": "prettier --check src",
"clean-typings": "npx rimraf dist-typings && mkdir dist-typings",
"build-typings": "npm run clean-typings && cp -r src/@types dist-typings/@types && tsc"
"build-typings": "npm run clean-typings && cp -r src/@types dist-typings/@types && tsc",
"check-typings": "tsc --noEmit --emitDeclarationOnly false",
"check-typings-coverage": "typescript-coverage-report"
},
"packageManager": "yarn@3.1.0"
}

View File

@ -98,3 +98,10 @@ interface JSX {
attrs: Record<string, unknown>;
};
}
interface Event {
/**
* Whether this event should trigger a Mithril redraw.
*/
redraw: boolean;
}

View File

@ -6,6 +6,32 @@ import Navigation from '../common/components/Navigation';
import AdminNav from './components/AdminNav';
import ExtensionData from './utils/ExtensionData';
export type Extension = {
id: string;
version: string;
description?: string;
icon?: {
name: string;
};
links: {
authors?: {
name?: string;
link?: string;
}[];
discuss?: string;
documentation?: string;
support?: string;
website?: string;
donate?: string;
source?: string;
};
extra: {
'flarum-extension': {
title: string;
};
};
};
export default class AdminApplication extends Application {
extensionData = new ExtensionData();
@ -24,6 +50,20 @@ export default class AdminApplication extends Application {
},
};
/**
* Settings are serialized to the admin dashboard as strings.
* Additional encoding/decoding is possible, but must take
* place on the client side.
*
* @inheritdoc
*/
data!: Application['data'] & {
extensions: Record<string, Extension>;
settings: Record<string, string>;
modelStatistics: Record<string, { total: number }>;
};
constructor() {
super();
@ -41,20 +81,20 @@ export default class AdminApplication extends Application {
m.route.prefix = '#';
super.mount();
m.mount(document.getElementById('app-navigation'), {
m.mount(document.getElementById('app-navigation')!, {
view: () =>
Navigation.component({
className: 'App-backControl',
drawer: true,
}),
});
m.mount(document.getElementById('header-navigation'), Navigation);
m.mount(document.getElementById('header-primary'), HeaderPrimary);
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
m.mount(document.getElementById('admin-navigation'), AdminNav);
m.mount(document.getElementById('header-navigation')!, Navigation);
m.mount(document.getElementById('header-primary')!, HeaderPrimary);
m.mount(document.getElementById('header-secondary')!, HeaderSecondary);
m.mount(document.getElementById('admin-navigation')!, AdminNav);
}
getRequiredPermissions(permission) {
getRequiredPermissions(permission: string) {
const required = [];
if (permission === 'startDiscussion' || permission.indexOf('discussion.') === 0) {

View File

@ -2,7 +2,7 @@ import Admin from './AdminApplication';
const app = new Admin();
// @ts-ignore
// @ts-expect-error We need to do this for backwards compatibility purposes.
window.app = app;
export default app;

View File

@ -12,26 +12,39 @@ import ExtensionPermissionGrid from './ExtensionPermissionGrid';
import isExtensionEnabled from '../utils/isExtensionEnabled';
import AdminPage from './AdminPage';
import ReadmeModal from './ReadmeModal';
import RequestError from '../../common/utils/RequestError';
import { Extension } from '../AdminApplication';
import { IPageAttrs } from '../../common/components/Page';
import type Mithril from 'mithril';
export default class ExtensionPage extends AdminPage {
oninit(vnode) {
export interface ExtensionPageAttrs extends IPageAttrs {
id: string;
}
export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionPageAttrs> extends AdminPage<Attrs> {
extension!: Extension;
changingState = false;
infoFields = {
discuss: 'fas fa-comment-alt',
documentation: 'fas fa-book',
support: 'fas fa-life-ring',
website: 'fas fa-link',
donate: 'fas fa-donate',
source: 'fas fa-code',
};
oninit(vnode: Mithril.Vnode<Attrs, this>) {
super.oninit(vnode);
this.extension = app.data.extensions[this.attrs.id];
this.changingState = false;
const extension = app.data.extensions[this.attrs.id];
this.infoFields = {
discuss: 'fas fa-comment-alt',
documentation: 'fas fa-book',
support: 'fas fa-life-ring',
website: 'fas fa-link',
donate: 'fas fa-donate',
source: 'fas fa-code',
};
if (!this.extension) {
if (!extension) {
return m.route.set(app.route('dashboard'));
}
this.extension = extension;
}
className() {
@ -40,7 +53,7 @@ export default class ExtensionPage extends AdminPage {
return this.extension.id + '-Page';
}
view() {
view(vnode: Mithril.VnodeDOM<Attrs, this>) {
if (!this.extension) return null;
return (
@ -51,7 +64,7 @@ export default class ExtensionPage extends AdminPage {
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.enable_to_see')}</h3>
</div>
) : (
<div className="ExtensionPage-body">{this.sections().toArray()}</div>
<div className="ExtensionPage-body">{this.sections(vnode).toArray()}</div>
)}
</div>
);
@ -92,10 +105,10 @@ export default class ExtensionPage extends AdminPage {
];
}
sections() {
sections(vnode: Mithril.VnodeDOM<Attrs, this>) {
const items = new ItemList();
items.add('content', this.content());
items.add('content', this.content(vnode));
items.add('permissions', [
<div className="ExtensionPage-permissions">
@ -117,7 +130,7 @@ export default class ExtensionPage extends AdminPage {
return items;
}
content() {
content(vnode: Mithril.VnodeDOM<Attrs, this>) {
const settings = app.extensionData.getSettings(this.extension.id);
return (
@ -126,7 +139,7 @@ export default class ExtensionPage extends AdminPage {
{settings ? (
<div className="Form">
{settings.map(this.buildSettingComponent.bind(this))}
<div className="Form-group">{this.submitButton()}</div>
<div className="Form-group">{this.submitButton(vnode)}</div>
</div>
) : (
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_settings')}</h3>
@ -172,21 +185,17 @@ export default class ExtensionPage extends AdminPage {
const links = this.extension.links;
if (links.authors.length) {
let authors = [];
links.authors.map((author) => {
authors.push(
<Link href={author.link} external={true} target="_blank">
{author.name}
</Link>
);
});
if (links.authors?.length) {
const authors = links.authors.map((author) => (
<Link href={author.link} external={true} target="_blank">
{author.name}
</Link>
));
items.add('authors', [icon('fas fa-user'), <span>{punctuateSeries(authors)}</span>]);
}
Object.keys(this.infoFields).map((field) => {
(Object.keys(this.infoFields) as (keyof ExtensionPage['infoFields'])[]).map((field) => {
if (links[field]) {
items.add(
field,
@ -240,7 +249,7 @@ export default class ExtensionPage extends AdminPage {
return isExtensionEnabled(this.extension.id);
}
onerror(e) {
onerror(e: RequestError) {
// We need to give the modal animation time to start; if we close the modal too early,
// it breaks the bootstrap modal library.
// TODO: This workaround should be removed when we move away from bootstrap JS for modals.
@ -254,14 +263,16 @@ export default class ExtensionPage extends AdminPage {
throw e;
}
const error = e.response.errors[0];
const error = e.response?.errors?.[0];
app.alerts.show(
{ type: 'error' },
app.translator.trans(`core.lib.error.${error.code}_message`, {
extension: error.extension,
extensions: error.extensions.join(', '),
})
);
if (error) {
app.alerts.show(
{ type: 'error' },
app.translator.trans(`core.lib.error.${error.code}_message`, {
extension: error.extension,
extensions: (error.extensions as string[]).join(', '),
})
);
}
}
}

View File

@ -1,11 +1,11 @@
import app from '../../admin/app';
import Modal from '../../common/components/Modal';
export default class LoadingModal extends Modal {
export default class LoadingModal<ModalAttrs = {}> extends Modal<ModalAttrs> {
/**
* @inheritdoc
*/
static isDismissible = false;
static readonly isDismissible: boolean = false;
className() {
return 'LoadingModal Modal--small';
@ -18,4 +18,8 @@ export default class LoadingModal extends Modal {
content() {
return '';
}
onsubmit(e: Event): void {
throw new Error('LoadingModal should not throw errors.');
}
}

View File

@ -8,6 +8,7 @@ export { app };
import compatObj from './compat';
import proxifyCompat from '../common/utils/proxifyCompat';
// @ts-expect-error The `app` instance needs to be available on compat.
compatObj.app = app;
export const compat = proxifyCompat(compatObj, 'admin');

View File

@ -1,14 +1,18 @@
import app from '../../admin/app';
import DefaultResolver from '../../common/resolvers/DefaultResolver';
import ExtensionPage, { ExtensionPageAttrs } from '../components/ExtensionPage';
/**
* A custom route resolver for ExtensionPage that generates handles routes
* to default extension pages or a page provided by an extension.
*/
export default class ExtensionPageResolver extends DefaultResolver {
export default class ExtensionPageResolver<
Attrs extends ExtensionPageAttrs = ExtensionPageAttrs,
RouteArgs extends Record<string, unknown> = {}
> extends DefaultResolver<Attrs, ExtensionPage<Attrs>, RouteArgs> {
static extension: string | null = null;
onmatch(args, requestedPath, route) {
onmatch(args: Attrs & RouteArgs, requestedPath: string, route: string) {
const extensionPage = app.extensionData.getPage(args.id);
if (extensionPage) {

View File

@ -11,9 +11,10 @@ import Session from './Session';
import extract from './utils/extract';
import Drawer from './utils/Drawer';
import mapRoutes from './utils/mapRoutes';
import RequestError from './utils/RequestError';
import RequestError, { InternalFlarumRequestOptions } from './utils/RequestError';
import ScrollListener from './utils/ScrollListener';
import liveHumanTimes from './utils/liveHumanTimes';
// @ts-expect-error We need to explicitly use the prefix to distinguish between the extend folder.
import { extend } from './extend.ts';
import Forum from './models/Forum';
@ -40,7 +41,7 @@ export type FlarumGenericRoute = RouteItem<
>;
export interface FlarumRequestOptions<ResponseType> extends Omit<Mithril.RequestOptions<ResponseType>, 'extract'> {
errorHandler: (errorMessage: string) => void;
errorHandler?: (error: RequestError) => void;
url: string;
// TODO: [Flarum 2.0] Remove deprecated option
/**
@ -48,13 +49,13 @@ export interface FlarumRequestOptions<ResponseType> extends Omit<Mithril.Request
*
* @deprecated Please use `modifyText` instead.
*/
extract: (responseText: string) => string;
extract?: (responseText: string) => string;
/**
* Manipulate the response text before it is parsed into JSON.
*
* This overrides any `extract` method provided.
*/
modifyText: (responseText: string) => string;
modifyText?: (responseText: string) => string;
}
/**
@ -248,12 +249,12 @@ export default class Application {
initialRoute!: string;
load(payload: Application['data']) {
public load(payload: Application['data']) {
this.data = payload;
this.translator.setLocale(payload.locale);
}
boot() {
public boot() {
this.initializers.toArray().forEach((initializer) => initializer(this));
this.store.pushPayload({ data: this.data.resources });
@ -268,7 +269,7 @@ export default class Application {
}
// TODO: This entire system needs a do-over for v2
bootExtensions(extensions: Record<string, { extend?: unknown[] }>) {
public bootExtensions(extensions: Record<string, { extend?: unknown[] }>) {
Object.keys(extensions).forEach((name) => {
const extension = extensions[name];
@ -278,12 +279,13 @@ export default class Application {
const extenders = extension.extend.flat(Infinity);
for (const extender of extenders) {
// @ts-expect-error This is beyond saving atm.
extender.extend(this, { name, exports: extension });
}
});
}
mount(basePath: string = '') {
protected mount(basePath: string = '') {
// An object with a callable view property is used in order to pass arguments to the component; see https://mithril.js.org/mount.html
m.mount(document.getElementById('modal')!, { view: () => ModalManager.component({ state: this.modal }) });
m.mount(document.getElementById('alerts')!, { view: () => AlertManager.component({ state: this.alerts }) });
@ -367,22 +369,36 @@ export default class Application {
document.title = count + pageTitleWithSeparator + title;
}
/**
* Make an AJAX request, handling any low-level errors that may occur.
*
* @see https://mithril.js.org/request.html
*
* @param options
* @return {Promise}
*/
request<ResponseType>(originalOptions: FlarumRequestOptions<ResponseType>): Promise<ResponseType | string> {
const options = { ...originalOptions };
protected transformRequestOptions<ResponseType>(flarumOptions: FlarumRequestOptions<ResponseType>): InternalFlarumRequestOptions<ResponseType> {
const { background, deserialize, errorHandler, extract, modifyText, ...tmpOptions } = { ...flarumOptions };
// Set some default options if they haven't been overridden. We want to
// authenticate all requests with the session token. We also want all
// requests to run asynchronously in the background, so that they don't
// prevent redraws from occurring.
options.background ||= true;
// Unless specified otherwise, requests should run asynchronously in the
// background, so that they don't prevent redraws from occurring.
const defaultBackground = true;
// When we deserialize JSON data, if for some reason the server has provided
// a dud response, we don't want the application to crash. We'll show an
// error message to the user instead.
// @ts-expect-error Typescript doesn't know we return promisified `ReturnType` OR `string`,
// so it errors due to Mithril's typings
const defaultDeserialize = (response: string) => response as ResponseType;
const defaultErrorHandler = (error: RequestError) => {
throw error;
};
// When extracting the data from the response, we can check the server
// response code and show an error message to the user if something's gone
// awry.
const originalExtract = modifyText || extract;
const options: InternalFlarumRequestOptions<ResponseType> = {
background: background ?? defaultBackground,
deserialize: deserialize ?? defaultDeserialize,
errorHandler: errorHandler ?? defaultErrorHandler,
...tmpOptions,
};
extend(options, 'config', (_: undefined, xhr: XMLHttpRequest) => {
xhr.setRequestHeader('X-CSRF-Token', this.session.csrfToken!);
@ -401,37 +417,19 @@ export default class Application {
options.method = 'POST';
}
// When we deserialize JSON data, if for some reason the server has provided
// a dud response, we don't want the application to crash. We'll show an
// error message to the user instead.
// @ts-expect-error Typescript doesn't know we return promisified `ReturnType` OR `string`,
// so it errors due to Mithril's typings
options.deserialize ||= (responseText: string) => responseText;
options.errorHandler ||= (error) => {
throw error;
};
// When extracting the data from the response, we can check the server
// response code and show an error message to the user if something's gone
// awry.
const original = options.modifyText || options.extract;
// @ts-expect-error
options.extract = (xhr: XMLHttpRequest) => {
let responseText;
if (original) {
responseText = original(xhr.responseText);
if (originalExtract) {
responseText = originalExtract(xhr.responseText);
} else {
responseText = xhr.responseText || null;
responseText = xhr.responseText;
}
const status = xhr.status;
if (status < 200 || status > 299) {
throw new RequestError(`${status}`, `${responseText}`, options, xhr);
throw new RequestError<ResponseType>(status, `${responseText}`, options, xhr);
}
if (xhr.getResponseHeader) {
@ -440,25 +438,38 @@ export default class Application {
}
try {
// @ts-expect-error
return JSON.parse(responseText);
} catch (e) {
throw new RequestError('500', `${responseText}`, options, xhr);
throw new RequestError<ResponseType>(500, `${responseText}`, options, xhr);
}
};
return options;
}
/**
* Make an AJAX request, handling any low-level errors that may occur.
*
* @see https://mithril.js.org/request.html
*
* @param options
* @return {Promise}
*/
request<ResponseType>(originalOptions: FlarumRequestOptions<ResponseType>): Promise<ResponseType | string> {
const options = this.transformRequestOptions(originalOptions);
if (this.requestErrorAlert) this.alerts.dismiss(this.requestErrorAlert);
// Now make the request. If it's a failure, inspect the error that was
// returned and show an alert containing its contents.
return m.request(options).then(
(response) => response,
(error) => {
(error: RequestError) => {
let content;
switch (error.status) {
case 422:
content = (error.response.errors as Record<string, unknown>[])
content = ((error.response?.errors ?? {}) as Record<string, unknown>[])
.map((error) => [error.detail, <br />])
.flat()
.slice(0, -1);
@ -490,7 +501,7 @@ export default class Application {
// contains a formatted errors if possible, response must be an JSON API array of errors
// the details property is decoded to transform escaped characters such as '\n'
const errors = error.response && error.response.errors;
const formattedError = Array.isArray(errors) && errors[0] && errors[0].detail && errors.map((e) => decodeURI(e.detail));
const formattedError = (Array.isArray(errors) && errors?.[0]?.detail && errors.map((e) => decodeURI(e.detail ?? ''))) || undefined;
error.alert = {
type: 'error',
@ -505,18 +516,22 @@ export default class Application {
try {
options.errorHandler(error);
} catch (error) {
if (isDebug && error.xhr) {
const { method, url } = error.options;
const { status = '' } = error.xhr;
if (error instanceof RequestError) {
if (isDebug && error.xhr) {
const { method, url } = error.options;
const { status = '' } = error.xhr;
console.group(`${method} ${url} ${status}`);
console.group(`${method} ${url} ${status}`);
console.error(...(formattedError || [error]));
console.error(...(formattedError || [error]));
console.groupEnd();
console.groupEnd();
}
this.requestErrorAlert = this.alerts.show(error.alert, error.alert.content);
} else {
throw error;
}
this.requestErrorAlert = this.alerts.show(error.alert, error.alert.content);
}
return Promise.reject(error);

View File

@ -121,7 +121,7 @@ export default abstract class Component<Attrs extends ComponentAttrs = Component
*
* @see https://mithril.js.org/hyperscript.html#mselector,-attributes,-children
*/
static component(attrs: Attrs = {}, children: Mithril.Children = null): Mithril.Vnode {
static component<SAttrs extends ComponentAttrs = ComponentAttrs>(attrs: SAttrs = {} as SAttrs, children: Mithril.Children = null): Mithril.Vnode {
const componentAttrs = { ...attrs };
return m(this as any, componentAttrs, children);

View File

@ -34,8 +34,8 @@ export default abstract class Fragment {
* @returns {jQuery} the jQuery object for the DOM node
* @final
*/
public $(selector) {
const $element = $(this.element);
public $(selector?: string): JQuery {
const $element = $(this.element) as JQuery<HTMLElement>;
return selector ? $element.find(selector) : $element;
}

View File

@ -1,6 +1,7 @@
import { RichMessageFormatter, mithrilRichHandler } from '@askvortsov/rich-icu-message-formatter';
import { pluralTypeHandler, selectTypeHandler } from '@ultraq/icu-message-formatter';
import username from './helpers/username';
import User from './models/User';
import extract from './utils/extract';
type Translations = Record<string, string>;
@ -50,7 +51,7 @@ export default class Translator {
// translation key is used.
if ('user' in parameters) {
const user = extract(parameters, 'user');
const user = extract(parameters, 'user') as User;
if (!parameters.username) parameters.username = username(user);
}

View File

@ -20,21 +20,21 @@ export interface AlertAttrs extends ComponentAttrs {
* some controls, and may be dismissible.
*/
export default class Alert<T extends AlertAttrs = AlertAttrs> extends Component<T> {
view(vnode: Mithril.Vnode) {
view(vnode: Mithril.VnodeDOM<T, this>) {
const attrs = Object.assign({}, this.attrs);
const type = extract(attrs, 'type');
attrs.className = 'Alert Alert--' + type + ' ' + (attrs.className || '');
const content = extract(attrs, 'content') || vnode.children;
const controls = (extract(attrs, 'controls') || []) as Mithril.ChildArray;
const controls = (extract(attrs, 'controls') || []) as Mithril.Vnode[];
// If the alert is meant to be dismissible (which is the case by default),
// then we will create a dismiss button to append as the final control in
// the alert.
const dismissible = extract(attrs, 'dismissible');
const ondismiss = extract(attrs, 'ondismiss');
const dismissControl = [];
const dismissControl: Mithril.Vnode[] = [];
if (dismissible || dismissible === undefined) {
dismissControl.push(<Button icon="fas fa-times" className="Button Button--link Button--icon Alert-dismiss" onclick={ondismiss} />);

View File

@ -67,7 +67,7 @@ export interface IButtonAttrs extends ComponentAttrs {
* styles can be applied by providing `className="Button"` to the Button component.
*/
export default class Button<CustomAttrs extends IButtonAttrs = IButtonAttrs> extends Component<CustomAttrs> {
view(vnode: Mithril.Vnode<IButtonAttrs, never>) {
view(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
let { type, title, 'aria-label': ariaLabel, icon: iconName, disabled, loading, className, class: _class, ...attrs } = this.attrs;
// If no `type` attr provided, set to "button"
@ -102,7 +102,7 @@ export default class Button<CustomAttrs extends IButtonAttrs = IButtonAttrs> ext
return <button {...buttonAttrs}>{this.getButtonContent(vnode.children)}</button>;
}
oncreate(vnode: Mithril.VnodeDOM<IButtonAttrs, this>) {
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
super.oncreate(vnode);
const { 'aria-label': ariaLabel } = this.attrs;

View File

@ -22,7 +22,7 @@ export default abstract class Modal<ModalAttrs = {}> extends Component<ModalAttr
/**
* Determine whether or not the modal should be dismissible via an 'x' button.
*/
static readonly isDismissible = true;
static readonly isDismissible: boolean = true;
protected loading: boolean = false;

View File

@ -1,3 +1,4 @@
import type Mithril from 'mithril';
import app from '../app';
import Component from '../Component';
import PageState from '../states/PageState';
@ -13,7 +14,22 @@ export interface IPageAttrs {
* @abstract
*/
export default abstract class Page<CustomAttrs extends IPageAttrs = IPageAttrs> extends Component<CustomAttrs> {
oninit(vnode) {
/**
* A class name to apply to the body while the route is active.
*/
protected bodyClass = '';
/**
* Whether we should scroll to the top of the page when its rendered.
*/
protected scrollTopOnCreate = true;
/**
* Whether the browser should restore scroll state on refreshes.
*/
protected useBrowserScrollRestoration = true;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
app.previous = app.current;
@ -21,30 +37,9 @@ export default abstract class Page<CustomAttrs extends IPageAttrs = IPageAttrs>
app.drawer.hide();
app.modal.close();
/**
* A class name to apply to the body while the route is active.
*
* @type {String}
*/
this.bodyClass = '';
/**
* Whether we should scroll to the top of the page when its rendered.
*
* @type {Boolean}
*/
this.scrollTopOnCreate = true;
/**
* Whether the browser should restore scroll state on refreshes.
*
* @type {Boolean}
*/
this.useBrowserScrollRestoration = true;
}
oncreate(vnode) {
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
super.oncreate(vnode);
if (this.bodyClass) {
@ -60,7 +55,7 @@ export default abstract class Page<CustomAttrs extends IPageAttrs = IPageAttrs>
}
}
onremove(vnode) {
onremove(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
super.onremove(vnode);
if (this.bodyClass) {

View File

@ -1,7 +1,6 @@
import Component from '../Component';
import type Mithril from 'mithril';
import classList from '../utils/classList';
import { TooltipCreationOptions } from '../../../@types/tooltips';
import extractText from '../utils/extractText';
export interface TooltipAttrs extends Mithril.CommonAttributes<TooltipAttrs, Tooltip> {

View File

@ -1,13 +1,16 @@
import type Mithril from 'mithril';
import type { ComponentAttrs } from '../Component';
import User from '../models/User';
export interface AvatarAttrs extends ComponentAttrs {}
/**
* The `avatar` helper displays a user's avatar.
*
* @param user
* @param attrs Attributes to apply to the avatar element
*/
export default function avatar(user: User, attrs: Object = {}): Mithril.Vnode {
export default function avatar(user: User, attrs: ComponentAttrs = {}): Mithril.Vnode {
attrs.className = 'Avatar ' + (attrs.className || '');
let content: string = '';

View File

@ -1,15 +1,29 @@
import type Mithril from 'mithril';
import Component, { ComponentAttrs } from '../Component';
import Separator from '../components/Separator';
import classList from '../utils/classList';
import type * as Component from '../Component';
function isSeparator(item: Mithril.Children): boolean {
export interface ModdedVnodeAttrs {
itemClassName?: string;
key?: string;
}
export type ModdedVnode<Attrs> = Mithril.Vnode<ModdedVnodeAttrs, Component<Attrs> | {}> & {
itemName?: string;
itemClassName?: string;
tag: Mithril.Vnode['tag'] & {
isListItem?: boolean;
isActive?: (attrs: ComponentAttrs) => boolean;
};
};
function isSeparator<Attrs>(item: ModdedVnode<Attrs>): boolean {
return item.tag === Separator;
}
function withoutUnnecessarySeparators(items: Mithril.Children): Mithril.Children {
const newItems: Mithril.Children = [];
let prevItem: Mithril.Child;
function withoutUnnecessarySeparators<Attrs>(items: ModdedVnode<Attrs>[]): ModdedVnode<Attrs>[] {
const newItems: ModdedVnode<Attrs>[] = [];
let prevItem: ModdedVnode<Attrs>;
items.filter(Boolean).forEach((item: Mithril.Vnode, i: number) => {
if (!isSeparator(item) || (prevItem && !isSeparator(prevItem) && i !== items.length - 1)) {
@ -21,16 +35,6 @@ function withoutUnnecessarySeparators(items: Mithril.Children): Mithril.Children
return newItems;
}
export interface ModdedVnodeAttrs {
itemClassName?: string;
key?: string;
}
export type ModdedVnode<Attrs> = Mithril.Vnode<ModdedVnodeAttrs, Component.default<Attrs> | {}> & {
itemName?: string;
itemClassName?: string;
};
/**
* The `listItems` helper wraps an array of components in the provided tag,
* stripping out any unnecessary `Separator` components.
@ -39,12 +43,11 @@ export type ModdedVnode<Attrs> = Mithril.Vnode<ModdedVnodeAttrs, Component.defau
* second function parameter, `customTag`.
*/
export default function listItems<Attrs extends Record<string, unknown>>(
items: ModdedVnode<Attrs> | ModdedVnode<Attrs>[],
customTag: string | Component.default<Attrs> = 'li',
attributes: Attrs = {}
rawItems: ModdedVnode<Attrs> | ModdedVnode<Attrs>[],
customTag: string | Component<Attrs> = 'li',
attributes: Attrs = {} as Attrs
): Mithril.Vnode[] {
if (!(items instanceof Array)) items = [items];
const items = rawItems instanceof Array ? rawItems : [rawItems];
const Tag = customTag;
return withoutUnnecessarySeparators(items).map((item: ModdedVnode<Attrs>) => {

View File

@ -5,8 +5,10 @@ import icon from './icon';
/**
* The `useronline` helper displays a green circle if the user is online
*/
export default function userOnline(user: User): Mithril.Vnode {
export default function userOnline(user: User): Mithril.Vnode<{}, {}> | null {
if (user.lastSeenAt() && user.isOnline()) {
return <span className="UserOnline">{icon('fas fa-circle')}</span>;
}
return null;
}

View File

@ -14,7 +14,7 @@ export default class ModalManagerState {
attrs?: Record<string, unknown>;
} = null;
private closeTimeout?: number;
private closeTimeout?: NodeJS.Timeout;
/**
* Shows a modal dialog.
@ -36,7 +36,7 @@ export default class ModalManagerState {
throw new Error(invalidModalWarning);
}
clearTimeout(this.closeTimeout);
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.modal = { componentClass, attrs };

View File

@ -15,18 +15,22 @@ export interface PaginationLocation {
endIndex?: number;
}
export default abstract class PaginatedListState<T extends Model> {
export interface PaginatedListParams {
[key: string]: any;
}
export default abstract class PaginatedListState<T extends Model, P extends PaginatedListParams = PaginatedListParams> {
protected location!: PaginationLocation;
protected pageSize: number;
protected pages: Page<T>[] = [];
protected params: any = {};
protected params: P = {} as P;
protected initialLoading: boolean = false;
protected loadingPrev: boolean = false;
protected loadingNext: boolean = false;
protected constructor(params: any = {}, page: number = 1, pageSize: number = 20) {
protected constructor(params: P = {} as P, page: number = 1, pageSize: number = 20) {
this.params = params;
this.location = { page };
@ -123,12 +127,14 @@ export default abstract class PaginatedListState<T extends Model> {
* @param page
* @see requestParams
*/
public refreshParams(newParams, page: number) {
public refreshParams(newParams: P, page: number): Promise<void> {
if (this.isEmpty() || this.paramsChanged(newParams)) {
this.params = newParams;
return this.refresh(page);
}
return Promise.resolve();
}
public refresh(page: number = 1) {
@ -222,7 +228,7 @@ export default abstract class PaginatedListState<T extends Model> {
}
}
protected paramsChanged(newParams): boolean {
protected paramsChanged(newParams: P): boolean {
return Object.keys(newParams).some((key) => this.getParams()[key] !== newParams[key]);
}

View File

@ -18,7 +18,7 @@ export default class BasicEditorDriver implements EditorDriverInterface {
this.el.placeholder = params.placeholder;
this.el.value = params.value;
const callInputListeners = (e) => {
const callInputListeners = (e: Event) => {
params.inputListeners.forEach((listener) => {
listener();
});
@ -46,7 +46,7 @@ export default class BasicEditorDriver implements EditorDriverInterface {
keyHandlers(params: EditorDriverParams): ItemList {
const items = new ItemList();
items.add('submit', function (e) {
items.add('submit', function (e: KeyboardEvent) {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
params.onsubmit();
}

View File

@ -1,14 +1,28 @@
export default class RequestError {
import type Mithril from 'mithril';
export type InternalFlarumRequestOptions<ResponseType> = Mithril.RequestOptions<ResponseType> & {
errorHandler: (error: RequestError) => void;
url: string;
};
export default class RequestError<ResponseType = string> {
status: number;
options: Record<string, unknown>;
options: InternalFlarumRequestOptions<ResponseType>;
xhr: XMLHttpRequest;
responseText: string | null;
response: Record<string, unknown> | null;
response: {
[key: string]: unknown;
errors?: {
detail?: string;
code?: string;
[key: string]: unknown;
}[];
} | null;
alert: any;
constructor(status: number, responseText: string | null, options: Record<string, unknown>, xhr: XMLHttpRequest) {
constructor(status: number, responseText: string | null, options: InternalFlarumRequestOptions<ResponseType>, xhr: XMLHttpRequest) {
this.status = status;
this.responseText = responseText;
this.options = options;

View File

@ -3,12 +3,17 @@
//
// Needed to provide support for Safari on iOS < 12
// ts-ignored because we can afford to encapsulate some messy logic behind the clean typings.
if (!Array.prototype['flat']) {
Array.prototype['flat'] = function flat(this: any[], depth: number = 1): any[] {
return depth > 0
? Array.prototype.reduce.call(this, (acc, val): any[] => acc.concat(Array.isArray(val) ? flat.call(val, depth - 1) : val), [])
Array.prototype['flat'] = function flat<A, D extends number = 1>(this: A, depth?: D | unknown): any[] {
// @ts-ignore
return (depth ?? 1) > 0
? // @ts-ignore
Array.prototype.reduce.call(this, (acc, val): any[] => acc.concat(Array.isArray(val) ? flat.call(val, depth - 1) : val), [])
: // If no depth is provided, or depth is 0, just return a copy of
// the array. Spread is supported in all major browsers (iOS 8+)
// @ts-ignore
[...this];
};
}

View File

@ -4,7 +4,7 @@ import dayjs from 'dayjs';
* The `humanTime` utility converts a date to a localized, human-readable time-
* ago string.
*/
export default function humanTime(time: Date): string {
export default function humanTime(time: dayjs.ConfigType): string {
let d = dayjs(time);
const now = dayjs();

View File

@ -1,9 +1,9 @@
type RGB = { r: number; g: number; b: number };
function hsvToRgb(h: number, s: number, v: number): RGB {
let r;
let g;
let b;
let r!: number;
let g!: number;
let b!: number;
const i = Math.floor(h * 6);
const f = h * 6 - i;

View File

@ -11,5 +11,5 @@
*/
export default (key: string, cb: Function) =>
function (this: Element) {
cb(this.getAttribute(key) || this[key]);
cb(this.getAttribute(key) || (this as any)[key]);
};

View File

@ -22,6 +22,7 @@ import isSafariMobile from './utils/isSafariMobile';
import type Notification from './components/Notification';
import type Post from './components/Post';
import Discussion from '../common/models/Discussion';
export default class ForumApplication extends Application {
/**
@ -134,11 +135,8 @@ export default class ForumApplication extends Application {
/**
* Check whether or not the user is currently viewing a discussion.
*
* @param {Discussion} discussion
* @return {Boolean}
*/
viewingDiscussion(discussion) {
public viewingDiscussion(discussion: Discussion): boolean {
return this.current.matches(DiscussionPage, { discussion });
}
@ -149,13 +147,8 @@ export default class ForumApplication extends Application {
* If the payload indicates that the user has been logged in, then the page
* will be reloaded. Otherwise, a SignUpModal will be opened, prefilled
* with the provided details.
*
* @param {Object} payload A dictionary of attrs to pass into the sign up
* modal. A truthy `loggedIn` attr indicates that the user has logged
* in, and thus the page is reloaded.
* @public
*/
authenticationComplete(payload) {
public authenticationComplete(payload: Record<string, unknown>): void {
if (payload.loggedIn) {
window.location.reload();
} else {

View File

@ -2,7 +2,7 @@ import Forum from './ForumApplication';
const app = new Forum();
// @ts-ignore
// @ts-expect-error We need to do this for backwards compatibility purposes.
window.app = app;
export default app;

View File

@ -1,5 +1,7 @@
import type Mithril from 'mithril';
import app from '../../forum/app';
import Page from '../../common/components/Page';
import Page, { IPageAttrs } from '../../common/components/Page';
import ItemList from '../../common/utils/ItemList';
import DiscussionHero from './DiscussionHero';
import DiscussionListPane from './DiscussionListPane';
@ -10,31 +12,39 @@ import SplitDropdown from '../../common/components/SplitDropdown';
import listItems from '../../common/helpers/listItems';
import DiscussionControls from '../utils/DiscussionControls';
import PostStreamState from '../states/PostStreamState';
import Discussion from '../../common/models/Discussion';
import Post from '../../common/models/Post';
export interface IDiscussionPageAttrs extends IPageAttrs {
id: string;
near?: number;
}
/**
* The `DiscussionPage` component displays a whole discussion page, including
* the discussion list pane, the hero, the posts, and the sidebar.
*/
export default class DiscussionPage extends Page {
oninit(vnode) {
export default class DiscussionPage<CustomAttrs extends IDiscussionPageAttrs = IDiscussionPageAttrs> extends Page<CustomAttrs> {
/**
* The discussion that is being viewed.
*/
protected discussion: Discussion | null = null;
/**
* A public API for interacting with the post stream.
*/
protected stream: PostStreamState | null = null;
/**
* The number of the first post that is currently visible in the viewport.
*/
protected near: number = 0;
protected useBrowserScrollRestoration = true;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.useBrowserScrollRestoration = false;
/**
* The discussion that is being viewed.
*
* @type {Discussion}
*/
this.discussion = null;
/**
* The number of the first post that is currently visible in the viewport.
*
* @type {number}
*/
this.near = m.route.param('near') || 0;
this.load();
// If the discussion list has been loaded, then we'll enable the pane (and
@ -43,25 +53,23 @@ export default class DiscussionPage extends Page {
// then the pane would redraw which would be slow and would cause problems with
// event handlers.
if (app.discussions.hasItems()) {
app.pane.enable();
app.pane.hide();
app.pane?.enable();
app.pane?.hide();
}
app.history.push('discussion');
this.bodyClass = 'App--discussion';
}
onremove(vnode) {
onremove(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
super.onremove(vnode);
// If we are indeed navigating away from this discussion, then disable the
// discussion list pane. Also, if we're composing a reply to this
// discussion, minimize the composer unless it's empty, in which case
// we'll just close it.
app.pane.disable();
app.pane?.disable();
if (app.composer.composingReplyTo(this.discussion) && !app.composer.fields.content()) {
if (app.composer.composingReplyTo(this.discussion) && !app.composer?.fields?.content()) {
app.composer.hide();
} else {
app.composer.minimize();
@ -155,7 +163,7 @@ export default class DiscussionPage extends Page {
* Load the discussion from the API or use the preloaded one.
*/
load() {
const preloadedDiscussion = app.preloadedApiDocument();
const preloadedDiscussion = app.preloadedApiDocument() as Discussion | null;
if (preloadedDiscussion) {
// We must wrap this in a setTimeout because if we are mounting this
// component for the first time on page load, then any calls to m.redraw
@ -186,10 +194,8 @@ export default class DiscussionPage extends Page {
/**
* Initialize the component to display the given discussion.
*
* @param {Discussion} discussion
*/
show(discussion) {
show(discussion: Discussion) {
app.history.push('discussion', discussion.title());
app.setTitle(discussion.title());
app.setTitleCount(0);
@ -214,7 +220,7 @@ export default class DiscussionPage extends Page {
record.relationships.discussion.data.id === discussionId
)
.map((record) => app.store.getById('posts', record.id))
.sort((a, b) => a.createdAt() - b.createdAt())
.sort((a: Post, b: Post) => a.createdAt() - b.createdAt())
.slice(0, 20);
}
@ -266,13 +272,12 @@ export default class DiscussionPage extends Page {
/**
* When the posts that are visible in the post stream change (i.e. the user
* scrolls up or down), then we update the URL and mark the posts as read.
*
* @param {Integer} startNumber
* @param {Integer} endNumber
*/
positionChanged(startNumber, endNumber) {
positionChanged(startNumber: number, endNumber: number): void {
const discussion = this.discussion;
if (!discussion) return;
// Construct a URL to this discussion with the updated position, then
// replace it into the window's history and our own history stack.
const url = app.route.discussion(discussion, (this.near = startNumber));

View File

@ -4,15 +4,16 @@ import LinkButton from '../../common/components/LinkButton';
import Link from '../../common/components/Link';
import { SearchSource } from './Search';
import type Mithril from 'mithril';
import Discussion from '../../common/models/Discussion';
/**
* The `DiscussionsSearchSource` finds and displays discussion search results in
* the search dropdown.
*/
export default class DiscussionsSearchSource implements SearchSource {
protected results = new Map<string, unknown[]>();
protected results = new Map<string, Discussion[]>();
search(query: string) {
async search(query: string): Promise<void> {
query = query.toLowerCase();
this.results.set(query, []);
@ -23,13 +24,16 @@ export default class DiscussionsSearchSource implements SearchSource {
include: 'mostRelevantPost',
};
return app.store.find('discussions', params).then((results) => this.results.set(query, results));
return app.store.find('discussions', params).then((results) => {
this.results.set(query, results);
m.redraw();
});
}
view(query: string): Array<Mithril.Vnode> {
query = query.toLowerCase();
const results = (this.results.get(query) || []).map((discussion: unknown) => {
const results = (this.results.get(query) || []).map((discussion) => {
const mostRelevantPost = discussion.mostRelevantPost();
return (

View File

@ -10,6 +10,7 @@ import SearchState from '../states/SearchState';
import DiscussionsSearchSource from './DiscussionsSearchSource';
import UsersSearchSource from './UsersSearchSource';
import type Mithril from 'mithril';
import Model from '../../common/Model';
/**
* The `SearchSource` interface defines a section of search results in the
@ -24,8 +25,9 @@ import type Mithril from 'mithril';
export interface SearchSource {
/**
* Make a request to get results for the given query.
* The results will be updated internally in the search source, not exposed.
*/
search(query: string);
search(query: string): Promise<void>;
/**
* Get an array of virtual <li>s that list the search results for the given
@ -57,7 +59,7 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
*/
protected static MIN_SEARCH_LEN = 3;
protected state!: SearchState;
protected searchState!: SearchState;
/**
* Whether or not the search input has focus.
@ -84,18 +86,18 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
protected navigator!: KeyboardNavigatable;
protected searchTimeout?: number;
protected searchTimeout?: NodeJS.Timeout;
private updateMaxHeightHandler?: () => void;
oninit(vnode: Mithril.Vnode<T, this>) {
super.oninit(vnode);
this.state = this.attrs.state;
this.searchState = this.attrs.state;
}
view() {
const currentSearch = this.state.getInitialSearch();
const currentSearch = this.searchState.getInitialSearch();
// Initialize search sources in the view rather than the constructor so
// that we have access to app.forum.
@ -107,15 +109,15 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
const searchLabel = extractText(app.translator.trans('core.forum.header.search_placeholder'));
const isActive = !!currentSearch;
const shouldShowResults = !!(!this.loadingSources && this.state.getValue() && this.hasFocus);
const shouldShowClearButton = !!(!this.loadingSources && this.state.getValue());
const shouldShowResults = !!(!this.loadingSources && this.searchState.getValue() && this.hasFocus);
const shouldShowClearButton = !!(!this.loadingSources && this.searchState.getValue());
return (
<div
role="search"
aria-label={app.translator.trans('core.forum.header.search_role_label')}
className={classList('Search', {
open: this.state.getValue() && this.hasFocus,
open: this.searchState.getValue() && this.hasFocus,
focused: this.hasFocus,
active: isActive,
loading: !!this.loadingSources,
@ -127,8 +129,8 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
className="FormControl"
type="search"
placeholder={searchLabel}
value={this.state.getValue()}
oninput={(e) => this.state.setValue(e.target.value)}
value={this.searchState.getValue()}
oninput={(e: InputEvent) => this.searchState.setValue((e?.target as HTMLInputElement)?.value)}
onfocus={() => (this.hasFocus = true)}
onblur={() => (this.hasFocus = false)}
/>
@ -148,7 +150,7 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
aria-hidden={!shouldShowResults || undefined}
aria-live={shouldShowResults ? 'polite' : undefined}
>
{shouldShowResults && this.sources.map((source) => source.view(this.state.getValue()))}
{shouldShowResults && this.sources.map((source) => source.view(this.searchState.getValue()))}
</ul>
</div>
);
@ -159,11 +161,12 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
// we need to calculate and set the max height dynamically.
const resultsElementMargin = 14;
const maxHeight =
window.innerHeight - this.element.querySelector('.Search-input>.FormControl').getBoundingClientRect().bottom - resultsElementMargin;
this.element.querySelector('.Search-results').style['max-height'] = `${maxHeight}px`;
window.innerHeight - this.element.querySelector('.Search-input>.FormControl')!.getBoundingClientRect().bottom - resultsElementMargin;
this.element.querySelector('.Search-results')?.setAttribute('style', `max-height: ${maxHeight}px`);
}
onupdate(vnode) {
onupdate(vnode: Mithril.VnodeDOM<T, this>) {
super.onupdate(vnode);
// Highlight the item that is currently selected.
@ -175,11 +178,11 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
this.updateMaxHeight();
}
oncreate(vnode) {
oncreate(vnode: Mithril.VnodeDOM<T, this>) {
super.oncreate(vnode);
const search = this;
const state = this.state;
const state = this.searchState;
// Highlight the item that is currently selected.
this.setIndex(this.getCurrentNumericIndex());
@ -210,7 +213,7 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
if (!query) return;
clearTimeout(search.searchTimeout);
if (search.searchTimeout) clearTimeout(search.searchTimeout);
search.searchTimeout = setTimeout(() => {
if (state.isCached(query)) return;
@ -242,21 +245,25 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
window.addEventListener('resize', this.updateMaxHeightHandler);
}
onremove(vnode) {
onremove(vnode: Mithril.VnodeDOM<T, this>) {
super.onremove(vnode);
window.removeEventListener('resize', this.updateMaxHeightHandler);
if (this.updateMaxHeightHandler) {
window.removeEventListener('resize', this.updateMaxHeightHandler);
}
}
/**
* Navigate to the currently selected search result and close the list.
*/
selectResult() {
clearTimeout(this.searchTimeout);
if (this.searchTimeout) clearTimeout(this.searchTimeout);
this.loadingSources = 0;
if (this.state.getValue()) {
m.route.set(this.getItem(this.index).find('a').attr('href'));
const selectedUrl = this.getItem(this.index).find('a').attr('href');
if (this.searchState.getValue() && selectedUrl) {
m.route.set(selectedUrl);
} else {
this.clear();
}
@ -268,7 +275,7 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
* Clear the search
*/
clear() {
this.state.clear();
this.searchState.clear();
}
/**
@ -331,11 +338,11 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
this.index = parseInt($item.attr('data-index') as string) || fixedIndex;
if (scrollToItem) {
const dropdownScroll = $dropdown.scrollTop();
const dropdownTop = $dropdown.offset().top;
const dropdownBottom = dropdownTop + $dropdown.outerHeight();
const itemTop = $item.offset().top;
const itemBottom = itemTop + $item.outerHeight();
const dropdownScroll = $dropdown.scrollTop()!;
const dropdownTop = $dropdown.offset()!.top;
const dropdownBottom = dropdownTop + $dropdown.outerHeight()!;
const itemTop = $item.offset()!.top;
const itemBottom = itemTop + $item.outerHeight()!;
let scrollTop;
if (itemTop < dropdownTop) {

View File

@ -1,19 +1,21 @@
import type Mithril from 'mithril';
import app from '../../forum/app';
import highlight from '../../common/helpers/highlight';
import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
import Link from '../../common/components/Link';
import { SearchSource } from './Search';
import type Mithril from 'mithril';
import User from '../../common/models/User';
/**
* The `UsersSearchSource` finds and displays user search results in the search
* dropdown.
*/
export default class UsersSearchResults implements SearchSource {
protected results = new Map<string, unknown[]>();
protected results = new Map<string, User[]>();
search(query: string) {
async search(query: string): Promise<void> {
return app.store
.find('users', {
filter: { q: query },

View File

@ -10,6 +10,7 @@ export { app };
import compatObj from './compat';
import proxifyCompat from '../common/utils/proxifyCompat';
// @ts-ignore
compatObj.app = app;
export const compat = proxifyCompat(compatObj, 'forum');

View File

@ -1,14 +1,19 @@
import type Mithril from 'mithril';
import app from '../../forum/app';
import DefaultResolver from '../../common/resolvers/DefaultResolver';
import DiscussionPage from '../components/DiscussionPage';
import DiscussionPage, { IDiscussionPageAttrs } from '../components/DiscussionPage';
/**
* A custom route resolver for DiscussionPage that generates the same key to all posts
* on the same discussion. It triggers a scroll when going from one post to another
* in the same discussion.
*/
export default class DiscussionPageResolver extends DefaultResolver {
static scrollToPostNumber: string | null = null;
export default class DiscussionPageResolver<
Attrs extends IDiscussionPageAttrs = IDiscussionPageAttrs,
RouteArgs extends Record<string, unknown> = {}
> extends DefaultResolver<Attrs, DiscussionPage<Attrs>, RouteArgs> {
static scrollToPostNumber: number | null = null;
/**
* Remove optional parts of a discussion's slug to keep the substring
@ -34,16 +39,16 @@ export default class DiscussionPageResolver extends DefaultResolver {
return this.routeName.replace('.near', '') + JSON.stringify(params);
}
onmatch(args, requestedPath, route) {
onmatch(args: Attrs & RouteArgs, requestedPath: string, route: string) {
if (app.current.matches(DiscussionPage) && this.canonicalizeDiscussionSlug(args.id) === this.canonicalizeDiscussionSlug(m.route.param('id'))) {
// By default, the first post number of any discussion is 1
DiscussionPageResolver.scrollToPostNumber = args.near || '1';
DiscussionPageResolver.scrollToPostNumber = args.near || 1;
}
return super.onmatch(args, requestedPath, route);
}
render(vnode) {
render(vnode: Mithril.Vnode<Attrs, DiscussionPage<Attrs>>) {
if (DiscussionPageResolver.scrollToPostNumber !== null) {
const number = DiscussionPageResolver.scrollToPostNumber;
// Scroll after a timeout to avoid clashes with the render.

View File

@ -1,5 +1,5 @@
import app from '../../forum/app';
import PaginatedListState, { Page } from '../../common/states/PaginatedListState';
import PaginatedListState, { Page, PaginatedListParams } from '../../common/states/PaginatedListState';
import Discussion from '../../common/models/Discussion';
export interface IRequestParams {
@ -8,10 +8,14 @@ export interface IRequestParams {
sort?: string;
}
export default class DiscussionListState extends PaginatedListState<Discussion> {
export interface DiscussionListParams extends PaginatedListParams {
sort?: string;
}
export default class DiscussionListState<P extends DiscussionListParams = DiscussionListParams> extends PaginatedListState<Discussion, P> {
protected extraDiscussions: Discussion[] = [];
constructor(params: any, page: number = 1) {
constructor(params: P, page: number = 1) {
super(params, page, 20);
}
@ -25,7 +29,7 @@ export default class DiscussionListState extends PaginatedListState<Discussion>
filter: this.params.filter || {},
};
params.sort = this.sortMap()[this.params.sort];
params.sort = this.sortMap()[this.params.sort ?? ''];
if (this.params.q) {
params.filter.q = this.params.q;

View File

@ -1,5 +1,11 @@
import setRouteWithForcedRefresh from '../../common/utils/setRouteWithForcedRefresh';
export interface HistoryEntry {
name: string;
title: string;
url: string;
}
/**
* The `History` class keeps track and manages a stack of routes that the user
* has navigated to in their session.
@ -12,46 +18,34 @@ import setRouteWithForcedRefresh from '../../common/utils/setRouteWithForcedRefr
* rather than the previous discussion.
*/
export default class History {
constructor(defaultRoute) {
/**
* The stack of routes that have been navigated to.
*
* @type {Array}
* @protected
*/
this.stack = [];
}
/**
* The stack of routes that have been navigated to.
*/
protected stack: HistoryEntry[] = [];
/**
* Get the item on the top of the stack.
*
* @return {Object}
* @public
*/
getCurrent() {
getCurrent(): HistoryEntry {
return this.stack[this.stack.length - 1];
}
/**
* Get the previous item on the stack.
*
* @return {Object}
* @public
*/
getPrevious() {
getPrevious(): HistoryEntry {
return this.stack[this.stack.length - 2];
}
/**
* Push an item to the top of the stack.
*
* @param {String} name The name of the route.
* @param {String} title The title of the route.
* @param {String} [url] The URL of the route. The current URL will be used if
* @param {string} name The name of the route.
* @param {string} title The title of the route.
* @param {string} [url] The URL of the route. The current URL will be used if
* not provided.
* @public
*/
push(name, title, url = m.route.get()) {
push(name: string, title: string, url = m.route.get()) {
// If we're pushing an item with the same name as second-to-top item in the
// stack, we will assume that the user has clicked the 'back' button in
// their browser. In this case, we don't want to push a new item, so we will
@ -74,18 +68,13 @@ export default class History {
/**
* Check whether or not the history stack is able to be popped.
*
* @return {Boolean}
* @public
*/
canGoBack() {
canGoBack(): boolean {
return this.stack.length > 1;
}
/**
* Go back to the previous route in the history stack.
*
* @public
*/
back() {
if (!this.canGoBack()) {
@ -99,10 +88,8 @@ export default class History {
/**
* Get the URL of the previous page.
*
* @public
*/
backUrl() {
backUrl(): string {
const secondTop = this.stack[this.stack.length - 2];
return secondTop.url;
@ -110,8 +97,6 @@ export default class History {
/**
* Go to the first route in the history stack.
*
* @public
*/
home() {
this.stack.splice(0);

View File

@ -91,7 +91,7 @@ export default class KeyboardNavigatable {
*/
onRemove(callback: KeyboardEventHandler): KeyboardNavigatable {
this.callbacks.set(8, (e) => {
if (e.target.selectionStart === 0 && e.target.selectionEnd === 0) {
if (e instanceof KeyboardEvent && e.target instanceof HTMLInputElement && e.target.selectionStart === 0 && e.target.selectionEnd === 0) {
callback(e);
e.preventDefault();
}
@ -114,7 +114,7 @@ export default class KeyboardNavigatable {
*/
bindTo($element: JQuery) {
// Handle navigation key events on the navigatable element.
$element.on('keydown', this.navigate.bind(this));
$element[0].addEventListener('keydown', this.navigate.bind(this));
}
/**

View File

@ -4,9 +4,9 @@
export default function isSafariMobile(): boolean {
return (
'ontouchstart' in window &&
navigator.vendor &&
navigator.vendor != null &&
navigator.vendor.includes('Apple') &&
navigator.userAgent &&
navigator.userAgent != null &&
!navigator.userAgent.includes('CriOS') &&
!navigator.userAgent.includes('FxiOS')
);

View File

@ -1321,6 +1321,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.1.2":
version: 7.16.3
resolution: "@babel/runtime@npm:7.16.3"
dependencies:
regenerator-runtime: ^0.13.4
checksum: ab8ac887096d76185ddbf291d28fb976cd32473696dc497ad4905b784acbd5aa462533ad83a5c5104e10ead28c2e0e119840ee28ed8eff90dcdde9d57f916eda
languageName: node
linkType: hard
"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.8.4":
version: 7.16.0
resolution: "@babel/runtime@npm:7.16.0"
@ -1404,6 +1413,7 @@ __metadata:
textarea-caret: ^3.1.0
throttle-debounce: ^3.0.1
typescript: ^4.4.4
typescript-coverage-report: ^0.6.1
webpack: ^5.61.0
webpack-cli: ^4.9.1
webpack-merge: ^5.8.0
@ -1417,6 +1427,46 @@ __metadata:
languageName: node
linkType: hard
"@hypnosphi/create-react-context@npm:^0.3.1":
version: 0.3.1
resolution: "@hypnosphi/create-react-context@npm:0.3.1"
dependencies:
gud: ^1.0.0
warning: ^4.0.3
peerDependencies:
prop-types: ^15.0.0
react: ">=0.14.0"
checksum: d2f069a562e138057aa067e1483e28cea3193bbacd33ca9528131f31e656939cfeb552af760b3be437d3a8074315a8854fc6d5d89878e2746618ad930c817122
languageName: node
linkType: hard
"@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5"
dependencies:
"@nodelib/fs.stat": 2.0.5
run-parallel: ^1.1.9
checksum: a970d595bd23c66c880e0ef1817791432dbb7acbb8d44b7e7d0e7a22f4521260d4a83f7f9fd61d44fda4610105577f8f58a60718105fb38352baed612fd79e59
languageName: node
linkType: hard
"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2":
version: 2.0.5
resolution: "@nodelib/fs.stat@npm:2.0.5"
checksum: 012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0
languageName: node
linkType: hard
"@nodelib/fs.walk@npm:^1.2.3":
version: 1.2.8
resolution: "@nodelib/fs.walk@npm:1.2.8"
dependencies:
"@nodelib/fs.scandir": 2.1.5
fastq: ^1.6.0
checksum: 190c643f156d8f8f277bf2a6078af1ffde1fd43f498f187c2db24d35b4b4b5785c02c7dc52e356497b9a1b65b13edc996de08de0b961c32844364da02986dc53
languageName: node
linkType: hard
"@polka/url@npm:^1.0.0-next.20":
version: 1.0.0-next.21
resolution: "@polka/url@npm:1.0.0-next.21"
@ -1424,6 +1474,46 @@ __metadata:
languageName: node
linkType: hard
"@semantic-ui-react/event-stack@npm:^3.1.0":
version: 3.1.2
resolution: "@semantic-ui-react/event-stack@npm:3.1.2"
dependencies:
exenv: ^1.2.2
prop-types: ^15.6.2
peerDependencies:
react: ^16.0.0 || ^17.0.0
react-dom: ^16.0.0 || ^17.0.0
checksum: 3e3a27294e24478d57f324552008975a8c7b2deec9a974f627a75d7bc9fc1257da6c62907cf4684b7cb9b3b1252f5e9b2f312200d5dafcf1f019a645fd21ad0b
languageName: node
linkType: hard
"@stardust-ui/react-component-event-listener@npm:~0.38.0":
version: 0.38.0
resolution: "@stardust-ui/react-component-event-listener@npm:0.38.0"
dependencies:
"@babel/runtime": ^7.1.2
prop-types: ^15.7.2
peerDependencies:
react: ^16.8.0
react-dom: ^16.8.0
checksum: f9f54c976781f5bed766cded56be1a35b0d02bf1aa244db5db61e508420ae76241254772c626ee99322455793a38ffdffd195c3515e8a628a08457ba711ce1ac
languageName: node
linkType: hard
"@stardust-ui/react-component-ref@npm:~0.38.0":
version: 0.38.0
resolution: "@stardust-ui/react-component-ref@npm:0.38.0"
dependencies:
"@babel/runtime": ^7.1.2
prop-types: ^15.7.2
react-is: ^16.6.3
peerDependencies:
react: ^16.8.0
react-dom: ^16.8.0
checksum: c5bd6ec22fdcfdaee37b255d68fef5420d84e47b080d3947bcd0d7063548e1a8aa1627285bdf8c6c85eb4516845c763d7c183e57505ca8eca7c3d87e266a83ec
languageName: node
linkType: hard
"@types/eslint-scope@npm:^3.7.0":
version: 3.7.1
resolution: "@types/eslint-scope@npm:3.7.1"
@ -1903,6 +1993,15 @@ __metadata:
languageName: node
linkType: hard
"braces@npm:^3.0.1":
version: 3.0.2
resolution: "braces@npm:3.0.2"
dependencies:
fill-range: ^7.0.1
checksum: e2a8e769a863f3d4ee887b5fe21f63193a891c68b612ddb4b68d82d1b5f3ff9073af066c343e9867a393fe4c2555dcb33e89b937195feb9c1613d259edfcd459
languageName: node
linkType: hard
"browserslist@npm:^4.14.5, browserslist@npm:^4.16.6, browserslist@npm:^4.17.6":
version: 4.17.6
resolution: "browserslist@npm:4.17.6"
@ -1952,7 +2051,7 @@ __metadata:
languageName: node
linkType: hard
"call-bind@npm:^1.0.0":
"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2":
version: 1.0.2
resolution: "call-bind@npm:1.0.2"
dependencies:
@ -2004,6 +2103,13 @@ __metadata:
languageName: node
linkType: hard
"classnames@npm:^2.2.6":
version: 2.3.1
resolution: "classnames@npm:2.3.1"
checksum: 14db8889d56c267a591f08b0834989fe542d47fac659af5a539e110cc4266694e8de86e4e3bbd271157dbd831361310a8293e0167141e80b0f03a0f175c80960
languageName: node
linkType: hard
"clone-deep@npm:^4.0.1":
version: 4.0.1
resolution: "clone-deep@npm:4.0.1"
@ -2068,6 +2174,13 @@ __metadata:
languageName: node
linkType: hard
"colors@npm:^1.0.3, colors@npm:^1.4.0":
version: 1.4.0
resolution: "colors@npm:1.4.0"
checksum: 98aa2c2418ad87dedf25d781be69dc5fc5908e279d9d30c34d8b702e586a0474605b3a189511482b9d5ed0d20c867515d22749537f7bc546256c6014f3ebdcec
languageName: node
linkType: hard
"commander@npm:^2.20.0":
version: 2.20.3
resolution: "commander@npm:2.20.3"
@ -2164,6 +2277,20 @@ __metadata:
languageName: node
linkType: hard
"deep-equal@npm:^1.1.1":
version: 1.1.1
resolution: "deep-equal@npm:1.1.1"
dependencies:
is-arguments: ^1.0.4
is-date-object: ^1.0.1
is-regex: ^1.0.4
object-is: ^1.0.1
object-keys: ^1.1.1
regexp.prototype.flags: ^1.2.0
checksum: f92686f2c5bcdf714a75a5fa7a9e47cb374a8ec9307e717b8d1ce61f56a75aaebf5619c2a12b8087a705b5a2f60d0292c35f8b58cb1f72e3268a3a15cab9f78d
languageName: node
linkType: hard
"define-properties@npm:^1.1.3":
version: 1.1.3
resolution: "define-properties@npm:1.1.3"
@ -2180,6 +2307,13 @@ __metadata:
languageName: node
linkType: hard
"eastasianwidth@npm:^0.1.0":
version: 0.1.1
resolution: "eastasianwidth@npm:0.1.1"
checksum: d387cada4bbb8f50634098f0b204a9046cfb3748add45442232bae57b715692fbf1795154178293550cc28e08eee8ab1218cdcb28b7342c238dfd3bf42fbba10
languageName: node
linkType: hard
"electron-to-chromium@npm:^1.3.886":
version: 1.3.888
resolution: "electron-to-chromium@npm:1.3.888"
@ -2307,6 +2441,13 @@ __metadata:
languageName: node
linkType: hard
"exenv@npm:^1.2.2":
version: 1.2.2
resolution: "exenv@npm:1.2.2"
checksum: a894f3b60ab8419e0b6eec99c690a009c8276b4c90655ccaf7d28faba2de3a6b93b3d92210f9dc5efd36058d44f04098f6bbccef99859151104bfd49939904dc
languageName: node
linkType: hard
"expose-loader@npm:^3.1.0":
version: 3.1.0
resolution: "expose-loader@npm:3.1.0"
@ -2323,6 +2464,19 @@ __metadata:
languageName: node
linkType: hard
"fast-glob@npm:3":
version: 3.2.7
resolution: "fast-glob@npm:3.2.7"
dependencies:
"@nodelib/fs.stat": ^2.0.2
"@nodelib/fs.walk": ^1.2.3
glob-parent: ^5.1.2
merge2: ^1.3.0
micromatch: ^4.0.4
checksum: 2f4708ff112d2b451888129fdd9a0938db88b105b0ddfd043c064e3c4d3e20eed8d7c7615f7565fee660db34ddcf08a2db1bf0ab3c00b87608e4719694642d78
languageName: node
linkType: hard
"fast-json-stable-stringify@npm:^2.0.0":
version: 2.1.0
resolution: "fast-json-stable-stringify@npm:2.1.0"
@ -2337,6 +2491,24 @@ __metadata:
languageName: node
linkType: hard
"fastq@npm:^1.6.0":
version: 1.13.0
resolution: "fastq@npm:1.13.0"
dependencies:
reusify: ^1.0.4
checksum: 32cf15c29afe622af187d12fc9cd93e160a0cb7c31a3bb6ace86b7dea3b28e7b72acde89c882663f307b2184e14782c6c664fa315973c03626c7d4bff070bb0b
languageName: node
linkType: hard
"fill-range@npm:^7.0.1":
version: 7.0.1
resolution: "fill-range@npm:7.0.1"
dependencies:
to-regex-range: ^5.0.1
checksum: cc283f4e65b504259e64fd969bcf4def4eb08d85565e906b7d36516e87819db52029a76b6363d0f02d0d532f0033c9603b9e2d943d56ee3b0d4f7ad3328ff917
languageName: node
linkType: hard
"find-cache-dir@npm:^3.3.1":
version: 3.3.2
resolution: "find-cache-dir@npm:3.3.2"
@ -2442,6 +2614,15 @@ __metadata:
languageName: node
linkType: hard
"glob-parent@npm:^5.1.2":
version: 5.1.2
resolution: "glob-parent@npm:5.1.2"
dependencies:
is-glob: ^4.0.1
checksum: f4f2bfe2425296e8a47e36864e4f42be38a996db40420fe434565e4480e3322f18eb37589617a98640c5dc8fdec1a387007ee18dbb1f3f5553409c34d17f425e
languageName: node
linkType: hard
"glob-to-regexp@npm:^0.4.1":
version: 0.4.1
resolution: "glob-to-regexp@npm:0.4.1"
@ -2449,7 +2630,7 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^7.1.2":
"glob@npm:^7.1.2, glob@npm:^7.1.3":
version: 7.2.0
resolution: "glob@npm:7.2.0"
dependencies:
@ -2477,6 +2658,13 @@ __metadata:
languageName: node
linkType: hard
"gud@npm:^1.0.0":
version: 1.0.0
resolution: "gud@npm:1.0.0"
checksum: 3e2eb37cf794364077c18f036d6aa259c821c7fd188f2b7935cb00d589d82a41e0ebb1be809e1a93679417f62f1ad0513e745c3cf5329596e489aef8c5e5feae
languageName: node
linkType: hard
"gzip-size@npm:^5.1.1":
version: 5.1.1
resolution: "gzip-size@npm:5.1.1"
@ -2510,13 +2698,22 @@ __metadata:
languageName: node
linkType: hard
"has-symbols@npm:^1.0.1":
"has-symbols@npm:^1.0.1, has-symbols@npm:^1.0.2":
version: 1.0.2
resolution: "has-symbols@npm:1.0.2"
checksum: 2309c426071731be792b5be43b3da6fb4ed7cbe8a9a6bcfca1862587709f01b33d575ce8f5c264c1eaad09fca2f9a8208c0a2be156232629daa2dd0c0740976b
languageName: node
linkType: hard
"has-tostringtag@npm:^1.0.0":
version: 1.0.0
resolution: "has-tostringtag@npm:1.0.0"
dependencies:
has-symbols: ^1.0.2
checksum: cc12eb28cb6ae22369ebaad3a8ab0799ed61270991be88f208d508076a1e99abe4198c965935ce85ea90b60c94ddda73693b0920b58e7ead048b4a391b502c1c
languageName: node
linkType: hard
"has@npm:^1.0.3":
version: 1.0.3
resolution: "has@npm:1.0.3"
@ -2576,6 +2773,16 @@ __metadata:
languageName: node
linkType: hard
"is-arguments@npm:^1.0.4":
version: 1.1.1
resolution: "is-arguments@npm:1.1.1"
dependencies:
call-bind: ^1.0.2
has-tostringtag: ^1.0.0
checksum: 7f02700ec2171b691ef3e4d0e3e6c0ba408e8434368504bb593d0d7c891c0dbfda6d19d30808b904a6cb1929bca648c061ba438c39f296c2a8ca083229c49f27
languageName: node
linkType: hard
"is-arrayish@npm:^0.2.1":
version: 0.2.1
resolution: "is-arrayish@npm:0.2.1"
@ -2592,6 +2799,38 @@ __metadata:
languageName: node
linkType: hard
"is-date-object@npm:^1.0.1":
version: 1.0.5
resolution: "is-date-object@npm:1.0.5"
dependencies:
has-tostringtag: ^1.0.0
checksum: baa9077cdf15eb7b58c79398604ca57379b2fc4cf9aa7a9b9e295278648f628c9b201400c01c5e0f7afae56507d741185730307cbe7cad3b9f90a77e5ee342fc
languageName: node
linkType: hard
"is-extglob@npm:^2.1.1":
version: 2.1.1
resolution: "is-extglob@npm:2.1.1"
checksum: df033653d06d0eb567461e58a7a8c9f940bd8c22274b94bf7671ab36df5719791aae15eef6d83bbb5e23283967f2f984b8914559d4449efda578c775c4be6f85
languageName: node
linkType: hard
"is-glob@npm:^4.0.1":
version: 4.0.3
resolution: "is-glob@npm:4.0.3"
dependencies:
is-extglob: ^2.1.1
checksum: d381c1319fcb69d341cc6e6c7cd588e17cd94722d9a32dbd60660b993c4fb7d0f19438674e68dfec686d09b7c73139c9166b47597f846af387450224a8101ab4
languageName: node
linkType: hard
"is-number@npm:^7.0.0":
version: 7.0.0
resolution: "is-number@npm:7.0.0"
checksum: 456ac6f8e0f3111ed34668a624e45315201dff921e5ac181f8ec24923b99e9f32ca1a194912dc79d539c97d33dba17dc635202ff0b2cf98326f608323276d27a
languageName: node
linkType: hard
"is-plain-object@npm:^2.0.4":
version: 2.0.4
resolution: "is-plain-object@npm:2.0.4"
@ -2601,6 +2840,16 @@ __metadata:
languageName: node
linkType: hard
"is-regex@npm:^1.0.4":
version: 1.1.4
resolution: "is-regex@npm:1.1.4"
dependencies:
call-bind: ^1.0.2
has-tostringtag: ^1.0.0
checksum: 362399b33535bc8f386d96c45c9feb04cf7f8b41c182f54174c1a45c9abbbe5e31290bbad09a458583ff6bf3b2048672cdb1881b13289569a7c548370856a652
languageName: node
linkType: hard
"is-stream@npm:^2.0.0":
version: 2.0.1
resolution: "is-stream@npm:2.0.1"
@ -2647,7 +2896,7 @@ __metadata:
languageName: node
linkType: hard
"js-tokens@npm:^4.0.0":
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
version: 4.0.0
resolution: "js-tokens@npm:4.0.0"
checksum: 8a95213a5a77deb6cbe94d86340e8d9ace2b93bc367790b260101d2f36a2eaf4e4e22d9fa9cf459b38af3a32fb4190e638024cf82ec95ef708680e405ea7cc78
@ -2722,6 +2971,13 @@ __metadata:
languageName: node
linkType: hard
"keyboard-key@npm:^1.0.4":
version: 1.1.0
resolution: "keyboard-key@npm:1.1.0"
checksum: 8bf7c0d796820a0d4802930ef103339e2ffddf369f441d8e19f0a1d3b841da71a0a4da8c7a5270f4814da54300434f5fc5b3489aae4ae3ba47418ac106b71a64
languageName: node
linkType: hard
"kind-of@npm:^6.0.2":
version: 6.0.3
resolution: "kind-of@npm:6.0.3"
@ -2777,13 +3033,24 @@ __metadata:
languageName: node
linkType: hard
"lodash@npm:^4.17.20":
"lodash@npm:^4.17.15, lodash@npm:^4.17.20":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7
languageName: node
linkType: hard
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
dependencies:
js-tokens: ^3.0.0 || ^4.0.0
bin:
loose-envify: cli.js
checksum: 6517e24e0cad87ec9888f500c5b5947032cdfe6ef65e1c1936a0c48a524b81e65542c9c3edc91c97d5bddc806ee2a985dbc79be89215d613b1de5db6d1cfe6f4
languageName: node
linkType: hard
"make-dir@npm:^3.0.2, make-dir@npm:^3.1.0":
version: 3.1.0
resolution: "make-dir@npm:3.1.0"
@ -2800,6 +3067,23 @@ __metadata:
languageName: node
linkType: hard
"merge2@npm:^1.3.0":
version: 1.4.1
resolution: "merge2@npm:1.4.1"
checksum: 7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2
languageName: node
linkType: hard
"micromatch@npm:^4.0.4":
version: 4.0.4
resolution: "micromatch@npm:4.0.4"
dependencies:
braces: ^3.0.1
picomatch: ^2.2.3
checksum: ef3d1c88e79e0a68b0e94a03137676f3324ac18a908c245a9e5936f838079fcc108ac7170a5fadc265a9c2596963462e402841406bda1a4bb7b68805601d631c
languageName: node
linkType: hard
"mime-db@npm:1.50.0":
version: 1.50.0
resolution: "mime-db@npm:1.50.0"
@ -2832,7 +3116,7 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^3.0.4":
"minimatch@npm:3, minimatch@npm:^3.0.4":
version: 3.0.4
resolution: "minimatch@npm:3.0.4"
dependencies:
@ -2873,6 +3157,15 @@ __metadata:
languageName: node
linkType: hard
"ncp@npm:^2.0.0":
version: 2.0.0
resolution: "ncp@npm:2.0.0"
bin:
ncp: ./bin/ncp
checksum: ea9b19221da1d1c5529bdb9f8e85c9d191d156bcaae408cce5e415b7fbfd8744c288e792bd7faf1fe3b70fd44c74e22f0d43c39b209bc7ac1fb8016f70793a16
languageName: node
linkType: hard
"neo-async@npm:^2.6.2":
version: 2.6.2
resolution: "neo-async@npm:2.6.2"
@ -2899,6 +3192,13 @@ __metadata:
languageName: node
linkType: hard
"normalize-path@npm:3":
version: 3.0.0
resolution: "normalize-path@npm:3.0.0"
checksum: 88eeb4da891e10b1318c4b2476b6e2ecbeb5ff97d946815ffea7794c31a89017c70d7f34b3c2ebf23ef4e9fc9fb99f7dffe36da22011b5b5c6ffa34f4873ec20
languageName: node
linkType: hard
"npm-run-path@npm:^4.0.1":
version: 4.0.1
resolution: "npm-run-path@npm:4.0.1"
@ -2908,6 +3208,23 @@ __metadata:
languageName: node
linkType: hard
"object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
checksum: fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f
languageName: node
linkType: hard
"object-is@npm:^1.0.1":
version: 1.1.5
resolution: "object-is@npm:1.1.5"
dependencies:
call-bind: ^1.0.2
define-properties: ^1.1.3
checksum: 989b18c4cba258a6b74dc1d74a41805c1a1425bce29f6cabb50dcb1a6a651ea9104a1b07046739a49a5bb1bc49727bcb00efd5c55f932f6ea04ec8927a7901fe
languageName: node
linkType: hard
"object-keys@npm:^1.0.12, object-keys@npm:^1.1.1":
version: 1.1.1
resolution: "object-keys@npm:1.1.1"
@ -3035,6 +3352,13 @@ __metadata:
languageName: node
linkType: hard
"picomatch@npm:^2.2.3":
version: 2.3.0
resolution: "picomatch@npm:2.3.0"
checksum: 16818720ea7c5872b6af110760dee856c8e4cd79aed1c7a006d076b1cc09eff3ae41ca5019966694c33fbd2e1cc6ea617ab10e4adac6df06556168f13be3fca2
languageName: node
linkType: hard
"pify@npm:^4.0.1":
version: 4.0.1
resolution: "pify@npm:4.0.1"
@ -3051,6 +3375,13 @@ __metadata:
languageName: node
linkType: hard
"popper.js@npm:^1.14.4":
version: 1.16.1
resolution: "popper.js@npm:1.16.1"
checksum: c56ae5001ec50a77ee297a8061a0221d99d25c7348d2e6bcd3e45a0d0f32a1fd81bca29d46cb0d4bdf13efb77685bd6a0ce93f9eb3c608311a461f945fffedbe
languageName: node
linkType: hard
"prettier@npm:^2.4.1":
version: 2.4.1
resolution: "prettier@npm:2.4.1"
@ -3060,6 +3391,17 @@ __metadata:
languageName: node
linkType: hard
"prop-types@npm:^15.6.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2":
version: 15.7.2
resolution: "prop-types@npm:15.7.2"
dependencies:
loose-envify: ^1.4.0
object-assign: ^4.1.1
react-is: ^16.8.1
checksum: 5eef82fdda64252c7e75aa5c8cc28a24bbdece0f540adb60ce67c205cf978a5bd56b83e4f269f91c6e4dcfd80b36f2a2dec24d362e278913db2086ca9c6f9430
languageName: node
linkType: hard
"punycode@npm:^2.1.0, punycode@npm:^2.1.1":
version: 2.1.1
resolution: "punycode@npm:2.1.1"
@ -3067,6 +3409,13 @@ __metadata:
languageName: node
linkType: hard
"queue-microtask@npm:^1.2.2":
version: 1.2.3
resolution: "queue-microtask@npm:1.2.3"
checksum: b676f8c040cdc5b12723ad2f91414d267605b26419d5c821ff03befa817ddd10e238d22b25d604920340fd73efd8ba795465a0377c4adf45a4a41e4234e42dc4
languageName: node
linkType: hard
"randombytes@npm:^2.1.0":
version: 2.1.0
resolution: "randombytes@npm:2.1.0"
@ -3076,6 +3425,55 @@ __metadata:
languageName: node
linkType: hard
"react-dom@npm:^16.13.1":
version: 16.14.0
resolution: "react-dom@npm:16.14.0"
dependencies:
loose-envify: ^1.1.0
object-assign: ^4.1.1
prop-types: ^15.6.2
scheduler: ^0.19.1
peerDependencies:
react: ^16.14.0
checksum: 5a5c49da0f106b2655a69f96c622c347febcd10532db391c262b26aec225b235357d9da1834103457683482ab1b229af7a50f6927a6b70e53150275e31785544
languageName: node
linkType: hard
"react-is@npm:^16.6.3, react-is@npm:^16.8.1, react-is@npm:^16.8.6":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f
languageName: node
linkType: hard
"react-popper@npm:^1.3.4":
version: 1.3.11
resolution: "react-popper@npm:1.3.11"
dependencies:
"@babel/runtime": ^7.1.2
"@hypnosphi/create-react-context": ^0.3.1
deep-equal: ^1.1.1
popper.js: ^1.14.4
prop-types: ^15.6.1
typed-styles: ^0.0.7
warning: ^4.0.2
peerDependencies:
react: 0.14.x || ^15.0.0 || ^16.0.0 || ^17.0.0
checksum: a0f5994f5799f1c7364498f74df123dd2561fff4ae834b10fdcca74d9a8e159b523ed1f0708db33bad606933ab4f0d5ce9c90e48cbb671bf30016c890f3c7ea4
languageName: node
linkType: hard
"react@npm:^16.13.1":
version: 16.14.0
resolution: "react@npm:16.14.0"
dependencies:
loose-envify: ^1.1.0
object-assign: ^4.1.1
prop-types: ^15.6.2
checksum: 8484f3ecb13414526f2a7412190575fc134da785c02695eb92bb6028c930bfe1c238d7be2a125088fec663cc7cda0a3623373c46807cf2c281f49c34b79881ac
languageName: node
linkType: hard
"read-pkg-up@npm:^7.0.1":
version: 7.0.1
resolution: "read-pkg-up@npm:7.0.1"
@ -3140,6 +3538,16 @@ __metadata:
languageName: node
linkType: hard
"regexp.prototype.flags@npm:^1.2.0":
version: 1.3.1
resolution: "regexp.prototype.flags@npm:1.3.1"
dependencies:
call-bind: ^1.0.2
define-properties: ^1.1.3
checksum: 343595db5a6bbbb3bfbda881f9c74832cfa9fc0039e64a43843f6bb9158b78b921055266510800ed69d4997638890b17a46d55fd9f32961f53ae56ac3ec4dd05
languageName: node
linkType: hard
"regexpu-core@npm:^4.7.1":
version: 4.8.0
resolution: "regexpu-core@npm:4.8.0"
@ -3208,6 +3616,33 @@ __metadata:
languageName: node
linkType: hard
"reusify@npm:^1.0.4":
version: 1.0.4
resolution: "reusify@npm:1.0.4"
checksum: c3076ebcc22a6bc252cb0b9c77561795256c22b757f40c0d8110b1300723f15ec0fc8685e8d4ea6d7666f36c79ccc793b1939c748bf36f18f542744a4e379fcc
languageName: node
linkType: hard
"rimraf@npm:^3.0.2":
version: 3.0.2
resolution: "rimraf@npm:3.0.2"
dependencies:
glob: ^7.1.3
bin:
rimraf: bin.js
checksum: 87f4164e396f0171b0a3386cc1877a817f572148ee13a7e113b238e48e8a9f2f31d009a92ec38a591ff1567d9662c6b67fd8818a2dbbaed74bc26a87a2a4a9a0
languageName: node
linkType: hard
"run-parallel@npm:^1.1.9":
version: 1.2.0
resolution: "run-parallel@npm:1.2.0"
dependencies:
queue-microtask: ^1.2.2
checksum: cb4f97ad25a75ebc11a8ef4e33bb962f8af8516bb2001082ceabd8902e15b98f4b84b4f8a9b222e5d57fc3bd1379c483886ed4619367a7680dad65316993021d
languageName: node
linkType: hard
"safe-buffer@npm:^5.1.0":
version: 5.2.1
resolution: "safe-buffer@npm:5.2.1"
@ -3222,6 +3657,16 @@ __metadata:
languageName: node
linkType: hard
"scheduler@npm:^0.19.1":
version: 0.19.1
resolution: "scheduler@npm:0.19.1"
dependencies:
loose-envify: ^1.1.0
object-assign: ^4.1.1
checksum: 73e185a59e2ff5aa3609f5b9cb97ddd376f89e1610579d29939d952411ca6eb7a24907a4ea4556569dacb931467a1a4a56d94fe809ef713aa76748642cd96a6c
languageName: node
linkType: hard
"schema-utils@npm:^2.6.5":
version: 2.7.1
resolution: "schema-utils@npm:2.7.1"
@ -3244,6 +3689,28 @@ __metadata:
languageName: node
linkType: hard
"semantic-ui-react@npm:^0.88.2":
version: 0.88.2
resolution: "semantic-ui-react@npm:0.88.2"
dependencies:
"@babel/runtime": ^7.1.2
"@semantic-ui-react/event-stack": ^3.1.0
"@stardust-ui/react-component-event-listener": ~0.38.0
"@stardust-ui/react-component-ref": ~0.38.0
classnames: ^2.2.6
keyboard-key: ^1.0.4
lodash: ^4.17.15
prop-types: ^15.7.2
react-is: ^16.8.6
react-popper: ^1.3.4
shallowequal: ^1.1.0
peerDependencies:
react: ^16.8.0
react-dom: ^16.8.0
checksum: b7d6d46ec2f8a08c50c5831977b2df8d7849cd5660d99d18be93e191853bb5259a0912e66c4d882e463ade4eacec62462f817b3c6000a65a6c63fbe17d6bd262
languageName: node
linkType: hard
"semver@npm:2 || 3 || 4 || 5":
version: 5.7.1
resolution: "semver@npm:5.7.1"
@ -3289,6 +3756,13 @@ __metadata:
languageName: node
linkType: hard
"shallowequal@npm:^1.1.0":
version: 1.1.0
resolution: "shallowequal@npm:1.1.0"
checksum: f4c1de0837f106d2dbbfd5d0720a5d059d1c66b42b580965c8f06bb1db684be8783538b684092648c981294bf817869f743a066538771dbecb293df78f765e00
languageName: node
linkType: hard
"shebang-command@npm:^2.0.0":
version: 2.0.0
resolution: "shebang-command@npm:2.0.0"
@ -3429,6 +3903,16 @@ __metadata:
languageName: node
linkType: hard
"terminal-table@npm:^0.0.12":
version: 0.0.12
resolution: "terminal-table@npm:0.0.12"
dependencies:
colors: ^1.0.3
eastasianwidth: ^0.1.0
checksum: ea678bf685170b228e5f36b83ff2a4e979b4664245431583a327f0c87ca64292e52f582fd5c869cf593249b5e5df7fc7113444c91b47da20ac1a6c914a6cba59
languageName: node
linkType: hard
"terser-webpack-plugin@npm:^5.1.3":
version: 5.2.4
resolution: "terser-webpack-plugin@npm:5.2.4"
@ -3486,6 +3970,15 @@ __metadata:
languageName: node
linkType: hard
"to-regex-range@npm:^5.0.1":
version: 5.0.1
resolution: "to-regex-range@npm:5.0.1"
dependencies:
is-number: ^7.0.0
checksum: f76fa01b3d5be85db6a2a143e24df9f60dd047d151062d0ba3df62953f2f697b16fe5dad9b0ac6191c7efc7b1d9dcaa4b768174b7b29da89d4428e64bc0a20ed
languageName: node
linkType: hard
"totalist@npm:^1.0.0":
version: 1.1.0
resolution: "totalist@npm:1.1.0"
@ -3493,6 +3986,46 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:1 || 2":
version: 2.3.1
resolution: "tslib@npm:2.3.1"
checksum: de17a98d4614481f7fcb5cd53ffc1aaf8654313be0291e1bfaee4b4bb31a20494b7d218ff2e15017883e8ea9626599b3b0e0229c18383ba9dce89da2adf15cb9
languageName: node
linkType: hard
"tslib@npm:^1.8.1":
version: 1.14.1
resolution: "tslib@npm:1.14.1"
checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd
languageName: node
linkType: hard
"tsutils@npm:3":
version: 3.21.0
resolution: "tsutils@npm:3.21.0"
dependencies:
tslib: ^1.8.1
peerDependencies:
typescript: ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
checksum: 1843f4c1b2e0f975e08c4c21caa4af4f7f65a12ac1b81b3b8489366826259323feb3fc7a243123453d2d1a02314205a7634e048d4a8009921da19f99755cdc48
languageName: node
linkType: hard
"type-coverage-core@npm:^2.17.2":
version: 2.18.3
resolution: "type-coverage-core@npm:2.18.3"
dependencies:
fast-glob: 3
minimatch: 3
normalize-path: 3
tslib: 1 || 2
tsutils: 3
peerDependencies:
typescript: 2 || 3 || 4
checksum: ba9f42fb9c71f49124bc0422dd14e7396806be1f4d4ead4af37d598d143cc03cb1fd5b951b685307f43a830d5288bf38ea0a8b5a3a40db5e87e7428d7430e939
languageName: node
linkType: hard
"type-fest@npm:^0.6.0":
version: 0.6.0
resolution: "type-fest@npm:0.6.0"
@ -3507,6 +4040,43 @@ __metadata:
languageName: node
linkType: hard
"typed-styles@npm:^0.0.7":
version: 0.0.7
resolution: "typed-styles@npm:0.0.7"
checksum: 36a6ad6bee008c15ddb8c2425eaf9aee37d2841985b4c44406ea4cf57080a9c30b6f9f3feb842ac952354733ac53299ee44f68d83f734486e8344d413f8c8c0d
languageName: node
linkType: hard
"typescript-coverage-report@npm:^0.6.1":
version: 0.6.1
resolution: "typescript-coverage-report@npm:0.6.1"
dependencies:
colors: ^1.4.0
commander: ^5.0.0
ncp: ^2.0.0
react: ^16.13.1
react-dom: ^16.13.1
rimraf: ^3.0.2
semantic-ui-react: ^0.88.2
terminal-table: ^0.0.12
type-coverage-core: ^2.17.2
typescript: 4.1.3
bin:
typescript-coverage-report: dist/bin/typescript-coverage-report.js
checksum: ee3703a56d53aeba723ca65563834becfa2e8ce67cf51fc4c839191db7215c4704f0affc3ca3095ff6baab724c5b0312c84af828e5f199ea6ffe49d1bf223400
languageName: node
linkType: hard
"typescript@npm:4.1.3":
version: 4.1.3
resolution: "typescript@npm:4.1.3"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 0a25f7d7cebbc5ad23f41cb30918643460477be265bd3bcd400ffedb77d16e97d46f2b0c31393b2f990c5cf5b9f7a829ad6aff8636988b8f30abf81c656237c0
languageName: node
linkType: hard
"typescript@npm:^4.3.2, typescript@npm:^4.4.4":
version: 4.4.4
resolution: "typescript@npm:4.4.4"
@ -3517,6 +4087,16 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@4.1.3#~builtin<compat/typescript>":
version: 4.1.3
resolution: "typescript@patch:typescript@npm%3A4.1.3#~builtin<compat/typescript>::version=4.1.3&hash=ddd1e8"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: ed3df76d9b6efb448e5e73bca671698cda353a3807199ad95c78782a857e7685ccc94b799ac885423ced65ee21c87cbbeb823642c5dfd715efae5ebbc6137070
languageName: node
linkType: hard
"typescript@patch:typescript@^4.3.2#~builtin<compat/typescript>, typescript@patch:typescript@^4.4.4#~builtin<compat/typescript>":
version: 4.4.4
resolution: "typescript@patch:typescript@npm%3A4.4.4#~builtin<compat/typescript>::version=4.4.4&hash=ddd1e8"
@ -3577,6 +4157,15 @@ __metadata:
languageName: node
linkType: hard
"warning@npm:^4.0.2, warning@npm:^4.0.3":
version: 4.0.3
resolution: "warning@npm:4.0.3"
dependencies:
loose-envify: ^1.0.0
checksum: 4f2cb6a9575e4faf71ddad9ad1ae7a00d0a75d24521c193fa464f30e6b04027bd97aa5d9546b0e13d3a150ab402eda216d59c1d0f2d6ca60124d96cd40dfa35c
languageName: node
linkType: hard
"watchpack@npm:^2.2.0":
version: 2.2.0
resolution: "watchpack@npm:2.2.0"