1
0
mirror of https://github.com/flarum/core.git synced 2025-08-06 16:36:47 +02:00

feat: theming and extensibility improvements (#3876)

* feat: make page structure customizable across different pages (#3867)

* feat: create `PageStructure` component
* feat: apply to `DiscussionPage`
* feat: apply to `UserPage`
* feat: apply to `TagsPage`
* fix: adapt subscriptions ext
* chore: cleanup

* chore: use grid & flexbox for the discussion list item (#3868)

* chore: rename `DiscussionPage-list` to `DiscussionListPane`
* chore: itemlistify `DiscussionListItem`
* chore: use flex and grid for `DiscussionListItem`

* chore: use flexbox for `App-header` (#3869)

* chore: use flex and grid for `App-header`
* chore: drop search floats
* fix: adapt admin styles

* chore: use flexbox in dropdowns and SplitDropdown for subscriptions (#3874)

* chore: flexbox dropdown menu items
* chore: normalize subscriptions menu (use slit dropdown)
* chore: cleanup

* chore: misc flexbox/grid changes (#3875)

* chore: `TagsPage` to tsx
* chore: `TagsPage` flexbox/grid
* chore: `IndexPage-toolbar` flexbox
* chore: `UserCard` flexbox & itemlists
* fix: `Post` improve spacing logic
* chore: `Post` grid and proper spacing
* fix: avatar editor hover layer layout
* chore: `Button` flex

* chore: normalize form semantics (#3877)

* chore: normalize fieldsets
* fix: `LinkButton` spacing
* chore: consistent form semantics

* fix: styling regressions (#3878)

* fix: post spacing goes off in other pages
* fix: regression

* feat: extract reusable components from `NotificationsDropdown` (#3879)

* feat: extensible global notices (#3880)

* fix: js error on null item list
* feat: extensible global notices

* chore: housekeeping (#3881)

* chore: use CSS variables where still not using
* chore: cleanup suspension modal
* chore: cleanup post flag
* fix: badge vertical align
* chore: use CSS variables for custom coloring
* chore: `icon` helper to `Icon` component
* chore: `avatar` helper to `Avatar` component
* fix: chunk loading fails on admin frontend
* chore: format

* feat: reusable `UploadImageButton` component (#3882)

* chore: convert `UploadImageButton` to tsx
* feat: reusable `UploadImageButton` component
* feat: add `image-upload` setting type

* feat: extensible default footer component (#3883)

* chore: yarn format
This commit is contained in:
Sami Mazouz
2023-10-10 21:36:08 +01:00
committed by GitHub
parent 24d13e33bb
commit 5ab5257ff5
174 changed files with 2671 additions and 2056 deletions

View File

@@ -1,4 +1,4 @@
import { extend } from 'flarum/common/extend';
import { extend, override } from 'flarum/common/extend';
import app from 'flarum/forum/app';
import Post from 'flarum/forum/components/Post';
import Button from 'flarum/common/components/Button';
@@ -75,7 +75,7 @@ export default function () {
return items;
};
extend(Post.prototype, 'content', function (vdom) {
override(Post.prototype, 'header', function (vdom) {
const post = this.attrs.post;
const flags = post.flags();
@@ -83,7 +83,7 @@ export default function () {
if (post.isHidden()) this.revealContent = true;
vdom.unshift(
return (
<div className="Post-flagged">
<div className="Post-flagged-flags">
{flags.map((flag) => (

View File

@@ -1,65 +0,0 @@
import app from 'flarum/forum/app';
import Component from 'flarum/common/Component';
import Link from 'flarum/common/components/Link';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
import avatar from 'flarum/common/helpers/avatar';
import username from 'flarum/common/helpers/username';
import icon from 'flarum/common/helpers/icon';
import humanTime from 'flarum/common/helpers/humanTime';
export default class FlagList extends Component {
oninit(vnode) {
super.oninit(vnode);
this.state = this.attrs.state;
}
view() {
const flags = this.state.cache || [];
return (
<div className="NotificationList FlagList">
<div className="NotificationList-header">
<h4 className="App-titleControl App-titleControl--text">{app.translator.trans('flarum-flags.forum.flagged_posts.title')}</h4>
</div>
<div className="NotificationList-content">
<ul className="NotificationGroup-content">
{flags.length ? (
flags.map((flag) => {
const post = flag.post();
return (
<li>
<Link
href={app.route.post(post)}
className="Notification Flag"
onclick={(e) => {
app.flags.index = post;
e.redraw = false;
}}
>
{avatar(post.user())}
{icon('fas fa-flag', { className: 'Notification-icon' })}
<span className="Notification-content">
{app.translator.trans('flarum-flags.forum.flagged_posts.item_text', {
username: username(post.user()),
em: <em />,
discussion: post.discussion().title(),
})}
</span>
{humanTime(flag.createdAt())}
<div className="Notification-excerpt">{post.contentPlain()}</div>
</Link>
</li>
);
})
) : !this.state.loading ? (
<div className="NotificationList-empty">{app.translator.trans('flarum-flags.forum.flagged_posts.empty_text')}</div>
) : (
<LoadingIndicator className="LoadingIndicator--block" />
)}
</ul>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,64 @@
import app from 'flarum/forum/app';
import Component from 'flarum/common/Component';
import type { ComponentAttrs } from 'flarum/common/Component';
import Avatar from 'flarum/common/components/Avatar';
import username from 'flarum/common/helpers/username';
import HeaderList from 'flarum/forum/components/HeaderList';
import HeaderListItem from 'flarum/forum/components/HeaderListItem';
import type Mithril from 'mithril';
import type Post from 'flarum/common/models/Post';
import type FlagListState from '../states/FlagListState';
export interface IFlagListAttrs extends ComponentAttrs {
state: FlagListState;
}
export default class FlagList<CustomAttrs extends IFlagListAttrs = IFlagListAttrs> extends Component<CustomAttrs, FlagListState> {
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.state = this.attrs.state;
}
view() {
const flags = this.state.cache || [];
return (
<HeaderList
className="FlagList"
title={app.translator.trans('flarum-flags.forum.flagged_posts.title')}
hasItems={flags.length}
loading={this.state.loading}
emptyText={app.translator.trans('flarum-flags.forum.flagged_posts.empty_text')}
>
<ul className="HeaderListGroup-content">
{!this.state.loading &&
flags.map((flag) => {
const post = flag.post() as Post;
return (
<li>
<HeaderListItem
className="Flag"
avatar={<Avatar user={post.user() || null} />}
icon="fas fa-flag"
content={app.translator.trans('flarum-flags.forum.flagged_posts.item_text', {
username: username(post.user()),
em: <em />,
discussion: post.discussion().title(),
})}
excerpt={post.contentPlain()}
datetime={flag.createdAt()}
href={app.route.post(post)}
onclick={(e: MouseEvent) => {
app.flags.index = post;
e.redraw = false;
}}
/>
</li>
);
})}
</ul>
</HeaderList>
);
}
}

View File

@@ -1,5 +1,6 @@
import app from 'flarum/forum/app';
import Modal from 'flarum/common/components/Modal';
import Form from 'flarum/common/components/Form';
import Button from 'flarum/common/components/Button';
import Stream from 'flarum/common/utils/Stream';
@@ -28,31 +29,31 @@ export default class FlagPostModal extends Modal {
if (this.success) {
return (
<div className="Modal-body">
<div className="Form Form--centered">
<Form className="Form--centered">
<p className="helpText">{app.translator.trans('flarum-flags.forum.flag_post.confirmation_message')}</p>
<div className="Form-group">
<div className="Form-group Form-controls">
<Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
{app.translator.trans('flarum-flags.forum.flag_post.dismiss_button')}
</Button>
</div>
</div>
</Form>
</div>
);
}
return (
<div className="Modal-body">
<div className="Form Form--centered">
<Form className="Form--centered">
<div className="Form-group">
<div>{this.flagReasons().toArray()}</div>
</div>
<div className="Form-group">
<div className="Form-group Form-controls">
<Button className="Button Button--primary Button--block" type="submit" loading={this.loading} disabled={!this.reason()}>
{app.translator.trans('flarum-flags.forum.flag_post.submit_button')}
</Button>
</div>
</div>
</Form>
</div>
);
}

View File

@@ -1,33 +0,0 @@
import app from 'flarum/forum/app';
import NotificationsDropdown from 'flarum/forum/components/NotificationsDropdown';
import FlagList from './FlagList';
export default class FlagsDropdown extends NotificationsDropdown {
static initAttrs(attrs) {
attrs.label = attrs.label || app.translator.trans('flarum-flags.forum.flagged_posts.tooltip');
attrs.icon = attrs.icon || 'fas fa-flag';
super.initAttrs(attrs);
}
getMenu() {
return (
<div className={'Dropdown-menu ' + this.attrs.menuClassName} onclick={this.menuClick.bind(this)}>
{this.showing && <FlagList state={this.attrs.state} />}
</div>
);
}
goToRoute() {
m.route.set(app.route('flags'));
}
getUnreadCount() {
return app.flags.cache ? app.flags.cache.length : app.forum.attribute('flagCount');
}
getNewCount() {
return app.session.user.attribute('newFlagCount');
}
}

View File

@@ -0,0 +1,34 @@
import app from 'flarum/forum/app';
import HeaderDropdown from 'flarum/forum/components/HeaderDropdown';
import type { IHeaderDropdownAttrs } from 'flarum/forum/components/HeaderDropdown';
import classList from 'flarum/common/utils/classList';
import FlagList from './FlagList';
export interface IFlagsDropdownAttrs extends IHeaderDropdownAttrs {}
export default class FlagsDropdown<CustomAttrs extends IFlagsDropdownAttrs = IFlagsDropdownAttrs> extends HeaderDropdown<CustomAttrs> {
static initAttrs(attrs: IFlagsDropdownAttrs) {
attrs.className = classList('FlagsDropdown', attrs.className);
attrs.label = attrs.label || app.translator.trans('flarum-flags.forum.flagged_posts.tooltip');
attrs.icon = attrs.icon || 'fas fa-flag';
super.initAttrs(attrs);
}
getContent() {
return <FlagList state={this.attrs.state} />;
}
goToRoute() {
m.route.set(app.route('flags'));
}
getUnreadCount() {
return app.flags.cache ? app.flags.cache.length : app.forum.attribute<number>('flagCount');
}
getNewCount() {
return app.session.user!.attribute<number>('newFlagCount');
}
}

View File

@@ -1,37 +0,0 @@
export default class FlagListState {
constructor(app) {
this.app = app;
/**
* Whether or not the flags are loading.
*
* @type {Boolean}
*/
this.loading = false;
}
/**
* Load flags into the application's cache if they haven't already
* been loaded.
*/
load() {
if (this.cache && !this.app.session.user.attribute('newFlagCount')) {
return;
}
this.loading = true;
m.redraw();
this.app.store
.find('flags')
.then((flags) => {
this.app.session.user.pushAttributes({ newFlagCount: 0 });
this.cache = flags.sort((a, b) => b.createdAt() - a.createdAt());
})
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
});
}
}

View File

@@ -0,0 +1,39 @@
import type ForumApplication from 'flarum/forum/ForumApplication';
import type Flag from '../models/Flag';
import type Post from 'flarum/common/models/Post';
export default class FlagListState {
public app: ForumApplication;
public loading = false;
public cache: Flag[] | null = null;
public index: Post | false | null = null;
constructor(app: ForumApplication) {
this.app = app;
}
/**
* Load flags into the application's cache if they haven't already
* been loaded.
*/
load() {
if (this.cache && !this.app.session.user!.attribute<number>('newFlagCount')) {
return;
}
this.loading = true;
m.redraw();
this.app.store
.find<Flag[]>('flags')
.then((flags) => {
this.app.session.user!.pushAttributes({ newFlagCount: 0 });
this.cache = flags.sort((a, b) => b.createdAt()!.getTime() - a.createdAt()!.getTime());
})
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
});
}
}

View File

@@ -1,6 +1,9 @@
.Post--flagged {
--border-width: 2px;
padding-top: 0 !important;
border: 2px solid @primary-color;
padding-left: var(--post-padding);
margin-left: calc(~"0px - var(--post-padding)");
border: var(--border-width) solid var(--primary-color);
}
.Post-header .item-flagged {
@@ -8,29 +11,22 @@
margin: 0;
}
.Post-flagged {
background: @primary-color;
margin-top: -2px;
margin-bottom: 20px;
margin-left: -22px;
margin-right: -22px;
background: var(--primary-color);
margin: calc(~"0px - var(--border-width)") calc(~"0px - var(--border-width) - var(--post-padding)") var(--post-padding);
padding: 10px;
border-radius: @border-radius @border-radius 0 0;
border-radius: var(--border-radius) var(--border-radius) 0 0;
overflow: hidden;
.light-contents(@color: @body-bg; @control-color: @body-bg);
@media @tablet-up {
margin-left: -22px - 85px;
}
display: flex;
align-items: center;
justify-content: space-between;
&, a {
color: @body-bg !important;
color: var(--body-bg) !important;
}
}
.Post-flagged-flags {
@media @tablet-up {
float: left;
}
font-size: 14px;
margin: 7px 10px;
text-align: left;
@@ -42,19 +38,10 @@
font-weight: normal;
}
.Post-flagged-actions {
@media @tablet-up {
float: right;
}
}
.Post-flagged-actions .Button {
margin-left: 5px;
}
.FlagsDropdown .Dropdown-toggle {
.Button-label,
.Button-caret {
display: none;
}
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 5px;
}
.FlagPostModal {
@@ -66,7 +53,16 @@
strong {
display: block;
color: @text-color;
color: var(--text-color);
}
}
}
.Flag .HeaderListItem-title {
justify-content: space-between;
flex-wrap: nowrap;
}
.Flag .HeaderListItem-time {
flex-shrink: 0;
}