Implement latest 'master' branch changes - not including files that haven't been ported yet

This commit is contained in:
David Sevilla Martin
2020-01-25 09:30:27 -05:00
parent 660cd1c81e
commit 6978c0aa77
14 changed files with 156 additions and 33 deletions

2
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,11 +4,12 @@ import Bus from './Bus';
import Translator from './Translator'; import Translator from './Translator';
import Session from './Session'; import Session from './Session';
import Store from './Store'; import Store from './Store';
import {extend} from './extend';
import extract from './utils/extract'; import extract from './utils/extract';
import mapRoutes from './utils/mapRoutes'; import mapRoutes from './utils/mapRoutes';
import Drawer from './utils/Drawer'; import Drawer from './utils/Drawer';
import {extend} from './extend'; import RequestError from './utils/RequestError';
import Forum from './models/Forum'; import Forum from './models/Forum';
import Discussion from './models/Discussion'; import Discussion from './models/Discussion';
@ -17,9 +18,10 @@ import Post from './models/Post';
import Group from './models/Group'; import Group from './models/Group';
import Notification from './models/Notification'; import Notification from './models/Notification';
import RequestError from './utils/RequestError';
import Alert from './components/Alert'; import Alert from './components/Alert';
import Button from './components/Button';
import ModalManager from './components/ModalManager'; import ModalManager from './components/ModalManager';
import RequestErrorModal from './components/RequestErrorModal';
export type ApplicationData = { export type ApplicationData = {
apiDocument: any; apiDocument: any;
@ -280,9 +282,20 @@ export default abstract class Application {
children = this.translator.trans('core.lib.error.generic_message'); children = this.translator.trans('core.lib.error.generic_message');
} }
const isDebug = app.forum.attribute('debug');
this.showDebug(error);
error.alert = Alert.component({ error.alert = Alert.component({
type: 'error', type: 'error',
children children,
controls: isDebug && [
Button.component({
className: 'Button Button--link',
onclick: this.showDebug.bind(this, error),
children: 'DEBUG', // TODO make translatable
})
]
}); });
try { try {
@ -294,7 +307,11 @@ export default abstract class Application {
return Promise.reject(error); return Promise.reject(error);
}); });
}
// return deferred.promise; private showDebug(error: RequestError) {
// this.alerts.dismiss(this.requestError.alert);
this.modal.show(RequestErrorModal.component({error}));
} }
} }

View File

@ -1,5 +1,9 @@
import * as extend from './extend'; import * as extend from './extend';
import Modal from './components/Modal';
export default { export default {
extend: extend, extend: extend,
'components/Modal': Modal
}; };

View File

@ -37,7 +37,13 @@ export default class Button<T extends ButtonProps = ButtonProps> extends Compone
attrs.className = attrs.className || ''; attrs.className = attrs.className || '';
attrs.type = attrs.type || 'button'; attrs.type = attrs.type || 'button';
// If nothing else is provided, we use the textual button content as tooltip // If a tooltip was provided for buttons without additional content, we also
// use this tooltip as text for screen readers
if (attrs.title && !this.props.children) {
attrs['aria-label'] = attrs.title;
}
// If nothing else is provided, we use the textual button content as tooltip
if (!attrs.title && this.props.children) { if (!attrs.title && this.props.children) {
attrs.title = extractText(this.props.children); attrs.title = extractText(this.props.children);
} }

View File

@ -21,10 +21,9 @@ interface LinkButtonProps extends ButtonProps {
export default class LinkButton extends Button<LinkButtonProps> { export default class LinkButton extends Button<LinkButtonProps> {
static initProps(props: LinkButtonProps) { static initProps(props: LinkButtonProps) {
props.active = this.isActive(props); props.active = this.isActive(props);
props.oncreate = props.oncreate;
} }
view(vnode) { view(vnode) {
const vdom = super.view(vnode); const vdom = super.view(vnode);
vdom.tag = m.route.Link; vdom.tag = m.route.Link;
@ -32,6 +31,12 @@ export default class LinkButton extends Button<LinkButtonProps> {
return vdom; return vdom;
} }
onupdate(vnode) {
super.onupdate(vnode);
this.props.active = LinkButton.isActive(this.props);
}
/** /**
* Determine whether a component with the given props is 'active'. * Determine whether a component with the given props is 'active'.
*/ */

View File

@ -48,6 +48,12 @@ export default abstract class Modal<T extends ComponentProps = ComponentProps> e
); );
} }
oncreate(vnode) {
super.oncreate(vnode);
app.modal.component = this;
}
/** /**
* Determine whether or not the modal should be dismissible via an 'x' button. * Determine whether or not the modal should be dismissible via an 'x' button.
*/ */

View File

@ -0,0 +1,36 @@
import Modal from './Modal';
import {ComponentProps} from '../Component';
import RequestError from '../utils/RequestError';
export interface RequestErrorModalProps extends ComponentProps {
error: RequestError,
}
export default class RequestErrorModal<T extends RequestErrorModalProps = RequestErrorModalProps> extends Modal<T> {
className(): string {
return 'RequestErrorModal Modal--large';
}
title(): string {
return this.props.error.xhr
? `${this.props.error.xhr.status} ${this.props.error.xhr.statusText}`
: '';
}
content() {
let responseText;
try {
responseText = JSON.stringify(JSON.parse(this.props.error.responseText), null, 2);
} catch (e) {
responseText = this.props.error.responseText;
}
return <div className="Modal-body">
<pre>
{this.props.error.options.method} {this.props.error.options.url}<br/><br/>
{responseText}
</pre>
</div>
}
}

View File

@ -81,6 +81,7 @@ export default class User extends Model {
user.freshness = new Date(); user.freshness = new Date();
m.redraw(); m.redraw();
}; };
image.crossOrigin = 'anonymous';
image.src = this.avatarUrl(); image.src = this.avatarUrl();
} }

View File

@ -44,7 +44,7 @@ export default class Post<T extends PostProps = PostProps> extends Component<Pos
const controls = PostControls.controls(this.props.post, this).toArray(); const controls = PostControls.controls(this.props.post, this).toArray();
const attrs = this.attrs(); const attrs = this.attrs();
attrs.className = classNames('Post', this.loading && 'Post--loading', attrs.className); attrs.className = classNames(this.classes(attrs.className));
return ( return (
<article {...attrs}> <article {...attrs}>
@ -102,6 +102,20 @@ export default class Post<T extends PostProps = PostProps> extends Component<Pos
return []; return [];
} }
classes(existing) {
let classes = (existing || '').split(' ').concat(['Post']);
if (this.loading) {
classes.push('Post--loading');
}
if (this.props.post.user() === app.session.user) {
classes.push('Post--by-actor');
}
return classes;
}
/** /**
* Build an item list for the post's actions. * Build an item list for the post's actions.
*/ */

View File

@ -5,6 +5,7 @@ import username from '../../common/helpers/username';
import userOnline from '../../common/helpers/userOnline'; import userOnline from '../../common/helpers/userOnline';
import listItems from '../../common/helpers/listItems'; import listItems from '../../common/helpers/listItems';
import {PostProps} from "./Post"; import {PostProps} from "./Post";
import LinkButton from "../../common/components/LinkButton";
/** /**
* The `PostUser` component shows the avatar and username of a post's author. * The `PostUser` component shows the avatar and username of a post's author.
@ -40,11 +41,11 @@ export default class PostUser extends Component<PostProps> {
return ( return (
<div className="PostUser"> <div className="PostUser">
<h3> <h3>
<m.route.Link href={app.route.user(user)}> <LinkButton href={app.route.user(user)}>
{avatar(user, {className: 'PostUser-avatar'})} {avatar(user, {className: 'PostUser-avatar'})}
{userOnline(user)} {userOnline(user)}
{username(user)} {username(user)}
</m.route.Link> </LinkButton>
</h3> </h3>
<ul className="PostUser-badges badges"> <ul className="PostUser-badges badges">
{listItems(user.badges().toArray())} {listItems(user.badges().toArray())}

View File

@ -78,12 +78,17 @@ export default abstract class UserPage extends Page {
loadUser(username: string) { loadUser(username: string) {
const lowercaseUsername = username.toLowerCase(); const lowercaseUsername = username.toLowerCase();
// Load the preloaded user object, if any, into the global app store
// We don't use the output of the method because it returns raw JSON
// instead of the parsed models
app.preloadedApiDocument();
if (lowercaseUsername == this.username) return; if (lowercaseUsername == this.username) return;
this.username = lowercaseUsername; this.username = lowercaseUsername;
app.store.all<User>('users').some(user => { app.store.all<User>('users').some(user => {
if (user.username().toLowerCase() === lowercaseUsername && user.joinTime()) { if ((user.username().toLowerCase() === lowercaseUsername || user.id() === username) && user.joinTime()) {
this.show(user); this.show(user);
return true; return true;
} }

View File

@ -1,3 +1,5 @@
export type KeyboardEventCallback = (KeyboardEvent) => boolean|void;
/** /**
* The `KeyboardNavigatable` class manages lists that can be navigated with the * The `KeyboardNavigatable` class manages lists that can be navigated with the
* keyboard, calling callbacks for each actions. * keyboard, calling callbacks for each actions.
@ -6,17 +8,24 @@
* API for use. * API for use.
*/ */
export default class KeyboardNavigatable { export default class KeyboardNavigatable {
callbacks = {}; /**
* Callback to be executed for a specified input.
*
* @callback KeyboardNavigatable~keyCallback
* @param {KeyboardEvent} event
* @returns {boolean}
*/
callbacks: { [key: number]: KeyboardEventCallback } = {};
// By default, always handle keyboard navigation. // By default, always handle keyboard navigation.
whenCallback = () => true; whenCallback: KeyboardEventCallback = () => true;
/** /**
* Provide a callback to be executed when navigating upwards. * Provide a callback to be executed when navigating upwards.
* *
* This will be triggered by the Up key. * This will be triggered by the Up key.
*/ */
onUp(callback: Function): this { onUp(callback: KeyboardEventCallback): this {
this.callbacks[38] = e => { this.callbacks[38] = e => {
e.preventDefault(); e.preventDefault();
callback(e); callback(e);
@ -30,7 +39,7 @@ export default class KeyboardNavigatable {
* *
* This will be triggered by the Down key. * This will be triggered by the Down key.
*/ */
onDown(callback: Function): this { onDown(callback: KeyboardEventCallback): this {
this.callbacks[40] = e => { this.callbacks[40] = e => {
e.preventDefault(); e.preventDefault();
callback(e); callback(e);
@ -44,7 +53,7 @@ export default class KeyboardNavigatable {
* *
* This will be triggered by the Return and Tab keys.. * This will be triggered by the Return and Tab keys..
*/ */
onSelect(callback: Function): this { onSelect(callback: KeyboardEventCallback): this {
this.callbacks[9] = this.callbacks[13] = e => { this.callbacks[9] = this.callbacks[13] = e => {
e.preventDefault(); e.preventDefault();
callback(e); callback(e);
@ -87,7 +96,7 @@ export default class KeyboardNavigatable {
/** /**
* Provide a callback that determines whether keyboard input should be handled. * Provide a callback that determines whether keyboard input should be handled.
*/ */
when(callback: () => boolean): this { when(callback: KeyboardEventCallback): this {
this.whenCallback = callback; this.whenCallback = callback;
return this; return this;
@ -106,7 +115,7 @@ export default class KeyboardNavigatable {
*/ */
navigate(event: KeyboardEvent) { navigate(event: KeyboardEvent) {
// This callback determines whether keyboard should be handled or ignored. // This callback determines whether keyboard should be handled or ignored.
if (!this.whenCallback()) return; if (!this.whenCallback(event)) return;
const keyCallback = this.callbacks[event.which]; const keyCallback = this.callbacks[event.which];
if (keyCallback) { if (keyCallback) {

View File

@ -1,3 +1,4 @@
import Alert from '../../common/components/Alert';
import Button from '../../common/components/Button'; import Button from '../../common/components/Button';
import Separator from '../../common/components/Separator'; import Separator from '../../common/components/Separator';
// import EditUserModal from '../components/EditUserModal'; // import EditUserModal from '../components/EditUserModal';
@ -48,7 +49,7 @@ export default {
items.add('edit', Button.component({ items.add('edit', Button.component({
icon: 'fas fa-pencil-alt', icon: 'fas fa-pencil-alt',
children: app.translator.trans('core.forum.user_controls.edit_button'), children: app.translator.trans('core.forum.user_controls.edit_button'),
onclick: this.editAction.bind(user) onclick: this.editAction.bind(this, user)
})); }));
} }
@ -65,7 +66,7 @@ export default {
items.add('delete', Button.component({ items.add('delete', Button.component({
icon: 'fas fa-times', icon: 'fas fa-times',
children: app.translator.trans('core.forum.user_controls.delete_button'), children: app.translator.trans('core.forum.user_controls.delete_button'),
onclick: this.deleteAction.bind(user) onclick: this.deleteAction.bind(this, user)
})); }));
} }
@ -75,22 +76,40 @@ export default {
/** /**
* Delete the user. * Delete the user.
*/ */
deleteAction(this: User) { deleteAction(user: User) {
if (confirm(app.translator.transText('core.forum.user_controls.delete_confirmation'))) { if (!confirm(app.translator.trans('core.forum.user_controls.delete_confirmation'))) {
this.delete().then(() => { return;
if (app.current instanceof UserPage && app.current.user === this) {
app.history.back();
} else {
window.location.reload();
}
});
} }
user.delete().then(() => {
this.showDeletionAlert(user, 'success');
if (app.current instanceof UserPage && app.current.user === user) {
app.history.back();
} else {
window.location.reload();
}
}).catch(() => this.showDeletionAlert(user, 'error'));
},
/**
* Show deletion alert of user.
*/
showDeletionAlert(user: User, type: string) {
const { username, email } = user.data.attributes;
const message = `core.forum.user_controls.delete_${type}_message`;
app.alerts.show(Alert.component({
type,
children: app.translator.trans(
message, { username, email }
)
}));
}, },
/** /**
* Edit the user. * Edit the user.
*/ */
editAction(this: User) { editAction(user: User) {
app.modal.show(new EditUserModal({user: this})); app.modal.show(new EditUserModal({user}));
} }
}; };