1
0
mirror of https://github.com/flarum/core.git synced 2025-08-04 07:27:39 +02:00

chore: major frontend JS cleanup (#3609)

This commit is contained in:
David Wheatley
2023-05-07 17:40:18 +01:00
committed by GitHub
parent 3264455068
commit e63e161be6
105 changed files with 817 additions and 1064 deletions

View File

@@ -80,7 +80,7 @@ export default function addComposerAutocomplete() {
dropdown.setIndex($(this).parent().index() - 1); dropdown.setIndex($(this).parent().index() - 1);
}} }}
> >
<img alt={emoji} class="emoji" draggable="false" loading="lazy" src={`${cdn}72x72/${code}.png`} /> <img alt={emoji} className="emoji" draggable="false" loading="lazy" src={`${cdn}72x72/${code}.png`} />
{name} {name}
</button> </button>
); );

View File

@@ -108,7 +108,7 @@ export default function () {
user, user,
reason, reason,
}), }),
detail ? <span className="Post-flagged-detail">{detail}</span> : '', !!detail && <span className="Post-flagged-detail">{detail}</span>,
]; ];
} }
}; };

View File

@@ -55,7 +55,7 @@ export default class FlagList extends Component {
) : !this.state.loading ? ( ) : !this.state.loading ? (
<div className="NotificationList-empty">{app.translator.trans('flarum-flags.forum.flagged_posts.empty_text')}</div> <div className="NotificationList-empty">{app.translator.trans('flarum-flags.forum.flagged_posts.empty_text')}</div>
) : ( ) : (
LoadingIndicator.component({ className: 'LoadingIndicator--block' }) <LoadingIndicator className="LoadingIndicator--block" />
)} )}
</ul> </ul>
</div> </div>

View File

@@ -67,15 +67,13 @@ export default class FlagPostModal extends Modal {
<input type="radio" name="reason" checked={this.reason() === 'off_topic'} value="off_topic" onclick={withAttr('value', this.reason)} /> <input type="radio" name="reason" checked={this.reason() === 'off_topic'} value="off_topic" onclick={withAttr('value', this.reason)} />
<strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_off_topic_label')}</strong> <strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_off_topic_label')}</strong>
{app.translator.trans('flarum-flags.forum.flag_post.reason_off_topic_text')} {app.translator.trans('flarum-flags.forum.flag_post.reason_off_topic_text')}
{this.reason() === 'off_topic' ? ( {this.reason() === 'off_topic' && (
<textarea <textarea
className="FormControl" className="FormControl"
placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')} placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')}
value={this.reasonDetail()} value={this.reasonDetail()}
oninput={withAttr('value', this.reasonDetail)} oninput={withAttr('value', this.reasonDetail)}
></textarea> ></textarea>
) : (
''
)} )}
</label>, </label>,
70 70
@@ -95,15 +93,13 @@ export default class FlagPostModal extends Modal {
{app.translator.trans('flarum-flags.forum.flag_post.reason_inappropriate_text', { {app.translator.trans('flarum-flags.forum.flag_post.reason_inappropriate_text', {
a: guidelinesUrl ? <a href={guidelinesUrl} target="_blank" /> : undefined, a: guidelinesUrl ? <a href={guidelinesUrl} target="_blank" /> : undefined,
})} })}
{this.reason() === 'inappropriate' ? ( {this.reason() === 'inappropriate' && (
<textarea <textarea
className="FormControl" className="FormControl"
placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')} placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')}
value={this.reasonDetail()} value={this.reasonDetail()}
oninput={withAttr('value', this.reasonDetail)} oninput={withAttr('value', this.reasonDetail)}
></textarea> ></textarea>
) : (
''
)} )}
</label>, </label>,
60 60
@@ -115,15 +111,13 @@ export default class FlagPostModal extends Modal {
<input type="radio" name="reason" checked={this.reason() === 'spam'} value="spam" onclick={withAttr('value', this.reason)} /> <input type="radio" name="reason" checked={this.reason() === 'spam'} value="spam" onclick={withAttr('value', this.reason)} />
<strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_spam_label')}</strong> <strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_spam_label')}</strong>
{app.translator.trans('flarum-flags.forum.flag_post.reason_spam_text')} {app.translator.trans('flarum-flags.forum.flag_post.reason_spam_text')}
{this.reason() === 'spam' ? ( {this.reason() === 'spam' && (
<textarea <textarea
className="FormControl" className="FormControl"
placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')} placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')}
value={this.reasonDetail()} value={this.reasonDetail()}
oninput={withAttr('value', this.reasonDetail)} oninput={withAttr('value', this.reasonDetail)}
></textarea> ></textarea>
) : (
''
)} )}
</label>, </label>,
50 50
@@ -134,10 +128,8 @@ export default class FlagPostModal extends Modal {
<label className="checkbox"> <label className="checkbox">
<input type="radio" name="reason" checked={this.reason() === 'other'} value="other" onclick={withAttr('value', this.reason)} /> <input type="radio" name="reason" checked={this.reason() === 'other'} value="other" onclick={withAttr('value', this.reason)} />
<strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_other_label')}</strong> <strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_other_label')}</strong>
{this.reason() === 'other' ? ( {this.reason() === 'other' && (
<textarea className="FormControl" value={this.reasonDetail()} oninput={withAttr('value', this.reasonDetail)}></textarea> <textarea className="FormControl" value={this.reasonDetail()} oninput={withAttr('value', this.reasonDetail)}></textarea>
) : (
''
)} )}
</label>, </label>,
10 10

View File

@@ -1,5 +1,5 @@
import app from 'flarum/forum/app'; import app from 'flarum/forum/app';
import NotificationsDropdown from 'flarum/components/NotificationsDropdown'; import NotificationsDropdown from 'flarum/forum/components/NotificationsDropdown';
import FlagList from './FlagList'; import FlagList from './FlagList';
@@ -14,7 +14,7 @@ export default class FlagsDropdown extends NotificationsDropdown {
getMenu() { getMenu() {
return ( return (
<div className={'Dropdown-menu ' + this.attrs.menuClassName} onclick={this.menuClick.bind(this)}> <div className={'Dropdown-menu ' + this.attrs.menuClassName} onclick={this.menuClick.bind(this)}>
{this.showing ? FlagList.component({ state: this.attrs.state }) : ''} {this.showing && <FlagList state={this.attrs.state} />}
</div> </div>
); );
} }

View File

@@ -15,32 +15,31 @@ export default function () {
items.add( items.add(
'like', 'like',
Button.component( <Button
{ className="Button Button--link"
className: 'Button Button--link', onclick={() => {
onclick: () => { isLiked = !isLiked;
isLiked = !isLiked;
post.save({ isLiked }); post.save({ isLiked });
// We've saved the fact that we do or don't like the post, but in order // We've saved the fact that we do or don't like the post, but in order
// to provide instantaneous feedback to the user, we'll need to add or // to provide instantaneous feedback to the user, we'll need to add or
// remove the like from the relationship data manually. // remove the like from the relationship data manually.
const data = post.data.relationships.likes.data; const data = post.data.relationships.likes.data;
data.some((like, i) => { data.some((like, i) => {
if (like.id === app.session.user.id()) { if (like.id === app.session.user.id()) {
data.splice(i, 1); data.splice(i, 1);
return true; return true;
}
});
if (isLiked) {
data.unshift({ type: 'users', id: app.session.user.id() });
} }
}, });
},
app.translator.trans(isLiked ? 'flarum-likes.forum.post.unlike_link' : 'flarum-likes.forum.post.like_link') if (isLiked) {
) data.unshift({ type: 'users', id: app.session.user.id() });
}
}}
>
{app.translator.trans(isLiked ? 'flarum-likes.forum.post.unlike_link' : 'flarum-likes.forum.post.like_link')}
</Button>
); );
}); });
} }

View File

@@ -59,7 +59,7 @@ export default function () {
'liked', 'liked',
<div className="Post-likedBy"> <div className="Post-likedBy">
{icon('far fa-thumbs-up')} {icon('far fa-thumbs-up')}
{app.translator.trans('flarum-likes.forum.post.liked_by' + (likes[0] === app.session.user ? '_self' : '') + '_text', { {app.translator.trans(`flarum-likes.forum.post.liked_by${likes[0] === app.session.user ? '_self' : ''}_text`, {
count: names.length, count: names.length,
users: punctuateSeries(names), users: punctuateSeries(names),
})} })}

View File

@@ -6,14 +6,7 @@ import Badge from 'flarum/common/components/Badge';
export default function addLockBadge() { export default function addLockBadge() {
extend(Discussion.prototype, 'badges', function (badges) { extend(Discussion.prototype, 'badges', function (badges) {
if (this.isLocked()) { if (this.isLocked()) {
badges.add( badges.add('locked', <Badge type="locked" label={app.translator.trans('flarum-lock.forum.badge.locked_tooltip')} icon="fas fa-lock" />);
'locked',
Badge.component({
type: 'locked',
label: app.translator.trans('flarum-lock.forum.badge.locked_tooltip'),
icon: 'fas fa-lock',
})
);
} }
}); });
} }

View File

@@ -9,15 +9,9 @@ export default function addLockControl() {
if (discussion.canLock()) { if (discussion.canLock()) {
items.add( items.add(
'lock', 'lock',
Button.component( <Button icon="fas fa-lock" onclick={this.lockAction.bind(discussion)}>
{ {app.translator.trans(`flarum-lock.forum.discussion_controls.${discussion.isLocked() ? 'unlock' : 'lock'}_button`)}
icon: 'fas fa-lock', </Button>
onclick: this.lockAction.bind(discussion),
},
app.translator.trans(
discussion.isLocked() ? 'flarum-lock.forum.discussion_controls.unlock_button' : 'flarum-lock.forum.discussion_controls.lock_button'
)
)
); );
} }
}); });

View File

@@ -2,6 +2,6 @@ import Component from 'flarum/common/Component';
export default class MarkdownToolbar extends Component { export default class MarkdownToolbar extends Component {
view(vnode) { view(vnode) {
return <div class="MarkdownToolbar">{vnode.children}</div>; return <div className="MarkdownToolbar">{vnode.children}</div>;
} }
} }

View File

@@ -41,13 +41,10 @@ export default function addMentionedByList() {
<> <>
{replies.map((reply) => ( {replies.map((reply) => (
<li data-number={reply.number()}> <li data-number={reply.number()}>
{PostPreview.component({ <PostPreview post={reply} onclick={hidePreview.bind(this)} />
post: reply,
onclick: hidePreview.bind(this),
})}
</li> </li>
))} ))}
{replies.length < post.mentionedByCount() ? ( {replies.length < post.mentionedByCount() && (
<li className="Post-mentionedBy-preview-more"> <li className="Post-mentionedBy-preview-more">
<Button <Button
className="PostPreview Button" className="PostPreview Button"
@@ -64,7 +61,7 @@ export default function addMentionedByList() {
</span> </span>
</Button> </Button>
</li> </li>
) : null} )}
</> </>
); );
@@ -149,7 +146,7 @@ export default function addMentionedByList() {
<div className="Post-mentionedBy"> <div className="Post-mentionedBy">
<span className="Post-mentionedBy-summary"> <span className="Post-mentionedBy-summary">
{icon('fas fa-reply')} {icon('fas fa-reply')}
{app.translator.trans('flarum-mentions.forum.post.mentioned_by' + (repliers[0].user() === app.session.user ? '_self' : '') + '_text', { {app.translator.trans(`flarum-mentions.forum.post.mentioned_by${repliers[0].user() === app.session.user ? '_self' : ''}_text`, {
count: names.length, count: names.length,
users: punctuateSeries(names), users: punctuateSeries(names),
})} })}

View File

@@ -80,14 +80,14 @@ export default function addPostMentionPreviews() {
const discussion = post.discussion(); const discussion = post.discussion();
m.render($preview[0], [ m.render($preview[0], [
discussion !== parentPost.discussion() ? ( discussion !== parentPost.discussion() && (
<li> <li>
<span className="PostMention-preview-discussion">{discussion.title()}</span> <span className="PostMention-preview-discussion">{discussion.title()}</span>
</li> </li>
) : (
''
), ),
<li>{PostPreview.component({ post })}</li>, <li>
<PostPreview post={post} />
</li>,
]); ]);
positionPreview(); positionPreview();
}; };
@@ -96,7 +96,7 @@ export default function addPostMentionPreviews() {
if (post && post.discussion()) { if (post && post.discussion()) {
showPost(post); showPost(post);
} else { } else {
m.render($preview[0], LoadingIndicator.component()); m.render($preview[0], <LoadingIndicator />);
app.store.find('posts', id).then(showPost); app.store.find('posts', id).then(showPost);
positionPreview(); positionPreview();
} }

View File

@@ -14,7 +14,7 @@ export default class PostQuoteButton extends Fragment {
view() { view() {
return ( return (
<button <button
class="Button PostQuoteButton" className="Button PostQuoteButton"
onclick={() => { onclick={() => {
reply(this.post, this.content); reply(this.post, this.content);
}} }}

View File

@@ -70,14 +70,9 @@ app.initializers.add('flarum-mentions', function () {
const user = this.user; const user = this.user;
items.add( items.add(
'mentions', 'mentions',
LinkButton.component( <LinkButton href={app.route('user.mentions', { username: user.slug() })} name="mentions" icon="fas fa-at">
{ {app.translator.trans('flarum-mentions.forum.user.mentions_link')}
href: app.route('user.mentions', { username: user.slug() }), </LinkButton>,
name: 'mentions',
icon: 'fas fa-at',
},
app.translator.trans('flarum-mentions.forum.user.mentions_link')
),
80 80
); );
}); });

View File

@@ -25,14 +25,9 @@ export default class NicknameModal extends Modal {
<input type="text" autocomplete="off" name="nickname" className="FormControl" bidi={this.nickname} disabled={this.loading} /> <input type="text" autocomplete="off" name="nickname" className="FormControl" bidi={this.nickname} disabled={this.loading} />
</div> </div>
<div className="Form-group"> <div className="Form-group">
{Button.component( <Button className="Button Button--primary Button--block" type="submit" loading={this.loading}>
{ {app.translator.trans('flarum-nicknames.forum.change_nickname.submit_button')}
className: 'Button Button--primary Button--block', </Button>
type: 'submit',
loading: this.loading,
},
app.translator.trans('flarum-nicknames.forum.change_nickname.submit_button')
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -13,7 +13,7 @@ interface PaginationAttrs extends ComponentAttrs {
export default class Pagination extends Component<PaginationAttrs> { export default class Pagination extends Component<PaginationAttrs> {
view() { view() {
return ( return (
<nav class="Pagination UserListPage-gridPagination"> <nav className="Pagination UserListPage-gridPagination">
<Button <Button
disabled={!this.attrs.list.hasPrev()} disabled={!this.attrs.list.hasPrev()}
title={app.translator.trans('core.admin.users.pagination.back_button')} title={app.translator.trans('core.admin.users.pagination.back_button')}
@@ -21,7 +21,7 @@ export default class Pagination extends Component<PaginationAttrs> {
icon="fas fa-chevron-left" icon="fas fa-chevron-left"
className="Button Button--icon UserListPage-backBtn" className="Button Button--icon UserListPage-backBtn"
/> />
<span class="UserListPage-pageNumber"> <span className="UserListPage-pageNumber">
{app.translator.trans('core.admin.users.pagination.page_counter', { {app.translator.trans('core.admin.users.pagination.page_counter', {
current: this.attrs.list.pageNumber() + 1, current: this.attrs.list.pageNumber() + 1,
total: this.attrs.list.getTotalPages(), total: this.attrs.list.getTotalPages(),

View File

@@ -75,7 +75,7 @@ export default class QueueSection extends Component<{}> {
return extension ? ( return extension ? (
<div className="PackageManager-queueTable-package"> <div className="PackageManager-queueTable-package">
<div className="PackageManager-queueTable-package-icon ExtensionIcon" style={extension.icon}> <div className="PackageManager-queueTable-package-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? icon(extension.icon.name) : ''} {!!extension.icon && icon(extension.icon.name)}
</div> </div>
<div className="PackageManager-queueTable-package-details"> <div className="PackageManager-queueTable-package-details">
<span className="PackageManager-queueTable-package-title">{extension.extra['flarum-extension'].title}</span> <span className="PackageManager-queueTable-package-title">{extension.extra['flarum-extension'].title}</span>

View File

@@ -87,22 +87,21 @@ app.initializers.add('flarum-pusher', () => {
if (count && typeof vdom === 'object' && vdom && 'children' in vdom && vdom.children instanceof Array) { if (count && typeof vdom === 'object' && vdom && 'children' in vdom && vdom.children instanceof Array) {
vdom.children.unshift( vdom.children.unshift(
Button.component( <Button
{ className="Button Button--block DiscussionList-update"
className: 'Button Button--block DiscussionList-update', onclick={() => {
onclick: () => { this.attrs.state.refresh().then(() => {
this.attrs.state.refresh().then(() => { this.loadingUpdated = false;
this.loadingUpdated = false; app.pushedUpdates = [];
app.pushedUpdates = []; app.setTitleCount(0);
app.setTitleCount(0); m.redraw();
m.redraw(); });
}); this.loadingUpdated = true;
this.loadingUpdated = true; }}
}, loading={this.loadingUpdated}
loading: this.loadingUpdated, >
}, {app.translator.trans('flarum-pusher.forum.discussion_list.show_updates_text', { count })}
app.translator.trans('flarum-pusher.forum.discussion_list.show_updates_text', { count }) </Button>
)
); );
} }
} }

View File

@@ -7,6 +7,7 @@ import extractText from 'flarum/common/utils/extractText';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
import Placeholder from 'flarum/common/components/Placeholder'; import Placeholder from 'flarum/common/components/Placeholder';
import icon from 'flarum/common/helpers/icon'; import icon from 'flarum/common/helpers/icon';
import classList from 'flarum/common/utils/classList';
import DashboardWidget, { IDashboardWidgetAttrs } from 'flarum/admin/components/DashboardWidget'; import DashboardWidget, { IDashboardWidgetAttrs } from 'flarum/admin/components/DashboardWidget';
@@ -277,7 +278,7 @@ export default class StatisticsWidget extends DashboardWidget {
return ( return (
<button <button
className={'Button--ua-reset StatisticsWidget-entity' + (this.selectedEntity === entity ? ' active' : '')} className={classList('Button--ua-reset StatisticsWidget-entity', { active: this.selectedEntity === entity })}
onclick={this.changeEntity.bind(this, entity)} onclick={this.changeEntity.bind(this, entity)}
> >
<h3 className="StatisticsWidget-heading">{app.translator.trans('flarum-statistics.admin.statistics.' + entity + '_heading')}</h3> <h3 className="StatisticsWidget-heading">{app.translator.trans('flarum-statistics.admin.statistics.' + entity + '_heading')}</h3>

View File

@@ -71,7 +71,7 @@ export default class StatisticsWidgetDateSelectionModal extends Modal<IStatistic
} }
content(): Mithril.Children { content(): Mithril.Children {
return <div class="Modal-body">{this.items().toArray()}</div>; return <div className="Modal-body">{this.items().toArray()}</div>;
} }
items(): ItemList<Mithril.Children> { items(): ItemList<Mithril.Children> {
@@ -81,7 +81,7 @@ export default class StatisticsWidgetDateSelectionModal extends Modal<IStatistic
items.add( items.add(
'date_start', 'date_start',
<div class="Form-group"> <div className="Form-group">
<label htmlFor={this.state.ids.startDate}>{app.translator.trans('flarum-statistics.admin.date_selection_modal.start_date')}</label> <label htmlFor={this.state.ids.startDate}>{app.translator.trans('flarum-statistics.admin.date_selection_modal.start_date')}</label>
<input <input
type="date" type="date"
@@ -96,7 +96,7 @@ export default class StatisticsWidgetDateSelectionModal extends Modal<IStatistic
items.add( items.add(
'date_end', 'date_end',
<div class="Form-group"> <div className="Form-group">
<label htmlFor={this.state.ids.endDate}>{app.translator.trans('flarum-statistics.admin.date_selection_modal.end_date')}</label> <label htmlFor={this.state.ids.endDate}>{app.translator.trans('flarum-statistics.admin.date_selection_modal.end_date')}</label>
<input <input
type="date" type="date"
@@ -111,7 +111,7 @@ export default class StatisticsWidgetDateSelectionModal extends Modal<IStatistic
items.add( items.add(
'submit', 'submit',
<Button class="Button Button--primary" type="submit"> <Button className="Button Button--primary" type="submit">
{app.translator.trans('flarum-statistics.admin.date_selection_modal.submit_button')} {app.translator.trans('flarum-statistics.admin.date_selection_modal.submit_button')}
</Button>, </Button>,
0 0

View File

@@ -8,11 +8,7 @@ export default function addStickyBadge() {
if (this.isSticky()) { if (this.isSticky()) {
badges.add( badges.add(
'sticky', 'sticky',
Badge.component({ <Badge type="sticky" label={app.translator.trans('flarum-sticky.forum.badge.sticky_tooltip')} icon="fas fa-thumbtack" />,
type: 'sticky',
label: app.translator.trans('flarum-sticky.forum.badge.sticky_tooltip'),
icon: 'fas fa-thumbtack',
}),
10 10
); );
} }

View File

@@ -9,17 +9,9 @@ export default function addStickyControl() {
if (discussion.canSticky()) { if (discussion.canSticky()) {
items.add( items.add(
'sticky', 'sticky',
Button.component( <Button icon="fas fa-thumbtack" onclick={this.stickyAction.bind(discussion)}>
{ {app.translator.trans(`flarum-sticky.forum.discussion_controls.${discussion.isSticky() ? 'unsticky' : 'sticky'}_button`)}
icon: 'fas fa-thumbtack', </Button>
onclick: this.stickyAction.bind(discussion),
},
app.translator.trans(
discussion.isSticky()
? 'flarum-sticky.forum.discussion_controls.unsticky_button'
: 'flarum-sticky.forum.discussion_controls.sticky_button'
)
)
); );
} }
}); });

View File

@@ -9,23 +9,12 @@ export default function addSubscriptionBadge() {
switch (this.subscription()) { switch (this.subscription()) {
case 'follow': case 'follow':
badge = Badge.component({ badge = <Badge label={app.translator.trans('flarum-subscriptions.forum.badge.following_tooltip')} icon="fas fa-star" type="following" />;
label: app.translator.trans('flarum-subscriptions.forum.badge.following_tooltip'),
icon: 'fas fa-star',
type: 'following',
});
break; break;
case 'ignore': case 'ignore':
badge = Badge.component({ badge = <Badge label={app.translator.trans('flarum-subscriptions.forum.badge.ignoring_tooltip')} icon="far fa-eye-slash" type="ignoring" />;
label: app.translator.trans('flarum-subscriptions.forum.badge.ignoring_tooltip'),
icon: 'far fa-eye-slash',
type: 'ignoring',
});
break; break;
default:
// no default
} }
if (badge) { if (badge) {

View File

@@ -19,13 +19,9 @@ export default function addSubscriptionControls() {
items.add( items.add(
'subscription', 'subscription',
Button.component( <Button icon={states[subscription].icon} onclick={discussion.save.bind(discussion, { subscription: states[subscription].save })}>
{ {states[subscription].label}
icon: states[subscription].icon, </Button>
onclick: discussion.save.bind(discussion, { subscription: states[subscription].save }),
},
states[subscription].label
)
); );
} }
}); });
@@ -34,7 +30,7 @@ export default function addSubscriptionControls() {
if (app.session.user) { if (app.session.user) {
const discussion = this.discussion; const discussion = this.discussion;
items.add('subscription', SubscriptionMenu.component({ discussion }), 80); items.add('subscription', <SubscriptionMenu discussion={discussion} />, 80);
} }
}); });
} }

View File

@@ -12,13 +12,9 @@ export default function addSubscriptionFilter() {
items.add( items.add(
'following', 'following',
LinkButton.component( <LinkButton href={app.route('following', params)} icon="fas fa-star">
{ {app.translator.trans('flarum-subscriptions.forum.index.following_link')}
href: app.route('following', params), </LinkButton>,
icon: 'fas fa-star',
},
app.translator.trans('flarum-subscriptions.forum.index.following_link')
),
50 50
); );
} }

View File

@@ -7,21 +7,20 @@ export default function () {
extend(SettingsPage.prototype, 'notificationsItems', function (this: SettingsPage, items) { extend(SettingsPage.prototype, 'notificationsItems', function (this: SettingsPage, items) {
items.add( items.add(
'followAfterReply', 'followAfterReply',
Switch.component( <Switch
{ state={this.user.preferences().followAfterReply}
state: this.user.preferences().followAfterReply, onchange={(value) => {
onchange: (value) => { this.followAfterReplyLoading = true;
this.followAfterReplyLoading = true;
this.user.savePreferences({ followAfterReply: value }).then(() => { this.user.savePreferences({ followAfterReply: value }).then(() => {
this.followAfterReplyLoading = false; this.followAfterReplyLoading = false;
m.redraw(); m.redraw();
}); });
}, }}
loading: this.followAfterReplyLoading, loading={this.followAfterReplyLoading}
}, >
app.translator.trans('flarum-subscriptions.forum.settings.follow_after_reply_label') {app.translator.trans('flarum-subscriptions.forum.settings.follow_after_reply_label')}
) </Switch>
); );
items.add( items.add(

View File

@@ -95,11 +95,11 @@ export default class SubscriptionMenu extends Dropdown {
<ul className="Dropdown-menu dropdown-menu Dropdown-menu--right"> <ul className="Dropdown-menu dropdown-menu Dropdown-menu--right">
{this.options.map((attrs) => ( {this.options.map((attrs) => (
<li> <li>
{SubscriptionMenuItem.component({ <SubscriptionMenuItem
...attrs, {...attrs}
onclick: this.saveSubscription.bind(this, discussion, attrs.subscription), onclick={this.saveSubscription.bind(this, discussion, attrs.subscription)}
active: subscription === attrs.subscription, active={subscription === attrs.subscription}
})} />
</li> </li>
))} ))}
</ul> </ul>

View File

@@ -5,7 +5,7 @@ export default class SubscriptionMenuItem extends Component {
view() { view() {
return ( return (
<button className="SubscriptionMenuItem hasIcon" onclick={this.attrs.onclick}> <button className="SubscriptionMenuItem hasIcon" onclick={this.attrs.onclick}>
{this.attrs.active ? icon('fas fa-check', { className: 'Button-icon' }) : ''} {this.attrs.active && icon('fas fa-check', { className: 'Button-icon' })}
<span className="SubscriptionMenuItem-label"> <span className="SubscriptionMenuItem-label">
{icon(this.attrs.icon, { className: 'Button-icon' })} {icon(this.attrs.icon, { className: 'Button-icon' })}
<strong>{this.attrs.label}</strong> <strong>{this.attrs.label}</strong>

View File

@@ -20,13 +20,9 @@ app.initializers.add('flarum-suspend', () => {
if (user.canSuspend()) { if (user.canSuspend()) {
items.add( items.add(
'suspend', 'suspend',
Button.component( <Button icon="fas fa-ban" onclick={() => app.modal.show(SuspendUserModal, { user })}>
{ {app.translator.trans('flarum-suspend.forum.user_controls.suspend_button')}
icon: 'fas fa-ban', </Button>
onclick: () => app.modal.show(SuspendUserModal, { user }),
},
app.translator.trans('flarum-suspend.forum.user_controls.suspend_button')
)
); );
} }
}); });
@@ -37,11 +33,8 @@ app.initializers.add('flarum-suspend', () => {
if (new Date() < until) { if (new Date() < until) {
items.add( items.add(
'suspended', 'suspended',
Badge.component({ <Badge icon="fas fa-ban" type="suspended" label={app.translator.trans('flarum-suspend.forum.user_badge.suspended_tooltip')} />,
icon: 'fas fa-ban', 100
type: 'suspended',
label: app.translator.trans('flarum-suspend.forum.user_badge.suspended_tooltip'),
})
); );
} }
}); });

View File

@@ -1,3 +1,4 @@
import app from 'flarum/admin/app';
import { extend } from 'flarum/common/extend'; import { extend } from 'flarum/common/extend';
import PermissionGrid from 'flarum/admin/components/PermissionGrid'; import PermissionGrid from 'flarum/admin/components/PermissionGrid';
import SettingDropdown from 'flarum/admin/components/SettingDropdown'; import SettingDropdown from 'flarum/admin/components/SettingDropdown';
@@ -12,17 +13,21 @@ export default function () {
setting: () => { setting: () => {
const minutes = parseInt(app.data.settings.allow_tag_change, 10); const minutes = parseInt(app.data.settings.allow_tag_change, 10);
return SettingDropdown.component({ return (
defaultLabel: minutes <SettingDropdown
? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { count: minutes }) defaultLabel={
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'), minutes
key: 'allow_tag_change', ? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { count: minutes })
options: [ : app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')
{ value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') }, }
{ value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') }, key="allow_tag_change"
{ value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') }, options={[
], { value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') },
}); { value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') },
{ value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') },
]}
/>
);
}, },
}, },
90 90

View File

@@ -53,21 +53,18 @@ export default function () {
label: tagLabel(tag), label: tagLabel(tag),
onremove: () => tag.save({ isRestricted: false }), onremove: () => tag.save({ isRestricted: false }),
render: (item) => { render: (item) => {
if ('setting' in item) return ''; if ('setting' in item) return null;
if ( if (
item.permission === 'viewForum' || item.permission === 'viewForum' ||
item.permission === 'startDiscussion' || item.permission === 'startDiscussion' ||
(item.permission && item.permission.indexOf('discussion.') === 0 && item.tagScoped !== false) || (item.permission.startsWith('discussion.') && item.tagScoped !== false) ||
item.tagScoped item.tagScoped
) { ) {
return PermissionDropdown.component({ return <PermissionDropdown permission={`tag${tag.id()}.${item.permission}`} allowGuest={item.allowGuest} />;
permission: 'tag' + tag.id() + '.' + item.permission,
allowGuest: item.allowGuest,
});
} }
return ''; return null;
}, },
}) })
); );

View File

@@ -140,20 +140,14 @@ export default class EditTagModal extends Modal<EditTagModalAttrs> {
items.add( items.add(
'submit', 'submit',
<div className="Form-group"> <div className="Form-group">
{Button.component( <Button type="submit" className="Button Button--primary EditTagModal-save" loading={this.loading}>
{ {app.translator.trans('flarum-tags.admin.edit_tag.submit_button')}
type: 'submit', </Button>
className: 'Button Button--primary EditTagModal-save',
loading: this.loading, {this.tag.exists && (
},
app.translator.trans('flarum-tags.admin.edit_tag.submit_button')
)}
{this.tag.exists ? (
<button type="button" className="Button EditTagModal-delete" onclick={this.delete.bind(this)}> <button type="button" className="Button EditTagModal-delete" onclick={this.delete.bind(this)}>
{app.translator.trans('flarum-tags.admin.edit_tag.delete_tag_button')} {app.translator.trans('flarum-tags.admin.edit_tag.delete_tag_button')}
</button> </button>
) : (
''
)} )}
</div>, </div>,
-10 -10

View File

@@ -16,20 +16,14 @@ function tagItem(tag) {
<div className="TagListItem-info"> <div className="TagListItem-info">
{tagIcon(tag)} {tagIcon(tag)}
<span className="TagListItem-name">{tag.name()}</span> <span className="TagListItem-name">{tag.name()}</span>
{Button.component({ <Button className="Button Button--link" icon="fas fa-pencil-alt" onclick={() => app.modal.show(EditTagModal, { model: tag })} />
className: 'Button Button--link',
icon: 'fas fa-pencil-alt',
onclick: () => app.modal.show(EditTagModal, { model: tag }),
})}
</div> </div>
{!tag.isChild() && tag.position() !== null ? ( {!tag.isChild() && tag.position() !== null && (
<ol className="TagListItem-children TagList"> <ol className="TagListItem-children TagList">
{sortTags(app.store.all('tags')) {sortTags(app.store.all('tags'))
.filter((child) => child.parent() === tag) .filter((child) => child.parent() === tag)
.map(tagItem)} .map(tagItem)}
</ol> </ol>
) : (
''
)} )}
</li> </li>
); );
@@ -75,14 +69,9 @@ export default class TagsPage extends ExtensionPage {
<div className="TagGroup"> <div className="TagGroup">
<label>{app.translator.trans('flarum-tags.admin.tags.primary_heading')}</label> <label>{app.translator.trans('flarum-tags.admin.tags.primary_heading')}</label>
<ol className="TagList TagList--primary">{tags.filter((tag) => tag.position() !== null && !tag.isChild()).map(tagItem)}</ol> <ol className="TagList TagList--primary">{tags.filter((tag) => tag.position() !== null && !tag.isChild()).map(tagItem)}</ol>
{Button.component( <Button className="Button TagList-button" icon="fas fa-plus" onclick={() => app.modal.show(EditTagModal, { primary: true })}>
{ {app.translator.trans('flarum-tags.admin.tags.create_primary_tag_button')}
className: 'Button TagList-button', </Button>
icon: 'fas fa-plus',
onclick: () => app.modal.show(EditTagModal, { primary: true }),
},
app.translator.trans('flarum-tags.admin.tags.create_primary_tag_button')
)}
</div> </div>
<div className="TagGroup TagGroup--secondary"> <div className="TagGroup TagGroup--secondary">
@@ -93,14 +82,9 @@ export default class TagsPage extends ExtensionPage {
.sort((a, b) => a.name().localeCompare(b.name())) .sort((a, b) => a.name().localeCompare(b.name()))
.map(tagItem)} .map(tagItem)}
</ul> </ul>
{Button.component( <Button className="Button TagList-button" icon="fas fa-plus" onclick={() => app.modal.show(EditTagModal, { primary: false })}>
{ {app.translator.trans('flarum-tags.admin.tags.create_secondary_tag_button')}
className: 'Button TagList-button', </Button>
icon: 'fas fa-plus',
onclick: () => app.modal.show(EditTagModal, { primary: false }),
},
app.translator.trans('flarum-tags.admin.tags.create_secondary_tag_button')
)}
</div> </div>
<div className="Form"> <div className="Form">
<label>{app.translator.trans('flarum-tags.admin.tags.settings_heading')}</label> <label>{app.translator.trans('flarum-tags.admin.tags.settings_heading')}</label>

View File

@@ -1,12 +1,13 @@
import extract from 'flarum/common/utils/extract'; import extract from 'flarum/common/utils/extract';
import tagLabel from './tagLabel'; import tagLabel from './tagLabel';
import sortTags from '../utils/sortTags'; import sortTags from '../utils/sortTags';
import classList from '@flarum/core/src/common/utils/classList';
export default function tagsLabel(tags, attrs = {}) { export default function tagsLabel(tags, attrs = {}) {
const children = []; const children = [];
const link = extract(attrs, 'link'); const { link, ...otherAttrs } = attrs;
attrs.className = 'TagsLabel ' + (attrs.className || ''); attrs.className = classList('TagsLabel', attrs.className);
if (tags) { if (tags) {
sortTags(tags).forEach((tag) => { sortTags(tags).forEach((tag) => {
@@ -18,5 +19,5 @@ export default function tagsLabel(tags, attrs = {}) {
children.push(tagLabel()); children.push(tagLabel());
} }
return <span {...attrs}>{children}</span>; return <span {...otherAttrs}>{children}</span>;
} }

View File

@@ -5,6 +5,7 @@ import LinkButton from 'flarum/common/components/LinkButton';
import TagLinkButton from './components/TagLinkButton'; import TagLinkButton from './components/TagLinkButton';
import TagsPage from './components/TagsPage'; import TagsPage from './components/TagsPage';
import app from 'flarum/admin/app';
import sortTags from '../common/utils/sortTags'; import sortTags from '../common/utils/sortTags';
export default function () { export default function () {
@@ -21,7 +22,7 @@ export default function () {
if (app.current.matches(TagsPage)) return; if (app.current.matches(TagsPage)) return;
items.add('separator', Separator.component(), -12); items.add('separator', <Separator />, -12);
const params = app.search.stickyParams(); const params = app.search.stickyParams();
const tags = app.store.all('tags'); const tags = app.store.all('tags');
@@ -39,7 +40,13 @@ export default function () {
// use its children to populate the dropdown. The problem here is that `view` // use its children to populate the dropdown. The problem here is that `view`
// on TagLinkButton is only called AFTER SelectDropdown, so no children are available // on TagLinkButton is only called AFTER SelectDropdown, so no children are available
// for SelectDropdown to use at the time. // for SelectDropdown to use at the time.
items.add('tag' + tag.id(), TagLinkButton.component({ model: tag, params, active }, tag?.name()), -14); items.add(
'tag' + tag.id(),
<TagLinkButton model={tag} params={params} active={active}>
{tag?.name()}
</TagLinkButton>,
-14
);
}; };
sortTags(tags) sortTags(tags)

View File

@@ -11,7 +11,7 @@ export default class TagHero extends Component {
return ( return (
<header <header
className={classList('Hero', 'TagHero', { 'TagHero--colored': color, [textContrastClass(color)]: color })} className={classList('Hero', 'TagHero', { 'TagHero--colored': color, [textContrastClass(color)]: color })}
style={color ? { '--hero-bg': color } : ''} style={color ? { '--hero-bg': color } : undefined}
> >
<div className="container"> <div className="container">
<div className="containerNarrow"> <div className="containerNarrow">

View File

@@ -6,12 +6,11 @@ import tagIcon from '../../common/helpers/tagIcon';
export default class TagLinkButton extends LinkButton { export default class TagLinkButton extends LinkButton {
view(vnode) { view(vnode) {
const tag = this.attrs.model; const tag = this.attrs.model;
const active = this.constructor.isActive(this.attrs);
const description = tag && tag.description(); const description = tag && tag.description();
const className = classList(['TagLinkButton', 'hasIcon', this.attrs.className, tag.isChild() && 'child']); const className = classList('TagLinkButton hasIcon', { child: tag.isChild() }, this.attrs.className);
return ( return (
<Link className={className} href={this.attrs.route} style={tag ? { '--color': tag.color() } : ''} title={description || ''}> <Link className={className} href={this.attrs.route} style={tag ? { '--color': tag.color() } : undefined} title={description || undefined}>
{tagIcon(tag, { className: 'Button-icon' })} {tagIcon(tag, { className: 'Button-icon' })}
<span className="Button-label">{tag ? tag.name() : app.translator.trans('flarum-tags.forum.index.untagged_link')}</span> <span className="Button-label">{tag ? tag.name() : app.translator.trans('flarum-tags.forum.index.untagged_link')}</span>
</Link> </Link>

View File

@@ -120,10 +120,8 @@ export default class TagsPage extends Page {
{tag.icon() && tagIcon(tag, {}, { useColor: false })} {tag.icon() && tagIcon(tag, {}, { useColor: false })}
<h3 className="TagTile-name">{tag.name()}</h3> <h3 className="TagTile-name">{tag.name()}</h3>
<p className="TagTile-description">{tag.description()}</p> <p className="TagTile-description">{tag.description()}</p>
{children ? ( {!!children && (
<div className="TagTile-children">{children.map((child) => [<Link href={app.route.tag(child)}>{child.name()}</Link>, ' '])}</div> <div className="TagTile-children">{children.map((child) => [<Link href={app.route.tag(child)}>{child.name()}</Link>, ' '])}</div>
) : (
''
)} )}
</Link> </Link>
{lastPostedDiscussion ? ( {lastPostedDiscussion ? (

View File

@@ -96,11 +96,7 @@ export default class AdminApplication extends Application {
super.mount(); super.mount();
m.mount(document.getElementById('app-navigation')!, { m.mount(document.getElementById('app-navigation')!, {
view: () => view: () => <Navigation className="App-backControl" drawer />,
Navigation.component({
className: 'App-backControl',
drawer: true,
}),
}); });
m.mount(document.getElementById('header-navigation')!, Navigation); m.mount(document.getElementById('header-navigation')!, Navigation);
m.mount(document.getElementById('header-primary')!, HeaderPrimary); m.mount(document.getElementById('header-primary')!, HeaderPrimary);

View File

@@ -217,7 +217,7 @@ export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAt
* return ( * return (
* <div className={attrs.className}> * <div className={attrs.className}>
* <label>{attrs.label}</label> * <label>{attrs.label}</label>
* {attrs.help && <p class="helpText">{attrs.help}</p>} * {attrs.help && <p className="helpText">{attrs.help}</p>}
* *
* My setting component! * My setting component!
* </div> * </div>

View File

@@ -19,62 +19,52 @@ export default class AppearancePage extends AdminPage {
} }
content() { content() {
return [ return (
<div className="Form"> <>
<fieldset className="AppearancePage-colors"> <div className="Form">
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend> <fieldset className="AppearancePage-colors">
{this.colorItems().toArray()} <legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
{this.colorItems().toArray()}
</fieldset>
</div>
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.logo_text')}</div>
<UploadImageButton name="logo" />
</fieldset> </fieldset>
</div>,
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.logo_text')}</div> <div className="helpText">{app.translator.trans('core.admin.appearance.favicon_text')}</div>
<UploadImageButton name="logo" /> <UploadImageButton name="favicon" />
</fieldset>, </fieldset>
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.favicon_text')}</div> <div className="helpText">{app.translator.trans('core.admin.appearance.custom_header_text')}</div>
<UploadImageButton name="favicon" /> <Button className="Button" onclick={() => app.modal.show(EditCustomHeaderModal)}>
</fieldset>, {app.translator.trans('core.admin.appearance.edit_header_button')}
</Button>
</fieldset>
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_header_text')}</div> <div className="helpText">{app.translator.trans('core.admin.appearance.custom_footer_text')}</div>
{Button.component( <Button className="Button" onclick={() => app.modal.show(EditCustomFooterModal)}>
{ {app.translator.trans('core.admin.appearance.edit_footer_button')}
className: 'Button', </Button>
onclick: () => app.modal.show(EditCustomHeaderModal), </fieldset>
},
app.translator.trans('core.admin.appearance.edit_header_button')
)}
</fieldset>,
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_footer_text')}</div> <div className="helpText">{app.translator.trans('core.admin.appearance.custom_styles_text')}</div>
{Button.component( <Button className="Button" onclick={() => app.modal.show(EditCustomCssModal)}>
{ {app.translator.trans('core.admin.appearance.edit_css_button')}
className: 'Button', </Button>
onclick: () => app.modal.show(EditCustomFooterModal), </fieldset>
}, </>
app.translator.trans('core.admin.appearance.edit_footer_button') );
)}
</fieldset>,
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_styles_text')}</div>
{Button.component(
{
className: 'Button',
onclick: () => app.modal.show(EditCustomCssModal),
},
app.translator.trans('core.admin.appearance.edit_css_button')
)}
</fieldset>,
];
} }
colorItems() { colorItems() {

View File

@@ -56,21 +56,21 @@ export default class BasicsPage<CustomAttrs extends IPageAttrs = IPageAttrs> ext
help: app.translator.trans('core.admin.basics.forum_description_text'), help: app.translator.trans('core.admin.basics.forum_description_text'),
})} })}
{Object.keys(this.localeOptions).length > 1 {Object.keys(this.localeOptions).length > 1 && (
? [ <>
this.buildSettingComponent({ {this.buildSettingComponent({
type: 'select', type: 'select',
setting: 'default_locale', setting: 'default_locale',
options: this.localeOptions, options: this.localeOptions,
label: app.translator.trans('core.admin.basics.default_language_heading'), label: app.translator.trans('core.admin.basics.default_language_heading'),
}), })}
this.buildSettingComponent({ {this.buildSettingComponent({
type: 'switch', type: 'switch',
setting: 'show_language_selector', setting: 'show_language_selector',
label: app.translator.trans('core.admin.basics.show_language_selector_label'), label: app.translator.trans('core.admin.basics.show_language_selector_label'),
}), })}
] </>
: ''} )}
<FieldSet className="BasicsPage-homePage Form-group" label={app.translator.trans('core.admin.basics.home_page_heading')}> <FieldSet className="BasicsPage-homePage Form-group" label={app.translator.trans('core.admin.basics.home_page_heading')}>
<div className="helpText">{app.translator.trans('core.admin.basics.home_page_text')}</div> <div className="helpText">{app.translator.trans('core.admin.basics.home_page_text')}</div>
@@ -91,15 +91,14 @@ export default class BasicsPage<CustomAttrs extends IPageAttrs = IPageAttrs> ext
<textarea className="FormControl" bidi={this.setting('welcome_message')} /> <textarea className="FormControl" bidi={this.setting('welcome_message')} />
</div> </div>
{Object.keys(this.displayNameOptions).length > 1 {Object.keys(this.displayNameOptions).length > 1 &&
? this.buildSettingComponent({ this.buildSettingComponent({
type: 'select', type: 'select',
setting: 'display_name_driver', setting: 'display_name_driver',
options: this.displayNameOptions, options: this.displayNameOptions,
label: app.translator.trans('core.admin.basics.display_name_heading'), label: app.translator.trans('core.admin.basics.display_name_heading'),
help: app.translator.trans('core.admin.basics.display_name_text'), help: app.translator.trans('core.admin.basics.display_name_text'),
}) })}
: ''}
{Object.keys(this.slugDriverOptions).map((model) => { {Object.keys(this.slugDriverOptions).map((model) => {
const options = this.slugDriverOptions[model]; const options = this.slugDriverOptions[model];

View File

@@ -43,16 +43,12 @@ export default class EditGroupModal<CustomAttrs extends IEditGroupModalAttrs = I
} }
title() { title() {
return [ return (
this.color() || this.icon() <>
? Badge.component({ {!!(this.color() || this.icon()) && <Badge icon={this.icon()} color={this.color()} />}{' '}
icon: this.icon(), {this.namePlural() || app.translator.trans('core.admin.edit_group.title')}
color: this.color(), </>
}) );
: '',
' ',
this.namePlural() || app.translator.trans('core.admin.edit_group.title'),
];
} }
content() { content() {
@@ -63,8 +59,8 @@ export default class EditGroupModal<CustomAttrs extends IEditGroupModalAttrs = I
); );
} }
fields() { fields(): ItemList<Mithril.Children> {
const items = new ItemList(); const items = new ItemList<Mithril.Children>();
items.add( items.add(
'name', 'name',
@@ -102,13 +98,9 @@ export default class EditGroupModal<CustomAttrs extends IEditGroupModalAttrs = I
items.add( items.add(
'hidden', 'hidden',
<div className="Form-group"> <div className="Form-group">
{Switch.component( <Switch state={this.isHidden()} onchange={this.isHidden}>
{ {app.translator.trans('core.admin.edit_group.hide_label')}
state: !!Number(this.isHidden()), </Switch>
onchange: this.isHidden,
},
app.translator.trans('core.admin.edit_group.hide_label')
)}
</div>, </div>,
10 10
); );
@@ -116,20 +108,14 @@ export default class EditGroupModal<CustomAttrs extends IEditGroupModalAttrs = I
items.add( items.add(
'submit', 'submit',
<div className="Form-group"> <div className="Form-group">
{Button.component( <Button type="submit" className="Button Button--primary EditGroupModal-save" loading={this.loading}>
{ {app.translator.trans('core.admin.edit_group.submit_button')}
type: 'submit', </Button>
className: 'Button Button--primary EditGroupModal-save',
loading: this.loading, {this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID && (
},
app.translator.trans('core.admin.edit_group.submit_button')
)}
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}> <button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
{app.translator.trans('core.admin.edit_group.delete_button')} {app.translator.trans('core.admin.edit_group.delete_button')}
</button> </button>
) : (
''
)} )}
</div>, </div>,
-10 -10

View File

@@ -12,7 +12,7 @@ export default class ExtensionLinkButton extends LinkButton {
content.unshift( content.unshift(
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}> <span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? icon(extension.icon.name) : ''} {!!extension.icon && icon(extension.icon.name)}
</span> </span>
); );
content.push(statuses); content.push(statuses);
@@ -23,7 +23,7 @@ export default class ExtensionLinkButton extends LinkButton {
statusItems(name) { statusItems(name) {
const items = new ItemList(); const items = new ItemList();
items.add('enabled', <span class={'ExtensionListItem-Dot ' + (isExtensionEnabled(name) ? 'enabled' : 'disabled')} />); items.add('enabled', <span className={'ExtensionListItem-Dot ' + (isExtensionEnabled(name) ? 'enabled' : 'disabled')} />);
return items; return items;
} }

View File

@@ -79,7 +79,7 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
<div className="container"> <div className="container">
<div className="ExtensionTitle"> <div className="ExtensionTitle">
<span className="ExtensionIcon" style={this.extension.icon}> <span className="ExtensionIcon" style={this.extension.icon}>
{this.extension.icon ? icon(this.extension.icon.name) : ''} {!!this.extension.icon && icon(this.extension.icon.name)}
</span> </span>
<div className="ExtensionName"> <div className="ExtensionName">
<h2>{this.extension.extra['flarum-extension'].title}</h2> <h2>{this.extension.extra['flarum-extension'].title}</h2>
@@ -111,7 +111,8 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
items.add('content', this.content(vnode)); items.add('content', this.content(vnode));
items.add('permissions', [ items.add(
'permissions',
<div className="ExtensionPage-permissions"> <div className="ExtensionPage-permissions">
<div className="ExtensionPage-permissions-header"> <div className="ExtensionPage-permissions-header">
<div className="container"> <div className="container">
@@ -120,13 +121,13 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
</div> </div>
<div className="container"> <div className="container">
{app.extensionData.extensionHasPermissions(this.extension.id) ? ( {app.extensionData.extensionHasPermissions(this.extension.id) ? (
ExtensionPermissionGrid.component({ extensionId: this.extension.id }) <ExtensionPermissionGrid extensionId={this.extension.id} />
) : ( ) : (
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_permissions')}</h3> <h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_permissions')}</h3>
)} )}
</div> </div>
</div>, </div>
]); );
return items; return items;
} }
@@ -210,16 +211,9 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
const extension = this.extension; const extension = this.extension;
items.add( items.add(
'readme', 'readme',
Button.component( <Button icon="fab fa-readme" className="Button Button--text" onclick={() => app.modal.show(ReadmeModal, { extension })}>
{ {app.translator.trans('core.admin.extension.readme.button_label')}
icon: 'fab fa-readme', </Button>,
class: 'Button Button--text',
onclick() {
app.modal.show(ReadmeModal, { extension });
},
},
app.translator.trans('core.admin.extension.readme.button_label')
),
10 10
); );

View File

@@ -4,6 +4,7 @@ import isExtensionEnabled from '../utils/isExtensionEnabled';
import getCategorizedExtensions from '../utils/getCategorizedExtensions'; import getCategorizedExtensions from '../utils/getCategorizedExtensions';
import Link from '../../common/components/Link'; import Link from '../../common/components/Link';
import icon from '../../common/helpers/icon'; import icon from '../../common/helpers/icon';
import classList from '../../common/utils/classList';
export default class ExtensionsWidget extends DashboardWidget { export default class ExtensionsWidget extends DashboardWidget {
oninit(vnode) { oninit(vnode) {
@@ -21,7 +22,7 @@ export default class ExtensionsWidget extends DashboardWidget {
return ( return (
<div className="ExtensionsWidget-list"> <div className="ExtensionsWidget-list">
{Object.keys(categories).map((category) => (this.categorizedExtensions[category] ? this.extensionCategory(category) : ''))} {Object.keys(categories).map((category) => !!this.categorizedExtensions[category] && this.extensionCategory(category))}
</div> </div>
); );
} }
@@ -37,11 +38,11 @@ export default class ExtensionsWidget extends DashboardWidget {
extensionWidget(extension) { extensionWidget(extension) {
return ( return (
<li className={'ExtensionListItem ' + (!isExtensionEnabled(extension.id) ? 'disabled' : '')}> <li className={classList('ExtensionListItem', { disabled: !isExtensionEnabled(extension.id) })}>
<Link href={app.route('extension', { id: extension.id })}> <Link href={app.route('extension', { id: extension.id })}>
<div className="ExtensionListItem-content"> <div className="ExtensionListItem-content">
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}> <span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? icon(extension.icon.name) : ''} {!!extension.icon && icon(extension.icon.name)}
</span> </span>
<span className="ExtensionListItem-title">{extension.extra['flarum-extension'].title}</span> <span className="ExtensionListItem-title">{extension.extra['flarum-extension'].title}</span>
</div> </div>

View File

@@ -18,7 +18,7 @@ export default class LoadingModal<ModalAttrs extends ILoadingModalAttrs = ILoadi
} }
content() { content() {
return ''; return null;
} }
onsubmit(e: Event): void { onsubmit(e: Event): void {

View File

@@ -81,29 +81,25 @@ export default class MailPage<CustomAttrs extends IPageAttrs = IPageAttrs> exten
options: Object.keys(this.driverFields!).reduce((memo, val) => ({ ...memo, [val]: val }), {}), options: Object.keys(this.driverFields!).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
label: app.translator.trans('core.admin.email.driver_heading'), label: app.translator.trans('core.admin.email.driver_heading'),
})} })}
{this.status!.sending || {this.status!.sending || <Alert dismissible={false}>{app.translator.trans('core.admin.email.not_sending_message')}</Alert>}
Alert.component(
{
dismissible: false,
},
app.translator.trans('core.admin.email.not_sending_message')
)}
{fieldKeys.length > 0 && ( {!!fieldKeys.length && (
<FieldSet label={app.translator.trans(`core.admin.email.${this.setting('mail_driver')()}_heading`)} className="MailPage-MailSettings"> <FieldSet label={app.translator.trans(`core.admin.email.${this.setting('mail_driver')()}_heading`)} className="MailPage-MailSettings">
<div className="MailPage-MailSettings-input"> <div className="MailPage-MailSettings-input">
{fieldKeys.map((field) => { {fieldKeys.map((field) => {
const fieldInfo = fields[field]; const fieldInfo = fields[field];
return [ return (
this.buildSettingComponent({ <>
type: typeof fieldInfo === 'string' ? 'text' : 'select', {this.buildSettingComponent({
label: app.translator.trans(`core.admin.email.${field}_label`), type: typeof fieldInfo === 'string' ? 'text' : 'select',
setting: field, label: app.translator.trans(`core.admin.email.${field}_label`),
options: fieldInfo, setting: field,
}), options: fieldInfo,
this.status!.errors[field] && <p className="ValidationError">{this.status!.errors[field]}</p>, })}
]; {this.status!.errors[field] && <p className="ValidationError">{this.status!.errors[field]}</p>}
</>
);
})} })}
</div> </div>
</FieldSet> </FieldSet>
@@ -112,14 +108,9 @@ export default class MailPage<CustomAttrs extends IPageAttrs = IPageAttrs> exten
<FieldSet label={app.translator.trans('core.admin.email.send_test_mail_heading')} className="MailPage-MailSettings"> <FieldSet label={app.translator.trans('core.admin.email.send_test_mail_heading')} className="MailPage-MailSettings">
<div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user!.email() })}</div> <div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user!.email() })}</div>
{Button.component( <Button className="Button Button--primary" disabled={this.sendingTest || this.isChanged()} onclick={() => this.sendTestEmail()}>
{ {app.translator.trans('core.admin.email.send_test_mail_button')}
className: 'Button Button--primary', </Button>
disabled: this.sendingTest || this.isChanged(),
onclick: () => this.sendTestEmail(),
},
app.translator.trans('core.admin.email.send_test_mail_button')
)}
</FieldSet> </FieldSet>
</div> </div>
); );

View File

@@ -10,7 +10,7 @@ import Mithril from 'mithril';
function badgeForId(id: string) { function badgeForId(id: string) {
const group = app.store.getById('groups', id); const group = app.store.getById('groups', id);
return group ? GroupBadge.component({ group, label: null }) : ''; return !!group && <GroupBadge group={group} label={null} />;
} }
function filterByRequiredPermissions(groupIds: string[], permission: string) { function filterByRequiredPermissions(groupIds: string[], permission: string) {
@@ -57,9 +57,9 @@ export default class PermissionDropdown<CustomAttrs extends IPermissionDropdownA
const adminGroup = app.store.getById<Group>('groups', Group.ADMINISTRATOR_ID)!; const adminGroup = app.store.getById<Group>('groups', Group.ADMINISTRATOR_ID)!;
if (everyone) { if (everyone) {
this.attrs.label = Badge.component({ icon: 'fas fa-globe' }); this.attrs.label = <Badge icon="fas fa-globe" />;
} else if (members) { } else if (members) {
this.attrs.label = Badge.component({ icon: 'fas fa-user' }); this.attrs.label = <Badge icon="fas fa-user" />;
} else { } else {
this.attrs.label = [badgeForId(Group.ADMINISTRATOR_ID), groupIds.map(badgeForId)]; this.attrs.label = [badgeForId(Group.ADMINISTRATOR_ID), groupIds.map(badgeForId)];
} }
@@ -67,40 +67,29 @@ export default class PermissionDropdown<CustomAttrs extends IPermissionDropdownA
if (this.showing) { if (this.showing) {
if (this.attrs.allowGuest) { if (this.attrs.allowGuest) {
children.push( children.push(
Button.component( <Button icon={everyone ? 'fas fa-check' : true} onclick={() => this.save([Group.GUEST_ID])} disabled={this.isGroupDisabled(Group.GUEST_ID)}>
{ <Badge icon="fas fa-globe" /> {app.translator.trans('core.admin.permissions_controls.everyone_button')}
icon: everyone ? 'fas fa-check' : true, </Button>
onclick: () => this.save([Group.GUEST_ID]),
disabled: this.isGroupDisabled(Group.GUEST_ID),
},
[Badge.component({ icon: 'fas fa-globe' }), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')]
)
); );
} }
children.push( children.push(
Button.component( <Button icon={members ? 'fas fa-check' : true} onclick={() => this.save([Group.MEMBER_ID])} disabled={this.isGroupDisabled(Group.MEMBER_ID)}>
{ <Badge icon="fas fa-user" /> {app.translator.trans('core.admin.permissions_controls.members_button')}
icon: members ? 'fas fa-check' : true, </Button>,
onclick: () => this.save([Group.MEMBER_ID]),
disabled: this.isGroupDisabled(Group.MEMBER_ID),
},
[Badge.component({ icon: 'fas fa-user' }), ' ', app.translator.trans('core.admin.permissions_controls.members_button')]
),
Separator.component(), <Separator />,
Button.component( <Button
{ icon={!everyone && !members ? 'fas fa-check' : true}
icon: !everyone && !members ? 'fas fa-check' : true, disabled={!everyone && !members}
disabled: !everyone && !members, onclick={(e: MouseEvent) => {
onclick: (e: MouseEvent) => { if (e.shiftKey) e.stopPropagation();
if (e.shiftKey) e.stopPropagation(); this.save([]);
this.save([]); }}
}, >
}, {badgeForId(adminGroup.id()!)} {adminGroup.namePlural()}
[badgeForId(adminGroup.id()!), ' ', adminGroup.namePlural()] </Button>
)
); );
// These groups are defined above, appearing first in the list. // These groups are defined above, appearing first in the list.
@@ -109,19 +98,18 @@ export default class PermissionDropdown<CustomAttrs extends IPermissionDropdownA
const groupButtons = app.store const groupButtons = app.store
.all<Group>('groups') .all<Group>('groups')
.filter((group) => !excludedGroups.includes(group.id()!)) .filter((group) => !excludedGroups.includes(group.id()!))
.map((group) => .map((group) => (
Button.component( <Button
{ icon={groupIds.includes(group.id()!) ? 'fas fa-check' : true}
icon: groupIds.includes(group.id()!) ? 'fas fa-check' : true, onclick={(e: MouseEvent) => {
onclick: (e: MouseEvent) => { if (e.shiftKey) e.stopPropagation();
if (e.shiftKey) e.stopPropagation(); this.toggle(group.id()!);
this.toggle(group.id()!); }}
}, disabled={this.isGroupDisabled(group.id()!) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID)}
disabled: this.isGroupDisabled(group.id()!) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID), >
}, {badgeForId(group.id()!)} {group.namePlural()}
[badgeForId(group.id()!), ' ', group.namePlural()] </Button>
) ));
);
children.push(...groupButtons); children.push(...groupButtons);
} }

View File

@@ -56,9 +56,9 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
{scopes.map((scope) => ( {scopes.map((scope) => (
<th> <th>
{scope.label}{' '} {scope.label}{' '}
{scope.onremove {!!scope.onremove && (
? Button.component({ icon: 'fas fa-times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove }) <Button icon="fas fa-times" className="Button Button--text PermissionGrid-removeScope" onclick={scope.onremove} />
: ''} )}
</th> </th>
))} ))}
<th>{this.scopeControlItems().toArray()}</th> <th>{this.scopeControlItems().toArray()}</th>
@@ -174,15 +174,16 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
{ {
icon: 'fas fa-user-plus', icon: 'fas fa-user-plus',
label: app.translator.trans('core.admin.permissions.sign_up_label'), label: app.translator.trans('core.admin.permissions.sign_up_label'),
setting: () => setting: () => (
SettingDropdown.component({ <SettingDropdown
key: 'allow_sign_up', key="allow_sign_up"
options: [ options={[
{ value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button') }, { value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button') },
{ value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button') }, { value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button') },
], ]}
lazyDraw: true, lazyDraw
}), />
),
}, },
90 90
); );
@@ -219,18 +220,22 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
setting: () => { setting: () => {
const minutes = parseInt(app.data.settings.allow_renaming, 10); const minutes = parseInt(app.data.settings.allow_renaming, 10);
return SettingDropdown.component({ return (
defaultLabel: minutes <SettingDropdown
? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { count: minutes }) defaultLabel={
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'), minutes
key: 'allow_renaming', ? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { count: minutes })
options: [ : app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')
{ value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') }, }
{ value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') }, key="allow_renaming"
{ value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') }, options={[
], { value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') },
lazyDraw: true, { value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') },
}); { value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') },
]}
lazyDraw
/>
);
}, },
}, },
90 90
@@ -272,17 +277,21 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
setting: () => { setting: () => {
const minutes = parseInt(app.data.settings.allow_post_editing, 10); const minutes = parseInt(app.data.settings.allow_post_editing, 10);
return SettingDropdown.component({ return (
defaultLabel: minutes <SettingDropdown
? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { count: minutes }) defaultLabel={
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'), minutes
key: 'allow_post_editing', ? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { count: minutes })
options: [ : app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')
{ value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') }, }
{ value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') }, key="allow_post_editing"
{ value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') }, options={[
], { value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') },
}); { value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') },
{ value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') },
]}
/>
);
}, },
}, },
90 90
@@ -457,10 +466,7 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
if ('setting' in item) { if ('setting' in item) {
return item.setting(); return item.setting();
} else if ('permission' in item) { } else if ('permission' in item) {
return PermissionDropdown.component({ return <PermissionDropdown permission={item.permission} allowGuest={item.allowGuest} />;
permission: item.permission,
allowGuest: item.allowGuest,
});
} }
return null; return null;

View File

@@ -17,28 +17,28 @@ export default class PermissionsPage extends AdminPage {
} }
content() { content() {
return [ return (
<div className="PermissionsPage-groups"> <>
{app.store <div className="PermissionsPage-groups">
.all<Group>('groups') {app.store
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()!) === -1) .all<Group>('groups')
.map((group) => ( .filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()!) === -1)
<button className="Button Group" onclick={() => app.modal.show(EditGroupModal, { group })}> .map((group) => (
{GroupBadge.component({ <button className="Button Group" onclick={() => app.modal.show(EditGroupModal, { group })}>
group, <GroupBadge group={group} className="Group-icon" label={null} />
className: 'Group-icon', <span className="Group-name">{group.namePlural()}</span>
label: null, </button>
})} ))}
<span className="Group-name">{group.namePlural()}</span> <button className="Button Group Group--add" onclick={() => app.modal.show(EditGroupModal)}>
</button> {icon('fas fa-plus', { className: 'Group-icon' })}
))} <span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
<button className="Button Group Group--add" onclick={() => app.modal.show(EditGroupModal)}> </button>
{icon('fas fa-plus', { className: 'Group-icon' })} </div>
<span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
</button>
</div>,
<div className="PermissionsPage-permissions">{PermissionGrid.component()}</div>, <div className="PermissionsPage-permissions">
]; <PermissionGrid />
</div>
</>
);
} }
} }

View File

@@ -39,14 +39,20 @@ export default class ReadmeModal<CustomAttrs extends IReadmeModalAttrs = IReadme
} }
content() { content() {
const text = app.translator.trans('core.admin.extension.readme.no_readme');
return ( return (
<div className="Modal-body"> <div className="Modal-body">
{this.loading ? ( {this.loading ? (
<div className="ReadmeModal-loading">{LoadingIndicator.component()}</div> <div className="ReadmeModal-loading">
<LoadingIndicator />
</div>
) : ( ) : (
<div>{this.readme.content() ? m.trust(this.readme.content()) : Placeholder.component({ text })}</div> <div>
{this.readme.content() ? (
m.trust(this.readme.content())
) : (
<Placeholder text={app.translator.trans('core.admin.extension.readme.no_readme')} />
)}
</div>
)} )}
</div> </div>
); );

View File

@@ -39,13 +39,9 @@ export default class SessionDropdown<CustomAttrs extends ISessionDropdownAttrs =
items.add( items.add(
'logOut', 'logOut',
Button.component( <Button icon="fas fa-sign-out-alt" onclick={app.session.logout.bind(app.session)}>
{ {app.translator.trans('core.admin.header.log_out_button')}
icon: 'fas fa-sign-out-alt', </Button>,
onclick: app.session.logout.bind(app.session),
},
app.translator.trans('core.admin.header.log_out_button')
),
-100 -100
); );

View File

@@ -35,13 +35,10 @@ export default class SettingDropdown<CustomAttrs extends ISettingDropdownAttrs =
children: this.attrs.options.map(({ value, label }) => { children: this.attrs.options.map(({ value, label }) => {
const active = app.data.settings[this.attrs.setting!] === value; const active = app.data.settings[this.attrs.setting!] === value;
return Button.component( return (
{ <Button icon={active ? 'fas fa-check' : true} onclick={saveSettings.bind(this, { [this.attrs.setting!]: value })} active={active}>
icon: active ? 'fas fa-check' : true, {label}
onclick: saveSettings.bind(this, { [this.attrs.setting!]: value }), </Button>
active,
},
label
); );
}), }),
}); });

View File

@@ -13,7 +13,7 @@ export default abstract class SettingsModal<CustomAttrs extends ISettingsModalAt
loading: boolean = false; loading: boolean = false;
form(): Mithril.Children { form(): Mithril.Children {
return ''; return null;
} }
content() { content() {

View File

@@ -1,12 +1,13 @@
import app from '../../admin/app'; import app from '../../admin/app';
import Button from '../../common/components/Button'; import Button from '../../common/components/Button';
import classList from '../../common/utils/classList';
export default class UploadImageButton extends Button { export default class UploadImageButton extends Button {
loading = false; loading = false;
view(vnode) { view(vnode) {
this.attrs.loading = this.loading; this.attrs.loading = this.loading;
this.attrs.className = (this.attrs.className || '') + ' Button'; this.attrs.className = classList(this.attrs.className, 'Button');
if (app.data.settings[this.attrs.name + '_path']) { if (app.data.settings[this.attrs.name + '_path']) {
this.attrs.onclick = this.remove.bind(this); this.attrs.onclick = this.remove.bind(this);
@@ -37,7 +38,7 @@ export default class UploadImageButton extends Button {
$input $input
.appendTo('body') .appendTo('body')
.hide() .hide()
.click() .trigger('click')
.on('change', (e) => { .on('change', (e) => {
const body = new FormData(); const body = new FormData();
body.append(this.attrs.name, $(e.target)[0].files[0]); body.append(this.attrs.name, $(e.target)[0].files[0]);

View File

@@ -107,7 +107,7 @@ export default class UserListPage extends AdminPage {
this.loadPage(this.pageNumber); this.loadPage(this.pageNumber);
return [ return [
<section class="UserListPage-grid UserListPage-grid--loading"> <section className="UserListPage-grid UserListPage-grid--loading">
<LoadingIndicator containerClassName="LoadingIndicator--block" size="large" /> <LoadingIndicator containerClassName="LoadingIndicator--block" size="large" />
</section>, </section>,
]; ];
@@ -128,9 +128,9 @@ export default class UserListPage extends AdminPage {
}} }}
/> />
</div>, </div>,
<p class="UserListPage-totalUsers">{app.translator.trans('core.admin.users.total_users', { count: this.userCount })}</p>, <p className="UserListPage-totalUsers">{app.translator.trans('core.admin.users.total_users', { count: this.userCount })}</p>,
<section <section
class={classList(['UserListPage-grid', this.isLoadingPage ? 'UserListPage-grid--loadingPage' : 'UserListPage-grid--loaded'])} className={classList(['UserListPage-grid', this.isLoadingPage ? 'UserListPage-grid--loadingPage' : 'UserListPage-grid--loaded'])}
style={{ '--columns': columns.length }} style={{ '--columns': columns.length }}
role="table" role="table"
// +1 to account for header // +1 to account for header
@@ -141,7 +141,7 @@ export default class UserListPage extends AdminPage {
> >
{/* Render columns */} {/* Render columns */}
{columns.map((column, colIndex) => ( {columns.map((column, colIndex) => (
<div class="UserListPage-grid-header" role="columnheader" aria-colindex={colIndex + 1} aria-rowindex={1}> <div className="UserListPage-grid-header" role="columnheader" aria-colindex={colIndex + 1} aria-rowindex={1}>
{column.name} {column.name}
</div> </div>
))} ))}
@@ -153,7 +153,7 @@ export default class UserListPage extends AdminPage {
return ( return (
<div <div
class={classList(['UserListPage-grid-rowItem', rowIndex % 2 > 0 && 'UserListPage-grid-rowItem--shaded'])} className={classList(['UserListPage-grid-rowItem', rowIndex % 2 > 0 && 'UserListPage-grid-rowItem--shaded'])}
data-user-id={user.id()} data-user-id={user.id()}
data-column-name={col.itemName} data-column-name={col.itemName}
aria-colindex={colIndex + 1} aria-colindex={colIndex + 1}
@@ -170,7 +170,7 @@ export default class UserListPage extends AdminPage {
{/* Loading spinner that shows when a new page is being loaded */} {/* Loading spinner that shows when a new page is being loaded */}
{this.isLoadingPage && <LoadingIndicator size="large" />} {this.isLoadingPage && <LoadingIndicator size="large" />}
</section>, </section>,
<nav class="UserListPage-gridPagination"> <nav className="UserListPage-gridPagination">
<Button <Button
disabled={this.pageNumber === 0} disabled={this.pageNumber === 0}
title={app.translator.trans('core.admin.users.pagination.first_page_button')} title={app.translator.trans('core.admin.users.pagination.first_page_button')}
@@ -185,7 +185,7 @@ export default class UserListPage extends AdminPage {
icon="fas fa-chevron-left" icon="fas fa-chevron-left"
className="Button Button--icon UserListPage-backBtn" className="Button Button--icon UserListPage-backBtn"
/> />
<span class="UserListPage-pageNumber"> <span className="UserListPage-pageNumber">
{app.translator.trans('core.admin.users.pagination.page_counter', { {app.translator.trans('core.admin.users.pagination.page_counter', {
// https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/ // https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/
current: ( current: (
@@ -260,7 +260,7 @@ export default class UserListPage extends AdminPage {
'id', 'id',
{ {
name: app.translator.trans('core.admin.users.grid.columns.user_id.title'), name: app.translator.trans('core.admin.users.grid.columns.user_id.title'),
content: (user: User) => user.id() ?? '', content: (user: User) => user.id() ?? null,
}, },
100 100
); );
@@ -300,7 +300,7 @@ export default class UserListPage extends AdminPage {
{ {
name: app.translator.trans('core.admin.users.grid.columns.join_time.title'), name: app.translator.trans('core.admin.users.grid.columns.join_time.title'),
content: (user: User) => ( content: (user: User) => (
<span class="UserList-joinDate" title={user.joinTime()}> <span className="UserList-joinDate" title={user.joinTime()}>
{dayjs(user.joinTime()).format('LLL')} {dayjs(user.joinTime()).format('LLL')}
</span> </span>
), ),
@@ -372,13 +372,13 @@ export default class UserListPage extends AdminPage {
} }
return ( return (
<div class="UserList-email" key={user.id()} data-email-shown="false"> <div className="UserList-email" key={user.id()} data-email-shown="false">
<span class="UserList-emailAddress" aria-hidden="true" onclick={() => setEmailVisibility(true)}> <span className="UserList-emailAddress" aria-hidden="true" onclick={() => setEmailVisibility(true)}>
{user.email()} {user.email()}
</span> </span>
<button <button
onclick={toggleEmailVisibility} onclick={toggleEmailVisibility}
class="Button Button--text UserList-emailIconBtn" className="Button Button--text UserList-emailIconBtn"
title={app.translator.trans('core.admin.users.grid.columns.email.visibility_show')} title={app.translator.trans('core.admin.users.grid.columns.email.visibility_show')}
> >
{icon('far fa-eye-slash fa-fw', { className: 'icon' })} {icon('far fa-eye-slash fa-fw', { className: 'icon' })}

View File

@@ -50,7 +50,7 @@ export default class Alert<T extends AlertAttrs = AlertAttrs> extends Component<
<Button <Button
aria-label={app.translator.trans('core.lib.alert.dismiss_a11y_label')} aria-label={app.translator.trans('core.lib.alert.dismiss_a11y_label')}
icon="fas fa-times" icon="fas fa-times"
class="Button Button--link Button--icon Alert-dismiss" className="Button Button--link Button--icon Alert-dismiss"
onclick={ondismiss} onclick={ondismiss}
/> />
); );
@@ -59,13 +59,13 @@ export default class Alert<T extends AlertAttrs = AlertAttrs> extends Component<
return ( return (
<div {...attrs}> <div {...attrs}>
{!!title && ( {!!title && (
<div class="Alert-title"> <div className="Alert-title">
{!!icon && <span class="Alert-title-icon">{iconHelper(icon)}</span>} {!!icon && <span className="Alert-title-icon">{iconHelper(icon)}</span>}
<span class="Alert-title-text">{title}</span> <span className="Alert-title-text">{title}</span>
</div> </div>
)} )}
<span class="Alert-body">{content}</span> <span className="Alert-body">{content}</span>
<ul class="Alert-controls">{listItems(controls.concat(dismissControl))}</ul> <ul className="Alert-controls">{listItems(controls.concat(dismissControl))}</ul>
</div> </div>
); );
} }

View File

@@ -21,7 +21,7 @@ export default class AlertManager<CustomAttrs extends IAlertManagerAttrs = IAler
const activeAlerts = this.state.getActiveAlerts(); const activeAlerts = this.state.getActiveAlerts();
return ( return (
<div class="AlertManager"> <div className="AlertManager">
{Object.keys(activeAlerts) {Object.keys(activeAlerts)
.map(Number) .map(Number)
.map((key) => { .map((key) => {
@@ -29,7 +29,7 @@ export default class AlertManager<CustomAttrs extends IAlertManagerAttrs = IAler
const urgent = alert.attrs.type === 'error'; const urgent = alert.attrs.type === 'error';
return ( return (
<div class="AlertManager-alert" role="alert" aria-live={urgent ? 'assertive' : 'polite'}> <div className="AlertManager-alert" role="alert" aria-live={urgent ? 'assertive' : 'polite'}>
<alert.componentClass {...alert.attrs} ondismiss={this.state.dismiss.bind(this.state, key)}> <alert.componentClass {...alert.attrs} ondismiss={this.state.dismiss.bind(this.state, key)}>
{alert.children} {alert.children}
</alert.componentClass> </alert.componentClass>

View File

@@ -89,19 +89,12 @@ export default class EditUserModal<CustomAttrs extends IEditUserModalAttrs = IEd
disabled={this.nonAdminEditingAdmin()} disabled={this.nonAdminEditingAdmin()}
/> />
</div> </div>
{!this.isEmailConfirmed() && this.userIsAdmin(app.session.user) ? ( {!this.isEmailConfirmed() && this.userIsAdmin(app.session.user) && (
<div> <div>
{Button.component( <Button className="Button Button--block" loading={this.loading} onclick={this.activate.bind(this)}>
{ {app.translator.trans('core.lib.edit_user.activate_button')}
className: 'Button Button--block', </Button>
loading: this.loading,
onclick: this.activate.bind(this),
},
app.translator.trans('core.lib.edit_user.activate_button')
)}
</div> </div>
) : (
''
)} )}
</div>, </div>,
30 30
@@ -126,7 +119,7 @@ export default class EditUserModal<CustomAttrs extends IEditUserModalAttrs = IEd
/> />
{app.translator.trans('core.lib.edit_user.set_password_label')} {app.translator.trans('core.lib.edit_user.set_password_label')}
</label> </label>
{this.setPassword() ? ( {this.setPassword() && (
<input <input
className="FormControl" className="FormControl"
type="password" type="password"
@@ -135,8 +128,6 @@ export default class EditUserModal<CustomAttrs extends IEditUserModalAttrs = IEd
bidi={this.password} bidi={this.password}
disabled={this.nonAdminEditingAdmin()} disabled={this.nonAdminEditingAdmin()}
/> />
) : (
''
)} )}
</div> </div>
</div>, </div>,
@@ -166,7 +157,7 @@ export default class EditUserModal<CustomAttrs extends IEditUserModalAttrs = IEd
group.id() === Group.ADMINISTRATOR_ID && (this.attrs.user === app.session.user || !this.userIsAdmin(app.session.user)) group.id() === Group.ADMINISTRATOR_ID && (this.attrs.user === app.session.user || !this.userIsAdmin(app.session.user))
} }
/> />
{GroupBadge.component({ group, label: '' })} {group.nameSingular()} <GroupBadge group={group} label={null} /> {group.nameSingular()}
</label> </label>
) )
)} )}
@@ -179,14 +170,9 @@ export default class EditUserModal<CustomAttrs extends IEditUserModalAttrs = IEd
items.add( items.add(
'submit', 'submit',
<div className="Form-group"> <div className="Form-group">
{Button.component( <Button className="Button Button--primary" type="submit" loading={this.loading}>
{ {app.translator.trans('core.lib.edit_user.submit_button')}
className: 'Button Button--primary', </Button>
type: 'submit',
loading: this.loading,
},
app.translator.trans('core.lib.edit_user.submit_button')
)}
</div>, </div>,
-10 -10
); );
@@ -255,14 +241,14 @@ export default class EditUserModal<CustomAttrs extends IEditUserModalAttrs = IEd
}); });
} }
nonAdminEditingAdmin() { nonAdminEditingAdmin(): boolean {
return this.userIsAdmin(this.attrs.user) && !this.userIsAdmin(app.session.user); return this.userIsAdmin(this.attrs.user) && !this.userIsAdmin(app.session.user);
} }
/** /**
* @internal * @internal
*/ */
protected userIsAdmin(user: User | null) { protected userIsAdmin(user: User | null): boolean {
return user && (user.groups() || []).some((g) => g?.id() === Group.ADMINISTRATOR_ID); return !!(user?.groups() || []).some((g) => g?.id() === Group.ADMINISTRATOR_ID);
} }
} }

View File

@@ -13,7 +13,7 @@ export default class Link extends Component {
view(vnode) { view(vnode) {
let { options = {}, ...attrs } = vnode.attrs; let { options = {}, ...attrs } = vnode.attrs;
attrs.href = attrs.href || ''; attrs.href ||= '';
// For some reason, m.route.Link does not like vnode.text, so if present, we // For some reason, m.route.Link does not like vnode.text, so if present, we
// need to convert it to text vnodes and store it in children. // need to convert it to text vnodes and store it in children.

View File

@@ -155,7 +155,11 @@ export default abstract class Modal<ModalAttrs extends IInternalModalAttrs = IIn
<h3 className="App-titleControl App-titleControl--text">{this.title()}</h3> <h3 className="App-titleControl App-titleControl--text">{this.title()}</h3>
</div> </div>
{this.alertAttrs ? <div className="Modal-alert">{Alert.component(this.alertAttrs)}</div> : ''} {!!this.alertAttrs && (
<div className="Modal-alert">
<Alert {...this.alertAttrs} />
</div>
)}
{this.content()} {this.content()}
</form> </form>

View File

@@ -37,7 +37,7 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
return ( return (
<div <div
key={modal.key} key={modal.key}
class="ModalManager modal" className="ModalManager modal"
data-modal-key={modal.key} data-modal-key={modal.key}
data-modal-number={i} data-modal-number={i}
role="dialog" role="dialog"
@@ -62,7 +62,7 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
{this.attrs.state.backdropShown && ( {this.attrs.state.backdropShown && (
<div <div
class="Modal-backdrop backdrop" className="Modal-backdrop backdrop"
ontransitionend={this.onBackdropTransitionEnd.bind(this)} ontransitionend={this.onBackdropTransitionEnd.bind(this)}
data-showing={!!this.attrs.state.modalList.length} data-showing={!!this.attrs.state.modalList.length}
style={{ '--modal-count': this.attrs.state.modalList.length }} style={{ '--modal-count': this.attrs.state.modalList.length }}

View File

@@ -3,6 +3,7 @@ import Component from '../Component';
import Button from './Button'; import Button from './Button';
import LinkButton from './LinkButton'; import LinkButton from './LinkButton';
import type Mithril from 'mithril'; import type Mithril from 'mithril';
import classList from '../utils/classList';
/** /**
* The `Navigation` component displays a set of navigation buttons. Typically * The `Navigation` component displays a set of navigation buttons. Typically
@@ -25,7 +26,7 @@ export default class Navigation extends Component {
return ( return (
<div <div
className={'Navigation ButtonGroup ' + (this.attrs.className || '')} className={classList('Navigation ButtonGroup', this.attrs.className)}
onmouseenter={pane && pane.show.bind(pane)} onmouseenter={pane && pane.show.bind(pane)}
onmouseleave={pane && pane.onmouseleave.bind(pane)} onmouseleave={pane && pane.onmouseleave.bind(pane)}
> >
@@ -41,17 +42,19 @@ export default class Navigation extends Component {
const { history } = app; const { history } = app;
const previous = history?.getPrevious(); const previous = history?.getPrevious();
return LinkButton.component({ return (
className: 'Button Navigation-back Button--icon', <LinkButton
href: history?.backUrl(), className="Button Navigation-back Button--icon"
icon: 'fas fa-chevron-left', href={history?.backUrl()}
'aria-label': previous?.title, icon="fas fa-chevron-left"
onclick: (e: MouseEvent) => { aria-label={previous?.title}
if (e.shiftKey || e.ctrlKey || e.metaKey || e.button === 1) return; onclick={(e: MouseEvent) => {
e.preventDefault(); if (e.shiftKey || e.ctrlKey || e.metaKey || e.which === 2) return;
history?.back(); e.preventDefault();
}, history?.back();
}); }}
/>
);
} }
/** /**
@@ -60,32 +63,36 @@ export default class Navigation extends Component {
protected getPaneButton(): Mithril.Children { protected getPaneButton(): Mithril.Children {
const { pane } = app; const { pane } = app;
if (!pane || !pane.active) return ''; if (!pane || !pane.active) return null;
return Button.component({ return (
className: 'Button Button--icon Navigation-pin' + (pane.pinned ? ' active' : ''), <Button
onclick: pane.togglePinned.bind(pane), className={classList('Button Button--icon Navigation-pin', { active: pane.pinned })}
icon: 'fas fa-thumbtack', onclick={pane.togglePinned.bind(pane)}
}); icon="fas fa-thumbtack"
/>
);
} }
/** /**
* Get the drawer toggle button. * Get the drawer toggle button.
*/ */
protected getDrawerButton(): Mithril.Children { protected getDrawerButton(): Mithril.Children {
if (!this.attrs.drawer) return ''; if (!this.attrs.drawer) return null;
const { drawer } = app; const { drawer } = app;
const user = app.session.user; const user = app.session.user;
return Button.component({ return (
className: 'Button Button--icon Navigation-drawer' + (user && user.newNotificationCount() ? ' new' : ''), <Button
onclick: (e: MouseEvent) => { className={classList('Button Button--icon Navigation-drawer', { new: user?.newNotificationCount() })}
e.stopPropagation(); onclick={(e: MouseEvent) => {
drawer.show(); e.stopPropagation();
}, drawer.show();
icon: 'fas fa-bars', }}
'aria-label': app.translator.trans('core.lib.nav.drawer_button'), icon="fas fa-bars"
}); aria-label={app.translator.trans('core.lib.nav.drawer_button')}
/>
);
} }
} }

View File

@@ -12,7 +12,7 @@ export default class RequestErrorModal<CustomAttrs extends IRequestErrorModalAtt
} }
title() { title() {
return this.attrs.error.xhr ? `${this.attrs.error.xhr.status} ${this.attrs.error.xhr.statusText}` : ''; return !!this.attrs.error.xhr && `${this.attrs.error.xhr.status} ${this.attrs.error.xhr.statusText}`;
} }
content() { content() {

View File

@@ -28,7 +28,7 @@ export default class SplitDropdown extends Dropdown {
return ( return (
<> <>
{Button.component(buttonAttrs, firstChild.children)} <Button {...buttonAttrs}>{firstChild.children}</Button>
<button <button
className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName} className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName}
aria-haspopup="menu" aria-haspopup="menu"

View File

@@ -1,3 +1,4 @@
import classList from '../utils/classList';
import Checkbox, { ICheckboxAttrs } from './Checkbox'; import Checkbox, { ICheckboxAttrs } from './Checkbox';
/** /**
@@ -8,10 +9,10 @@ export default class Switch extends Checkbox {
static initAttrs(attrs: ICheckboxAttrs) { static initAttrs(attrs: ICheckboxAttrs) {
super.initAttrs(attrs); super.initAttrs(attrs);
attrs.className = (attrs.className || '') + ' Checkbox--switch'; attrs.className = classList(attrs.className, 'Checkbox--switch');
} }
getDisplay() { getDisplay() {
return this.attrs.loading ? super.getDisplay() : ''; return !!this.attrs.loading && super.getDisplay();
} }
} }

View File

@@ -97,15 +97,9 @@ export default class TextEditor extends Component {
items.add( items.add(
'submit', 'submit',
Button.component( <Button icon="fas fa-paper-plane" className="Button Button--primary" itemClassName="App-primaryControl" onclick={this.onsubmit.bind(this)}>
{ {this.attrs.submitLabel}
icon: 'fas fa-paper-plane', </Button>
className: 'Button Button--primary',
itemClassName: 'App-primaryControl',
onclick: this.onsubmit.bind(this),
},
this.attrs.submitLabel
)
); );
if (this.attrs.preview) { if (this.attrs.preview) {

View File

@@ -1,6 +1,7 @@
import type Mithril from 'mithril'; import type Mithril from 'mithril';
import type { ComponentAttrs } from '../Component'; import type { ComponentAttrs } from '../Component';
import User from '../models/User'; import User from '../models/User';
import classList from '../utils/classList';
export interface AvatarAttrs extends ComponentAttrs {} export interface AvatarAttrs extends ComponentAttrs {}
@@ -11,7 +12,7 @@ export interface AvatarAttrs extends ComponentAttrs {}
* @param attrs Attributes to apply to the avatar element * @param attrs Attributes to apply to the avatar element
*/ */
export default function avatar(user: User | null, attrs: ComponentAttrs = {}): Mithril.Vnode { export default function avatar(user: User | null, attrs: ComponentAttrs = {}): Mithril.Vnode {
attrs.className = 'Avatar ' + (attrs.className || ''); attrs.className = classList('Avatar', attrs.className);
attrs.loading ??= 'lazy'; attrs.loading ??= 'lazy';
let content: string = ''; let content: string = '';

View File

@@ -1,4 +1,5 @@
import type Mithril from 'mithril'; import type Mithril from 'mithril';
import classList from '../utils/classList';
/** /**
* The `icon` helper displays an icon. * The `icon` helper displays an icon.
@@ -7,7 +8,7 @@ import type Mithril from 'mithril';
* @param attrs Any other attributes to apply. * @param attrs Any other attributes to apply.
*/ */
export default function icon(fontClass: string, attrs: Mithril.Attributes = {}): Mithril.Vnode { export default function icon(fontClass: string, attrs: Mithril.Attributes = {}): Mithril.Vnode {
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || ''); attrs.className = classList('icon', fontClass, attrs.className);
return <i aria-hidden="true" {...attrs} />; return <i aria-hidden="true" {...attrs} />;
} }

View File

@@ -3,7 +3,7 @@ import type Mithril from 'mithril';
import User from '../models/User'; import User from '../models/User';
/** /**
* The `username` helper displays a user's username in a <span class="username"> * The `username` helper displays a user's username in a <span className="username">
* tag. If the user doesn't exist, the username will be displayed as [deleted]. * tag. If the user doesn't exist, the username will be displayed as [deleted].
*/ */
export default function username(user: User | null | undefined | false): Mithril.Vnode { export default function username(user: User | null | undefined | false): Mithril.Vnode {

View File

@@ -161,7 +161,7 @@ export default class User extends Model {
savePreferences(newPreferences: Record<string, unknown>): Promise<this> { savePreferences(newPreferences: Record<string, unknown>): Promise<this> {
const preferences = this.preferences(); const preferences = this.preferences();
Object.assign(preferences || {}, newPreferences); Object.assign(preferences ?? {}, newPreferences);
return this.save({ preferences }); return this.save({ preferences });
} }

View File

@@ -1,4 +1,4 @@
import app from '../forum/app'; import app from './app';
import History from './utils/History'; import History from './utils/History';
import Pane from './utils/Pane'; import Pane from './utils/Pane';
@@ -115,11 +115,11 @@ export default class ForumApplication extends Application {
// We mount navigation and header components after the page, so components // We mount navigation and header components after the page, so components
// like the back button can access the updated state when rendering. // like the back button can access the updated state when rendering.
m.mount(document.getElementById('app-navigation')!, { view: () => Navigation.component({ className: 'App-backControl', drawer: true }) }); m.mount(document.getElementById('app-navigation')!, { view: () => <Navigation className="App-backControl" drawer /> });
m.mount(document.getElementById('header-navigation')!, Navigation); m.mount(document.getElementById('header-navigation')!, Navigation);
m.mount(document.getElementById('header-primary')!, HeaderPrimary); m.mount(document.getElementById('header-primary')!, HeaderPrimary);
m.mount(document.getElementById('header-secondary')!, HeaderSecondary); m.mount(document.getElementById('header-secondary')!, HeaderSecondary);
m.mount(document.getElementById('composer')!, { view: () => Composer.component({ state: this.composer }) }); m.mount(document.getElementById('composer')!, { view: () => <Composer state={this.composer} /> });
alertEmailConfirmation(this); alertEmailConfirmation(this);

View File

@@ -95,17 +95,13 @@ export default class CommentPost extends Post {
const post = this.attrs.post; const post = this.attrs.post;
const attrs = super.elementAttrs(); const attrs = super.elementAttrs();
attrs.className = attrs.className = classList(attrs.className, 'CommentPost', {
(attrs.className || '') + 'Post--renderFailed': post.renderFailed(),
' ' + 'Post--hidden': post.isHidden(),
classList({ 'Post--edited': post.isEdited(),
CommentPost: true, revealContent: this.revealContent,
'Post--renderFailed': post.renderFailed(), editing: this.isEditing(),
'Post--hidden': post.isHidden(), });
'Post--edited': post.isEdited(),
revealContent: this.revealContent,
editing: this.isEditing(),
});
if (this.isEditing()) attrs['aria-busy'] = 'true'; if (this.isEditing()) attrs['aria-busy'] = 'true';
@@ -130,24 +126,24 @@ export default class CommentPost extends Post {
items.add( items.add(
'user', 'user',
PostUser.component({ <PostUser
post, post={post}
cardVisible: this.cardVisible, cardVisible={this.cardVisible}
oncardshow: () => { oncardshow={() => {
this.cardVisible = true; this.cardVisible = true;
m.redraw(); m.redraw();
}, }}
oncardhide: () => { oncardhide={() => {
this.cardVisible = false; this.cardVisible = false;
m.redraw(); m.redraw();
}, }}
}), />,
100 100
); );
items.add('meta', PostMeta.component({ post })); items.add('meta', <PostMeta post={post} />);
if (post.isEdited() && !post.isHidden()) { if (post.isEdited() && !post.isHidden()) {
items.add('edited', PostEdited.component({ post })); items.add('edited', <PostEdited post={post} />);
} }
// If the post is hidden, add a button that allows toggling the visibility // If the post is hidden, add a button that allows toggling the visibility
@@ -155,11 +151,7 @@ export default class CommentPost extends Post {
if (post.isHidden()) { if (post.isHidden()) {
items.add( items.add(
'toggle', 'toggle',
Button.component({ <Button className="Button Button--default Button--more" icon="fas fa-ellipsis-h" onclick={this.toggleContent.bind(this)} />
className: 'Button Button--default Button--more',
icon: 'fas fa-ellipsis-h',
onclick: this.toggleContent.bind(this),
})
); );
} }

View File

@@ -46,12 +46,14 @@ export default class Composer extends Component {
// Set up a handler so that clicks on the content will show the composer. // Set up a handler so that clicks on the content will show the composer.
const showIfMinimized = this.state.position === ComposerState.Position.MINIMIZED ? this.state.show.bind(this.state) : undefined; const showIfMinimized = this.state.position === ComposerState.Position.MINIMIZED ? this.state.show.bind(this.state) : undefined;
const ComposerBody = body.componentClass;
return ( return (
<div className={'Composer ' + classList(classes)}> <div className={'Composer ' + classList(classes)}>
<div className="Composer-handle" oncreate={this.configHandle.bind(this)} /> <div className="Composer-handle" oncreate={this.configHandle.bind(this)} />
<ul className="Composer-controls">{listItems(this.controlItems().toArray())}</ul> <ul className="Composer-controls">{listItems(this.controlItems().toArray())}</ul>
<div className="Composer-content" onclick={showIfMinimized}> <div className="Composer-content" onclick={showIfMinimized}>
{body.componentClass ? body.componentClass.component({ ...body.attrs, composer: this.state, disabled: classes.minimized }) : ''} {ComposerBody && <ComposerBody {...body.attrs} composer={this.state} disabled={classes.minimized} />}
</div> </div>
</div> </div>
); );
@@ -325,41 +327,41 @@ export default class Composer extends Component {
if (this.state.position === ComposerState.Position.FULLSCREEN) { if (this.state.position === ComposerState.Position.FULLSCREEN) {
items.add( items.add(
'exitFullScreen', 'exitFullScreen',
ComposerButton.component({ <ComposerButton
icon: 'fas fa-compress', icon="fas fa-compress"
title: app.translator.trans('core.forum.composer.exit_full_screen_tooltip'), title={app.translator.trans('core.forum.composer.exit_full_screen_tooltip')}
onclick: this.state.exitFullScreen.bind(this.state), onclick={this.state.exitFullScreen.bind(this.state)}
}) />
); );
} else { } else {
if (this.state.position !== ComposerState.Position.MINIMIZED) { if (this.state.position !== ComposerState.Position.MINIMIZED) {
items.add( items.add(
'minimize', 'minimize',
ComposerButton.component({ <ComposerButton
icon: 'fas fa-minus minimize', icon="fas fa-minus minimize"
title: app.translator.trans('core.forum.composer.minimize_tooltip'), title={app.translator.trans('core.forum.composer.minimize_tooltip')}
onclick: this.state.minimize.bind(this.state), onclick={this.state.minimize.bind(this.state)}
itemClassName: 'App-backControl', itemClassName="App-backControl"
}) />
); );
items.add( items.add(
'fullScreen', 'fullScreen',
ComposerButton.component({ <ComposerButton
icon: 'fas fa-expand', icon="fas fa-expand"
title: app.translator.trans('core.forum.composer.full_screen_tooltip'), title={app.translator.trans('core.forum.composer.full_screen_tooltip')}
onclick: this.state.fullScreen.bind(this.state), onclick={this.state.fullScreen.bind(this.state)}
}) />
); );
} }
items.add( items.add(
'close', 'close',
ComposerButton.component({ <ComposerButton
icon: 'fas fa-times', icon="fas fa-times"
title: app.translator.trans('core.forum.composer.close_tooltip'), title={app.translator.trans('core.forum.composer.close_tooltip')}
onclick: this.state.close.bind(this.state), onclick={this.state.close.bind(this.state)}
}) />
); );
} }

View File

@@ -50,21 +50,21 @@ export default class ComposerBody extends Component {
view() { view() {
return ( return (
<ConfirmDocumentUnload when={this.hasChanges.bind(this)}> <ConfirmDocumentUnload when={this.hasChanges.bind(this)}>
<div className={'ComposerBody ' + (this.attrs.className || '')}> <div className={classList('ComposerBody', this.attrs.className)}>
{avatar(this.attrs.user, { className: 'ComposerBody-avatar' })} {avatar(this.attrs.user, { className: 'ComposerBody-avatar' })}
<div className="ComposerBody-content"> <div className="ComposerBody-content">
<ul className="ComposerBody-header">{listItems(this.headerItems().toArray())}</ul> <ul className="ComposerBody-header">{listItems(this.headerItems().toArray())}</ul>
<div className="ComposerBody-editor"> <div className="ComposerBody-editor">
{TextEditor.component({ <TextEditor
submitLabel: this.attrs.submitLabel, submitLabel={this.attrs.submitLabel}
placeholder: this.attrs.placeholder, placeholder={this.attrs.placeholder}
disabled: this.loading || this.attrs.disabled, disabled={this.loading || this.attrs.disabled}
composer: this.composer, composer={this.composer}
preview: this.jumpToPreview && this.jumpToPreview.bind(this), preview={this.jumpToPreview?.bind(this)}
onchange: this.composer.fields.content, onchange={this.composer.fields.content}
onsubmit: this.onsubmit.bind(this), onsubmit={this.onsubmit.bind(this)}
value: this.composer.fields.content(), value={this.composer.fields.content()}
})} />
</div> </div>
</div> </div>
<LoadingIndicator display="unset" containerClassName={classList('ComposerBody-loading', this.loading && 'active')} size="large" /> <LoadingIndicator display="unset" containerClassName={classList('ComposerBody-loading', this.loading && 'active')} size="large" />

View File

@@ -28,24 +28,26 @@ export default class DiscussionList extends Component {
if (isLoading) { if (isLoading) {
loading = <LoadingIndicator />; loading = <LoadingIndicator />;
} else if (state.hasNext()) { } else if (state.hasNext()) {
loading = Button.component( loading = (
{ <Button className="Button" onclick={state.loadNext.bind(state)}>
className: 'Button', {app.translator.trans('core.forum.discussion_list.load_more_button')}
onclick: state.loadNext.bind(state), </Button>
},
app.translator.trans('core.forum.discussion_list.load_more_button')
); );
} }
if (state.isEmpty()) { if (state.isEmpty()) {
const text = app.translator.trans('core.forum.discussion_list.empty_text'); const text = app.translator.trans('core.forum.discussion_list.empty_text');
return <div className="DiscussionList">{Placeholder.component({ text })}</div>; return (
<div className="DiscussionList">
<Placeholder text={text} />
</div>
);
} }
const pageSize = state.pageSize; const pageSize = state.pageSize;
return ( return (
<div class={classList('DiscussionList', { 'DiscussionList--searchResults': state.isSearchResults() })}> <div className={classList('DiscussionList', { 'DiscussionList--searchResults': state.isSearchResults() })}>
<ul role="feed" aria-busy={isLoading} className="DiscussionList-discussions"> <ul role="feed" aria-busy={isLoading} className="DiscussionList-discussions">
{state.getPages().map((pg, pageNum) => { {state.getPages().map((pg, pageNum) => {
return pg.items.map((discussion, itemNum) => ( return pg.items.map((discussion, itemNum) => (

View File

@@ -79,17 +79,16 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
controlsView(controls: Mithril.ChildArray): Mithril.Children { controlsView(controls: Mithril.ChildArray): Mithril.Children {
return ( return (
(controls.length > 0 && !!controls.length && (
Dropdown.component( <Dropdown
{ icon="fas fa-ellipsis-v"
icon: 'fas fa-ellipsis-v', className="DiscussionListItem-controls"
className: 'DiscussionListItem-controls', buttonClassName="Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right"
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right', accessibleToggleLabel={app.translator.trans('core.forum.discussion_controls.toggle_dropdown_accessible_label')}
accessibleToggleLabel: app.translator.trans('core.forum.discussion_controls.toggle_dropdown_accessible_label'), >
}, {controls}
controls </Dropdown>
)) || )
null
); );
} }
@@ -99,7 +98,7 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
return ( return (
<span <span
className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')} className={classList('Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic', { disabled: isUnread })}
onclick={this.markAsRead.bind(this)} onclick={this.markAsRead.bind(this)}
> >
{icon('fas fa-check')} {icon('fas fa-check')}
@@ -247,13 +246,7 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
items.add('excerpt', excerpt, -100); items.add('excerpt', excerpt, -100);
} }
} else { } else {
items.add( items.add('terminalPost', <TerminalPost discussion={this.attrs.discussion} lastPost={!this.showFirstPost()} />);
'terminalPost',
TerminalPost.component({
discussion: this.attrs.discussion,
lastPost: !this.showFirstPost(),
})
);
} }
return items; return items;
@@ -268,7 +261,7 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
<button className="Button--ua-reset DiscussionListItem-count" onclick={this.markAsRead.bind(this)}> <button className="Button--ua-reset DiscussionListItem-count" onclick={this.markAsRead.bind(this)}>
<span aria-hidden="true">{abbreviateNumber(discussion.unreadCount())}</span> <span aria-hidden="true">{abbreviateNumber(discussion.unreadCount())}</span>
<span class="visually-hidden"> <span className="visually-hidden">
{app.translator.trans('core.forum.discussion_list.unread_replies_a11y_label', { count: discussion.replyCount() })} {app.translator.trans('core.forum.discussion_list.unread_replies_a11y_label', { count: discussion.replyCount() })}
</span> </span>
</button> </button>
@@ -279,7 +272,7 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
<span className="DiscussionListItem-count"> <span className="DiscussionListItem-count">
<span aria-hidden="true">{abbreviateNumber(discussion.replyCount())}</span> <span aria-hidden="true">{abbreviateNumber(discussion.replyCount())}</span>
<span class="visually-hidden"> <span className="visually-hidden">
{app.translator.trans('core.forum.discussion_list.total_replies_a11y_label', { count: discussion.replyCount() })} {app.translator.trans('core.forum.discussion_list.total_replies_a11y_label', { count: discussion.replyCount() })}
</span> </span>
</span> </span>

View File

@@ -140,11 +140,7 @@ export default class DiscussionPage<CustomAttrs extends IDiscussionPageAttrs = I
items.add( items.add(
'poststream', 'poststream',
<div className="DiscussionPage-stream"> <div className="DiscussionPage-stream">
{PostStream.component({ <PostStream discussion={this.discussion} stream={this.stream} onPositionChange={this.positionChanged.bind(this)} />
discussion: this.discussion,
stream: this.stream,
onPositionChange: this.positionChanged.bind(this),
})}
</div>, </div>,
10 10
); );
@@ -241,27 +237,19 @@ export default class DiscussionPage<CustomAttrs extends IDiscussionPageAttrs = I
if (this.discussion) { if (this.discussion) {
items.add( items.add(
'controls', 'controls',
SplitDropdown.component( <SplitDropdown
{ icon="fas fa-ellipsis-v"
icon: 'fas fa-ellipsis-v', className="App-primaryControl"
className: 'App-primaryControl', buttonClassName="Button--primary"
buttonClassName: 'Button--primary', accessibleToggleLabel={app.translator.trans('core.forum.discussion_controls.toggle_dropdown_accessible_label')}
accessibleToggleLabel: app.translator.trans('core.forum.discussion_controls.toggle_dropdown_accessible_label'), >
}, {DiscussionControls.controls(this.discussion, this).toArray()}
DiscussionControls.controls(this.discussion, this).toArray() </SplitDropdown>,
),
100 100
); );
} }
items.add( items.add('scrubber', <PostStreamScrubber stream={this.stream} className="App-titleControl" />, -100);
'scrubber',
PostStreamScrubber.component({
stream: this.stream,
className: 'App-titleControl',
}),
-100
);
return items; return items;
} }

View File

@@ -40,10 +40,8 @@ export default class DiscussionsSearchSource implements SearchSource {
<li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()}> <li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()}>
<Link href={app.route.discussion(discussion, (mostRelevantPost && mostRelevantPost.number()) || 0)}> <Link href={app.route.discussion(discussion, (mostRelevantPost && mostRelevantPost.number()) || 0)}>
<div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div> <div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div>
{mostRelevantPost ? ( {!!mostRelevantPost && (
<div className="DiscussionSearchResult-excerpt">{highlight(mostRelevantPost.contentPlain() ?? '', query, 100)}</div> <div className="DiscussionSearchResult-excerpt">{highlight(mostRelevantPost.contentPlain() ?? '', query, 100)}</div>
) : (
''
)} )}
</Link> </Link>
</li> </li>

View File

@@ -27,6 +27,10 @@ export default class DiscussionsUserPage extends UserPage<IUserPageAttrs, Discus
} }
content() { content() {
return <div className="DiscussionsUserPage">{DiscussionList.component({ state: this.state })}</div>; return (
<div className="DiscussionsUserPage">
<DiscussionList state={this.state} />
</div>
);
} }
} }

View File

@@ -86,21 +86,20 @@ export default class EditPostComposer extends ComposerBody {
// Otherwise, we'll create an alert message to inform the user that // Otherwise, we'll create an alert message to inform the user that
// their edit has been made, containing a button which will // their edit has been made, containing a button which will
// transition to their edited post when clicked. // transition to their edited post when clicked.
let alert; const alert = app.alerts.show(
const viewButton = Button.component(
{
className: 'Button Button--link',
onclick: () => {
m.route.set(app.route.post(post));
app.alerts.dismiss(alert);
},
},
app.translator.trans('core.forum.composer_edit.view_button')
);
alert = app.alerts.show(
{ {
type: 'success', type: 'success',
controls: [viewButton], controls: [
<Button
className="Button Button--link"
onclick={() => {
m.route.set(app.route.post(post));
app.alerts.dismiss(alert);
}}
>
{app.translator.trans('core.forum.composer_edit.view_button')}
</Button>,
],
}, },
app.translator.trans('core.forum.composer_edit.edited_message') app.translator.trans('core.forum.composer_edit.edited_message')
); );

View File

@@ -5,6 +5,7 @@ import usernameHelper from '../../common/helpers/username';
import icon from '../../common/helpers/icon'; import icon from '../../common/helpers/icon';
import Link from '../../common/components/Link'; import Link from '../../common/components/Link';
import humanTime from '../../common/helpers/humanTime'; import humanTime from '../../common/helpers/humanTime';
import classList from '../../common/utils/classList';
/** /**
* The `EventPost` component displays a post which indicating a discussion * The `EventPost` component displays a post which indicating a discussion
@@ -21,7 +22,7 @@ export default class EventPost extends Post {
elementAttrs() { elementAttrs() {
const attrs = super.elementAttrs(); const attrs = super.elementAttrs();
attrs.className = (attrs.className || '') + ' EventPost ' + ucfirst(this.attrs.post.contentType()) + 'Post'; attrs.className = classList(attrs.className, 'EventPost', ucfirst(this.attrs.post.contentType()) + 'Post');
return attrs; return attrs;
} }
@@ -41,7 +42,9 @@ export default class EventPost extends Post {
time: humanTime(this.attrs.post.createdAt()), time: humanTime(this.attrs.post.createdAt()),
}); });
return super.content().concat([icon(this.icon(), { className: 'EventPost-icon' }), <div class="EventPost-info">{this.description(data)}</div>]); return super
.content()
.concat([icon(this.icon(), { className: 'EventPost-icon' }), <div className="EventPost-info">{this.description(data)}</div>]);
} }
/** /**

View File

@@ -87,14 +87,9 @@ export default class ForgotPasswordModal<CustomAttrs extends IForgotPasswordModa
items.add( items.add(
'submit', 'submit',
<div className="Form-group"> <div className="Form-group">
{Button.component( <Button className="Button Button--primary Button--block" type="submit" loading={this.loading}>
{ {app.translator.trans('core.forum.forgot_password.submit_button')}
className: 'Button Button--primary Button--block', </Button>
type: 'submit',
loading: this.loading,
},
app.translator.trans('core.forum.forgot_password.submit_button')
)}
</div>, </div>,
-10 -10
); );

View File

@@ -28,71 +28,61 @@ export default class HeaderSecondary extends Component {
items() { items() {
const items = new ItemList(); const items = new ItemList();
items.add('search', Search.component({ state: app.search }), 30); items.add('search', <Search state={app.search} />, 30);
if (app.forum.attribute('showLanguageSelector') && Object.keys(app.data.locales).length > 1) { if (app.forum.attribute('showLanguageSelector') && Object.keys(app.data.locales).length > 1) {
const locales = []; const locales = [];
for (const locale in app.data.locales) { for (const locale in app.data.locales) {
locales.push( locales.push(
Button.component( <Button
{ active={app.data.locale === locale}
active: app.data.locale === locale, icon={app.data.locale === locale ? 'fas fa-check' : true}
icon: app.data.locale === locale ? 'fas fa-check' : true, onclick={() => {
onclick: () => { if (app.session.user) {
if (app.session.user) { app.session.user.savePreferences({ locale }).then(() => window.location.reload());
app.session.user.savePreferences({ locale }).then(() => window.location.reload()); } else {
} else { document.cookie = `locale=${locale}; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT`;
document.cookie = `locale=${locale}; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT`; window.location.reload();
window.location.reload(); }
} }}
}, >
}, {app.data.locales[locale]}
app.data.locales[locale] </Button>
)
); );
} }
items.add( items.add(
'locale', 'locale',
SelectDropdown.component( <SelectDropdown
{ buttonClassName="Button Button--link"
buttonClassName: 'Button Button--link', accessibleToggleLabel={app.translator.trans('core.forum.header.locale_dropdown_accessible_label')}
accessibleToggleLabel: app.translator.trans('core.forum.header.locale_dropdown_accessible_label'), >
}, {locales}
locales </SelectDropdown>,
),
20 20
); );
} }
if (app.session.user) { if (app.session.user) {
items.add('notifications', NotificationsDropdown.component({ state: app.notifications }), 10); items.add('notifications', <NotificationsDropdown state={app.notifications} />, 10);
items.add('session', SessionDropdown.component(), 0); items.add('session', <SessionDropdown />, 0);
} else { } else {
if (app.forum.attribute('allowSignUp')) { if (app.forum.attribute('allowSignUp')) {
items.add( items.add(
'signUp', 'signUp',
Button.component( <Button className="Button Button--link" onclick={() => app.modal.show(SignUpModal)}>
{ {app.translator.trans('core.forum.header.sign_up_link')}
className: 'Button Button--link', </Button>,
onclick: () => app.modal.show(SignUpModal),
},
app.translator.trans('core.forum.header.sign_up_link')
),
10 10
); );
} }
items.add( items.add(
'logIn', 'logIn',
Button.component( <Button className="Button Button--link" onclick={() => app.modal.show(LogInModal)}>
{ {app.translator.trans('core.forum.header.log_in_link')}
className: 'Button Button--link', </Button>,
onclick: () => app.modal.show(LogInModal),
},
app.translator.trans('core.forum.header.log_in_link')
),
0 0
); );
} }

View File

@@ -141,7 +141,7 @@ export default class IndexPage<CustomAttrs extends IIndexPageAttrs = IIndexPageA
* Get the component to display as the hero. * Get the component to display as the hero.
*/ */
hero() { hero() {
return WelcomeHero.component(); return <WelcomeHero />;
} }
/** /**
@@ -155,32 +155,30 @@ export default class IndexPage<CustomAttrs extends IIndexPageAttrs = IIndexPageA
items.add( items.add(
'newDiscussion', 'newDiscussion',
Button.component( <Button
{ icon="fas fa-edit"
icon: 'fas fa-edit', className="Button Button--primary IndexPage-newDiscussion"
className: 'Button Button--primary IndexPage-newDiscussion', itemClassName="App-primaryControl"
itemClassName: 'App-primaryControl', onclick={() => {
onclick: () => { // If the user is not logged in, the promise rejects, and a login modal shows up.
// If the user is not logged in, the promise rejects, and a login modal shows up. // Since that's already handled, we dont need to show an error message in the console.
// Since that's already handled, we dont need to show an error message in the console. return this.newDiscussionAction().catch(() => {});
return this.newDiscussionAction().catch(() => {}); }}
}, disabled={!canStartDiscussion}
disabled: !canStartDiscussion, >
}, {app.translator.trans(`core.forum.index.${canStartDiscussion ? 'start_discussion_button' : 'cannot_start_discussion_button'}`)}
app.translator.trans(canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button') </Button>
)
); );
items.add( items.add(
'nav', 'nav',
SelectDropdown.component( <SelectDropdown
{ buttonClassName="Button"
buttonClassName: 'Button', className="App-titleControl"
className: 'App-titleControl', accessibleToggleLabel={app.translator.trans('core.forum.index.toggle_sidenav_dropdown_accessible_label')}
accessibleToggleLabel: app.translator.trans('core.forum.index.toggle_sidenav_dropdown_accessible_label'), >
}, {this.navItems().toArray()}
this.navItems().toArray() </SelectDropdown>
)
); );
return items; return items;
@@ -196,13 +194,9 @@ export default class IndexPage<CustomAttrs extends IIndexPageAttrs = IIndexPageA
items.add( items.add(
'allDiscussions', 'allDiscussions',
LinkButton.component( <LinkButton href={app.route('index', params)} icon="far fa-comments">
{ {app.translator.trans('core.forum.index.all_discussions_link')}
href: app.route('index', params), </LinkButton>,
icon: 'far fa-comments',
},
app.translator.trans('core.forum.index.all_discussions_link')
),
100 100
); );
@@ -225,26 +219,22 @@ export default class IndexPage<CustomAttrs extends IIndexPageAttrs = IIndexPageA
items.add( items.add(
'sort', 'sort',
Dropdown.component( <Dropdown
{ buttonClassName="Button"
buttonClassName: 'Button', label={sortOptions[app.search.params().sort] || Object.keys(sortMap).map((key) => sortOptions[key])[0]}
label: sortOptions[app.search.params().sort] || Object.keys(sortMap).map((key) => sortOptions[key])[0], accessibleToggleLabel={app.translator.trans('core.forum.index_sort.toggle_dropdown_accessible_label')}
accessibleToggleLabel: app.translator.trans('core.forum.index_sort.toggle_dropdown_accessible_label'), >
}, {Object.keys(sortOptions).map((value) => {
Object.keys(sortOptions).map((value) => {
const label = sortOptions[value]; const label = sortOptions[value];
const active = (app.search.params().sort || Object.keys(sortMap)[0]) === value; const active = (app.search.params().sort || Object.keys(sortMap)[0]) === value;
return Button.component( return (
{ <Button icon={active ? 'fas fa-check' : true} onclick={app.search.changeSort.bind(app.search, value)} active={active}>
icon: active ? 'fas fa-check' : true, {label}
onclick: app.search.changeSort.bind(app.search, value), </Button>
active: active,
},
label
); );
}) })}
) </Dropdown>
); );
return items; return items;
@@ -259,29 +249,29 @@ export default class IndexPage<CustomAttrs extends IIndexPageAttrs = IIndexPageA
items.add( items.add(
'refresh', 'refresh',
Button.component({ <Button
title: app.translator.trans('core.forum.index.refresh_tooltip'), title={app.translator.trans('core.forum.index.refresh_tooltip')}
icon: 'fas fa-sync', icon="fas fa-sync"
className: 'Button Button--icon', className="Button Button--icon"
onclick: () => { onclick={() => {
app.discussions.refresh(); app.discussions.refresh();
if (app.session.user) { if (app.session.user) {
app.store.find('users', app.session.user.id()!); app.store.find('users', app.session.user.id()!);
m.redraw(); m.redraw();
} }
}, }}
}) />
); );
if (app.session.user) { if (app.session.user) {
items.add( items.add(
'markAllAsRead', 'markAllAsRead',
Button.component({ <Button
title: app.translator.trans('core.forum.index.mark_all_as_read_tooltip'), title={app.translator.trans('core.forum.index.mark_all_as_read_tooltip')}
icon: 'fas fa-check', icon="fas fa-check"
className: 'Button Button--icon', className="Button Button--icon"
onclick: this.markAllAsRead.bind(this), onclick={this.markAllAsRead.bind(this)}
}) />
); );
} }

View File

@@ -1,5 +1,6 @@
import app from '../../forum/app'; import app from '../../forum/app';
import Button from '../../common/components/Button'; import Button from '../../common/components/Button';
import classList from '../../common/utils/classList';
/** /**
* The `LogInButton` component displays a social login button which will open * The `LogInButton` component displays a social login button which will open
@@ -11,7 +12,7 @@ import Button from '../../common/components/Button';
*/ */
export default class LogInButton extends Button { export default class LogInButton extends Button {
static initAttrs(attrs) { static initAttrs(attrs) {
attrs.className = (attrs.className || '') + ' LogInButton'; attrs.className = classList(attrs.className, 'LogInButton');
attrs.onclick = function () { attrs.onclick = function () {
const width = 580; const width = 580;

View File

@@ -110,14 +110,9 @@ export default class LogInModal<CustomAttrs extends ILoginModalAttrs = ILoginMod
items.add( items.add(
'submit', 'submit',
<div className="Form-group"> <div className="Form-group">
{Button.component( <Button className="Button Button--primary Button--block" type="submit" loading={this.loading}>
{ {app.translator.trans('core.forum.log_in.submit_button')}
className: 'Button Button--primary Button--block', </Button>
type: 'submit',
loading: this.loading,
},
app.translator.trans('core.forum.log_in.submit_button')
)}
</div>, </div>,
-10 -10
); );
@@ -126,17 +121,16 @@ export default class LogInModal<CustomAttrs extends ILoginModalAttrs = ILoginMod
} }
footer() { footer() {
return [ return (
<p className="LogInModal-forgotPassword"> <>
<a onclick={this.forgotPassword.bind(this)}>{app.translator.trans('core.forum.log_in.forgot_password_link')}</a> <p className="LogInModal-forgotPassword">
</p>, <a onclick={this.forgotPassword.bind(this)}>{app.translator.trans('core.forum.log_in.forgot_password_link')}</a>
</p>
app.forum.attribute('allowSignUp') ? ( {app.forum.attribute<boolean>('allowSignUp') && (
<p className="LogInModal-signUp">{app.translator.trans('core.forum.log_in.sign_up_text', { a: <a onclick={this.signUp.bind(this)} /> })}</p> <p className="LogInModal-signUp">{app.translator.trans('core.forum.log_in.sign_up_text', { a: <a onclick={this.signUp.bind(this)} /> })}</p>
) : ( )}
'' </>
), );
];
} }
/** /**
@@ -165,7 +159,7 @@ export default class LogInModal<CustomAttrs extends ILoginModalAttrs = ILoginMod
} }
onready() { onready() {
this.$('[name=' + (this.identification() ? 'password' : 'identification') + ']').select(); this.$('[name=' + (this.identification() ? 'password' : 'identification') + ']').trigger('select');
} }
onsubmit(e: SubmitEvent) { onsubmit(e: SubmitEvent) {

View File

@@ -71,7 +71,7 @@ export default class NotificationGrid extends Component {
disabled={!(key in preferences)} disabled={!(key in preferences)}
onchange={this.toggle.bind(this, [key])} onchange={this.toggle.bind(this, [key])}
> >
<span class="sr-only"> <span className="sr-only">
{app.translator.trans('core.forum.settings.notification_checkbox_a11y_label_template', { {app.translator.trans('core.forum.settings.notification_checkbox_a11y_label_template', {
description: type.label, description: type.label,
method: method.label, method: method.label,

View File

@@ -118,7 +118,13 @@ export default class NotificationList extends Component {
<ul className="NotificationGroup-content"> <ul className="NotificationGroup-content">
{group.notifications.map((notification) => { {group.notifications.map((notification) => {
const NotificationComponent = app.notificationComponents[notification.contentType()]; const NotificationComponent = app.notificationComponents[notification.contentType()];
return NotificationComponent ? <li>{NotificationComponent.component({ notification })}</li> : ''; return (
!!NotificationComponent && (
<li>
<NotificationComponent notification={notification} />
</li>
)
);
})} })}
</ul> </ul>
</div> </div>

View File

@@ -48,7 +48,7 @@ export default class NotificationsDropdown<CustomAttrs extends IDropdownAttrs =
getMenu() { getMenu() {
return ( return (
<div className={classList('Dropdown-menu', this.attrs.menuClassName)} onclick={this.menuClick.bind(this)}> <div className={classList('Dropdown-menu', this.attrs.menuClassName)} onclick={this.menuClick.bind(this)}>
{this.showing && NotificationList.component({ state: this.attrs.state })} {this.showing && <NotificationList state={this.attrs.state} />}
</div> </div>
); );
} }

View File

@@ -60,7 +60,7 @@ export default abstract class Post<CustomAttrs extends IPostAttrs = IPostAttrs>
<aside className="Post-actions"> <aside className="Post-actions">
<ul> <ul>
{listItems(this.actionItems().toArray())} {listItems(this.actionItems().toArray())}
{controls.length ? ( {!!controls.length && (
<li> <li>
<Dropdown <Dropdown
className="Post-controls" className="Post-controls"
@@ -74,8 +74,6 @@ export default abstract class Post<CustomAttrs extends IPostAttrs = IPostAttrs>
{controls} {controls}
</Dropdown> </Dropdown>
</li> </li>
) : (
''
)} )}
</ul> </ul>
</aside> </aside>

View File

@@ -23,7 +23,7 @@ export default class PostEdited extends Component {
return ( return (
<Tooltip text={editedInfo}> <Tooltip text={editedInfo}>
<span class="PostEdited">{app.translator.trans('core.forum.post.edited_text')}</span> <span className="PostEdited">{app.translator.trans('core.forum.post.edited_text')}</span>
</Tooltip> </Tooltip>
); );
} }

View File

@@ -48,7 +48,7 @@ export default class PostStream extends Component {
if (post) { if (post) {
const time = post.createdAt(); const time = post.createdAt();
const PostComponent = app.postComponents[post.contentType()]; const PostComponent = app.postComponents[post.contentType()];
content = PostComponent ? PostComponent.component({ post }) : ''; content = !!PostComponent && <PostComponent post={post} />;
attrs.key = 'post' + post.id(); attrs.key = 'post' + post.id();
attrs.oncreate = postFadeIn; attrs.oncreate = postFadeIn;
@@ -75,7 +75,7 @@ export default class PostStream extends Component {
} else { } else {
attrs.key = 'post' + postIds[this.stream.visibleStart + i]; attrs.key = 'post' + postIds[this.stream.visibleStart + i];
content = PostLoading.component(); content = <PostLoading />;
} }
return ( return (
@@ -105,7 +105,7 @@ export default class PostStream extends Component {
if (viewingEnd && (!app.session.user || this.discussion.canReply())) { if (viewingEnd && (!app.session.user || this.discussion.canReply())) {
items.push( items.push(
<div className="PostStream-item" key="reply" data-index={this.stream.count()} oncreate={postFadeIn}> <div className="PostStream-item" key="reply" data-index={this.stream.count()} oncreate={postFadeIn}>
{ReplyPlaceholder.component({ discussion: this.discussion })} <ReplyPlaceholder discussion={this.discussion} />
</div> </div>
); );
} }

View File

@@ -22,26 +22,16 @@ export default class PostUser extends Component {
if (!user) { if (!user) {
return ( return (
<div className="PostUser"> <div className="PostUser">
<h3 class="PostUser-name"> <h3 className="PostUser-name">
{avatar(user, { className: 'PostUser-avatar' })} {username(user)} {avatar(user, { className: 'PostUser-avatar' })} {username(user)}
</h3> </h3>
</div> </div>
); );
} }
let card = '';
if (!post.isHidden() && this.attrs.cardVisible) {
card = UserCard.component({
user,
className: 'UserCard--popover',
controlsButtonClassName: 'Button Button--icon Button--flat',
});
}
return ( return (
<div className="PostUser"> <div className="PostUser">
<h3 class="PostUser-name"> <h3 className="PostUser-name">
<Link href={app.route.user(user)}> <Link href={app.route.user(user)}>
{avatar(user, { className: 'PostUser-avatar' })} {avatar(user, { className: 'PostUser-avatar' })}
{userOnline(user)} {userOnline(user)}
@@ -49,7 +39,10 @@ export default class PostUser extends Component {
</Link> </Link>
</h3> </h3>
<ul className="PostUser-badges badges">{listItems(user.badges().toArray())}</ul> <ul className="PostUser-badges badges">{listItems(user.badges().toArray())}</ul>
{card}
{!post.isHidden() && this.attrs.cardVisible && (
<UserCard user={user} className="UserCard--popover" controlsButtonClassName="Button Button--icon Button--flat" />
)}
</div> </div>
); );
} }

View File

@@ -42,14 +42,9 @@ export default class RenameDiscussionModal<CustomAttrs extends IRenameDiscussion
<input className="FormControl" bidi={this.newTitle} type="text" /> <input className="FormControl" bidi={this.newTitle} type="text" />
</div> </div>
<div className="Form-group"> <div className="Form-group">
{Button.component( <Button className="Button Button--primary Button--block" type="submit" loading={this.loading}>
{ {app.translator.trans('core.forum.rename_discussion.submit_button')}
className: 'Button Button--primary Button--block', </Button>
type: 'submit',
loading: this.loading,
},
app.translator.trans('core.forum.rename_discussion.submit_button')
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -90,15 +90,16 @@ export default class ReplyComposer extends ComposerBody {
// their reply has been posted, containing a button which will // their reply has been posted, containing a button which will
// transition to their new post when clicked. // transition to their new post when clicked.
let alert; let alert;
const viewButton = Button.component( const viewButton = (
{ <Button
className: 'Button Button--link', className="Button Button--link"
onclick: () => { onclick={() => {
m.route.set(app.route.post(post)); m.route.set(app.route.post(post));
app.alerts.dismiss(alert); app.alerts.dismiss(alert);
}, }}
}, >
app.translator.trans('core.forum.composer_reply.view_button') {app.translator.trans('core.forum.composer_reply.view_button')}
</Button>
); );
alert = app.alerts.show( alert = app.alerts.show(
{ {

View File

@@ -21,7 +21,7 @@ export default class ReplyPlaceholder extends Component {
<article className="Post CommentPost editing" aria-busy="true"> <article className="Post CommentPost editing" aria-busy="true">
<header className="Post-header"> <header className="Post-header">
<div className="PostUser"> <div className="PostUser">
<h3 class="PostUser-name"> <h3 className="PostUser-name">
{avatar(app.session.user, { className: 'PostUser-avatar' })} {avatar(app.session.user, { className: 'PostUser-avatar' })}
{username(app.session.user)} {username(app.session.user)}
</h3> </h3>

View File

@@ -45,54 +45,37 @@ export default class SessionDropdown<CustomAttrs extends ISessionDropdownAttrs =
items.add( items.add(
'profile', 'profile',
LinkButton.component( <LinkButton icon="fas fa-user" href={app.route.user(user)}>
{ {app.translator.trans('core.forum.header.profile_button')}
icon: 'fas fa-user', </LinkButton>,
href: app.route.user(user),
},
app.translator.trans('core.forum.header.profile_button')
),
100 100
); );
items.add( items.add(
'settings', 'settings',
LinkButton.component( <LinkButton icon="fas fa-cog" href={app.route('settings')}>
{ {app.translator.trans('core.forum.header.settings_button')}
icon: 'fas fa-cog', </LinkButton>,
href: app.route('settings'),
},
app.translator.trans('core.forum.header.settings_button')
),
50 50
); );
if (app.forum.attribute('adminUrl')) { if (app.forum.attribute('adminUrl')) {
items.add( items.add(
'administration', 'administration',
LinkButton.component( <LinkButton icon="fas fa-wrench" href={app.forum.attribute('adminUrl')} target="_blank">
{ {app.translator.trans('core.forum.header.admin_button')}
icon: 'fas fa-wrench', </LinkButton>,
href: app.forum.attribute('adminUrl'),
target: '_blank',
},
app.translator.trans('core.forum.header.admin_button')
),
0 0
); );
} }
items.add('separator', Separator.component(), -90); items.add('separator', <Separator />, -90);
items.add( items.add(
'logOut', 'logOut',
Button.component( <Button icon="fas fa-sign-out-alt" onclick={app.session.logout.bind(app.session)}>
{ {app.translator.trans('core.forum.header.log_out_button')}
icon: 'fas fa-sign-out-alt', </Button>,
onclick: app.session.logout.bind(app.session),
},
app.translator.trans('core.forum.header.log_out_button')
),
-100 -100
); );

View File

@@ -21,7 +21,7 @@ export default class TerminalPost extends Component {
return ( return (
<span> <span>
{lastPost ? icon('fas fa-reply') : ''}{' '} {!!lastPost && icon('fas fa-reply')}{' '}
{app.translator.trans('core.forum.discussion_list.' + (lastPost ? 'replied' : 'started') + '_text', { {app.translator.trans('core.forum.discussion_list.' + (lastPost ? 'replied' : 'started') + '_text', {
user, user,
ago: humanTime(time), ago: humanTime(time),

Some files were not shown because too many files have changed in this diff Show More