mirror of
https://github.com/flarum/framework.git
synced 2025-06-22 18:41:30 +08:00
Implement latest 'master' branch changes - not including files that haven't been ported yet
This commit is contained in:
2
js/dist/forum.js
vendored
2
js/dist/forum.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/forum.js.map
vendored
2
js/dist/forum.js.map
vendored
File diff suppressed because one or more lines are too long
@ -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}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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'.
|
||||||
*/
|
*/
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
36
js/src/common/components/RequestErrorModal.tsx
Normal file
36
js/src/common/components/RequestErrorModal.tsx
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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())}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user