1
0
mirror of https://github.com/flarum/core.git synced 2025-08-04 15:37:51 +02:00

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 Session from './Session';
import Store from './Store';
import {extend} from './extend';
import extract from './utils/extract';
import mapRoutes from './utils/mapRoutes';
import Drawer from './utils/Drawer';
import {extend} from './extend';
import RequestError from './utils/RequestError';
import Forum from './models/Forum';
import Discussion from './models/Discussion';
@@ -17,9 +18,10 @@ import Post from './models/Post';
import Group from './models/Group';
import Notification from './models/Notification';
import RequestError from './utils/RequestError';
import Alert from './components/Alert';
import Button from './components/Button';
import ModalManager from './components/ModalManager';
import RequestErrorModal from './components/RequestErrorModal';
export type ApplicationData = {
apiDocument: any;
@@ -280,9 +282,20 @@ export default abstract class Application {
children = this.translator.trans('core.lib.error.generic_message');
}
const isDebug = app.forum.attribute('debug');
this.showDebug(error);
error.alert = Alert.component({
type: 'error',
children
children,
controls: isDebug && [
Button.component({
className: 'Button Button--link',
onclick: this.showDebug.bind(this, error),
children: 'DEBUG', // TODO make translatable
})
]
});
try {
@@ -294,7 +307,11 @@ export default abstract class Application {
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 Modal from './components/Modal';
export default {
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.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) {
attrs.title = extractText(this.props.children);
}

View File

@@ -21,10 +21,9 @@ interface LinkButtonProps extends ButtonProps {
export default class LinkButton extends Button<LinkButtonProps> {
static initProps(props: LinkButtonProps) {
props.active = this.isActive(props);
props.oncreate = props.oncreate;
}
view(vnode) {
view(vnode) {
const vdom = super.view(vnode);
vdom.tag = m.route.Link;
@@ -32,6 +31,12 @@ export default class LinkButton extends Button<LinkButtonProps> {
return vdom;
}
onupdate(vnode) {
super.onupdate(vnode);
this.props.active = LinkButton.isActive(this.props);
}
/**
* 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.
*/

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();
m.redraw();
};
image.crossOrigin = 'anonymous';
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 attrs = this.attrs();
attrs.className = classNames('Post', this.loading && 'Post--loading', attrs.className);
attrs.className = classNames(this.classes(attrs.className));
return (
<article {...attrs}>
@@ -102,6 +102,20 @@ export default class Post<T extends PostProps = PostProps> extends Component<Pos
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.
*/

View File

@@ -5,6 +5,7 @@ import username from '../../common/helpers/username';
import userOnline from '../../common/helpers/userOnline';
import listItems from '../../common/helpers/listItems';
import {PostProps} from "./Post";
import LinkButton from "../../common/components/LinkButton";
/**
* 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 (
<div className="PostUser">
<h3>
<m.route.Link href={app.route.user(user)}>
<LinkButton href={app.route.user(user)}>
{avatar(user, {className: 'PostUser-avatar'})}
{userOnline(user)}
{username(user)}
</m.route.Link>
</LinkButton>
</h3>
<ul className="PostUser-badges badges">
{listItems(user.badges().toArray())}

View File

@@ -78,12 +78,17 @@ export default abstract class UserPage extends Page {
loadUser(username: string) {
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;
this.username = lowercaseUsername;
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);
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
* keyboard, calling callbacks for each actions.
@@ -6,17 +8,24 @@
* API for use.
*/
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.
whenCallback = () => true;
whenCallback: KeyboardEventCallback = () => true;
/**
* Provide a callback to be executed when navigating upwards.
*
* This will be triggered by the Up key.
*/
onUp(callback: Function): this {
onUp(callback: KeyboardEventCallback): this {
this.callbacks[38] = e => {
e.preventDefault();
callback(e);
@@ -30,7 +39,7 @@ export default class KeyboardNavigatable {
*
* This will be triggered by the Down key.
*/
onDown(callback: Function): this {
onDown(callback: KeyboardEventCallback): this {
this.callbacks[40] = e => {
e.preventDefault();
callback(e);
@@ -44,7 +53,7 @@ export default class KeyboardNavigatable {
*
* 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 => {
e.preventDefault();
callback(e);
@@ -87,7 +96,7 @@ export default class KeyboardNavigatable {
/**
* Provide a callback that determines whether keyboard input should be handled.
*/
when(callback: () => boolean): this {
when(callback: KeyboardEventCallback): this {
this.whenCallback = callback;
return this;
@@ -106,7 +115,7 @@ export default class KeyboardNavigatable {
*/
navigate(event: KeyboardEvent) {
// 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];
if (keyCallback) {

View File

@@ -1,3 +1,4 @@
import Alert from '../../common/components/Alert';
import Button from '../../common/components/Button';
import Separator from '../../common/components/Separator';
// import EditUserModal from '../components/EditUserModal';
@@ -48,7 +49,7 @@ export default {
items.add('edit', Button.component({
icon: 'fas fa-pencil-alt',
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({
icon: 'fas fa-times',
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.
*/
deleteAction(this: User) {
if (confirm(app.translator.transText('core.forum.user_controls.delete_confirmation'))) {
this.delete().then(() => {
if (app.current instanceof UserPage && app.current.user === this) {
app.history.back();
} else {
window.location.reload();
}
});
deleteAction(user: User) {
if (!confirm(app.translator.trans('core.forum.user_controls.delete_confirmation'))) {
return;
}
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.
*/
editAction(this: User) {
app.modal.show(new EditUserModal({user: this}));
editAction(user: User) {
app.modal.show(new EditUserModal({user}));
}
};