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:
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 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}));
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,9 @@
|
||||
import * as extend from './extend';
|
||||
|
||||
import Modal from './components/Modal';
|
||||
|
||||
export default {
|
||||
extend: extend,
|
||||
|
||||
'components/Modal': Modal
|
||||
};
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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'.
|
||||
*/
|
||||
|
@@ -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.
|
||||
*/
|
||||
|
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();
|
||||
m.redraw();
|
||||
};
|
||||
image.crossOrigin = 'anonymous';
|
||||
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 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.
|
||||
*/
|
||||
|
@@ -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())}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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}));
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user