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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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);
}}
>
<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}
</button>
);

View File

@ -108,7 +108,7 @@ export default function () {
user,
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 ? (
<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>
</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)} />
<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')}
{this.reason() === 'off_topic' ? (
{this.reason() === 'off_topic' && (
<textarea
className="FormControl"
placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')}
value={this.reasonDetail()}
oninput={withAttr('value', this.reasonDetail)}
></textarea>
) : (
''
)}
</label>,
70
@ -95,15 +93,13 @@ export default class FlagPostModal extends Modal {
{app.translator.trans('flarum-flags.forum.flag_post.reason_inappropriate_text', {
a: guidelinesUrl ? <a href={guidelinesUrl} target="_blank" /> : undefined,
})}
{this.reason() === 'inappropriate' ? (
{this.reason() === 'inappropriate' && (
<textarea
className="FormControl"
placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')}
value={this.reasonDetail()}
oninput={withAttr('value', this.reasonDetail)}
></textarea>
) : (
''
)}
</label>,
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)} />
<strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_spam_label')}</strong>
{app.translator.trans('flarum-flags.forum.flag_post.reason_spam_text')}
{this.reason() === 'spam' ? (
{this.reason() === 'spam' && (
<textarea
className="FormControl"
placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')}
value={this.reasonDetail()}
oninput={withAttr('value', this.reasonDetail)}
></textarea>
) : (
''
)}
</label>,
50
@ -134,10 +128,8 @@ export default class FlagPostModal extends Modal {
<label className="checkbox">
<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>
{this.reason() === 'other' ? (
{this.reason() === 'other' && (
<textarea className="FormControl" value={this.reasonDetail()} oninput={withAttr('value', this.reasonDetail)}></textarea>
) : (
''
)}
</label>,
10

View File

@ -1,5 +1,5 @@
import app from 'flarum/forum/app';
import NotificationsDropdown from 'flarum/components/NotificationsDropdown';
import NotificationsDropdown from 'flarum/forum/components/NotificationsDropdown';
import FlagList from './FlagList';
@ -14,7 +14,7 @@ export default class FlagsDropdown extends NotificationsDropdown {
getMenu() {
return (
<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>
);
}

View File

@ -15,10 +15,9 @@ export default function () {
items.add(
'like',
Button.component(
{
className: 'Button Button--link',
onclick: () => {
<Button
className="Button Button--link"
onclick={() => {
isLiked = !isLiked;
post.save({ isLiked });
@ -37,10 +36,10 @@ export default function () {
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')
)
}}
>
{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',
<div className="Post-likedBy">
{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,
users: punctuateSeries(names),
})}

View File

@ -6,14 +6,7 @@ import Badge from 'flarum/common/components/Badge';
export default function addLockBadge() {
extend(Discussion.prototype, 'badges', function (badges) {
if (this.isLocked()) {
badges.add(
'locked',
Badge.component({
type: 'locked',
label: app.translator.trans('flarum-lock.forum.badge.locked_tooltip'),
icon: 'fas fa-lock',
})
);
badges.add('locked', <Badge 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()) {
items.add(
'lock',
Button.component(
{
icon: 'fas fa-lock',
onclick: this.lockAction.bind(discussion),
},
app.translator.trans(
discussion.isLocked() ? 'flarum-lock.forum.discussion_controls.unlock_button' : 'flarum-lock.forum.discussion_controls.lock_button'
)
)
<Button icon="fas fa-lock" onclick={this.lockAction.bind(discussion)}>
{app.translator.trans(`flarum-lock.forum.discussion_controls.${discussion.isLocked() ? 'unlock' : 'lock'}_button`)}
</Button>
);
}
});

View File

@ -2,6 +2,6 @@ import Component from 'flarum/common/Component';
export default class MarkdownToolbar extends Component {
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) => (
<li data-number={reply.number()}>
{PostPreview.component({
post: reply,
onclick: hidePreview.bind(this),
})}
<PostPreview post={reply} onclick={hidePreview.bind(this)} />
</li>
))}
{replies.length < post.mentionedByCount() ? (
{replies.length < post.mentionedByCount() && (
<li className="Post-mentionedBy-preview-more">
<Button
className="PostPreview Button"
@ -64,7 +61,7 @@ export default function addMentionedByList() {
</span>
</Button>
</li>
) : null}
)}
</>
);
@ -149,7 +146,7 @@ export default function addMentionedByList() {
<div className="Post-mentionedBy">
<span className="Post-mentionedBy-summary">
{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,
users: punctuateSeries(names),
})}

View File

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

View File

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

View File

@ -70,14 +70,9 @@ app.initializers.add('flarum-mentions', function () {
const user = this.user;
items.add(
'mentions',
LinkButton.component(
{
href: app.route('user.mentions', { username: user.slug() }),
name: 'mentions',
icon: 'fas fa-at',
},
app.translator.trans('flarum-mentions.forum.user.mentions_link')
),
<LinkButton href={app.route('user.mentions', { username: user.slug() })} name="mentions" icon="fas fa-at">
{app.translator.trans('flarum-mentions.forum.user.mentions_link')}
</LinkButton>,
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} />
</div>
<div className="Form-group">
{Button.component(
{
className: 'Button Button--primary Button--block',
type: 'submit',
loading: this.loading,
},
app.translator.trans('flarum-nicknames.forum.change_nickname.submit_button')
)}
<Button className="Button Button--primary Button--block" type="submit" loading={this.loading}>
{app.translator.trans('flarum-nicknames.forum.change_nickname.submit_button')}
</Button>
</div>
</div>
</div>

View File

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

View File

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

View File

@ -87,10 +87,9 @@ app.initializers.add('flarum-pusher', () => {
if (count && typeof vdom === 'object' && vdom && 'children' in vdom && vdom.children instanceof Array) {
vdom.children.unshift(
Button.component(
{
className: 'Button Button--block DiscussionList-update',
onclick: () => {
<Button
className="Button Button--block DiscussionList-update"
onclick={() => {
this.attrs.state.refresh().then(() => {
this.loadingUpdated = false;
app.pushedUpdates = [];
@ -98,11 +97,11 @@ app.initializers.add('flarum-pusher', () => {
m.redraw();
});
this.loadingUpdated = true;
},
loading: this.loadingUpdated,
},
app.translator.trans('flarum-pusher.forum.discussion_list.show_updates_text', { count })
)
}}
loading={this.loadingUpdated}
>
{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 Placeholder from 'flarum/common/components/Placeholder';
import icon from 'flarum/common/helpers/icon';
import classList from 'flarum/common/utils/classList';
import DashboardWidget, { IDashboardWidgetAttrs } from 'flarum/admin/components/DashboardWidget';
@ -277,7 +278,7 @@ export default class StatisticsWidget extends DashboardWidget {
return (
<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)}
>
<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 {
return <div class="Modal-body">{this.items().toArray()}</div>;
return <div className="Modal-body">{this.items().toArray()}</div>;
}
items(): ItemList<Mithril.Children> {
@ -81,7 +81,7 @@ export default class StatisticsWidgetDateSelectionModal extends Modal<IStatistic
items.add(
'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>
<input
type="date"
@ -96,7 +96,7 @@ export default class StatisticsWidgetDateSelectionModal extends Modal<IStatistic
items.add(
'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>
<input
type="date"
@ -111,7 +111,7 @@ export default class StatisticsWidgetDateSelectionModal extends Modal<IStatistic
items.add(
'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')}
</Button>,
0

View File

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

View File

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

View File

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

View File

@ -19,13 +19,9 @@ export default function addSubscriptionControls() {
items.add(
'subscription',
Button.component(
{
icon: states[subscription].icon,
onclick: discussion.save.bind(discussion, { subscription: states[subscription].save }),
},
states[subscription].label
)
<Button icon={states[subscription].icon} onclick={discussion.save.bind(discussion, { subscription: states[subscription].save })}>
{states[subscription].label}
</Button>
);
}
});
@ -34,7 +30,7 @@ export default function addSubscriptionControls() {
if (app.session.user) {
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(
'following',
LinkButton.component(
{
href: app.route('following', params),
icon: 'fas fa-star',
},
app.translator.trans('flarum-subscriptions.forum.index.following_link')
),
<LinkButton href={app.route('following', params)} icon="fas fa-star">
{app.translator.trans('flarum-subscriptions.forum.index.following_link')}
</LinkButton>,
50
);
}

View File

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

View File

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

View File

@ -5,7 +5,7 @@ export default class SubscriptionMenuItem extends Component {
view() {
return (
<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">
{icon(this.attrs.icon, { className: 'Button-icon' })}
<strong>{this.attrs.label}</strong>

View File

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

View File

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

View File

@ -53,21 +53,18 @@ export default function () {
label: tagLabel(tag),
onremove: () => tag.save({ isRestricted: false }),
render: (item) => {
if ('setting' in item) return '';
if ('setting' in item) return null;
if (
item.permission === 'viewForum' ||
item.permission === 'startDiscussion' ||
(item.permission && item.permission.indexOf('discussion.') === 0 && item.tagScoped !== false) ||
(item.permission.startsWith('discussion.') && item.tagScoped !== false) ||
item.tagScoped
) {
return PermissionDropdown.component({
permission: 'tag' + tag.id() + '.' + item.permission,
allowGuest: item.allowGuest,
});
return <PermissionDropdown 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(
'submit',
<div className="Form-group">
{Button.component(
{
type: 'submit',
className: 'Button Button--primary EditTagModal-save',
loading: this.loading,
},
app.translator.trans('flarum-tags.admin.edit_tag.submit_button')
)}
{this.tag.exists ? (
<Button type="submit" className="Button Button--primary EditTagModal-save" loading={this.loading}>
{app.translator.trans('flarum-tags.admin.edit_tag.submit_button')}
</Button>
{this.tag.exists && (
<button type="button" className="Button EditTagModal-delete" onclick={this.delete.bind(this)}>
{app.translator.trans('flarum-tags.admin.edit_tag.delete_tag_button')}
</button>
) : (
''
)}
</div>,
-10

View File

@ -16,20 +16,14 @@ function tagItem(tag) {
<div className="TagListItem-info">
{tagIcon(tag)}
<span className="TagListItem-name">{tag.name()}</span>
{Button.component({
className: 'Button Button--link',
icon: 'fas fa-pencil-alt',
onclick: () => app.modal.show(EditTagModal, { model: tag }),
})}
<Button className="Button Button--link" icon="fas fa-pencil-alt" onclick={() => app.modal.show(EditTagModal, { model: tag })} />
</div>
{!tag.isChild() && tag.position() !== null ? (
{!tag.isChild() && tag.position() !== null && (
<ol className="TagListItem-children TagList">
{sortTags(app.store.all('tags'))
.filter((child) => child.parent() === tag)
.map(tagItem)}
</ol>
) : (
''
)}
</li>
);
@ -75,14 +69,9 @@ export default class TagsPage extends ExtensionPage {
<div className="TagGroup">
<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>
{Button.component(
{
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')
)}
<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')}
</Button>
</div>
<div className="TagGroup TagGroup--secondary">
@ -93,14 +82,9 @@ export default class TagsPage extends ExtensionPage {
.sort((a, b) => a.name().localeCompare(b.name()))
.map(tagItem)}
</ul>
{Button.component(
{
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')
)}
<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')}
</Button>
</div>
<div className="Form">
<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 tagLabel from './tagLabel';
import sortTags from '../utils/sortTags';
import classList from '@flarum/core/src/common/utils/classList';
export default function tagsLabel(tags, attrs = {}) {
const children = [];
const link = extract(attrs, 'link');
const { link, ...otherAttrs } = attrs;
attrs.className = 'TagsLabel ' + (attrs.className || '');
attrs.className = classList('TagsLabel', attrs.className);
if (tags) {
sortTags(tags).forEach((tag) => {
@ -18,5 +19,5 @@ export default function tagsLabel(tags, attrs = {}) {
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 TagsPage from './components/TagsPage';
import app from 'flarum/admin/app';
import sortTags from '../common/utils/sortTags';
export default function () {
@ -21,7 +22,7 @@ export default function () {
if (app.current.matches(TagsPage)) return;
items.add('separator', Separator.component(), -12);
items.add('separator', <Separator />, -12);
const params = app.search.stickyParams();
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`
// on TagLinkButton is only called AFTER SelectDropdown, so no children are available
// 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)

View File

@ -11,7 +11,7 @@ export default class TagHero extends Component {
return (
<header
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="containerNarrow">

View File

@ -6,12 +6,11 @@ import tagIcon from '../../common/helpers/tagIcon';
export default class TagLinkButton extends LinkButton {
view(vnode) {
const tag = this.attrs.model;
const active = this.constructor.isActive(this.attrs);
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 (
<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' })}
<span className="Button-label">{tag ? tag.name() : app.translator.trans('flarum-tags.forum.index.untagged_link')}</span>
</Link>

View File

@ -120,10 +120,8 @@ export default class TagsPage extends Page {
{tag.icon() && tagIcon(tag, {}, { useColor: false })}
<h3 className="TagTile-name">{tag.name()}</h3>
<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>
) : (
''
)}
</Link>
{lastPostedDiscussion ? (

View File

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

View File

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

View File

@ -19,62 +19,52 @@ export default class AppearancePage extends AdminPage {
}
content() {
return [
return (
<>
<div className="Form">
<fieldset className="AppearancePage-colors">
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
{this.colorItems().toArray()}
</fieldset>
</div>,
</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>
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.favicon_text')}</div>
<UploadImageButton name="favicon" />
</fieldset>,
</fieldset>
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_header_text')}</div>
{Button.component(
{
className: 'Button',
onclick: () => app.modal.show(EditCustomHeaderModal),
},
app.translator.trans('core.admin.appearance.edit_header_button')
)}
</fieldset>,
<Button className="Button" onclick={() => app.modal.show(EditCustomHeaderModal)}>
{app.translator.trans('core.admin.appearance.edit_header_button')}
</Button>
</fieldset>
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend>
<div className="helpText">{app.translator.trans('core.admin.appearance.custom_footer_text')}</div>
{Button.component(
{
className: 'Button',
onclick: () => app.modal.show(EditCustomFooterModal),
},
app.translator.trans('core.admin.appearance.edit_footer_button')
)}
</fieldset>,
<Button className="Button" onclick={() => app.modal.show(EditCustomFooterModal)}>
{app.translator.trans('core.admin.appearance.edit_footer_button')}
</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>,
];
<Button className="Button" onclick={() => app.modal.show(EditCustomCssModal)}>
{app.translator.trans('core.admin.appearance.edit_css_button')}
</Button>
</fieldset>
</>
);
}
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'),
})}
{Object.keys(this.localeOptions).length > 1
? [
this.buildSettingComponent({
{Object.keys(this.localeOptions).length > 1 && (
<>
{this.buildSettingComponent({
type: 'select',
setting: 'default_locale',
options: this.localeOptions,
label: app.translator.trans('core.admin.basics.default_language_heading'),
}),
this.buildSettingComponent({
})}
{this.buildSettingComponent({
type: 'switch',
setting: 'show_language_selector',
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')}>
<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')} />
</div>
{Object.keys(this.displayNameOptions).length > 1
? this.buildSettingComponent({
{Object.keys(this.displayNameOptions).length > 1 &&
this.buildSettingComponent({
type: 'select',
setting: 'display_name_driver',
options: this.displayNameOptions,
label: app.translator.trans('core.admin.basics.display_name_heading'),
help: app.translator.trans('core.admin.basics.display_name_text'),
})
: ''}
})}
{Object.keys(this.slugDriverOptions).map((model) => {
const options = this.slugDriverOptions[model];

View File

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

View File

@ -12,7 +12,7 @@ export default class ExtensionLinkButton extends LinkButton {
content.unshift(
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? icon(extension.icon.name) : ''}
{!!extension.icon && icon(extension.icon.name)}
</span>
);
content.push(statuses);
@ -23,7 +23,7 @@ export default class ExtensionLinkButton extends LinkButton {
statusItems(name) {
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;
}

View File

@ -79,7 +79,7 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
<div className="container">
<div className="ExtensionTitle">
<span className="ExtensionIcon" style={this.extension.icon}>
{this.extension.icon ? icon(this.extension.icon.name) : ''}
{!!this.extension.icon && icon(this.extension.icon.name)}
</span>
<div className="ExtensionName">
<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('permissions', [
items.add(
'permissions',
<div className="ExtensionPage-permissions">
<div className="ExtensionPage-permissions-header">
<div className="container">
@ -120,13 +121,13 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
</div>
<div className="container">
{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>
)}
</div>
</div>,
]);
</div>
);
return items;
}
@ -210,16 +211,9 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
const extension = this.extension;
items.add(
'readme',
Button.component(
{
icon: 'fab fa-readme',
class: 'Button Button--text',
onclick() {
app.modal.show(ReadmeModal, { extension });
},
},
app.translator.trans('core.admin.extension.readme.button_label')
),
<Button icon="fab fa-readme" className="Button Button--text" onclick={() => app.modal.show(ReadmeModal, { extension })}>
{app.translator.trans('core.admin.extension.readme.button_label')}
</Button>,
10
);

View File

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

View File

@ -18,7 +18,7 @@ export default class LoadingModal<ModalAttrs extends ILoadingModalAttrs = ILoadi
}
content() {
return '';
return null;
}
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 }), {}),
label: app.translator.trans('core.admin.email.driver_heading'),
})}
{this.status!.sending ||
Alert.component(
{
dismissible: false,
},
app.translator.trans('core.admin.email.not_sending_message')
)}
{this.status!.sending || <Alert dismissible={false}>{app.translator.trans('core.admin.email.not_sending_message')}</Alert>}
{fieldKeys.length > 0 && (
{!!fieldKeys.length && (
<FieldSet label={app.translator.trans(`core.admin.email.${this.setting('mail_driver')()}_heading`)} className="MailPage-MailSettings">
<div className="MailPage-MailSettings-input">
{fieldKeys.map((field) => {
const fieldInfo = fields[field];
return [
this.buildSettingComponent({
return (
<>
{this.buildSettingComponent({
type: typeof fieldInfo === 'string' ? 'text' : 'select',
label: app.translator.trans(`core.admin.email.${field}_label`),
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>
</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">
<div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user!.email() })}</div>
{Button.component(
{
className: 'Button Button--primary',
disabled: this.sendingTest || this.isChanged(),
onclick: () => this.sendTestEmail(),
},
app.translator.trans('core.admin.email.send_test_mail_button')
)}
<Button className="Button Button--primary" disabled={this.sendingTest || this.isChanged()} onclick={() => this.sendTestEmail()}>
{app.translator.trans('core.admin.email.send_test_mail_button')}
</Button>
</FieldSet>
</div>
);

View File

@ -10,7 +10,7 @@ import Mithril from 'mithril';
function badgeForId(id: string) {
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) {
@ -57,9 +57,9 @@ export default class PermissionDropdown<CustomAttrs extends IPermissionDropdownA
const adminGroup = app.store.getById<Group>('groups', Group.ADMINISTRATOR_ID)!;
if (everyone) {
this.attrs.label = Badge.component({ icon: 'fas fa-globe' });
this.attrs.label = <Badge icon="fas fa-globe" />;
} else if (members) {
this.attrs.label = Badge.component({ icon: 'fas fa-user' });
this.attrs.label = <Badge icon="fas fa-user" />;
} else {
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.attrs.allowGuest) {
children.push(
Button.component(
{
icon: everyone ? 'fas fa-check' : true,
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')]
)
<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')}
</Button>
);
}
children.push(
Button.component(
{
icon: members ? 'fas fa-check' : true,
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')]
),
<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')}
</Button>,
Separator.component(),
<Separator />,
Button.component(
{
icon: !everyone && !members ? 'fas fa-check' : true,
disabled: !everyone && !members,
onclick: (e: MouseEvent) => {
<Button
icon={!everyone && !members ? 'fas fa-check' : true}
disabled={!everyone && !members}
onclick={(e: MouseEvent) => {
if (e.shiftKey) e.stopPropagation();
this.save([]);
},
},
[badgeForId(adminGroup.id()!), ' ', adminGroup.namePlural()]
)
}}
>
{badgeForId(adminGroup.id()!)} {adminGroup.namePlural()}
</Button>
);
// 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
.all<Group>('groups')
.filter((group) => !excludedGroups.includes(group.id()!))
.map((group) =>
Button.component(
{
icon: groupIds.includes(group.id()!) ? 'fas fa-check' : true,
onclick: (e: MouseEvent) => {
.map((group) => (
<Button
icon={groupIds.includes(group.id()!) ? 'fas fa-check' : true}
onclick={(e: MouseEvent) => {
if (e.shiftKey) e.stopPropagation();
this.toggle(group.id()!);
},
disabled: this.isGroupDisabled(group.id()!) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID),
},
[badgeForId(group.id()!), ' ', group.namePlural()]
)
);
}}
disabled={this.isGroupDisabled(group.id()!) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID)}
>
{badgeForId(group.id()!)} {group.namePlural()}
</Button>
));
children.push(...groupButtons);
}

View File

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

View File

@ -17,18 +17,15 @@ export default class PermissionsPage extends AdminPage {
}
content() {
return [
return (
<>
<div className="PermissionsPage-groups">
{app.store
.all<Group>('groups')
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()!) === -1)
.map((group) => (
<button className="Button Group" onclick={() => app.modal.show(EditGroupModal, { group })}>
{GroupBadge.component({
group,
className: 'Group-icon',
label: null,
})}
<GroupBadge group={group} className="Group-icon" label={null} />
<span className="Group-name">{group.namePlural()}</span>
</button>
))}
@ -36,9 +33,12 @@ export default class PermissionsPage extends AdminPage {
{icon('fas fa-plus', { className: 'Group-icon' })}
<span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
</button>
</div>,
</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() {
const text = app.translator.trans('core.admin.extension.readme.no_readme');
return (
<div className="Modal-body">
{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>
);

View File

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

View File

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

View File

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

View File

@ -1,12 +1,13 @@
import app from '../../admin/app';
import Button from '../../common/components/Button';
import classList from '../../common/utils/classList';
export default class UploadImageButton extends Button {
loading = false;
view(vnode) {
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']) {
this.attrs.onclick = this.remove.bind(this);
@ -37,7 +38,7 @@ export default class UploadImageButton extends Button {
$input
.appendTo('body')
.hide()
.click()
.trigger('click')
.on('change', (e) => {
const body = new FormData();
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);
return [
<section class="UserListPage-grid UserListPage-grid--loading">
<section className="UserListPage-grid UserListPage-grid--loading">
<LoadingIndicator containerClassName="LoadingIndicator--block" size="large" />
</section>,
];
@ -128,9 +128,9 @@ export default class UserListPage extends AdminPage {
}}
/>
</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
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 }}
role="table"
// +1 to account for header
@ -141,7 +141,7 @@ export default class UserListPage extends AdminPage {
>
{/* Render columns */}
{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}
</div>
))}
@ -153,7 +153,7 @@ export default class UserListPage extends AdminPage {
return (
<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-column-name={col.itemName}
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 */}
{this.isLoadingPage && <LoadingIndicator size="large" />}
</section>,
<nav class="UserListPage-gridPagination">
<nav className="UserListPage-gridPagination">
<Button
disabled={this.pageNumber === 0}
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"
className="Button Button--icon UserListPage-backBtn"
/>
<span class="UserListPage-pageNumber">
<span className="UserListPage-pageNumber">
{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/
current: (
@ -260,7 +260,7 @@ export default class UserListPage extends AdminPage {
'id',
{
name: app.translator.trans('core.admin.users.grid.columns.user_id.title'),
content: (user: User) => user.id() ?? '',
content: (user: User) => user.id() ?? null,
},
100
);
@ -300,7 +300,7 @@ export default class UserListPage extends AdminPage {
{
name: app.translator.trans('core.admin.users.grid.columns.join_time.title'),
content: (user: User) => (
<span class="UserList-joinDate" title={user.joinTime()}>
<span className="UserList-joinDate" title={user.joinTime()}>
{dayjs(user.joinTime()).format('LLL')}
</span>
),
@ -372,13 +372,13 @@ export default class UserListPage extends AdminPage {
}
return (
<div class="UserList-email" key={user.id()} data-email-shown="false">
<span class="UserList-emailAddress" aria-hidden="true" onclick={() => setEmailVisibility(true)}>
<div className="UserList-email" key={user.id()} data-email-shown="false">
<span className="UserList-emailAddress" aria-hidden="true" onclick={() => setEmailVisibility(true)}>
{user.email()}
</span>
<button
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')}
>
{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
aria-label={app.translator.trans('core.lib.alert.dismiss_a11y_label')}
icon="fas fa-times"
class="Button Button--link Button--icon Alert-dismiss"
className="Button Button--link Button--icon Alert-dismiss"
onclick={ondismiss}
/>
);
@ -59,13 +59,13 @@ export default class Alert<T extends AlertAttrs = AlertAttrs> extends Component<
return (
<div {...attrs}>
{!!title && (
<div class="Alert-title">
{!!icon && <span class="Alert-title-icon">{iconHelper(icon)}</span>}
<span class="Alert-title-text">{title}</span>
<div className="Alert-title">
{!!icon && <span className="Alert-title-icon">{iconHelper(icon)}</span>}
<span className="Alert-title-text">{title}</span>
</div>
)}
<span class="Alert-body">{content}</span>
<ul class="Alert-controls">{listItems(controls.concat(dismissControl))}</ul>
<span className="Alert-body">{content}</span>
<ul className="Alert-controls">{listItems(controls.concat(dismissControl))}</ul>
</div>
);
}

View File

@ -21,7 +21,7 @@ export default class AlertManager<CustomAttrs extends IAlertManagerAttrs = IAler
const activeAlerts = this.state.getActiveAlerts();
return (
<div class="AlertManager">
<div className="AlertManager">
{Object.keys(activeAlerts)
.map(Number)
.map((key) => {
@ -29,7 +29,7 @@ export default class AlertManager<CustomAttrs extends IAlertManagerAttrs = IAler
const urgent = alert.attrs.type === 'error';
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.children}
</alert.componentClass>

View File

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

View File

@ -13,7 +13,7 @@ export default class Link extends Component {
view(vnode) {
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
// 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>
</div>
{this.alertAttrs ? <div className="Modal-alert">{Alert.component(this.alertAttrs)}</div> : ''}
{!!this.alertAttrs && (
<div className="Modal-alert">
<Alert {...this.alertAttrs} />
</div>
)}
{this.content()}
</form>

View File

@ -37,7 +37,7 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
return (
<div
key={modal.key}
class="ModalManager modal"
className="ModalManager modal"
data-modal-key={modal.key}
data-modal-number={i}
role="dialog"
@ -62,7 +62,7 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
{this.attrs.state.backdropShown && (
<div
class="Modal-backdrop backdrop"
className="Modal-backdrop backdrop"
ontransitionend={this.onBackdropTransitionEnd.bind(this)}
data-showing={!!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 LinkButton from './LinkButton';
import type Mithril from 'mithril';
import classList from '../utils/classList';
/**
* The `Navigation` component displays a set of navigation buttons. Typically
@ -25,7 +26,7 @@ export default class Navigation extends Component {
return (
<div
className={'Navigation ButtonGroup ' + (this.attrs.className || '')}
className={classList('Navigation ButtonGroup', this.attrs.className)}
onmouseenter={pane && pane.show.bind(pane)}
onmouseleave={pane && pane.onmouseleave.bind(pane)}
>
@ -41,17 +42,19 @@ export default class Navigation extends Component {
const { history } = app;
const previous = history?.getPrevious();
return LinkButton.component({
className: 'Button Navigation-back Button--icon',
href: history?.backUrl(),
icon: 'fas fa-chevron-left',
'aria-label': previous?.title,
onclick: (e: MouseEvent) => {
if (e.shiftKey || e.ctrlKey || e.metaKey || e.button === 1) return;
return (
<LinkButton
className="Button Navigation-back Button--icon"
href={history?.backUrl()}
icon="fas fa-chevron-left"
aria-label={previous?.title}
onclick={(e: MouseEvent) => {
if (e.shiftKey || e.ctrlKey || e.metaKey || e.which === 2) return;
e.preventDefault();
history?.back();
},
});
}}
/>
);
}
/**
@ -60,32 +63,36 @@ export default class Navigation extends Component {
protected getPaneButton(): Mithril.Children {
const { pane } = app;
if (!pane || !pane.active) return '';
if (!pane || !pane.active) return null;
return Button.component({
className: 'Button Button--icon Navigation-pin' + (pane.pinned ? ' active' : ''),
onclick: pane.togglePinned.bind(pane),
icon: 'fas fa-thumbtack',
});
return (
<Button
className={classList('Button Button--icon Navigation-pin', { active: pane.pinned })}
onclick={pane.togglePinned.bind(pane)}
icon="fas fa-thumbtack"
/>
);
}
/**
* Get the drawer toggle button.
*/
protected getDrawerButton(): Mithril.Children {
if (!this.attrs.drawer) return '';
if (!this.attrs.drawer) return null;
const { drawer } = app;
const user = app.session.user;
return Button.component({
className: 'Button Button--icon Navigation-drawer' + (user && user.newNotificationCount() ? ' new' : ''),
onclick: (e: MouseEvent) => {
return (
<Button
className={classList('Button Button--icon Navigation-drawer', { new: user?.newNotificationCount() })}
onclick={(e: MouseEvent) => {
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() {
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() {

View File

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

View File

@ -1,3 +1,4 @@
import classList from '../utils/classList';
import Checkbox, { ICheckboxAttrs } from './Checkbox';
/**
@ -8,10 +9,10 @@ export default class Switch extends Checkbox {
static initAttrs(attrs: ICheckboxAttrs) {
super.initAttrs(attrs);
attrs.className = (attrs.className || '') + ' Checkbox--switch';
attrs.className = classList(attrs.className, 'Checkbox--switch');
}
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(
'submit',
Button.component(
{
icon: 'fas fa-paper-plane',
className: 'Button Button--primary',
itemClassName: 'App-primaryControl',
onclick: this.onsubmit.bind(this),
},
this.attrs.submitLabel
)
<Button icon="fas fa-paper-plane" className="Button Button--primary" itemClassName="App-primaryControl" onclick={this.onsubmit.bind(this)}>
{this.attrs.submitLabel}
</Button>
);
if (this.attrs.preview) {

View File

@ -1,6 +1,7 @@
import type Mithril from 'mithril';
import type { ComponentAttrs } from '../Component';
import User from '../models/User';
import classList from '../utils/classList';
export interface AvatarAttrs extends ComponentAttrs {}
@ -11,7 +12,7 @@ export interface AvatarAttrs extends ComponentAttrs {}
* @param attrs Attributes to apply to the avatar element
*/
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';
let content: string = '';

View File

@ -1,4 +1,5 @@
import type Mithril from 'mithril';
import classList from '../utils/classList';
/**
* The `icon` helper displays an icon.
@ -7,7 +8,7 @@ import type Mithril from 'mithril';
* @param attrs Any other attributes to apply.
*/
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} />;
}

View File

@ -3,7 +3,7 @@ import type Mithril from 'mithril';
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].
*/
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> {
const preferences = this.preferences();
Object.assign(preferences || {}, newPreferences);
Object.assign(preferences ?? {}, newPreferences);
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 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
// 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-primary')!, HeaderPrimary);
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);

View File

@ -95,11 +95,7 @@ export default class CommentPost extends Post {
const post = this.attrs.post;
const attrs = super.elementAttrs();
attrs.className =
(attrs.className || '') +
' ' +
classList({
CommentPost: true,
attrs.className = classList(attrs.className, 'CommentPost', {
'Post--renderFailed': post.renderFailed(),
'Post--hidden': post.isHidden(),
'Post--edited': post.isEdited(),
@ -130,24 +126,24 @@ export default class CommentPost extends Post {
items.add(
'user',
PostUser.component({
post,
cardVisible: this.cardVisible,
oncardshow: () => {
<PostUser
post={post}
cardVisible={this.cardVisible}
oncardshow={() => {
this.cardVisible = true;
m.redraw();
},
oncardhide: () => {
}}
oncardhide={() => {
this.cardVisible = false;
m.redraw();
},
}),
}}
/>,
100
);
items.add('meta', PostMeta.component({ post }));
items.add('meta', <PostMeta post={post} />);
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
@ -155,11 +151,7 @@ export default class CommentPost extends Post {
if (post.isHidden()) {
items.add(
'toggle',
Button.component({
className: 'Button Button--default Button--more',
icon: 'fas fa-ellipsis-h',
onclick: this.toggleContent.bind(this),
})
<Button 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.
const showIfMinimized = this.state.position === ComposerState.Position.MINIMIZED ? this.state.show.bind(this.state) : undefined;
const ComposerBody = body.componentClass;
return (
<div className={'Composer ' + classList(classes)}>
<div className="Composer-handle" oncreate={this.configHandle.bind(this)} />
<ul className="Composer-controls">{listItems(this.controlItems().toArray())}</ul>
<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>
);
@ -325,41 +327,41 @@ export default class Composer extends Component {
if (this.state.position === ComposerState.Position.FULLSCREEN) {
items.add(
'exitFullScreen',
ComposerButton.component({
icon: 'fas fa-compress',
title: app.translator.trans('core.forum.composer.exit_full_screen_tooltip'),
onclick: this.state.exitFullScreen.bind(this.state),
})
<ComposerButton
icon="fas fa-compress"
title={app.translator.trans('core.forum.composer.exit_full_screen_tooltip')}
onclick={this.state.exitFullScreen.bind(this.state)}
/>
);
} else {
if (this.state.position !== ComposerState.Position.MINIMIZED) {
items.add(
'minimize',
ComposerButton.component({
icon: 'fas fa-minus minimize',
title: app.translator.trans('core.forum.composer.minimize_tooltip'),
onclick: this.state.minimize.bind(this.state),
itemClassName: 'App-backControl',
})
<ComposerButton
icon="fas fa-minus minimize"
title={app.translator.trans('core.forum.composer.minimize_tooltip')}
onclick={this.state.minimize.bind(this.state)}
itemClassName="App-backControl"
/>
);
items.add(
'fullScreen',
ComposerButton.component({
icon: 'fas fa-expand',
title: app.translator.trans('core.forum.composer.full_screen_tooltip'),
onclick: this.state.fullScreen.bind(this.state),
})
<ComposerButton
icon="fas fa-expand"
title={app.translator.trans('core.forum.composer.full_screen_tooltip')}
onclick={this.state.fullScreen.bind(this.state)}
/>
);
}
items.add(
'close',
ComposerButton.component({
icon: 'fas fa-times',
title: app.translator.trans('core.forum.composer.close_tooltip'),
onclick: this.state.close.bind(this.state),
})
<ComposerButton
icon="fas fa-times"
title={app.translator.trans('core.forum.composer.close_tooltip')}
onclick={this.state.close.bind(this.state)}
/>
);
}

View File

@ -50,21 +50,21 @@ export default class ComposerBody extends Component {
view() {
return (
<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' })}
<div className="ComposerBody-content">
<ul className="ComposerBody-header">{listItems(this.headerItems().toArray())}</ul>
<div className="ComposerBody-editor">
{TextEditor.component({
submitLabel: this.attrs.submitLabel,
placeholder: this.attrs.placeholder,
disabled: this.loading || this.attrs.disabled,
composer: this.composer,
preview: this.jumpToPreview && this.jumpToPreview.bind(this),
onchange: this.composer.fields.content,
onsubmit: this.onsubmit.bind(this),
value: this.composer.fields.content(),
})}
<TextEditor
submitLabel={this.attrs.submitLabel}
placeholder={this.attrs.placeholder}
disabled={this.loading || this.attrs.disabled}
composer={this.composer}
preview={this.jumpToPreview?.bind(this)}
onchange={this.composer.fields.content}
onsubmit={this.onsubmit.bind(this)}
value={this.composer.fields.content()}
/>
</div>
</div>
<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) {
loading = <LoadingIndicator />;
} else if (state.hasNext()) {
loading = Button.component(
{
className: 'Button',
onclick: state.loadNext.bind(state),
},
app.translator.trans('core.forum.discussion_list.load_more_button')
loading = (
<Button className="Button" onclick={state.loadNext.bind(state)}>
{app.translator.trans('core.forum.discussion_list.load_more_button')}
</Button>
);
}
if (state.isEmpty()) {
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;
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">
{state.getPages().map((pg, pageNum) => {
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 {
return (
(controls.length > 0 &&
Dropdown.component(
{
icon: 'fas fa-ellipsis-v',
className: 'DiscussionListItem-controls',
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right',
accessibleToggleLabel: app.translator.trans('core.forum.discussion_controls.toggle_dropdown_accessible_label'),
},
controls
)) ||
null
!!controls.length && (
<Dropdown
icon="fas fa-ellipsis-v"
className="DiscussionListItem-controls"
buttonClassName="Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right"
accessibleToggleLabel={app.translator.trans('core.forum.discussion_controls.toggle_dropdown_accessible_label')}
>
{controls}
</Dropdown>
)
);
}
@ -99,7 +98,7 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
return (
<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)}
>
{icon('fas fa-check')}
@ -247,13 +246,7 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
items.add('excerpt', excerpt, -100);
}
} else {
items.add(
'terminalPost',
TerminalPost.component({
discussion: this.attrs.discussion,
lastPost: !this.showFirstPost(),
})
);
items.add('terminalPost', <TerminalPost discussion={this.attrs.discussion} lastPost={!this.showFirstPost()} />);
}
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)}>
<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() })}
</span>
</button>
@ -279,7 +272,7 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
<span className="DiscussionListItem-count">
<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() })}
</span>
</span>

View File

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

View File

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

View File

@ -27,6 +27,10 @@ export default class DiscussionsUserPage extends UserPage<IUserPageAttrs, Discus
}
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
// their edit has been made, containing a button which will
// transition to their edited post when clicked.
let alert;
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(
const alert = app.alerts.show(
{
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')
);

View File

@ -5,6 +5,7 @@ import usernameHelper from '../../common/helpers/username';
import icon from '../../common/helpers/icon';
import Link from '../../common/components/Link';
import humanTime from '../../common/helpers/humanTime';
import classList from '../../common/utils/classList';
/**
* The `EventPost` component displays a post which indicating a discussion
@ -21,7 +22,7 @@ export default class EventPost extends Post {
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;
}
@ -41,7 +42,9 @@ export default class EventPost extends Post {
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(
'submit',
<div className="Form-group">
{Button.component(
{
className: 'Button Button--primary Button--block',
type: 'submit',
loading: this.loading,
},
app.translator.trans('core.forum.forgot_password.submit_button')
)}
<Button className="Button Button--primary Button--block" type="submit" loading={this.loading}>
{app.translator.trans('core.forum.forgot_password.submit_button')}
</Button>
</div>,
-10
);

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import app from '../../forum/app';
import Button from '../../common/components/Button';
import classList from '../../common/utils/classList';
/**
* 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 {
static initAttrs(attrs) {
attrs.className = (attrs.className || '') + ' LogInButton';
attrs.className = classList(attrs.className, 'LogInButton');
attrs.onclick = function () {
const width = 580;

View File

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

View File

@ -71,7 +71,7 @@ export default class NotificationGrid extends Component {
disabled={!(key in preferences)}
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', {
description: type.label,
method: method.label,

View File

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

View File

@ -48,7 +48,7 @@ export default class NotificationsDropdown<CustomAttrs extends IDropdownAttrs =
getMenu() {
return (
<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>
);
}

View File

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

View File

@ -23,7 +23,7 @@ export default class PostEdited extends Component {
return (
<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>
);
}

View File

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

View File

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

View File

@ -42,14 +42,9 @@ export default class RenameDiscussionModal<CustomAttrs extends IRenameDiscussion
<input className="FormControl" bidi={this.newTitle} type="text" />
</div>
<div className="Form-group">
{Button.component(
{
className: 'Button Button--primary Button--block',
type: 'submit',
loading: this.loading,
},
app.translator.trans('core.forum.rename_discussion.submit_button')
)}
<Button className="Button Button--primary Button--block" type="submit" loading={this.loading}>
{app.translator.trans('core.forum.rename_discussion.submit_button')}
</Button>
</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
// transition to their new post when clicked.
let alert;
const viewButton = Button.component(
{
className: 'Button Button--link',
onclick: () => {
const viewButton = (
<Button
className="Button Button--link"
onclick={() => {
m.route.set(app.route.post(post));
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(
{

View File

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

View File

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

View File

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

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