Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite application files to TS #3006

Merged
merged 14 commits into from
Oct 30, 2021
File renamed without changes.
330 changes: 206 additions & 124 deletions js/src/common/Application.js → js/src/common/Application.tsx

Large diffs are not rendered by default.

22 changes: 15 additions & 7 deletions js/src/common/resolvers/DefaultResolver.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import type Mithril from 'mithril';
import type { RouteResolver } from '../Application';
import type { default as Component, ComponentAttrs } from '../Component';

/**
* Generates a route resolver for a given component.
*
* In addition to regular route resolver functionality:
* - It provide the current route name as an attr
* - It sets a key on the component so a rerender will be triggered on route change.
*/
export default class DefaultResolver {
component: Mithril.Component;
export default class DefaultResolver<
Attrs extends ComponentAttrs,
Comp extends Component<Attrs & { routeName: string }>,
RouteArgs extends Record<string, unknown> = {}
> implements RouteResolver<Attrs, Comp, RouteArgs>
{
component: { new (): Comp };
routeName: string;

constructor(component, routeName) {
constructor(component: { new (): Comp }, routeName: string) {
this.component = component;
this.routeName = routeName;
}
Expand All @@ -20,22 +28,22 @@ export default class DefaultResolver {
* rerender occurs. This method can be overriden in subclasses
* to prevent rerenders on some route changes.
*/
makeKey() {
makeKey(): string {
return this.routeName + JSON.stringify(m.route.param());
}

makeAttrs(vnode) {
makeAttrs(vnode: Mithril.Vnode<Attrs, Comp>): Attrs & { routeName: string } {
return {
...vnode.attrs,
routeName: this.routeName,
};
}

onmatch(args, requestedPath, route) {
onmatch(args: RouteArgs, requestedPath: string, route: string): { new (): Comp } {
return this.component;
}

render(vnode) {
render(vnode: Mithril.Vnode<Attrs, Comp>): Mithril.Children {
return [{ ...vnode, attrs: this.makeAttrs(vnode), key: this.makeKey() }];
}
}
2 changes: 1 addition & 1 deletion js/src/common/utils/ScrollListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const later =
*/
export default class ScrollListener {
/**
* @param {Function} callback The callback to run when the scroll position
* @param {(top: number) => void} callback The callback to run when the scroll position
* changes.
* @public
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { FlarumGenericRoute, RouteResolver } from '../Application';
import type Component from '../Component';
import DefaultResolver from '../resolvers/DefaultResolver';

/**
Expand All @@ -6,20 +8,20 @@ import DefaultResolver from '../resolvers/DefaultResolver';
* to provide each route with the current route name.
*
* @see https://mithril.js.org/route.html#signature
* @param {Object} routes
* @param {String} [basePath]
* @return {Object}
*/
export default function mapRoutes(routes, basePath = '') {
const map = {};
export default function mapRoutes(routes: Record<string, FlarumGenericRoute>, basePath: string = '') {
const map: Record<
string,
RouteResolver<Record<string, unknown>, Component<{ routeName: string; [key: string]: unknown }>, Record<string, unknown>>
> = {};

for (const routeName in routes) {
const route = routes[routeName];

if ('resolver' in route) {
map[basePath + route.path] = route.resolver;
} else if ('component' in route) {
const resolverClass = 'resolverClass' in route ? route.resolverClass : DefaultResolver;
const resolverClass = 'resolverClass' in route ? route.resolverClass! : DefaultResolver;
map[basePath + route.path] = new resolverClass(route.component, routeName);
} else {
throw new Error(`Either a resolver or a component must be provided for the route [${routeName}]`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import app from '../forum/app';

import History from './utils/History';
import Pane from './utils/Pane';
import DiscussionPage from './components/DiscussionPage';
Expand All @@ -19,79 +20,62 @@ import DiscussionListState from './states/DiscussionListState';
import ComposerState from './states/ComposerState';
import isSafariMobile from './utils/isSafariMobile';

import type Notification from './components/Notification';
import type Post from './components/Post';

export default class ForumApplication extends Application {
/**
* A map of notification types to their components.
*
* @type {Object}
*/
notificationComponents = {
notificationComponents: Record<string, typeof Notification> = {
discussionRenamed: DiscussionRenamedNotification,
};

/**
* A map of post types to their components.
*
* @type {Object}
*/
postComponents = {
postComponents: Record<string, typeof Post> = {
comment: CommentPost,
discussionRenamed: DiscussionRenamedPost,
};

/**
* An object which controls the state of the page's side pane.
*
* @type {Pane}
*/
pane = null;

/**
* An object which controls the state of the page's drawer.
*
* @type {Drawer}
*/
drawer = null;
pane: Pane | null = null;

/**
* The app's history stack, which keeps track of which routes the user visits
* so that they can easily navigate back to the previous route.
*
* @type {History}
*/
history = new History();
history: History = new History();

/**
* An object which controls the state of the user's notifications.
*
* @type {NotificationListState}
*/
notifications = new NotificationListState(this);
notifications: NotificationListState = new NotificationListState();

/*
/**
* An object which stores previously searched queries and provides convenient
* tools for retrieving and managing search values.
*
* @type {GlobalSearchState}
*/
search = new GlobalSearchState();
search: GlobalSearchState = new GlobalSearchState();

/*
/**
* An object which controls the state of the composer.
*/
composer = new ComposerState();
composer: ComposerState = new ComposerState();

/**
* An object which controls the state of the cached discussion list, which
* is used in the index page and the slideout pane.
*/
discussions: DiscussionListState = new DiscussionListState({});

constructor() {
super();

routes(this);

/**
* An object which controls the state of the cached discussion list, which
* is used in the index page and the slideout pane.
*
* @type {DiscussionListState}
*/
this.discussions = new DiscussionListState({});
}

/**
Expand Down Expand Up @@ -119,17 +103,17 @@ export default class ForumApplication extends Application {

// We mount navigation and header components after the page, so components
// like the back button can access the updated state when rendering.
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('composer'), { view: () => Composer.component({ state: this.composer }) });
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('composer')!, { view: () => Composer.component({ state: this.composer }) });

alertEmailConfirmation(this);

// Route the home link back home when clicked. We do not want it to register
// if the user is opening it in a new tab, however.
$('#home-link').click((e) => {
document.getElementById('home-link')!.addEventListener('click', (e) => {
if (e.ctrlKey || e.metaKey || e.which === 2) return;
e.preventDefault();
app.history.home();
Expand Down
2 changes: 1 addition & 1 deletion js/src/forum/states/DiscussionListState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Discussion from '../../common/models/Discussion';
export default class DiscussionListState extends PaginatedListState<Discussion> {
protected extraDiscussions: Discussion[] = [];

constructor(params: any, page: number) {
constructor(params: any, page: number = 1) {
super(params, page, 20);
}

Expand Down
1 change: 1 addition & 0 deletions locale/core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ core:

# Translations in this namespace are used by the forum and admin interfaces.
lib:
debug_button: Debug

# These translations are displayed as tooltips for discussion badges.
badge:
Expand Down