mirror of
https://github.com/flarum/framework.git
synced 2025-03-15 00:05:12 +08:00
chore: extensibility improvements (#3729)
* chore: improve tags page extensibility * chore: improve discussion list item extensibility * chore: improve change password modal extensibility * chore: item-listify tags page * chore: item-listify change email modal * chore: simplify data flow Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>
This commit is contained in:
parent
8b11fef3ee
commit
b89a01c010
@ -34,7 +34,8 @@ export default function tagLabel(tag, attrs = {}) {
|
||||
link ? Link : 'span',
|
||||
attrs,
|
||||
<span className="TagLabel-text">
|
||||
{tag && tag.icon() && tagIcon(tag, {}, { useColor: false })} {tagText}
|
||||
{tag && tag.icon() && tagIcon(tag, { className: 'TagLabel-icon' }, { useColor: false })}
|
||||
<span className="TagLabel-name">{tagText}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import IndexPage from 'flarum/forum/components/IndexPage';
|
||||
import Link from 'flarum/common/components/Link';
|
||||
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||
import listItems from 'flarum/common/helpers/listItems';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
import humanTime from 'flarum/common/helpers/humanTime';
|
||||
import textContrastClass from 'flarum/common/helpers/textContrastClass';
|
||||
import classList from 'flarum/common/utils/classList';
|
||||
@ -37,69 +38,107 @@ export default class TagsPage extends Page {
|
||||
});
|
||||
}
|
||||
|
||||
view() {
|
||||
if (this.loading) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
const pinned = this.tags.filter((tag) => tag.position() !== null);
|
||||
const cloud = this.tags.filter((tag) => tag.position() === null);
|
||||
|
||||
return (
|
||||
<div className="TagsPage">
|
||||
{IndexPage.prototype.hero()}
|
||||
<div className="container">
|
||||
<nav className="TagsPage-nav IndexPage-nav sideNav">
|
||||
<ul>{listItems(IndexPage.prototype.sidebarItems().toArray())}</ul>
|
||||
</nav>
|
||||
|
||||
<div className="TagsPage-content sideNavOffset">
|
||||
<ul className="TagTiles">
|
||||
{pinned.map((tag) => {
|
||||
const lastPostedDiscussion = tag.lastPostedDiscussion();
|
||||
const children = sortTags(tag.children() || []);
|
||||
|
||||
return (
|
||||
<li className={classList('TagTile', { colored: tag.color() }, textContrastClass(tag.color()))} style={{ '--tag-bg': tag.color() }}>
|
||||
<Link className="TagTile-info" href={app.route.tag(tag)}>
|
||||
{tag.icon() && tagIcon(tag, {}, { useColor: false })}
|
||||
<h3 className="TagTile-name">{tag.name()}</h3>
|
||||
<p className="TagTile-description">{tag.description()}</p>
|
||||
{children ? (
|
||||
<div className="TagTile-children">
|
||||
{children.map((child) => [<Link href={app.route.tag(child)}>{child.name()}</Link>, ' '])}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Link>
|
||||
{lastPostedDiscussion ? (
|
||||
<Link
|
||||
className="TagTile-lastPostedDiscussion"
|
||||
href={app.route.discussion(lastPostedDiscussion, lastPostedDiscussion.lastPostNumber())}
|
||||
>
|
||||
<span className="TagTile-lastPostedDiscussion-title">{lastPostedDiscussion.title()}</span>
|
||||
{humanTime(lastPostedDiscussion.lastPostedAt())}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="TagTile-lastPostedDiscussion" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{cloud.length ? <div className="TagCloud">{cloud.map((tag) => [tagLabel(tag, { link: true }), ' '])}</div> : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
app.setTitle(app.translator.trans('flarum-tags.forum.all_tags.meta_title_text'));
|
||||
app.setTitleCount(0);
|
||||
}
|
||||
|
||||
view() {
|
||||
return <div className="TagsPage">{this.pageContent().toArray()}</div>;
|
||||
}
|
||||
|
||||
pageContent() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('hero', this.hero(), 100);
|
||||
items.add('main', <div className="container">{this.mainContent().toArray()}</div>, 10);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
mainContent() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('sidebar', this.sidebar(), 100);
|
||||
items.add('content', this.content(), 10);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
content() {
|
||||
return <div className="TagsPage-content sideNavOffset">{this.contentItems().toArray()}</div>;
|
||||
}
|
||||
|
||||
contentItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
if (this.loading) {
|
||||
items.add('loading', <LoadingIndicator />);
|
||||
} else {
|
||||
const pinned = this.tags.filter((tag) => tag.position() !== null);
|
||||
const cloud = this.tags.filter((tag) => tag.position() === null);
|
||||
|
||||
items.add('tagTiles', this.tagTileListView(pinned), 100);
|
||||
|
||||
if (cloud.length) {
|
||||
items.add('cloud', this.cloudView(cloud), 10);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
hero() {
|
||||
return IndexPage.prototype.hero();
|
||||
}
|
||||
|
||||
sidebar() {
|
||||
return (
|
||||
<nav className="TagsPage-nav IndexPage-nav sideNav">
|
||||
<ul>{listItems(this.sidebarItems().toArray())}</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
sidebarItems() {
|
||||
return IndexPage.prototype.sidebarItems();
|
||||
}
|
||||
|
||||
tagTileListView(pinned) {
|
||||
return <ul className="TagTiles">{pinned.map(this.tagTileView.bind(this))}</ul>;
|
||||
}
|
||||
|
||||
tagTileView(tag) {
|
||||
const lastPostedDiscussion = tag.lastPostedDiscussion();
|
||||
const children = sortTags(tag.children() || []);
|
||||
|
||||
return (
|
||||
<li className={classList('TagTile', { colored: tag.color() }, textContrastClass(tag.color()))} style={{ '--tag-bg': tag.color() }}>
|
||||
<Link className="TagTile-info" href={app.route.tag(tag)}>
|
||||
{tag.icon() && tagIcon(tag, {}, { useColor: false })}
|
||||
<h3 className="TagTile-name">{tag.name()}</h3>
|
||||
<p className="TagTile-description">{tag.description()}</p>
|
||||
{children ? (
|
||||
<div className="TagTile-children">{children.map((child) => [<Link href={app.route.tag(child)}>{child.name()}</Link>, ' '])}</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Link>
|
||||
{lastPostedDiscussion ? (
|
||||
<Link className="TagTile-lastPostedDiscussion" href={app.route.discussion(lastPostedDiscussion, lastPostedDiscussion.lastPostNumber())}>
|
||||
<span className="TagTile-lastPostedDiscussion-title">{lastPostedDiscussion.title()}</span>
|
||||
{humanTime(lastPostedDiscussion.lastPostedAt())}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="TagTile-lastPostedDiscussion" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
cloudView(cloud) {
|
||||
return <div className="TagCloud">{cloud.map((tag) => [tagLabel(tag, { link: true }), ' '])}</div>;
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,9 @@ import app from '../../forum/app';
|
||||
import Modal, { IInternalModalAttrs } from '../../common/components/Modal';
|
||||
import Button from '../../common/components/Button';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
import Mithril from 'mithril';
|
||||
import type Mithril from 'mithril';
|
||||
import RequestError from '../../common/utils/RequestError';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
|
||||
/**
|
||||
* The `ChangeEmailModal` component shows a modal dialog which allows the user
|
||||
@ -41,60 +42,75 @@ export default class ChangeEmailModal<CustomAttrs extends IInternalModalAttrs =
|
||||
}
|
||||
|
||||
content() {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">{this.fields().toArray()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fields(): ItemList<Mithril.Children> {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
if (this.success) {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<p className="helpText">
|
||||
{app.translator.trans('core.forum.change_email.confirmation_message', { email: <strong>{this.email()}</strong> })}
|
||||
</p>
|
||||
<div className="Form-group">
|
||||
<Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
|
||||
{app.translator.trans('core.forum.change_email.dismiss_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
items.add(
|
||||
'help',
|
||||
<p className="helpText">
|
||||
{app.translator.trans('core.forum.change_email.confirmation_message', {
|
||||
email: <strong>{this.email()}</strong>,
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
|
||||
items.add(
|
||||
'dismiss',
|
||||
<div className="Form-group">
|
||||
<Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
|
||||
{app.translator.trans('core.forum.change_email.dismiss_button')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
items.add(
|
||||
'email',
|
||||
<div className="Form-group">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
className="FormControl"
|
||||
placeholder={app.session.user!.email()}
|
||||
bidi={this.email}
|
||||
disabled={this.loading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
items.add(
|
||||
'password',
|
||||
<div className="Form-group">
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
className="FormControl"
|
||||
autocomplete="current-password"
|
||||
placeholder={app.translator.trans('core.forum.change_email.confirm_password_placeholder')}
|
||||
bidi={this.password}
|
||||
disabled={this.loading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
items.add(
|
||||
'submit',
|
||||
<div className="Form-group">
|
||||
<Button className="Button Button--primary Button--block" type="submit" loading={this.loading}>
|
||||
{app.translator.trans('core.forum.change_email.submit_button')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<div className="Form-group">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
className="FormControl"
|
||||
placeholder={app.session.user!.email()}
|
||||
bidi={this.email}
|
||||
disabled={this.loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="Form-group">
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
className="FormControl"
|
||||
autocomplete="current-password"
|
||||
placeholder={app.translator.trans('core.forum.change_email.confirm_password_placeholder')}
|
||||
bidi={this.password}
|
||||
disabled={this.loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="Form-group">
|
||||
{Button.component(
|
||||
{
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
},
|
||||
app.translator.trans('core.forum.change_email.submit_button')
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return items;
|
||||
}
|
||||
|
||||
onsubmit(e: SubmitEvent) {
|
||||
@ -111,13 +127,10 @@ export default class ChangeEmailModal<CustomAttrs extends IInternalModalAttrs =
|
||||
this.alertAttrs = null;
|
||||
|
||||
app.session
|
||||
.user!.save(
|
||||
{ email: this.email() },
|
||||
{
|
||||
errorHandler: this.onerror.bind(this),
|
||||
meta: { password: this.password() },
|
||||
}
|
||||
)
|
||||
.user!.save(this.requestAttributes(), {
|
||||
errorHandler: this.onerror.bind(this),
|
||||
meta: { password: this.password() },
|
||||
})
|
||||
.then(() => {
|
||||
this.success = true;
|
||||
})
|
||||
@ -125,6 +138,10 @@ export default class ChangeEmailModal<CustomAttrs extends IInternalModalAttrs =
|
||||
.then(this.loaded.bind(this));
|
||||
}
|
||||
|
||||
requestAttributes() {
|
||||
return { email: this.email() };
|
||||
}
|
||||
|
||||
onerror(error: RequestError) {
|
||||
if (error.status === 401 && error.alert) {
|
||||
error.alert.content = app.translator.trans('core.forum.change_email.incorrect_password_message');
|
||||
|
@ -1,6 +1,8 @@
|
||||
import app from '../../forum/app';
|
||||
import Modal, { IInternalModalAttrs } from '../../common/components/Modal';
|
||||
import Button from '../../common/components/Button';
|
||||
import Mithril from 'mithril';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
|
||||
/**
|
||||
* The `ChangePasswordModal` component shows a modal dialog which allows the
|
||||
@ -18,23 +20,28 @@ export default class ChangePasswordModal<CustomAttrs extends IInternalModalAttrs
|
||||
content() {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<p className="helpText">{app.translator.trans('core.forum.change_password.text')}</p>
|
||||
<div className="Form-group">
|
||||
{Button.component(
|
||||
{
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
},
|
||||
app.translator.trans('core.forum.change_password.send_button')
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="Form Form--centered">{this.fields().toArray()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fields() {
|
||||
const fields = new ItemList<Mithril.Children>();
|
||||
|
||||
fields.add('help', <p className="helpText">{app.translator.trans('core.forum.change_password.text')}</p>);
|
||||
|
||||
fields.add(
|
||||
'submit',
|
||||
<div className="Form-group">
|
||||
<Button className="Button Button--primary Button--block" type="submit" loading={this.loading}>
|
||||
{app.translator.trans('core.forum.change_password.send_button')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
onsubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
@ -44,8 +51,12 @@ export default class ChangePasswordModal<CustomAttrs extends IInternalModalAttrs
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/forgot',
|
||||
body: { email: app.session.user!.email() },
|
||||
body: this.requestBody(),
|
||||
})
|
||||
.then(this.hide.bind(this), this.loaded.bind(this));
|
||||
}
|
||||
|
||||
requestBody() {
|
||||
return { email: app.session.user!.email() };
|
||||
}
|
||||
}
|
||||
|
@ -64,13 +64,101 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
|
||||
|
||||
view() {
|
||||
const discussion = this.attrs.discussion;
|
||||
const user = discussion.user();
|
||||
|
||||
const controls = DiscussionControls.controls(discussion, this).toArray();
|
||||
const attrs = this.elementAttrs();
|
||||
|
||||
return (
|
||||
<div {...attrs}>
|
||||
{this.controlsView(controls)}
|
||||
{this.slidableUnderneathView()}
|
||||
{this.contentView()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
controlsView(controls: Mithril.ChildArray): Mithril.Children {
|
||||
return (
|
||||
(controls.length > 0 &&
|
||||
Dropdown.component(
|
||||
{
|
||||
icon: 'fas fa-ellipsis-v',
|
||||
className: 'DiscussionListItem-controls',
|
||||
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right',
|
||||
accessibleToggleLabel: app.translator.trans('core.forum.discussion_controls.toggle_dropdown_accessible_label'),
|
||||
},
|
||||
controls
|
||||
)) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
slidableUnderneathView(): Mithril.Children {
|
||||
const discussion = this.attrs.discussion;
|
||||
const isUnread = discussion.isUnread();
|
||||
|
||||
return (
|
||||
<span
|
||||
className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')}
|
||||
onclick={this.markAsRead.bind(this)}
|
||||
>
|
||||
{icon('fas fa-check')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
contentView(): Mithril.Children {
|
||||
const discussion = this.attrs.discussion;
|
||||
const isUnread = discussion.isUnread();
|
||||
const isRead = discussion.isRead();
|
||||
|
||||
return (
|
||||
<div className={classList('DiscussionListItem-content', 'Slidable-content', { unread: isUnread, read: isRead })}>
|
||||
{this.authorAvatarView()}
|
||||
{this.badgesView()}
|
||||
{this.mainView()}
|
||||
{this.replyCountItem()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
authorAvatarView(): Mithril.Children {
|
||||
const discussion = this.attrs.discussion;
|
||||
const user = discussion.user();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
text={app.translator.trans('core.forum.discussion_list.started_text', { user, ago: humanTime(discussion.createdAt()) })}
|
||||
position="right"
|
||||
>
|
||||
<Link className="DiscussionListItem-author" href={user ? app.route.user(user) : '#'}>
|
||||
{avatar(user || null, { title: '' })}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
badgesView(): Mithril.Children {
|
||||
const discussion = this.attrs.discussion;
|
||||
|
||||
return <ul className="DiscussionListItem-badges badges">{listItems(discussion.badges().toArray())}</ul>;
|
||||
}
|
||||
|
||||
mainView(): Mithril.Children {
|
||||
const discussion = this.attrs.discussion;
|
||||
const jumpTo = this.getJumpTo();
|
||||
|
||||
return (
|
||||
<Link href={app.route.discussion(discussion, jumpTo)} className="DiscussionListItem-main">
|
||||
<h2 className="DiscussionListItem-title">{highlight(discussion.title(), this.highlightRegExp)}</h2>
|
||||
<ul className="DiscussionListItem-info">{listItems(this.infoItems().toArray())}</ul>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
getJumpTo() {
|
||||
const discussion = this.attrs.discussion;
|
||||
let jumpTo = 0;
|
||||
const controls = DiscussionControls.controls(discussion, this).toArray();
|
||||
const attrs = this.elementAttrs();
|
||||
|
||||
if (this.attrs.params.q) {
|
||||
const post = discussion.mostRelevantPost();
|
||||
@ -84,46 +172,7 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
|
||||
jumpTo = Math.min(discussion.lastPostNumber() ?? 0, (discussion.lastReadPostNumber() || 0) + 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...attrs}>
|
||||
{controls.length > 0 &&
|
||||
Dropdown.component(
|
||||
{
|
||||
icon: 'fas fa-ellipsis-v',
|
||||
className: 'DiscussionListItem-controls',
|
||||
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right',
|
||||
accessibleToggleLabel: app.translator.trans('core.forum.discussion_controls.toggle_dropdown_accessible_label'),
|
||||
},
|
||||
controls
|
||||
)}
|
||||
|
||||
<span
|
||||
className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')}
|
||||
onclick={this.markAsRead.bind(this)}
|
||||
>
|
||||
{icon('fas fa-check')}
|
||||
</span>
|
||||
|
||||
<div className={classList('DiscussionListItem-content', 'Slidable-content', { unread: isUnread, read: isRead })}>
|
||||
<Tooltip
|
||||
text={app.translator.trans('core.forum.discussion_list.started_text', { user, ago: humanTime(discussion.createdAt()) })}
|
||||
position="right"
|
||||
>
|
||||
<Link className="DiscussionListItem-author" href={user ? app.route.user(user) : '#'}>
|
||||
{avatar(user || null, { title: '' })}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
|
||||
<ul className="DiscussionListItem-badges badges">{listItems(discussion.badges().toArray())}</ul>
|
||||
|
||||
<Link href={app.route.discussion(discussion, jumpTo)} className="DiscussionListItem-main">
|
||||
<h2 className="DiscussionListItem-title">{highlight(discussion.title(), this.highlightRegExp)}</h2>
|
||||
<ul className="DiscussionListItem-info">{listItems(this.infoItems().toArray())}</ul>
|
||||
</Link>
|
||||
{this.replyCountItem()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return jumpTo;
|
||||
}
|
||||
|
||||
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user