mirror of
https://github.com/flarum/core.git
synced 2025-08-16 13:24:11 +02:00
Compare commits
12 Commits
as/dropdow
...
as/writabl
Author | SHA1 | Date | |
---|---|---|---|
|
31a2f67462 | ||
|
3da7d7d221 | ||
|
02f351001c | ||
|
6a909386b2 | ||
|
17d25ba4ce | ||
|
c7662a320f | ||
|
5a9f60d250 | ||
|
c522657212 | ||
|
2b87f10738 | ||
|
29c290e78f | ||
|
38c3ccd6be | ||
|
71cb8c378f |
@@ -1,4 +1,6 @@
|
||||
export default class AppearancePage extends AdminPage<import("../../common/components/Page").IPageAttrs> {
|
||||
constructor();
|
||||
colorItems(): ItemList<any>;
|
||||
}
|
||||
import AdminPage from "./AdminPage";
|
||||
import ItemList from "../../common/utils/ItemList";
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/// <reference path="../../../src/common/translator-icu-rich.d.ts" />
|
||||
import Modal from '../../common/components/Modal';
|
||||
export default class LoadingModal<ModalAttrs = {}> extends Modal<ModalAttrs> {
|
||||
/**
|
||||
@@ -5,7 +6,7 @@ export default class LoadingModal<ModalAttrs = {}> extends Modal<ModalAttrs> {
|
||||
*/
|
||||
static readonly isDismissible: boolean;
|
||||
className(): string;
|
||||
title(): any;
|
||||
title(): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
|
||||
content(): string;
|
||||
onsubmit(e: Event): void;
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/// <reference path="../../../src/common/translator-icu-rich.d.ts" />
|
||||
/// <reference types="mithril" />
|
||||
import type User from '../../common/models/User';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
@@ -65,8 +66,8 @@ export default class UserListPage extends AdminPage {
|
||||
headerInfo(): {
|
||||
className: string;
|
||||
icon: string;
|
||||
title: any;
|
||||
description: any;
|
||||
title: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
|
||||
description: import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
|
||||
};
|
||||
/**
|
||||
* Asynchronously fetch the next set of users to be rendered.
|
||||
|
5
js/dist-typings/admin/routes.d.ts
vendored
5
js/dist-typings/admin/routes.d.ts
vendored
@@ -1,6 +1,5 @@
|
||||
import AdminApplication from './AdminApplication';
|
||||
/**
|
||||
* The `routes` initializer defines the forum app's routes.
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
export default function _default(app: any): void;
|
||||
export default function (app: AdminApplication): void;
|
||||
|
13
js/dist-typings/common/Application.d.ts
vendored
13
js/dist-typings/common/Application.d.ts
vendored
@@ -13,10 +13,7 @@ import type Mithril from 'mithril';
|
||||
import type Component from './Component';
|
||||
import type { ComponentAttrs } from './Component';
|
||||
export declare type FlarumScreens = 'phone' | 'tablet' | 'desktop' | 'desktop-hd';
|
||||
export declare type FlarumGenericRoute = RouteItem<Record<string, unknown>, Component<{
|
||||
routeName: string;
|
||||
[key: string]: unknown;
|
||||
}>, Record<string, unknown>>;
|
||||
export declare type FlarumGenericRoute = RouteItem<any, any, any>;
|
||||
export interface FlarumRequestOptions<ResponseType> extends Omit<Mithril.RequestOptions<ResponseType>, 'extract'> {
|
||||
errorHandler?: (error: RequestError) => void;
|
||||
url: string;
|
||||
@@ -52,18 +49,14 @@ export declare type RouteItem<Attrs extends ComponentAttrs, Comp extends Compone
|
||||
/**
|
||||
* The component to render when this route matches.
|
||||
*/
|
||||
component: {
|
||||
new (): Comp;
|
||||
};
|
||||
component: new () => Comp;
|
||||
/**
|
||||
* A custom resolver class.
|
||||
*
|
||||
* This should be the class itself, and **not** an instance of the
|
||||
* class.
|
||||
*/
|
||||
resolverClass?: {
|
||||
new (): DefaultResolver<Attrs, Comp, RouteArgs>;
|
||||
};
|
||||
resolverClass?: new (component: new () => Comp, routeName: string) => DefaultResolver<Attrs, Comp, RouteArgs>;
|
||||
} | {
|
||||
/**
|
||||
* An instance of a route resolver.
|
||||
|
11
js/dist-typings/common/Translator.d.ts
vendored
11
js/dist-typings/common/Translator.d.ts
vendored
@@ -1,3 +1,6 @@
|
||||
/// <reference path="../../src/common/translator-icu-rich.d.ts" />
|
||||
import { RichMessageFormatter } from '@askvortsov/rich-icu-message-formatter';
|
||||
import { pluralTypeHandler, selectTypeHandler } from '@ultraq/icu-message-formatter';
|
||||
declare type Translations = Record<string, string>;
|
||||
declare type TranslatorParameters = Record<string, unknown>;
|
||||
export default class Translator {
|
||||
@@ -8,15 +11,15 @@ export default class Translator {
|
||||
/**
|
||||
* The underlying ICU MessageFormatter util.
|
||||
*/
|
||||
protected formatter: any;
|
||||
protected formatter: RichMessageFormatter;
|
||||
setLocale(locale: string): void;
|
||||
addTranslations(translations: Translations): void;
|
||||
/**
|
||||
* An extensible entrypoint for extenders to register type handlers for translations.
|
||||
*/
|
||||
protected formatterTypeHandlers(): {
|
||||
plural: any;
|
||||
select: any;
|
||||
plural: typeof pluralTypeHandler;
|
||||
select: typeof selectTypeHandler;
|
||||
};
|
||||
/**
|
||||
* A temporary system to preprocess parameters.
|
||||
@@ -26,6 +29,6 @@ export default class Translator {
|
||||
* @internal
|
||||
*/
|
||||
protected preprocessParameters(parameters: TranslatorParameters): TranslatorParameters;
|
||||
trans(id: string, parameters?: TranslatorParameters): any;
|
||||
trans(id: string, parameters?: TranslatorParameters): import("@askvortsov/rich-icu-message-formatter").NestedStringArray;
|
||||
}
|
||||
export {};
|
||||
|
@@ -11,13 +11,9 @@ import type { default as Component, ComponentAttrs } from '../Component';
|
||||
export default class DefaultResolver<Attrs extends ComponentAttrs, Comp extends Component<Attrs & {
|
||||
routeName: string;
|
||||
}>, RouteArgs extends Record<string, unknown> = {}> implements RouteResolver<Attrs, Comp, RouteArgs> {
|
||||
component: {
|
||||
new (): Comp;
|
||||
};
|
||||
component: new () => Comp;
|
||||
routeName: string;
|
||||
constructor(component: {
|
||||
new (): Comp;
|
||||
}, routeName: string);
|
||||
constructor(component: new () => Comp, routeName: string);
|
||||
/**
|
||||
* When a route change results in a changed key, a full page
|
||||
* rerender occurs. This method can be overriden in subclasses
|
||||
|
2
js/dist-typings/forum/ForumApplication.d.ts
vendored
2
js/dist-typings/forum/ForumApplication.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
import History from './utils/History';
|
||||
import Pane from './utils/Pane';
|
||||
import { makeRouteHelpers } from './routes';
|
||||
import Application from '../common/Application';
|
||||
import NotificationListState from './states/NotificationListState';
|
||||
import GlobalSearchState from './states/GlobalSearchState';
|
||||
@@ -44,6 +45,7 @@ export default class ForumApplication extends Application {
|
||||
* is used in the index page and the slideout pane.
|
||||
*/
|
||||
discussions: DiscussionListState;
|
||||
route: typeof Application.prototype.route & ReturnType<typeof makeRouteHelpers>;
|
||||
constructor();
|
||||
/**
|
||||
* @inheritdoc
|
||||
|
22
js/dist-typings/forum/routes.d.ts
vendored
22
js/dist-typings/forum/routes.d.ts
vendored
@@ -1,6 +1,22 @@
|
||||
import ForumApplication from './ForumApplication';
|
||||
import Discussion from '../common/models/Discussion';
|
||||
import Post from '../common/models/Post';
|
||||
import User from '../common/models/User';
|
||||
/**
|
||||
* The `routes` initializer defines the forum app's routes.
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
export default function _default(app: any): void;
|
||||
export default function (app: ForumApplication): void;
|
||||
export declare function makeRouteHelpers(app: ForumApplication): {
|
||||
/**
|
||||
* Generate a URL to a discussion.
|
||||
*/
|
||||
discussion: (discussion: Discussion, near: number) => string;
|
||||
/**
|
||||
* Generate a URL to a post.
|
||||
*/
|
||||
post: (post: Post) => string;
|
||||
/**
|
||||
* Generate a URL to a user.
|
||||
*/
|
||||
user: (user: User) => string;
|
||||
};
|
||||
|
2
js/dist/admin.js
generated
vendored
2
js/dist/admin.js
generated
vendored
File diff suppressed because one or more lines are too long
2
js/dist/admin.js.map
generated
vendored
2
js/dist/admin.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
js/dist/forum.js
generated
vendored
2
js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
js/dist/forum.js.map
generated
vendored
2
js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ import EditCustomHeaderModal from './EditCustomHeaderModal';
|
||||
import EditCustomFooterModal from './EditCustomFooterModal';
|
||||
import UploadImageButton from './UploadImageButton';
|
||||
import AdminPage from './AdminPage';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
|
||||
export default class AppearancePage extends AdminPage {
|
||||
headerInfo() {
|
||||
@@ -21,34 +22,7 @@ export default class AppearancePage extends AdminPage {
|
||||
<div className="Form">
|
||||
<fieldset className="AppearancePage-colors">
|
||||
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
|
||||
<div className="helpText">{app.translator.trans('core.admin.appearance.colors_text')}</div>
|
||||
|
||||
<div className="AppearancePage-colors-input">
|
||||
{this.buildSettingComponent({
|
||||
type: 'color-preview',
|
||||
setting: 'theme_primary_color',
|
||||
placeholder: '#aaaaaa',
|
||||
})}
|
||||
{this.buildSettingComponent({
|
||||
type: 'color-preview',
|
||||
setting: 'theme_secondary_color',
|
||||
placeholder: '#aaaaaa',
|
||||
})}
|
||||
</div>
|
||||
|
||||
{this.buildSettingComponent({
|
||||
type: 'switch',
|
||||
setting: 'theme_dark_mode',
|
||||
label: app.translator.trans('core.admin.appearance.dark_mode_label'),
|
||||
})}
|
||||
|
||||
{this.buildSettingComponent({
|
||||
type: 'switch',
|
||||
setting: 'theme_colored_header',
|
||||
label: app.translator.trans('core.admin.appearance.colored_header_label'),
|
||||
})}
|
||||
|
||||
{this.submitButton()}
|
||||
{this.colorItems().toArray()}
|
||||
</fieldset>
|
||||
</div>,
|
||||
|
||||
@@ -102,6 +76,53 @@ export default class AppearancePage extends AdminPage {
|
||||
];
|
||||
}
|
||||
|
||||
colorItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('helpText', <div className="helpText">{app.translator.trans('core.admin.appearance.colors_text')}</div>, 80);
|
||||
|
||||
items.add(
|
||||
'theme-colors',
|
||||
<div className="AppearancePage-colors-input">
|
||||
{this.buildSettingComponent({
|
||||
type: 'color-preview',
|
||||
setting: 'theme_primary_color',
|
||||
placeholder: '#aaaaaa',
|
||||
})}
|
||||
{this.buildSettingComponent({
|
||||
type: 'color-preview',
|
||||
setting: 'theme_secondary_color',
|
||||
placeholder: '#aaaaaa',
|
||||
})}
|
||||
</div>,
|
||||
70
|
||||
);
|
||||
|
||||
items.add(
|
||||
'dark-mode',
|
||||
this.buildSettingComponent({
|
||||
type: 'switch',
|
||||
setting: 'theme_dark_mode',
|
||||
label: app.translator.trans('core.admin.appearance.dark_mode_label'),
|
||||
}),
|
||||
60
|
||||
);
|
||||
|
||||
items.add(
|
||||
'colored-header',
|
||||
this.buildSettingComponent({
|
||||
type: 'switch',
|
||||
setting: 'theme_colored_header',
|
||||
label: app.translator.trans('core.admin.appearance.colored_header_label'),
|
||||
}),
|
||||
50
|
||||
);
|
||||
|
||||
items.add('submit', this.submitButton(), 0);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
onsaved() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
@@ -38,13 +38,11 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
|
||||
|
||||
const permissionCells = (permission: PermissionGridEntry | { children: PermissionGridEntry[] }) => {
|
||||
return scopes.map((scope) => {
|
||||
// This indicates the "permission" is a permission category,
|
||||
// in which case we return an empty table cell.
|
||||
if ('children' in permission) {
|
||||
return <td></td>;
|
||||
}
|
||||
|
||||
return <td>{scope.render(permission)}</td>;
|
||||
return scope.render(permission);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -418,7 +416,7 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return '';
|
||||
},
|
||||
},
|
||||
100
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import app from '../app';
|
||||
import app from '../../admin/app';
|
||||
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||
import Button from '../../common/components/Button';
|
||||
import saveSettings from '../utils/saveSettings';
|
||||
@@ -11,23 +11,18 @@ export default class SettingDropdown extends SelectDropdown {
|
||||
attrs.buttonClassName = 'Button Button--text';
|
||||
attrs.caretIcon = 'fas fa-caret-down';
|
||||
attrs.defaultLabel = 'Custom';
|
||||
|
||||
if (key in attrs) {
|
||||
attrs.setting = attrs.key;
|
||||
delete attrs.key;
|
||||
}
|
||||
}
|
||||
|
||||
view(vnode) {
|
||||
return super.view({
|
||||
...vnode,
|
||||
children: this.attrs.options.map(({ value, label }) => {
|
||||
const active = app.data.settings[this.attrs.setting] === value;
|
||||
const active = app.data.settings[this.attrs.key] === value;
|
||||
|
||||
return Button.component(
|
||||
{
|
||||
icon: active ? 'fas fa-check' : true,
|
||||
onclick: saveSettings.bind(this, { [this.attrs.setting]: value }),
|
||||
onclick: saveSettings.bind(this, { [this.attrs.key]: value }),
|
||||
active,
|
||||
},
|
||||
label
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import AdminApplication from './AdminApplication';
|
||||
import DashboardPage from './components/DashboardPage';
|
||||
import BasicsPage from './components/BasicsPage';
|
||||
import PermissionsPage from './components/PermissionsPage';
|
||||
@@ -9,10 +10,8 @@ import ExtensionPageResolver from './resolvers/ExtensionPageResolver';
|
||||
|
||||
/**
|
||||
* The `routes` initializer defines the forum app's routes.
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
export default function (app) {
|
||||
export default function (app: AdminApplication) {
|
||||
app.routes = {
|
||||
dashboard: { path: '/', component: DashboardPage },
|
||||
basics: { path: '/basics', component: BasicsPage },
|
@@ -34,11 +34,7 @@ import type { ComponentAttrs } from './Component';
|
||||
|
||||
export type FlarumScreens = 'phone' | 'tablet' | 'desktop' | 'desktop-hd';
|
||||
|
||||
export type FlarumGenericRoute = RouteItem<
|
||||
Record<string, unknown>,
|
||||
Component<{ routeName: string; [key: string]: unknown }>,
|
||||
Record<string, unknown>
|
||||
>;
|
||||
export type FlarumGenericRoute = RouteItem<any, any, any>;
|
||||
|
||||
export interface FlarumRequestOptions<ResponseType> extends Omit<Mithril.RequestOptions<ResponseType>, 'extract'> {
|
||||
errorHandler?: (error: RequestError) => void;
|
||||
@@ -80,14 +76,14 @@ export type RouteItem<
|
||||
/**
|
||||
* The component to render when this route matches.
|
||||
*/
|
||||
component: { new (): Comp };
|
||||
component: new () => Comp;
|
||||
/**
|
||||
* A custom resolver class.
|
||||
*
|
||||
* This should be the class itself, and **not** an instance of the
|
||||
* class.
|
||||
*/
|
||||
resolverClass?: { new (): DefaultResolver<Attrs, Comp, RouteArgs> };
|
||||
resolverClass?: new (component: new () => Comp, routeName: string) => DefaultResolver<Attrs, Comp, RouteArgs>;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
|
@@ -1,61 +1,27 @@
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
import app from '../../common/app';
|
||||
import Component, { ComponentAttrs } from '../Component';
|
||||
import Component from '../Component';
|
||||
import icon from '../helpers/icon';
|
||||
import listItems from '../helpers/listItems';
|
||||
|
||||
export interface IDropdownAttrs extends ComponentAttrs{
|
||||
/**
|
||||
* A class name to apply to the dropdown toggle button.
|
||||
*/
|
||||
buttonClassName?: string;
|
||||
|
||||
/**
|
||||
* A class name to apply to the dropdown menu.
|
||||
*/
|
||||
menuClassName?: string;
|
||||
|
||||
/**
|
||||
* The name of an icon to show in the dropdown toggle button.
|
||||
*/
|
||||
icon?: string;
|
||||
|
||||
/**
|
||||
* The name of an icon to show on the right of the button.
|
||||
*/
|
||||
caretIcon?: string;
|
||||
|
||||
/**
|
||||
* The label of the dropdown toggle button. Defaults to 'Controls'.
|
||||
*/
|
||||
label?: Mithril.Children;
|
||||
|
||||
/**
|
||||
* The label used to describe the dropdown toggle button to assistive readers.
|
||||
* Defaults to 'Toggle dropdown menu'.
|
||||
*/
|
||||
accessibleToggleLabel?: string;
|
||||
|
||||
/**
|
||||
* A callback to run when the dropdown is shown.
|
||||
*/
|
||||
onshow?: () => void;
|
||||
|
||||
/**
|
||||
* A callback to run when the dropdown is hidden.
|
||||
*/
|
||||
onhide?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `Dropdown` component displays a button which, when clicked, shows a
|
||||
* dropdown menu beneath it.
|
||||
*
|
||||
* ### Attrs
|
||||
*
|
||||
* - `buttonClassName` A class name to apply to the dropdown toggle button.
|
||||
* - `menuClassName` A class name to apply to the dropdown menu.
|
||||
* - `icon` The name of an icon to show in the dropdown toggle button.
|
||||
* - `caretIcon` The name of an icon to show on the right of the button.
|
||||
* - `label` The label of the dropdown toggle button. Defaults to 'Controls'.
|
||||
* - `accessibleToggleLabel` The label used to describe the dropdown toggle button to assistive readers. Defaults to 'Toggle dropdown menu'.
|
||||
* - `onhide`
|
||||
* - `onshow`
|
||||
*
|
||||
* The children will be displayed as a list inside of the dropdown menu.
|
||||
*/
|
||||
export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttrs> extends Component<CustomAttrs> {
|
||||
static initAttrs(attrs: IDropdownAttrs) {
|
||||
export default class Dropdown extends Component {
|
||||
static initAttrs(attrs) {
|
||||
attrs.className = attrs.className || '';
|
||||
attrs.buttonClassName = attrs.buttonClassName || '';
|
||||
attrs.menuClassName = attrs.menuClassName || '';
|
||||
@@ -64,9 +30,13 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
|
||||
attrs.accessibleToggleLabel = attrs.accessibleToggleLabel || app.translator.trans('core.lib.dropdown.toggle_dropdown_accessible_label');
|
||||
}
|
||||
|
||||
protected showing = false;
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
view(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
this.showing = false;
|
||||
}
|
||||
|
||||
view(vnode) {
|
||||
const items = vnode.children ? listItems(vnode.children) : [];
|
||||
const renderItems = this.attrs.lazyDraw ? this.showing : true;
|
||||
|
||||
@@ -78,7 +48,7 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
|
||||
);
|
||||
}
|
||||
|
||||
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
// When opening the dropdown menu, work out if the menu goes beyond the
|
||||
@@ -110,13 +80,13 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
|
||||
|
||||
$menu.removeClass('Dropdown-menu--top Dropdown-menu--right');
|
||||
|
||||
$menu.toggleClass('Dropdown-menu--top', $menu.offset()!.top + $menu.height()! > $(window).scrollTop()! + $(window).height()!);
|
||||
$menu.toggleClass('Dropdown-menu--top', $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height());
|
||||
|
||||
if ($menu.offset()!.top < 0) {
|
||||
if ($menu.offset().top < 0) {
|
||||
$menu.removeClass('Dropdown-menu--top');
|
||||
}
|
||||
|
||||
$menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset()!.left + $menu.width()! > $(window).scrollLeft()! + $(window).width()!);
|
||||
$menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width());
|
||||
});
|
||||
|
||||
this.$().on('hidden.bs.dropdown', () => {
|
||||
@@ -132,8 +102,11 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
|
||||
|
||||
/**
|
||||
* Get the template for the button.
|
||||
*
|
||||
* @return {*}
|
||||
* @protected
|
||||
*/
|
||||
protected getButton(children: Mithril.Children): Mithril.Children {
|
||||
getButton(children) {
|
||||
return (
|
||||
<button
|
||||
className={'Dropdown-toggle ' + this.attrs.buttonClassName}
|
||||
@@ -149,8 +122,11 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
|
||||
|
||||
/**
|
||||
* Get the template for the button's content.
|
||||
*
|
||||
* @return {*}
|
||||
* @protected
|
||||
*/
|
||||
protected getButtonContent(children: Mithril.Children): Mithril.Children {
|
||||
getButtonContent(children) {
|
||||
return [
|
||||
this.attrs.icon ? icon(this.attrs.icon, { className: 'Button-icon' }) : '',
|
||||
<span className="Button-label">{this.attrs.label}</span>,
|
||||
@@ -158,7 +134,7 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
|
||||
];
|
||||
}
|
||||
|
||||
protected getMenu(items: Mithril.Children): Mithril.Children {
|
||||
getMenu(items) {
|
||||
return <ul className={'Dropdown-menu dropdown-menu ' + this.attrs.menuClassName}>{items}</ul>;
|
||||
}
|
||||
}
|
@@ -1,7 +1,5 @@
|
||||
import type Mithril from 'mithril';
|
||||
import Dropdown, { IDropdownAttrs } from './Dropdown';
|
||||
import Dropdown from './Dropdown';
|
||||
import icon from '../helpers/icon';
|
||||
import { ModdedVnode } from '../helpers/listItems';
|
||||
|
||||
/**
|
||||
* Determines via a vnode is currently "active".
|
||||
@@ -11,8 +9,8 @@ import { ModdedVnode } from '../helpers/listItems';
|
||||
*
|
||||
* This is a temporary patch, and as so, is not exported / placed in utils.
|
||||
*/
|
||||
function isActive(vnode: ModdedVnode<{}>) {
|
||||
const tag = vnode.tag as VnodeElementTag;
|
||||
function isActive(vnode) {
|
||||
const tag = vnode.tag;
|
||||
|
||||
// Allow non-selectable dividers/headers to be added.
|
||||
if (typeof tag === 'string' && tag !== 'a' && tag !== 'button') return false;
|
||||
@@ -21,29 +19,21 @@ function isActive(vnode: ModdedVnode<{}>) {
|
||||
tag.initAttrs(vnode.attrs);
|
||||
}
|
||||
|
||||
return 'isActive' in tag ? tag.isActive(vnode.attrs) : (vnode.attrs as any).active;
|
||||
}
|
||||
|
||||
export interface ISelectDropdownAttrs extends IDropdownAttrs {
|
||||
/**
|
||||
* An icon for the select dropdown's caret.
|
||||
*/
|
||||
caretIcon?: string;
|
||||
|
||||
/**
|
||||
* The default label if no child is active.
|
||||
*/
|
||||
defaultLabel?: Mithril.Children;
|
||||
|
||||
return 'isActive' in tag ? tag.isActive(vnode.attrs) : vnode.attrs.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `SelectDropdown` component is the same as a `Dropdown`, except the toggle
|
||||
* button's label is set as the label of the first child which has a truthy
|
||||
* `active` prop.
|
||||
*
|
||||
* ### Attrs
|
||||
*
|
||||
* - `caretIcon`
|
||||
* - `defaultLabel`
|
||||
*/
|
||||
export default class SelectDropdown<CustomAttrs extends ISelectDropdownAttrs = ISelectDropdownAttrs> extends Dropdown<CustomAttrs> {
|
||||
static initAttrs(attrs: ISelectDropdownAttrs) {
|
||||
export default class SelectDropdown extends Dropdown {
|
||||
static initAttrs(attrs) {
|
||||
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-sort';
|
||||
|
||||
super.initAttrs(attrs);
|
||||
@@ -51,12 +41,12 @@ export default class SelectDropdown<CustomAttrs extends ISelectDropdownAttrs = I
|
||||
attrs.className += ' Dropdown--select';
|
||||
}
|
||||
|
||||
protected getButtonContent(children: Mithril.Children): Mithril.ChildArray {
|
||||
const activeChild = Array.isArray(children) ? children.find(isActive) : children;
|
||||
let label = (activeChild && typeof activeChild === 'object' && 'children' in activeChild && activeChild.children) || this.attrs.defaultLabel;
|
||||
getButtonContent(children) {
|
||||
const activeChild = children.find(isActive);
|
||||
let label = (activeChild && activeChild.children) || this.attrs.defaultLabel;
|
||||
|
||||
if (label instanceof Array) label = label[0];
|
||||
|
||||
return [<span className="Button-label">{label}</span>, icon(this.attrs.caretIcon!, { className: 'Button-caret' })];
|
||||
return [<span className="Button-label">{label}</span>, icon(this.attrs.caretIcon, { className: 'Button-caret' })];
|
||||
}
|
||||
}
|
@@ -1,25 +1,20 @@
|
||||
import type Mithril from 'mithril';
|
||||
import Dropdown, { IDropdownAttrs } from './Dropdown';
|
||||
import Dropdown from './Dropdown';
|
||||
import Button from './Button';
|
||||
import icon from '../helpers/icon';
|
||||
|
||||
export interface ISplitDropdownAttrs extends IDropdownAttrs {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The `SplitDropdown` component is similar to `Dropdown`, but the first child
|
||||
* is displayed as its own button prior to the toggle button.
|
||||
*/
|
||||
export default class SplitDropdown<CustomAttrs extends ISplitDropdownAttrs = ISplitDropdownAttrs> extends Dropdown<CustomAttrs> {
|
||||
static initAttrs(attrs: ISplitDropdownAttrs) {
|
||||
export default class SplitDropdown extends Dropdown {
|
||||
static initAttrs(attrs) {
|
||||
super.initAttrs(attrs);
|
||||
|
||||
attrs.className += ' Dropdown--split';
|
||||
attrs.menuClassName += ' Dropdown-menu--right';
|
||||
}
|
||||
|
||||
getButton(children: Mithril.ChildArray): Mithril.Children {
|
||||
getButton(children) {
|
||||
// Make a copy of the attrs of the first child component. We will assign
|
||||
// these attrs to a new button, so that it has exactly the same behaviour as
|
||||
// the first child.
|
||||
@@ -44,8 +39,11 @@ export default class SplitDropdown<CustomAttrs extends ISplitDropdownAttrs = ISp
|
||||
/**
|
||||
* Get the first child. If the first child is an array, the first item in that
|
||||
* array will be returned.
|
||||
*
|
||||
* @return {*}
|
||||
* @protected
|
||||
*/
|
||||
protected getFirstChild(children: Mithril.Children): Mithril.Vnode {
|
||||
getFirstChild(children) {
|
||||
let firstChild = children;
|
||||
|
||||
while (firstChild instanceof Array) firstChild = firstChild[0];
|
@@ -15,10 +15,10 @@ export default class DefaultResolver<
|
||||
RouteArgs extends Record<string, unknown> = {}
|
||||
> implements RouteResolver<Attrs, Comp, RouteArgs>
|
||||
{
|
||||
component: { new (): Comp };
|
||||
component: new () => Comp;
|
||||
routeName: string;
|
||||
|
||||
constructor(component: { new (): Comp }, routeName: string) {
|
||||
constructor(component: new () => Comp, routeName: string) {
|
||||
this.component = component;
|
||||
this.routeName = routeName;
|
||||
}
|
||||
|
26
js/src/common/translator-icu-rich.d.ts
vendored
Normal file
26
js/src/common/translator-icu-rich.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
declare module '@askvortsov/rich-icu-message-formatter' {
|
||||
type IValues = Record<string, any>;
|
||||
|
||||
type ITypeHandler = (
|
||||
value: string,
|
||||
matches: string,
|
||||
locale: string,
|
||||
values: IValues,
|
||||
format: (message: string, values: IValues) => string
|
||||
) => string;
|
||||
type IRichHandler = (tag: any, values: IValues, contents: string) => any;
|
||||
|
||||
type ValueOrArray<T> = T | ValueOrArray<T>[];
|
||||
type NestedStringArray = ValueOrArray<string>;
|
||||
|
||||
export class RichMessageFormatter {
|
||||
locale: string | null;
|
||||
constructor(locale: string | null, typeHandlers: Record<string, ITypeHandler>, richHandler: IRichHandler);
|
||||
|
||||
format(message: string, values: IValues): string;
|
||||
process(message: string, values: IValues): NestedStringArray;
|
||||
rich(message: string, values: IValues): NestedStringArray;
|
||||
}
|
||||
|
||||
export function mithrilRichHandler(tag: any, values: IValues, contents: string): any;
|
||||
}
|
17
js/src/common/translator-icu.d.ts
vendored
Normal file
17
js/src/common/translator-icu.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
declare module '@ultraq/icu-message-formatter' {
|
||||
export function pluralTypeHandler(
|
||||
value: string,
|
||||
matches: string,
|
||||
locale: string,
|
||||
values: Record<string, any>,
|
||||
format: (text: string, values: Record<string, any>) => string
|
||||
): string;
|
||||
|
||||
export function selectTypeHandler(
|
||||
value: string,
|
||||
matches: string,
|
||||
locale: string,
|
||||
values: Record<string, any>,
|
||||
format: (text: string, values: Record<string, any>) => string
|
||||
): string;
|
||||
}
|
@@ -10,7 +10,7 @@ import Composer from './components/Composer';
|
||||
import DiscussionRenamedNotification from './components/DiscussionRenamedNotification';
|
||||
import CommentPost from './components/CommentPost';
|
||||
import DiscussionRenamedPost from './components/DiscussionRenamedPost';
|
||||
import routes from './routes';
|
||||
import routes, { makeRouteHelpers } from './routes';
|
||||
import alertEmailConfirmation from './utils/alertEmailConfirmation';
|
||||
import Application from '../common/Application';
|
||||
import Navigation from '../common/components/Navigation';
|
||||
@@ -73,10 +73,14 @@ export default class ForumApplication extends Application {
|
||||
*/
|
||||
discussions: DiscussionListState = new DiscussionListState({});
|
||||
|
||||
route: typeof Application.prototype.route & ReturnType<typeof makeRouteHelpers>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
routes(this);
|
||||
|
||||
this.route = Object.assign((Object.getPrototypeOf(Object.getPrototypeOf(this)) as Application).route.bind(this), makeRouteHelpers(this));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -149,7 +149,7 @@ export default class AvatarEditor extends Component {
|
||||
|
||||
// Create a hidden HTML input element and click on it so the user can select
|
||||
// an avatar file. Once they have, we will upload it via the API.
|
||||
const $input = $('<input type="file">');
|
||||
const $input = $('<input type="file" accept=".jpg, .jpeg, .png, .bmp, .gif">');
|
||||
|
||||
$input
|
||||
.appendTo('body')
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import ForumApplication from './ForumApplication';
|
||||
import IndexPage from './components/IndexPage';
|
||||
import DiscussionPage from './components/DiscussionPage';
|
||||
import PostsUserPage from './components/PostsUserPage';
|
||||
@@ -5,13 +6,14 @@ import DiscussionsUserPage from './components/DiscussionsUserPage';
|
||||
import SettingsPage from './components/SettingsPage';
|
||||
import NotificationsPage from './components/NotificationsPage';
|
||||
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
|
||||
import Discussion from '../common/models/Discussion';
|
||||
import Post from '../common/models/Post';
|
||||
import User from '../common/models/User';
|
||||
|
||||
/**
|
||||
* The `routes` initializer defines the forum app's routes.
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
export default function (app) {
|
||||
export default function (app: ForumApplication) {
|
||||
app.routes = {
|
||||
index: { path: '/all', component: IndexPage },
|
||||
|
||||
@@ -25,40 +27,34 @@ export default function (app) {
|
||||
settings: { path: '/settings', component: SettingsPage },
|
||||
notifications: { path: '/notifications', component: NotificationsPage },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a URL to a discussion.
|
||||
*
|
||||
* @param {Discussion} discussion
|
||||
* @param {Integer} [near]
|
||||
* @return {String}
|
||||
*/
|
||||
app.route.discussion = (discussion, near) => {
|
||||
return app.route(near && near !== 1 ? 'discussion.near' : 'discussion', {
|
||||
id: discussion.slug(),
|
||||
near: near && near !== 1 ? near : undefined,
|
||||
});
|
||||
};
|
||||
export function makeRouteHelpers(app: ForumApplication) {
|
||||
return {
|
||||
/**
|
||||
* Generate a URL to a discussion.
|
||||
*/
|
||||
discussion: (discussion: Discussion, near: number) => {
|
||||
return app.route(near && near !== 1 ? 'discussion.near' : 'discussion', {
|
||||
id: discussion.slug(),
|
||||
near: near && near !== 1 ? near : undefined,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a URL to a post.
|
||||
*
|
||||
* @param {Post} post
|
||||
* @return {String}
|
||||
*/
|
||||
app.route.post = (post) => {
|
||||
return app.route.discussion(post.discussion(), post.number());
|
||||
};
|
||||
/**
|
||||
* Generate a URL to a post.
|
||||
*/
|
||||
post: (post: Post) => {
|
||||
return app.route.discussion(post.discussion(), post.number());
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a URL to a user.
|
||||
*
|
||||
* @param {User} user
|
||||
* @return {String}
|
||||
*/
|
||||
app.route.user = (user) => {
|
||||
return app.route('user', {
|
||||
username: user.slug(),
|
||||
});
|
||||
/**
|
||||
* Generate a URL to a user.
|
||||
*/
|
||||
user: (user: User) => {
|
||||
return app.route('user', {
|
||||
username: user.slug(),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
@@ -28,8 +28,33 @@ class WritablePaths implements PrerequisiteInterface
|
||||
|
||||
public function problems(): Collection
|
||||
{
|
||||
return $this->getMissingPaths()
|
||||
$problems = $this->getMissingPaths()
|
||||
->concat($this->getNonWritablePaths());
|
||||
|
||||
if (! $problems->isEmpty()) {
|
||||
return $problems->prepend($this->getServerInfo());
|
||||
}
|
||||
|
||||
return $problems;
|
||||
}
|
||||
|
||||
private function getServerInfo(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'Server Metadata',
|
||||
'detail' => implode('<br />', [
|
||||
'The following information might be useful for troubleshooting the errors below.',
|
||||
'Current User:'.get_current_user(),
|
||||
'Current File Permissions:',
|
||||
$this->paths->map(function ($path) {
|
||||
return " - $path: ".substr(sprintf('%o', fileperms($path)), -4);
|
||||
})->implode('<br />'),
|
||||
'Current File Ownership:',
|
||||
$this->paths->map(function ($path) {
|
||||
return " - $path: ".posix_getpwuid(fileowner($path))['name'].':'.posix_getgrgid(filegroup($path))['name'];
|
||||
})->implode('<br />'),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function getMissingPaths(): Collection
|
||||
|
@@ -11,6 +11,8 @@ namespace Flarum\User;
|
||||
|
||||
use Flarum\Foundation\AbstractValidator;
|
||||
use Flarum\Foundation\ValidationException;
|
||||
use Intervention\Image\Exception\NotReadableException;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use Symfony\Component\Mime\MimeTypes;
|
||||
|
||||
@@ -69,6 +71,12 @@ class AvatarValidator extends AbstractValidator
|
||||
if (! in_array($guessedExtension, $allowedTypes)) {
|
||||
$this->raise('mimes', [':values' => implode(', ', $allowedTypes)]);
|
||||
}
|
||||
|
||||
try {
|
||||
(new ImageManager)->make($file->getStream());
|
||||
} catch (NotReadableException $_e) {
|
||||
$this->raise('image');
|
||||
}
|
||||
}
|
||||
|
||||
protected function assertFileSize(UploadedFileInterface $file)
|
||||
@@ -103,6 +111,6 @@ class AvatarValidator extends AbstractValidator
|
||||
|
||||
protected function getAllowedTypes()
|
||||
{
|
||||
return ['jpg', 'png', 'bmp', 'gif'];
|
||||
return ['jpeg', 'jpg', 'png', 'bmp', 'gif'];
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user