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

refactor: convert AlertManager IndexPage and UserPage components to TS #3536

Merged
merged 7 commits into from
Jul 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
31 changes: 0 additions & 31 deletions framework/core/js/src/common/components/AlertManager.js

This file was deleted.

42 changes: 42 additions & 0 deletions framework/core/js/src/common/components/AlertManager.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
}
2 changes: 1 addition & 1 deletion framework/core/js/src/common/components/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
6 changes: 4 additions & 2 deletions framework/core/js/src/common/states/AlertManagerState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import Alert, { AlertAttrs } from '../components/Alert';
*/
export type AlertIdentifier = number;

export type AlertArray = { [id: AlertIdentifier]: AlertState };

export interface AlertState {
componentClass: typeof Alert;
attrs: AlertAttrs;
children: Mithril.Children;
}

export default class AlertManagerState {
protected activeAlerts: { [id: number]: AlertState } = {};
protected alertId = 0;
protected activeAlerts: AlertArray = {};
protected alertId: AlertIdentifier = 0;

getActiveAlerts() {
return this.activeAlerts;
Expand Down
Original file line number Diff line number Diff line change
@@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -68,23 +74,23 @@ 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();

// 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;
Expand All @@ -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);
Expand All @@ -116,24 +123,22 @@ 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
// discussion list.
app.cache.scrollTop = $(window).scrollTop();
}

onremove(vnode) {
onremove(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
super.onremove(vnode);

$('#app').css('min-height', '');
}

/**
* Get the component to display as the hero.
*
* @return {import('mithril').Children}
*/
hero() {
return WelcomeHero.component();
Expand All @@ -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(
Expand Down Expand Up @@ -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()
)
);

Expand All @@ -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(
Expand All @@ -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;
}, {});
Expand Down Expand Up @@ -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',
Expand All @@ -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();
}
},
Expand All @@ -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 });
Expand All @@ -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() });
}
}
}
Loading