mirror of
https://github.com/flarum/framework.git
synced 2024-11-29 04:33:47 +08:00
refactor: convert AlertManager
IndexPage
and UserPage
components to TS (#3536)
* chore: convert `AlertManager` component to TypeScript * chore: `compat.js` to `compat.ts` * chore: convert `IndexPage` component to TypeScript * chore: convert `UserPage` component and inheritors to TypeScript * chore: `yarn format` * chore: import types instead
This commit is contained in:
parent
5721a2f487
commit
0c017c2aa0
|
@ -1,3 +1,4 @@
|
|||
// @ts-expect-error We need to explicitly use the prefix to distinguish between the extend folder.
|
||||
import * as extend from './extend.ts';
|
||||
import Session from './Session';
|
||||
import Store from './Store';
|
|
@ -1,31 +0,0 @@
|
|||
import Component from '../Component';
|
||||
|
||||
/**
|
||||
* The `AlertManager` component provides an area in which `Alert` components can
|
||||
* be shown and dismissed.
|
||||
*/
|
||||
export default class AlertManager extends Component {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.state = this.attrs.state;
|
||||
}
|
||||
|
||||
view() {
|
||||
return (
|
||||
<div class="AlertManager">
|
||||
{Object.entries(this.state.getActiveAlerts()).map(([key, alert]) => {
|
||||
const urgent = alert.attrs.type === 'error';
|
||||
|
||||
return (
|
||||
<div class="AlertManager-alert" role="alert" aria-live={urgent ? 'assertive' : 'polite'}>
|
||||
<alert.componentClass {...alert.attrs} ondismiss={this.state.dismiss.bind(this.state, key)}>
|
||||
{alert.children}
|
||||
</alert.componentClass>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
42
framework/core/js/src/common/components/AlertManager.tsx
Normal file
42
framework/core/js/src/common/components/AlertManager.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import Component, { ComponentAttrs } from '../Component';
|
||||
import AlertManagerState from '../states/AlertManagerState';
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
export interface IAlertManagerAttrs extends ComponentAttrs {
|
||||
state: AlertManagerState;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `AlertManager` component provides an area in which `Alert` components can
|
||||
* be shown and dismissed.
|
||||
*/
|
||||
export default class AlertManager<CustomAttrs extends IAlertManagerAttrs = IAlertManagerAttrs> extends Component<CustomAttrs, AlertManagerState> {
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.state = this.attrs.state;
|
||||
}
|
||||
|
||||
view() {
|
||||
const activeAlerts = this.state.getActiveAlerts();
|
||||
|
||||
return (
|
||||
<div class="AlertManager">
|
||||
{Object.keys(activeAlerts)
|
||||
.map(Number)
|
||||
.map((key) => {
|
||||
const alert = activeAlerts[key];
|
||||
const urgent = alert.attrs.type === 'error';
|
||||
|
||||
return (
|
||||
<div class="AlertManager-alert" role="alert" aria-live={urgent ? 'assertive' : 'polite'}>
|
||||
<alert.componentClass {...alert.attrs} ondismiss={this.state.dismiss.bind(this.state, key)}>
|
||||
{alert.children}
|
||||
</alert.componentClass>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ export interface IPageAttrs {
|
|||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default abstract class Page<CustomAttrs extends IPageAttrs = IPageAttrs> extends Component<CustomAttrs> {
|
||||
export default abstract class Page<CustomAttrs extends IPageAttrs = IPageAttrs, CustomState = undefined> extends Component<CustomAttrs, CustomState> {
|
||||
/**
|
||||
* A class name to apply to the body while the route is active.
|
||||
*/
|
||||
|
|
|
@ -6,6 +6,8 @@ import Alert, { AlertAttrs } from '../components/Alert';
|
|||
*/
|
||||
export type AlertIdentifier = number;
|
||||
|
||||
export type AlertArray = { [id: AlertIdentifier]: AlertState };
|
||||
|
||||
export interface AlertState {
|
||||
componentClass: typeof Alert;
|
||||
attrs: AlertAttrs;
|
||||
|
@ -13,8 +15,8 @@ export interface AlertState {
|
|||
}
|
||||
|
||||
export default class AlertManagerState {
|
||||
protected activeAlerts: { [id: number]: AlertState } = {};
|
||||
protected alertId = 0;
|
||||
protected activeAlerts: AlertArray = {};
|
||||
protected alertId: AlertIdentifier = 0;
|
||||
|
||||
getActiveAlerts() {
|
||||
return this.activeAlerts;
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import UserPage from './UserPage';
|
||||
import UserPage, { IUserPageAttrs } from './UserPage';
|
||||
import DiscussionList from './DiscussionList';
|
||||
import DiscussionListState from '../states/DiscussionListState';
|
||||
import type Mithril from 'mithril';
|
||||
import type User from '../../common/models/User';
|
||||
|
||||
/**
|
||||
* The `DiscussionsUserPage` component shows a discussion list inside of a user
|
||||
* page.
|
||||
*/
|
||||
export default class DiscussionsUserPage extends UserPage {
|
||||
oninit(vnode) {
|
||||
export default class DiscussionsUserPage extends UserPage<IUserPageAttrs, DiscussionListState> {
|
||||
oninit(vnode: Mithril.Vnode<IUserPageAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.loadUser(m.route.param('username'));
|
||||
}
|
||||
|
||||
show(user) {
|
||||
show(user: User): void {
|
||||
super.show(user);
|
||||
|
||||
this.state = new DiscussionListState({
|
|
@ -1,5 +1,5 @@
|
|||
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 listItems from '../../common/helpers/listItems';
|
||||
import DiscussionList from './DiscussionList';
|
||||
|
@ -11,15 +11,21 @@ import Dropdown from '../../common/components/Dropdown';
|
|||
import Button from '../../common/components/Button';
|
||||
import LinkButton from '../../common/components/LinkButton';
|
||||
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import type Mithril from 'mithril';
|
||||
import type Discussion from '../../common/models/Discussion';
|
||||
|
||||
export interface IIndexPageAttrs extends IPageAttrs {}
|
||||
|
||||
/**
|
||||
* The `IndexPage` component displays the index page, including the welcome
|
||||
* hero, the sidebar, and the discussion list.
|
||||
*/
|
||||
export default class IndexPage extends Page {
|
||||
export default class IndexPage<CustomAttrs extends IIndexPageAttrs = IIndexPageAttrs, CustomState = {}> extends Page<CustomAttrs, CustomState> {
|
||||
static providesInitialSearch = true;
|
||||
lastDiscussion?: Discussion;
|
||||
|
||||
oninit(vnode) {
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
// If the user is returning from a discussion page, then take note of which
|
||||
|
@ -37,9 +43,9 @@ export default class IndexPage extends Page {
|
|||
app.discussions.clear();
|
||||
}
|
||||
|
||||
app.discussions.refreshParams(app.search.params(), m.route.param('page'));
|
||||
app.discussions.refreshParams(app.search.params(), Number(m.route.param('page')));
|
||||
|
||||
app.history.push('index', app.translator.trans('core.forum.header.back_to_index_tooltip'));
|
||||
app.history.push('index', extractText(app.translator.trans('core.forum.header.back_to_index_tooltip')));
|
||||
|
||||
this.bodyClass = 'App--index';
|
||||
this.scrollTopOnCreate = false;
|
||||
|
@ -68,11 +74,11 @@ export default class IndexPage extends Page {
|
|||
}
|
||||
|
||||
setTitle() {
|
||||
app.setTitle(app.translator.trans('core.forum.index.meta_title_text'));
|
||||
app.setTitle(extractText(app.translator.trans('core.forum.index.meta_title_text')));
|
||||
app.setTitleCount(0);
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
this.setTitle();
|
||||
|
@ -80,11 +86,11 @@ export default class IndexPage extends Page {
|
|||
// Work out the difference between the height of this hero and that of the
|
||||
// previous hero. Maintain the same scroll position relative to the bottom
|
||||
// of the hero so that the sidebar doesn't jump around.
|
||||
const oldHeroHeight = app.cache.heroHeight;
|
||||
const oldHeroHeight = app.cache.heroHeight as number;
|
||||
const heroHeight = (app.cache.heroHeight = this.$('.Hero').outerHeight() || 0);
|
||||
const scrollTop = app.cache.scrollTop;
|
||||
const scrollTop = app.cache.scrollTop as number;
|
||||
|
||||
$('#app').css('min-height', $(window).height() + heroHeight);
|
||||
$('#app').css('min-height', ($(window).height() || 0) + heroHeight);
|
||||
|
||||
// Let browser handle scrolling on page reload.
|
||||
if (app.previous.type == null) return;
|
||||
|
@ -104,10 +110,11 @@ export default class IndexPage extends Page {
|
|||
const $discussion = this.$(`li[data-id="${this.lastDiscussion.id()}"] .DiscussionListItem`);
|
||||
|
||||
if ($discussion.length) {
|
||||
const indexTop = $('#header').outerHeight();
|
||||
const indexBottom = $(window).height();
|
||||
const discussionTop = $discussion.offset().top;
|
||||
const discussionBottom = discussionTop + $discussion.outerHeight();
|
||||
const indexTop = $('#header').outerHeight() || 0;
|
||||
const indexBottom = $(window).height() || 0;
|
||||
const discussionOffset = $discussion.offset();
|
||||
const discussionTop = (discussionOffset && discussionOffset.top) || 0;
|
||||
const discussionBottom = discussionTop + ($discussion.outerHeight() || 0);
|
||||
|
||||
if (discussionTop < scrollTop + indexTop || discussionBottom > scrollTop + indexBottom) {
|
||||
$(window).scrollTop(discussionTop - indexTop);
|
||||
|
@ -116,7 +123,7 @@ export default class IndexPage extends Page {
|
|||
}
|
||||
}
|
||||
|
||||
onbeforeremove(vnode) {
|
||||
onbeforeremove(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
super.onbeforeremove(vnode);
|
||||
|
||||
// Save the scroll position so we can restore it when we return to the
|
||||
|
@ -124,7 +131,7 @@ export default class IndexPage extends Page {
|
|||
app.cache.scrollTop = $(window).scrollTop();
|
||||
}
|
||||
|
||||
onremove(vnode) {
|
||||
onremove(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
super.onremove(vnode);
|
||||
|
||||
$('#app').css('min-height', '');
|
||||
|
@ -132,8 +139,6 @@ export default class IndexPage extends Page {
|
|||
|
||||
/**
|
||||
* Get the component to display as the hero.
|
||||
*
|
||||
* @return {import('mithril').Children}
|
||||
*/
|
||||
hero() {
|
||||
return WelcomeHero.component();
|
||||
|
@ -143,11 +148,9 @@ export default class IndexPage extends Page {
|
|||
* Build an item list for the sidebar of the index page. By default this is a
|
||||
* "New Discussion" button, and then a DropdownSelect component containing a
|
||||
* list of navigation items.
|
||||
*
|
||||
* @return {ItemList<import('mithril').Children>}
|
||||
*/
|
||||
sidebarItems() {
|
||||
const items = new ItemList();
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
const canStartDiscussion = app.forum.attribute('canStartDiscussion') || !app.session.user;
|
||||
|
||||
items.add(
|
||||
|
@ -176,7 +179,7 @@ export default class IndexPage extends Page {
|
|||
className: 'App-titleControl',
|
||||
accessibleToggleLabel: app.translator.trans('core.forum.index.toggle_sidenav_dropdown_accessible_label'),
|
||||
},
|
||||
this.navItems(this).toArray()
|
||||
this.navItems().toArray()
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -186,11 +189,9 @@ export default class IndexPage extends Page {
|
|||
/**
|
||||
* Build an item list for the navigation in the sidebar of the index page. By
|
||||
* default this is just the 'All Discussions' link.
|
||||
*
|
||||
* @return {ItemList<import('mithril').Children>}
|
||||
*/
|
||||
navItems() {
|
||||
const items = new ItemList();
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
const params = app.search.stickyParams();
|
||||
|
||||
items.add(
|
||||
|
@ -212,14 +213,12 @@ export default class IndexPage extends Page {
|
|||
* Build an item list for the part of the toolbar which is concerned with how
|
||||
* the results are displayed. By default this is just a select box to change
|
||||
* the way discussions are sorted.
|
||||
*
|
||||
* @return {ItemList<import('mithril').Children>}
|
||||
*/
|
||||
viewItems() {
|
||||
const items = new ItemList();
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
const sortMap = app.discussions.sortMap();
|
||||
|
||||
const sortOptions = Object.keys(sortMap).reduce((acc, sortId) => {
|
||||
const sortOptions = Object.keys(sortMap).reduce((acc: any, sortId) => {
|
||||
acc[sortId] = app.translator.trans(`core.forum.index_sort.${sortId}_button`);
|
||||
return acc;
|
||||
}, {});
|
||||
|
@ -254,11 +253,9 @@ export default class IndexPage extends Page {
|
|||
/**
|
||||
* Build an item list for the part of the toolbar which is about taking action
|
||||
* on the results. By default this is just a "mark all as read" button.
|
||||
*
|
||||
* @return {ItemList<import('mithril').Children>}
|
||||
*/
|
||||
actionItems() {
|
||||
const items = new ItemList();
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
items.add(
|
||||
'refresh',
|
||||
|
@ -269,7 +266,7 @@ export default class IndexPage extends Page {
|
|||
onclick: () => {
|
||||
app.discussions.refresh();
|
||||
if (app.session.user) {
|
||||
app.store.find('users', app.session.user.id());
|
||||
app.store.find('users', app.session.user.id()!);
|
||||
m.redraw();
|
||||
}
|
||||
},
|
||||
|
@ -293,10 +290,8 @@ export default class IndexPage extends Page {
|
|||
|
||||
/**
|
||||
* Open the composer for a new discussion or prompt the user to login.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
newDiscussionAction() {
|
||||
newDiscussionAction(): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (app.session.user) {
|
||||
app.composer.load(DiscussionComposer, { user: app.session.user });
|
||||
|
@ -315,10 +310,10 @@ export default class IndexPage extends Page {
|
|||
* Mark all discussions as read.
|
||||
*/
|
||||
markAllAsRead() {
|
||||
const confirmation = confirm(app.translator.trans('core.forum.index.mark_all_as_read_confirmation'));
|
||||
const confirmation = confirm(extractText(app.translator.trans('core.forum.index.mark_all_as_read_confirmation')));
|
||||
|
||||
if (confirmation) {
|
||||
app.session.user.save({ markedAllAsReadAt: new Date() });
|
||||
app.session.user?.save({ markedAllAsReadAt: new Date() });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +1,41 @@
|
|||
import app from '../../forum/app';
|
||||
import UserPage from './UserPage';
|
||||
import UserPage, { IUserPageAttrs } from './UserPage';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import Button from '../../common/components/Button';
|
||||
import Link from '../../common/components/Link';
|
||||
import Placeholder from '../../common/components/Placeholder';
|
||||
import CommentPost from './CommentPost';
|
||||
import type Post from '../../common/models/Post';
|
||||
import type Mithril from 'mithril';
|
||||
import type User from '../../common/models/User';
|
||||
|
||||
/**
|
||||
* The `PostsUserPage` component shows a user's activity feed inside of their
|
||||
* profile.
|
||||
*/
|
||||
export default class PostsUserPage extends UserPage {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
/**
|
||||
* Whether or not the activity feed is currently loading.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.loading = true;
|
||||
loading: boolean = true;
|
||||
|
||||
/**
|
||||
* Whether or not there are any more activity items that can be loaded.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.moreResults = false;
|
||||
moreResults: boolean = false;
|
||||
|
||||
/**
|
||||
* The Post models in the feed.
|
||||
*
|
||||
* @type {Post[]}
|
||||
*/
|
||||
this.posts = [];
|
||||
posts: Post[] = [];
|
||||
|
||||
/**
|
||||
* The number of activity items to load per request.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
this.loadLimit = 20;
|
||||
loadLimit: number = 20;
|
||||
|
||||
oninit(vnode: Mithril.Vnode<IUserPageAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.loadUser(m.route.param('username'));
|
||||
}
|
||||
|
@ -92,7 +87,7 @@ export default class PostsUserPage extends UserPage {
|
|||
* Initialize the component with a user, and trigger the loading of their
|
||||
* activity feed.
|
||||
*/
|
||||
show(user) {
|
||||
show(user: User): void {
|
||||
super.show(user);
|
||||
|
||||
this.refresh();
|
||||
|
@ -113,14 +108,12 @@ export default class PostsUserPage extends UserPage {
|
|||
/**
|
||||
* Load a new page of the user's activity feed.
|
||||
*
|
||||
* @param {number} [offset] The position to start getting results from.
|
||||
* @return {Promise<import('../../common/models/Post').default[]>}
|
||||
* @protected
|
||||
*/
|
||||
loadResults(offset) {
|
||||
return app.store.find('posts', {
|
||||
loadResults(offset = 0) {
|
||||
return app.store.find<Post[]>('posts', {
|
||||
filter: {
|
||||
author: this.user.username(),
|
||||
author: this.user!.username(),
|
||||
type: 'comment',
|
||||
},
|
||||
page: { offset, limit: this.loadLimit },
|
||||
|
@ -138,11 +131,8 @@ export default class PostsUserPage extends UserPage {
|
|||
|
||||
/**
|
||||
* Parse results and append them to the activity feed.
|
||||
*
|
||||
* @param {import('../../common/models/Post').default[]} results
|
||||
* @return {import('../../common/models/Post').default[]}
|
||||
*/
|
||||
parseResults(results) {
|
||||
parseResults(results: Post[]): Post[] {
|
||||
this.loading = false;
|
||||
|
||||
this.posts.push(...results);
|
|
@ -1,5 +1,5 @@
|
|||
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 UserCard from './UserCard';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
|
@ -8,6 +8,10 @@ import LinkButton from '../../common/components/LinkButton';
|
|||
import Separator from '../../common/components/Separator';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import AffixedSidebar from './AffixedSidebar';
|
||||
import type User from '../../common/models/User';
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
export interface IUserPageAttrs extends IPageAttrs {}
|
||||
|
||||
/**
|
||||
* The `UserPage` component shows a user's profile. It can be extended to show
|
||||
|
@ -16,24 +20,20 @@ import AffixedSidebar from './AffixedSidebar';
|
|||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class UserPage extends Page {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
export default class UserPage<CustomAttrs extends IUserPageAttrs = IUserPageAttrs, CustomState = undefined> extends Page<CustomAttrs, CustomState> {
|
||||
/**
|
||||
* The user this page is for.
|
||||
*
|
||||
* @type {User}
|
||||
*/
|
||||
this.user = null;
|
||||
user: User | null = null;
|
||||
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.bodyClass = 'App--user';
|
||||
}
|
||||
|
||||
/**
|
||||
* Base view template for the user page.
|
||||
*
|
||||
* @return {import('mithril').Children}
|
||||
*/
|
||||
view() {
|
||||
return (
|
||||
|
@ -64,19 +64,16 @@ export default class UserPage extends Page {
|
|||
|
||||
/**
|
||||
* Get the content to display in the user page.
|
||||
*
|
||||
* @return {import('mithril').Children}
|
||||
*/
|
||||
content() {}
|
||||
content(): Mithril.Children | void {}
|
||||
|
||||
/**
|
||||
* Initialize the component with a user, and trigger the loading of their
|
||||
* activity feed.
|
||||
*
|
||||
* @param {import('../../common/models/User').default} user
|
||||
* @protected
|
||||
*/
|
||||
show(user) {
|
||||
show(user: User): void {
|
||||
this.user = user;
|
||||
|
||||
app.current.set('user', user);
|
||||
|
@ -89,10 +86,8 @@ export default class UserPage extends Page {
|
|||
/**
|
||||
* Given a username, load the user's profile from the store, or make a request
|
||||
* if we don't have it yet. Then initialize the profile page with that user.
|
||||
*
|
||||
* @param {string} username
|
||||
*/
|
||||
loadUser(username) {
|
||||
loadUser(username: string) {
|
||||
const lowercaseUsername = username.toLowerCase();
|
||||
|
||||
// Load the preloaded user object, if any, into the global app store
|
||||
|
@ -100,25 +95,25 @@ export default class UserPage extends Page {
|
|||
// instead of the parsed models
|
||||
app.preloadedApiDocument();
|
||||
|
||||
app.store.all('users').some((user) => {
|
||||
app.store.all<User>('users').some((user) => {
|
||||
if ((user.username().toLowerCase() === lowercaseUsername || user.id() === username) && user.joinTime()) {
|
||||
this.show(user);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!this.user) {
|
||||
app.store.find('users', username, { bySlug: true }).then(this.show.bind(this));
|
||||
app.store.find<User>('users', username, { bySlug: true }).then(this.show.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the content of the sidebar.
|
||||
*
|
||||
* @return {ItemList<import('mithril').Children>}
|
||||
*/
|
||||
sidebarItems() {
|
||||
const items = new ItemList();
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
items.add(
|
||||
'nav',
|
||||
|
@ -132,12 +127,10 @@ export default class UserPage extends Page {
|
|||
|
||||
/**
|
||||
* Build an item list for the navigation in the sidebar.
|
||||
*
|
||||
* @return {ItemList<import('mithril').Children>}
|
||||
*/
|
||||
navItems() {
|
||||
const items = new ItemList();
|
||||
const user = this.user;
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
const user = this.user!;
|
||||
|
||||
items.add(
|
||||
'posts',
|
Loading…
Reference in New Issue
Block a user