1
0
mirror of https://github.com/flarum/core.git synced 2025-07-12 20:36:28 +02:00

chore: enable and set up prettier for flarum/tags (#3496)

This commit is contained in:
David Wheatley
2022-06-20 13:00:01 +01:00
committed by GitHub
parent 4923253fbf
commit 824fb2feff
29 changed files with 501 additions and 450 deletions

View File

@ -7,7 +7,7 @@ jobs:
uses: ./.github/workflows/REUSABLE_frontend.yml uses: ./.github/workflows/REUSABLE_frontend.yml
with: with:
enable_bundlewatch: false enable_bundlewatch: false
enable_prettier: false enable_prettier: true
enable_typescript: true enable_typescript: true
frontend_directory: ./extensions/tags/js frontend_directory: ./extensions/tags/js

View File

@ -2,6 +2,7 @@
"private": true, "private": true,
"name": "@flarum/tags", "name": "@flarum/tags",
"version": "0.0.0", "version": "0.0.0",
"prettier": "@flarum/prettier-config",
"dependencies": { "dependencies": {
"sortablejs": "^1.14.0" "sortablejs": "^1.14.0"
}, },
@ -13,14 +14,17 @@
"build-typings": "yarn run clean-typings && ([ -e src/@types ] && cp -r src/@types dist-typings/@types || true) && tsc && yarn run post-build-typings", "build-typings": "yarn run clean-typings && ([ -e src/@types ] && cp -r src/@types dist-typings/@types || true) && tsc && yarn run post-build-typings",
"post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'", "post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'",
"check-typings": "tsc --noEmit --emitDeclarationOnly false", "check-typings": "tsc --noEmit --emitDeclarationOnly false",
"check-typings-coverage": "typescript-coverage-report" "check-typings-coverage": "typescript-coverage-report",
"format": "prettier --write src",
"format-check": "prettier --check src"
}, },
"devDependencies": { "devDependencies": {
"flarum-webpack-config": "^2.0.0",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"flarum-tsconfig": "^1.0.2", "flarum-tsconfig": "^1.0.2",
"flarum-webpack-config": "^2.0.0",
"prettier": "^2.7.1",
"typescript": "^4.5.4", "typescript": "^4.5.4",
"typescript-coverage-report": "^0.6.1" "typescript-coverage-report": "^0.6.1",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1"
} }
} }

View File

@ -1,5 +1,5 @@
import type Tag from "../common/models/Tag"; import type Tag from '../common/models/Tag';
import type TagListState from "../forum/states/TagListState"; import type TagListState from '../forum/states/TagListState';
declare module 'flarum/forum/routes' { declare module 'flarum/forum/routes' {
export interface ForumRoutes { export interface ForumRoutes {

View File

@ -2,26 +2,30 @@ import { extend } from 'flarum/common/extend';
import PermissionGrid from 'flarum/admin/components/PermissionGrid'; import PermissionGrid from 'flarum/admin/components/PermissionGrid';
import SettingDropdown from 'flarum/admin/components/SettingDropdown'; import SettingDropdown from 'flarum/admin/components/SettingDropdown';
export default function() { export default function () {
extend(PermissionGrid.prototype, 'startItems', items => { extend(PermissionGrid.prototype, 'startItems', (items) => {
items.add('allowTagChange', { items.add(
icon: 'fas fa-tag', 'allowTagChange',
label: app.translator.trans('flarum-tags.admin.permissions.allow_edit_tags_label'), {
setting: () => { icon: 'fas fa-tag',
const minutes = parseInt(app.data.settings.allow_tag_change, 10); label: app.translator.trans('flarum-tags.admin.permissions.allow_edit_tags_label'),
setting: () => {
const minutes = parseInt(app.data.settings.allow_tag_change, 10);
return SettingDropdown.component({ return SettingDropdown.component({
defaultLabel: minutes defaultLabel: minutes
? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', {count: minutes}) ? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', { count: minutes })
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'), : app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
key: 'allow_tag_change', key: 'allow_tag_change',
options: [ options: [
{value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')}, { value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') },
{value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')}, { 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')} { value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') },
] ],
}); });
} },
}, 90); },
90
);
}); });
} }

View File

@ -1,14 +1,22 @@
export default function () { export default function () {
app.extensionData app.extensionData
.for('flarum-tags') .for('flarum-tags')
.registerPermission({ .registerPermission(
icon: 'fas fa-tag', {
label: app.translator.trans('flarum-tags.admin.permissions.tag_discussions_label'), icon: 'fas fa-tag',
permission: 'discussion.tag', label: app.translator.trans('flarum-tags.admin.permissions.tag_discussions_label'),
}, 'moderate', 95) permission: 'discussion.tag',
.registerPermission({ },
icon: 'fas fa-tags', 'moderate',
label: app.translator.trans('flarum-tags.admin.permissions.bypass_tag_counts_label'), 95
permission: 'bypassTagCounts', )
}, 'start', 89); .registerPermission(
{
icon: 'fas fa-tags',
label: app.translator.trans('flarum-tags.admin.permissions.bypass_tag_counts_label'),
permission: 'bypassTagCounts',
},
'start',
89
);
} }

View File

@ -1,11 +1,11 @@
import { extend } from 'flarum/common/extend'; import { extend } from 'flarum/common/extend';
import BasicsPage from 'flarum/admin/components/BasicsPage'; import BasicsPage from 'flarum/admin/components/BasicsPage';
export default function() { export default function () {
extend(BasicsPage.prototype, 'homePageItems', items => { extend(BasicsPage.prototype, 'homePageItems', (items) => {
items.add('tags', { items.add('tags', {
path: '/tags', path: '/tags',
label: app.translator.trans('flarum-tags.admin.basics.tags_label') label: app.translator.trans('flarum-tags.admin.basics.tags_label'),
}); });
}); });
} }

View File

@ -11,10 +11,10 @@ import tagIcon from '../common/helpers/tagIcon';
import sortTags from '../common/utils/sortTags'; import sortTags from '../common/utils/sortTags';
import Tag from '../common/models/Tag'; import Tag from '../common/models/Tag';
export default function() { export default function () {
extend(PermissionGrid.prototype, 'oninit', function () { extend(PermissionGrid.prototype, 'oninit', function () {
this.loading = true; this.loading = true;
}) });
extend(PermissionGrid.prototype, 'oncreate', function () { extend(PermissionGrid.prototype, 'oncreate', function () {
app.store.find<Tag[]>('tags', {}).then(() => { app.store.find<Tag[]>('tags', {}).then(() => {
@ -30,7 +30,7 @@ export default function() {
} }
return original(vnode); return original(vnode);
}) });
override(app, 'getRequiredPermissions', (original, permission) => { override(app, 'getRequiredPermissions', (original, permission) => {
const tagPrefix = permission.match(/^tag\d+\./); const tagPrefix = permission.match(/^tag\d+\./);
@ -40,45 +40,60 @@ export default function() {
const required = original(globalPermission); const required = original(globalPermission);
return required.map(required => tagPrefix[0] + required); return required.map((required) => tagPrefix[0] + required);
} }
return original(permission); return original(permission);
}); });
extend(PermissionGrid.prototype, 'scopeItems', items => { extend(PermissionGrid.prototype, 'scopeItems', (items) => {
sortTags(app.store.all('tags')) sortTags(app.store.all('tags'))
.filter(tag => tag.isRestricted()) .filter((tag) => tag.isRestricted())
.forEach(tag => items.add('tag' + tag.id(), { .forEach((tag) =>
label: tagLabel(tag), items.add('tag' + tag.id(), {
onremove: () => tag.save({isRestricted: false}), label: tagLabel(tag),
render: item => { onremove: () => tag.save({ isRestricted: false }),
if ('setting' in item) return ''; render: (item) => {
if ('setting' in item) return '';
if (item.permission === 'viewForum' if (
|| item.permission === 'startDiscussion' item.permission === 'viewForum' ||
|| (item.permission && item.permission.indexOf('discussion.') === 0 && item.tagScoped !== false) item.permission === 'startDiscussion' ||
|| item.tagScoped) { (item.permission && item.permission.indexOf('discussion.') === 0 && item.tagScoped !== false) ||
return PermissionDropdown.component({ item.tagScoped
permission: 'tag' + tag.id() + '.' + item.permission, ) {
allowGuest: item.allowGuest return PermissionDropdown.component({
}); permission: 'tag' + tag.id() + '.' + item.permission,
} allowGuest: item.allowGuest,
});
}
return ''; return '';
} },
})); })
);
}); });
extend(PermissionGrid.prototype, 'scopeControlItems', items => { extend(PermissionGrid.prototype, 'scopeControlItems', (items) => {
const tags = sortTags(app.store.all<Tag>('tags').filter(tag => !tag.isRestricted())); const tags = sortTags(app.store.all<Tag>('tags').filter((tag) => !tag.isRestricted()));
if (tags.length) { if (tags.length) {
items.add('tag', <Dropdown className='Dropdown--restrictByTag' buttonClassName='Button Button--text' label={app.translator.trans('flarum-tags.admin.permissions.restrict_by_tag_heading')} icon='fas fa-plus' caretIcon={null}> items.add(
{tags.map(tag => <Button icon={true} onclick={() => tag.save({ isRestricted: true })}> 'tag',
{[tagIcon(tag, { className: 'Button-icon' }), ' ', tag.name()]} <Dropdown
</Button>)} className="Dropdown--restrictByTag"
</Dropdown>); buttonClassName="Button Button--text"
label={app.translator.trans('flarum-tags.admin.permissions.restrict_by_tag_heading')}
icon="fas fa-plus"
caretIcon={null}
>
{tags.map((tag) => (
<Button icon={true} onclick={() => tag.save({ isRestricted: true })}>
{[tagIcon(tag, { className: 'Button-icon' }), ' ', tag.name()]}
</Button>
))}
</Dropdown>
);
} }
}); });
} }

View File

@ -59,9 +59,7 @@ export default class EditTagModal extends Modal<EditTagModalAttrs> {
content() { content() {
return ( return (
<div className="Modal-body"> <div className="Modal-body">
<div className="Form"> <div className="Form">{this.fields().toArray()}</div>
{this.fields().toArray()}
</div>
</div> </div>
); );
} }
@ -69,59 +67,97 @@ export default class EditTagModal extends Modal<EditTagModalAttrs> {
fields() { fields() {
const items = new ItemList(); const items = new ItemList();
items.add('name', <div className="Form-group"> items.add(
<label>{app.translator.trans('flarum-tags.admin.edit_tag.name_label')}</label> 'name',
<input className="FormControl" placeholder={app.translator.trans('flarum-tags.admin.edit_tag.name_placeholder')} value={this.name()} oninput={(e: InputEvent) => { <div className="Form-group">
const target = e.target as HTMLInputElement; <label>{app.translator.trans('flarum-tags.admin.edit_tag.name_label')}</label>
this.name(target.value); <input
this.slug(slug(target.value)); className="FormControl"
}} /> placeholder={app.translator.trans('flarum-tags.admin.edit_tag.name_placeholder')}
</div>, 50); value={this.name()}
oninput={(e: InputEvent) => {
const target = e.target as HTMLInputElement;
this.name(target.value);
this.slug(slug(target.value));
}}
/>
</div>,
50
);
items.add('slug', <div className="Form-group"> items.add(
<label>{app.translator.trans('flarum-tags.admin.edit_tag.slug_label')}</label> 'slug',
<input className="FormControl" bidi={this.slug} /> <div className="Form-group">
</div>, 40); <label>{app.translator.trans('flarum-tags.admin.edit_tag.slug_label')}</label>
<input className="FormControl" bidi={this.slug} />
</div>,
40
);
items.add('description', <div className="Form-group"> items.add(
<label>{app.translator.trans('flarum-tags.admin.edit_tag.description_label')}</label> 'description',
<textarea className="FormControl" bidi={this.description} /> <div className="Form-group">
</div>, 30); <label>{app.translator.trans('flarum-tags.admin.edit_tag.description_label')}</label>
<textarea className="FormControl" bidi={this.description} />
</div>,
30
);
items.add('color', <div className="Form-group"> items.add(
<label>{app.translator.trans('flarum-tags.admin.edit_tag.color_label')}</label> 'color',
<ColorPreviewInput className="FormControl" placeholder="#aaaaaa" bidi={this.color} /> <div className="Form-group">
</div>, 20); <label>{app.translator.trans('flarum-tags.admin.edit_tag.color_label')}</label>
<ColorPreviewInput className="FormControl" placeholder="#aaaaaa" bidi={this.color} />
</div>,
20
);
items.add('icon', <div className="Form-group"> items.add(
<label>{app.translator.trans('flarum-tags.admin.edit_tag.icon_label')}</label> 'icon',
<div className="helpText"> <div className="Form-group">
{app.translator.trans('flarum-tags.admin.edit_tag.icon_text', { a: <a href="https://fontawesome.com/icons?m=free" tabindex="-1" /> })} <label>{app.translator.trans('flarum-tags.admin.edit_tag.icon_label')}</label>
</div> <div className="helpText">
<input className="FormControl" placeholder="fas fa-bolt" bidi={this.icon} /> {app.translator.trans('flarum-tags.admin.edit_tag.icon_text', { a: <a href="https://fontawesome.com/icons?m=free" tabindex="-1" /> })}
</div>, 10); </div>
<input className="FormControl" placeholder="fas fa-bolt" bidi={this.icon} />
</div>,
10
);
items.add('hidden', <div className="Form-group"> items.add(
<div> 'hidden',
<label className="checkbox"> <div className="Form-group">
<input type="checkbox" bidi={this.isHidden} /> <div>
{app.translator.trans('flarum-tags.admin.edit_tag.hide_label')} <label className="checkbox">
</label> <input type="checkbox" bidi={this.isHidden} />
</div> {app.translator.trans('flarum-tags.admin.edit_tag.hide_label')}
</div>, 10); </label>
</div>
</div>,
10
);
items.add('submit', <div className="Form-group"> items.add(
{Button.component({ 'submit',
type: 'submit', <div className="Form-group">
className: 'Button Button--primary EditTagModal-save', {Button.component(
loading: this.loading, {
}, app.translator.trans('flarum-tags.admin.edit_tag.submit_button'))} type: 'submit',
{this.tag.exists ? ( className: 'Button Button--primary EditTagModal-save',
<button type="button" className="Button EditTagModal-delete" onclick={this.delete.bind(this)}> loading: this.loading,
{app.translator.trans('flarum-tags.admin.edit_tag.delete_tag_button')} },
</button> app.translator.trans('flarum-tags.admin.edit_tag.submit_button')
) : ''} )}
</div>, -10); {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
);
return items; return items;
} }
@ -147,20 +183,22 @@ export default class EditTagModal extends Modal<EditTagModalAttrs> {
// This is done for better error visibility on smaller screen heights. // This is done for better error visibility on smaller screen heights.
this.tag.save(this.submitData()).then( this.tag.save(this.submitData()).then(
() => this.hide(), () => this.hide(),
() => this.loading = false () => (this.loading = false)
); );
} }
delete() { delete() {
if (confirm(extractText(app.translator.trans('flarum-tags.admin.edit_tag.delete_tag_confirmation')))) { if (confirm(extractText(app.translator.trans('flarum-tags.admin.edit_tag.delete_tag_confirmation')))) {
const children = app.store.all<Tag>('tags').filter(tag => tag.parent() === this.tag); const children = app.store.all<Tag>('tags').filter((tag) => tag.parent() === this.tag);
this.tag.delete().then(() => { this.tag.delete().then(() => {
children.forEach(tag => tag.pushData({ children.forEach((tag) =>
attributes: { isChild: false }, tag.pushData({
// @deprecated. Temporary hack for type safety, remove before v1.3. attributes: { isChild: false },
relationships: { parent: null as any as [] } // @deprecated. Temporary hack for type safety, remove before v1.3.
})); relationships: { parent: null as any as [] },
})
);
m.redraw(); m.redraw();
}); });

View File

@ -1,5 +1,6 @@
import sortable from 'sortablejs'; import sortable from 'sortablejs';
import app from 'flarum/admin/app';
import ExtensionPage from 'flarum/admin/components/ExtensionPage'; import ExtensionPage from 'flarum/admin/components/ExtensionPage';
import Button from 'flarum/common/components/Button'; import Button from 'flarum/common/components/Button';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
@ -18,16 +19,18 @@ function tagItem(tag) {
{Button.component({ {Button.component({
className: 'Button Button--link', className: 'Button Button--link',
icon: 'fas fa-pencil-alt', icon: 'fas fa-pencil-alt',
onclick: () => app.modal.show(EditTagModal, { model: tag }) onclick: () => app.modal.show(EditTagModal, { model: tag }),
})} })}
</div> </div>
{!tag.isChild() && tag.position() !== null ? ( {!tag.isChild() && tag.position() !== null ? (
<ol className="TagListItem-children TagList"> <ol className="TagListItem-children TagList">
{sortTags(app.store.all('tags')) {sortTags(app.store.all('tags'))
.filter(child => child.parent() === tag) .filter((child) => child.parent() === tag)
.map(tagItem)} .map(tagItem)}
</ol> </ol>
) : ''} ) : (
''
)}
</li> </li>
); );
} }
@ -62,81 +65,78 @@ export default class TagsPage extends ExtensionPage {
const minSecondaryTags = this.setting('flarum-tags.min_secondary_tags', 0); const minSecondaryTags = this.setting('flarum-tags.min_secondary_tags', 0);
const maxSecondaryTags = this.setting('flarum-tags.max_secondary_tags', 0); const maxSecondaryTags = this.setting('flarum-tags.max_secondary_tags', 0);
const tags = sortTags(app.store.all('tags').filter(tag => !tag.parent())); const tags = sortTags(app.store.all('tags').filter((tag) => !tag.parent()));
return ( return (
<div className="TagsContent"> <div className="TagsContent">
<div className="TagsContent-list"> <div className="TagsContent-list">
<div className="container" key={this.forcedRefreshKey} oncreate={this.onListOnCreate.bind(this)}><div className="SettingsGroups"> <div className="container" key={this.forcedRefreshKey} oncreate={this.onListOnCreate.bind(this)}>
<div className="TagGroup"> <div className="SettingsGroups">
<label>{app.translator.trans('flarum-tags.admin.tags.primary_heading')}</label> <div className="TagGroup">
<ol className="TagList TagList--primary"> <label>{app.translator.trans('flarum-tags.admin.tags.primary_heading')}</label>
{tags <ol className="TagList TagList--primary">{tags.filter((tag) => tag.position() !== null && !tag.isChild()).map(tagItem)}</ol>
.filter(tag => tag.position() !== null && !tag.isChild()) {Button.component(
.map(tagItem)} {
</ol> className: 'Button TagList-button',
{Button.component( icon: 'fas fa-plus',
{ onclick: () => app.modal.show(EditTagModal, { primary: true }),
className: 'Button TagList-button', },
icon: 'fas fa-plus', app.translator.trans('flarum-tags.admin.tags.create_primary_tag_button')
onclick: () => app.modal.show(EditTagModal, { primary: true }), )}
}, </div>
app.translator.trans('flarum-tags.admin.tags.create_primary_tag_button')
)}
</div>
<div className="TagGroup TagGroup--secondary"> <div className="TagGroup TagGroup--secondary">
<label>{app.translator.trans('flarum-tags.admin.tags.secondary_heading')}</label> <label>{app.translator.trans('flarum-tags.admin.tags.secondary_heading')}</label>
<ul className="TagList"> <ul className="TagList">
{tags {tags
.filter(tag => tag.position() === null) .filter((tag) => tag.position() === null)
.sort((a, b) => a.name().localeCompare(b.name())) .sort((a, b) => a.name().localeCompare(b.name()))
.map(tagItem)} .map(tagItem)}
</ul> </ul>
{Button.component( {Button.component(
{ {
className: 'Button TagList-button', className: 'Button TagList-button',
icon: 'fas fa-plus', icon: 'fas fa-plus',
onclick: () => app.modal.show(EditTagModal, { primary: false }), onclick: () => app.modal.show(EditTagModal, { primary: false }),
}, },
app.translator.trans('flarum-tags.admin.tags.create_secondary_tag_button') app.translator.trans('flarum-tags.admin.tags.create_secondary_tag_button')
)} )}
</div>
<div className="Form">
<label>{app.translator.trans('flarum-tags.admin.tags.settings_heading')}</label>
<div className="Form-group">
<label>{app.translator.trans('flarum-tags.admin.tag_settings.required_primary_heading')}</label>
<div className="helpText">{app.translator.trans('flarum-tags.admin.tag_settings.required_primary_text')}</div>
<div className="TagSettings-rangeInput">
<input
className="FormControl"
type="number"
min="0"
value={minPrimaryTags()}
oninput={withAttr('value', this.setMinTags.bind(this, minPrimaryTags, maxPrimaryTags))}
/>
{app.translator.trans('flarum-tags.admin.tag_settings.range_separator_text')}
<input className="FormControl" type="number" min={minPrimaryTags()} bidi={maxPrimaryTags} />
</div>
</div> </div>
<div className="Form-group"> <div className="Form">
<label>{app.translator.trans('flarum-tags.admin.tag_settings.required_secondary_heading')}</label> <label>{app.translator.trans('flarum-tags.admin.tags.settings_heading')}</label>
<div className="helpText">{app.translator.trans('flarum-tags.admin.tag_settings.required_secondary_text')}</div> <div className="Form-group">
<div className="TagSettings-rangeInput"> <label>{app.translator.trans('flarum-tags.admin.tag_settings.required_primary_heading')}</label>
<input <div className="helpText">{app.translator.trans('flarum-tags.admin.tag_settings.required_primary_text')}</div>
className="FormControl" <div className="TagSettings-rangeInput">
type="number" <input
min="0" className="FormControl"
value={minSecondaryTags()} type="number"
oninput={withAttr('value', this.setMinTags.bind(this, minSecondaryTags, maxSecondaryTags))} min="0"
/> value={minPrimaryTags()}
{app.translator.trans('flarum-tags.admin.tag_settings.range_separator_text')} oninput={withAttr('value', this.setMinTags.bind(this, minPrimaryTags, maxPrimaryTags))}
<input className="FormControl" type="number" min={minSecondaryTags()} bidi={maxSecondaryTags} /> />
{app.translator.trans('flarum-tags.admin.tag_settings.range_separator_text')}
<input className="FormControl" type="number" min={minPrimaryTags()} bidi={maxPrimaryTags} />
</div>
</div> </div>
<div className="Form-group">
<label>{app.translator.trans('flarum-tags.admin.tag_settings.required_secondary_heading')}</label>
<div className="helpText">{app.translator.trans('flarum-tags.admin.tag_settings.required_secondary_text')}</div>
<div className="TagSettings-rangeInput">
<input
className="FormControl"
type="number"
min="0"
value={minSecondaryTags()}
oninput={withAttr('value', this.setMinTags.bind(this, minSecondaryTags, maxSecondaryTags))}
/>
{app.translator.trans('flarum-tags.admin.tag_settings.range_separator_text')}
<input className="FormControl" type="number" min={minSecondaryTags()} bidi={maxSecondaryTags} />
</div>
</div>
<div className="Form-group">{this.submitButton()}</div>
</div> </div>
<div className="Form-group">{this.submitButton()}</div>
</div> </div>
</div>
<div className="TagsContent-footer"> <div className="TagsContent-footer">
<p>{app.translator.trans('flarum-tags.admin.tags.about_tags_text')}</p> <p>{app.translator.trans('flarum-tags.admin.tags.about_tags_text')}</p>
</div> </div>
@ -147,19 +147,21 @@ export default class TagsPage extends ExtensionPage {
} }
onListOnCreate(vnode) { onListOnCreate(vnode) {
this.$('.TagList').get().map(e => { this.$('.TagList')
sortable.create(e, { .get()
group: 'tags', .map((e) => {
delay: 50, sortable.create(e, {
delayOnTouchOnly: true, group: 'tags',
touchStartThreshold: 5, delay: 50,
animation: 150, delayOnTouchOnly: true,
swapThreshold: 0.65, touchStartThreshold: 5,
dragClass: 'sortable-dragging', animation: 150,
ghostClass: 'sortable-placeholder', swapThreshold: 0.65,
onSort: (e) => this.onSortUpdate(e) dragClass: 'sortable-dragging',
}) ghostClass: 'sortable-placeholder',
}); onSort: (e) => this.onSortUpdate(e),
});
});
} }
setMinTags(minTags, maxTags, value) { setMinTags(minTags, maxTags, value) {
@ -175,9 +177,9 @@ export default class TagsPage extends ExtensionPage {
app.store.getById('tags', e.item.getAttribute('data-id')).pushData({ app.store.getById('tags', e.item.getAttribute('data-id')).pushData({
attributes: { attributes: {
position: null, position: null,
isChild: false isChild: false,
}, },
relationships: { parent: null } relationships: { parent: null },
}); });
} }
@ -187,12 +189,15 @@ export default class TagsPage extends ExtensionPage {
.map(function () { .map(function () {
return { return {
id: $(this).data('id'), id: $(this).data('id'),
children: $(this).find('li') children: $(this)
.find('li')
.map(function () { .map(function () {
return $(this).data('id'); return $(this).data('id');
}).get() })
.get(),
}; };
}).get(); })
.get();
// Now that we have an accurate representation of the order which the // Now that we have an accurate representation of the order which the
// primary tags are in, we will update the tag attributes in our local // primary tags are in, we will update the tag attributes in our local
@ -202,18 +207,18 @@ export default class TagsPage extends ExtensionPage {
parent.pushData({ parent.pushData({
attributes: { attributes: {
position: i, position: i,
isChild: false isChild: false,
}, },
relationships: { parent: null } relationships: { parent: null },
}); });
tag.children.forEach((child, j) => { tag.children.forEach((child, j) => {
app.store.getById('tags', child).pushData({ app.store.getById('tags', child).pushData({
attributes: { attributes: {
position: j, position: j,
isChild: true isChild: true,
}, },
relationships: { parent } relationships: { parent },
}); });
}); });
}); });
@ -221,7 +226,7 @@ export default class TagsPage extends ExtensionPage {
app.request({ app.request({
url: app.forum.attribute('apiUrl') + '/tags/order', url: app.forum.attribute('apiUrl') + '/tags/order',
method: 'POST', method: 'POST',
body: { order } body: { order },
}); });
this.forcedRefreshKey++; this.forcedRefreshKey++;

View File

@ -6,7 +6,7 @@ import addTagsHomePageOption from './addTagsHomePageOption';
import addTagChangePermission from './addTagChangePermission'; import addTagChangePermission from './addTagChangePermission';
import TagsPage from './components/TagsPage'; import TagsPage from './components/TagsPage';
app.initializers.add('flarum-tags', app => { app.initializers.add('flarum-tags', (app) => {
app.store.models.tags = Tag; app.store.models.tags = Tag;
app.extensionData.for('flarum-tags').registerPage(TagsPage); app.extensionData.for('flarum-tags').registerPage(TagsPage);
@ -17,7 +17,6 @@ app.initializers.add('flarum-tags', app => {
addTagChangePermission(); addTagChangePermission();
}); });
// Expose compat API // Expose compat API
import tagsCompat from './compat'; import tagsCompat from './compat';
import { compat } from '@flarum/core/admin'; import { compat } from '@flarum/core/admin';

View File

@ -9,5 +9,5 @@ export default {
'tags/models/Tag': Tag, 'tags/models/Tag': Tag,
'tags/helpers/tagsLabel': tagsLabel, 'tags/helpers/tagsLabel': tagsLabel,
'tags/helpers/tagIcon': tagIcon, 'tags/helpers/tagIcon': tagIcon,
'tags/helpers/tagLabel': tagLabel 'tags/helpers/tagLabel': tagLabel,
}; };

View File

@ -4,11 +4,7 @@ export default function tagIcon(tag, attrs = {}, settings = {}) {
const hasIcon = tag && tag.icon(); const hasIcon = tag && tag.icon();
const { useColor = true } = settings; const { useColor = true } = settings;
attrs.className = classList([ attrs.className = classList([attrs.className, 'icon', hasIcon ? tag.icon() : 'TagIcon']);
attrs.className,
'icon',
hasIcon ? tag.icon() : 'TagIcon'
]);
if (tag && useColor) { if (tag && useColor) {
attrs.style = attrs.style || {}; attrs.style = attrs.style || {};
@ -21,5 +17,5 @@ export default function tagIcon(tag, attrs = {}, settings = {}) {
attrs.className += ' untagged'; attrs.className += ' untagged';
} }
return hasIcon ? <i {...attrs}/> : <span {...attrs}/>; return hasIcon ? <i {...attrs} /> : <span {...attrs} />;
} }

View File

@ -18,7 +18,7 @@ export default function tagLabel(tag, attrs = {}) {
if (link) { if (link) {
attrs.title = tag.description() || ''; attrs.title = tag.description() || '';
attrs.href = app.route('tag', {tags: tag.slug()}); attrs.href = app.route('tag', { tags: tag.slug() });
} }
if (tag.isChild()) { if (tag.isChild()) {
@ -28,11 +28,11 @@ export default function tagLabel(tag, attrs = {}) {
attrs.className += ' untagged'; attrs.className += ' untagged';
} }
return ( return m(
m((link ? Link : 'span'), attrs, link ? Link : 'span',
<span className="TagLabel-text"> attrs,
{tag && tag.icon() && tagIcon(tag, {}, {useColor: false})} {tagText} <span className="TagLabel-text">
</span> {tag && tag.icon() && tagIcon(tag, {}, { useColor: false })} {tagText}
) </span>
); );
} }

View File

@ -9,9 +9,9 @@ export default function tagsLabel(tags, attrs = {}) {
attrs.className = 'TagsLabel ' + (attrs.className || ''); attrs.className = 'TagsLabel ' + (attrs.className || '');
if (tags) { if (tags) {
sortTags(tags).forEach(tag => { sortTags(tags).forEach((tag) => {
if (tag || tags.length === 1) { if (tag || tags.length === 1) {
children.push(tagLabel(tag, {link})); children.push(tagLabel(tag, { link }));
} }
}); });
} else { } else {

View File

@ -1,4 +1,4 @@
import Tag from "../models/Tag"; import Tag from '../models/Tag';
export default function sortTags(tags: Tag[]) { export default function sortTags(tags: Tag[]) {
return tags.slice(0).sort((a, b) => { return tags.slice(0).sort((a, b) => {
@ -7,8 +7,7 @@ export default function sortTags(tags: Tag[]) {
// If they're both secondary tags, sort them by their discussions count, // If they're both secondary tags, sort them by their discussions count,
// descending. // descending.
if (aPos === null && bPos === null) if (aPos === null && bPos === null) return b.discussionCount() - a.discussionCount();
return b.discussionCount() - a.discussionCount();
// If just one is a secondary tag, then the primary tag should // If just one is a secondary tag, then the primary tag should
// come first. // come first.
@ -23,20 +22,14 @@ export default function sortTags(tags: Tag[]) {
// If they both have the same parent, then their positions are local, // If they both have the same parent, then their positions are local,
// so we can compare them directly. // so we can compare them directly.
if (aParent === bParent) return aPos - bPos; if (aParent === bParent) return aPos - bPos;
// If they are both child tags, then we will compare the positions of their // If they are both child tags, then we will compare the positions of their
// parents. // parents.
else if (aParent && bParent) else if (aParent && bParent) return aParent.position()! - bParent.position()!;
return aParent.position()! - bParent.position()!;
// If we are comparing a child tag with its parent, then we let the parent // If we are comparing a child tag with its parent, then we let the parent
// come first. If we are comparing an unrelated parent/child, then we // come first. If we are comparing an unrelated parent/child, then we
// compare both of the parents. // compare both of the parents.
else if (aParent) else if (aParent) return aParent === b ? 1 : aParent.position()! - bPos;
return aParent === b ? 1 : aParent.position()! - bPos; else if (bParent) return bParent === a ? -1 : aPos - bParent.position()!;
else if (bParent)
return bParent === a ? -1 : aPos - bParent.position()!;
return 0; return 0;
}); });

View File

@ -15,15 +15,14 @@ export default function () {
if (tag) { if (tag) {
const parent = tag.parent(); const parent = tag.parent();
const tags = parent ? [parent, tag] : [tag]; const tags = parent ? [parent, tag] : [tag];
promise.then(composer => composer.fields.tags = tags); promise.then((composer) => (composer.fields.tags = tags));
} else { } else {
app.composer.fields.tags = []; app.composer.fields.tags = [];
} }
}); });
extend(DiscussionComposer.prototype, 'oninit', function () { extend(DiscussionComposer.prototype, 'oninit', function () {
app.tagList.load(['parent']).then(() => m.redraw()) app.tagList.load(['parent']).then(() => m.redraw());
}); });
// Add tag-selection abilities to the discussion composer. // Add tag-selection abilities to the discussion composer.
@ -34,10 +33,10 @@ export default function () {
app.modal.show(TagDiscussionModal, { app.modal.show(TagDiscussionModal, {
selectedTags: (this.composer.fields.tags || []).slice(0), selectedTags: (this.composer.fields.tags || []).slice(0),
onsubmit: tags => { onsubmit: (tags) => {
this.composer.fields.tags = tags; this.composer.fields.tags = tags;
this.$('textarea').focus(); this.$('textarea').focus();
} },
}); });
}; };
@ -47,32 +46,38 @@ export default function () {
const tags = this.composer.fields.tags || []; const tags = this.composer.fields.tags || [];
const selectableTags = getSelectableTags(); const selectableTags = getSelectableTags();
items.add('tags', ( items.add(
'tags',
<a className={classList(['DiscussionComposer-changeTags', !selectableTags.length && 'disabled'])} onclick={this.chooseTags.bind(this)}> <a className={classList(['DiscussionComposer-changeTags', !selectableTags.length && 'disabled'])} onclick={this.chooseTags.bind(this)}>
{tags.length {tags.length ? (
? tagsLabel(tags) tagsLabel(tags)
: <span className="TagLabel untagged">{app.translator.trans('flarum-tags.forum.composer_discussion.choose_tags_link')}</span>} ) : (
</a> <span className="TagLabel untagged">{app.translator.trans('flarum-tags.forum.composer_discussion.choose_tags_link')}</span>
), 10); )}
</a>,
10
);
}); });
override(DiscussionComposer.prototype, 'onsubmit', function (original) { override(DiscussionComposer.prototype, 'onsubmit', function (original) {
const chosenTags = this.composer.fields.tags || []; const chosenTags = this.composer.fields.tags || [];
const chosenPrimaryTags = chosenTags.filter(tag => tag.position() !== null && !tag.isChild()); const chosenPrimaryTags = chosenTags.filter((tag) => tag.position() !== null && !tag.isChild());
const chosenSecondaryTags = chosenTags.filter(tag => tag.position() === null); const chosenSecondaryTags = chosenTags.filter((tag) => tag.position() === null);
const selectableTags = getSelectableTags(); const selectableTags = getSelectableTags();
if ((!chosenTags.length if (
|| (chosenPrimaryTags.length < app.forum.attribute('minPrimaryTags')) (!chosenTags.length ||
|| (chosenSecondaryTags.length < app.forum.attribute('minSecondaryTags')) chosenPrimaryTags.length < app.forum.attribute('minPrimaryTags') ||
) && selectableTags.length) { chosenSecondaryTags.length < app.forum.attribute('minSecondaryTags')) &&
selectableTags.length
) {
app.modal.show(TagDiscussionModal, { app.modal.show(TagDiscussionModal, {
selectedTags: chosenTags, selectedTags: chosenTags,
onsubmit: tags => { onsubmit: (tags) => {
this.composer.fields.tags = tags; this.composer.fields.tags = tags;
original(); original();
} },
}); });
} else { } else {
original(); original();
} }

View File

@ -4,13 +4,16 @@ import Button from 'flarum/common/components/Button';
import TagDiscussionModal from './components/TagDiscussionModal'; import TagDiscussionModal from './components/TagDiscussionModal';
export default function() { export default function () {
// Add a control allowing the discussion to be moved to another category. // Add a control allowing the discussion to be moved to another category.
extend(DiscussionControls, 'moderationControls', function(items, discussion) { extend(DiscussionControls, 'moderationControls', function (items, discussion) {
if (discussion.canTag()) { if (discussion.canTag()) {
items.add('tags', <Button icon="fas fa-tag" onclick={() => app.modal.show(TagDiscussionModal, { discussion })}> items.add(
{app.translator.trans('flarum-tags.forum.discussion_controls.edit_tags_button')} 'tags',
</Button>); <Button icon="fas fa-tag" onclick={() => app.modal.show(TagDiscussionModal, { discussion })}>
{app.translator.trans('flarum-tags.forum.discussion_controls.edit_tags_button')}
</Button>
);
} }
}); });
} }

View File

@ -10,10 +10,10 @@ import TagHero from './components/TagHero';
import Tag from '../common/models/Tag'; import Tag from '../common/models/Tag';
import { ComponentAttrs } from 'flarum/common/Component'; import { ComponentAttrs } from 'flarum/common/Component';
const findTag = (slug: string) => app.store.all<Tag>('tags').find(tag => tag.slug().localeCompare(slug, undefined, { sensitivity: 'base' }) === 0); const findTag = (slug: string) => app.store.all<Tag>('tags').find((tag) => tag.slug().localeCompare(slug, undefined, { sensitivity: 'base' }) === 0);
export default function() { export default function () {
IndexPage.prototype.currentTag = function() { IndexPage.prototype.currentTag = function () {
if (this.currentActiveTag) { if (this.currentActiveTag) {
return this.currentActiveTag; return this.currentActiveTag;
} }
@ -25,7 +25,7 @@ export default function() {
tag = findTag(slug); tag = findTag(slug);
} }
if (slug && !tag || (tag && !tag.isChild() && !tag.children())) { if ((slug && !tag) || (tag && !tag.isChild() && !tag.children())) {
if (this.currentTagLoading) { if (this.currentTagLoading) {
return; return;
} }
@ -36,13 +36,16 @@ export default function() {
// a child tag page, then either: // a child tag page, then either:
// - We loaded in that child tag (and its siblings) in the API document // - We loaded in that child tag (and its siblings) in the API document
// - We first navigated to the current tag's parent, which would have loaded in the current tag's siblings. // - We first navigated to the current tag's parent, which would have loaded in the current tag's siblings.
app.store.find('tags', slug, { include: 'children,children.parent,parent,state'}).then(() => { app.store
this.currentActiveTag = findTag(slug); .find('tags', slug, { include: 'children,children.parent,parent,state' })
.then(() => {
this.currentActiveTag = findTag(slug);
m.redraw(); m.redraw();
}).finally(() => { })
this.currentTagLoading = false; .finally(() => {
}); this.currentTagLoading = false;
});
} }
if (tag) { if (tag) {
@ -54,7 +57,7 @@ export default function() {
}; };
// If currently viewing a tag, insert a tag hero at the top of the view. // If currently viewing a tag, insert a tag hero at the top of the view.
override(IndexPage.prototype, 'hero', function(original) { override(IndexPage.prototype, 'hero', function (original) {
const tag = this.currentTag(); const tag = this.currentTag();
if (tag) return <TagHero model={tag} />; if (tag) return <TagHero model={tag} />;
@ -62,13 +65,13 @@ export default function() {
return original(); return original();
}); });
extend(IndexPage.prototype, 'view', function(vdom: Mithril.Vnode<ComponentAttrs, {}>) { extend(IndexPage.prototype, 'view', function (vdom: Mithril.Vnode<ComponentAttrs, {}>) {
const tag = this.currentTag(); const tag = this.currentTag();
if (tag) vdom.attrs.className += ' IndexPage--tag'+tag.id(); if (tag) vdom.attrs.className += ' IndexPage--tag' + tag.id();
}); });
extend(IndexPage.prototype, 'setTitle', function() { extend(IndexPage.prototype, 'setTitle', function () {
const tag = this.currentTag(); const tag = this.currentTag();
if (tag) { if (tag) {
@ -78,7 +81,7 @@ export default function() {
// If currently viewing a tag, restyle the 'new discussion' button to use // If currently viewing a tag, restyle the 'new discussion' button to use
// the tag's color, and disable if the user isn't allowed to edit. // the tag's color, and disable if the user isn't allowed to edit.
extend(IndexPage.prototype, 'sidebarItems', function(items) { extend(IndexPage.prototype, 'sidebarItems', function (items) {
const tag = this.currentTag(); const tag = this.currentTag();
if (tag) { if (tag) {
@ -92,18 +95,20 @@ export default function() {
} }
newDiscussion.attrs.disabled = !canStartDiscussion; newDiscussion.attrs.disabled = !canStartDiscussion;
newDiscussion.children = app.translator.trans(canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button'); newDiscussion.children = app.translator.trans(
canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button'
);
} }
}); });
// Add a parameter for the global search state to pass on to the // Add a parameter for the global search state to pass on to the
// DiscussionListState that will let us filter discussions by tag. // DiscussionListState that will let us filter discussions by tag.
extend(GlobalSearchState.prototype, 'params', function(params) { extend(GlobalSearchState.prototype, 'params', function (params) {
params.tags = m.route.param('tags'); params.tags = m.route.param('tags');
}); });
// Translate that parameter into a gambit appended to the search query. // Translate that parameter into a gambit appended to the search query.
extend(DiscussionListState.prototype, 'requestParams', function(this: DiscussionListState, params) { extend(DiscussionListState.prototype, 'requestParams', function (this: DiscussionListState, params) {
if (typeof params.include === 'string') { if (typeof params.include === 'string') {
params.include = [params.include]; params.include = [params.include];
} else { } else {
@ -118,7 +123,7 @@ export default function() {
if (q) { if (q) {
filter.q = `${q} tag:${this.params.tags}`; filter.q = `${q} tag:${this.params.tags}`;
} }
params.filter = filter params.filter = filter;
} }
}); });
} }

View File

@ -5,9 +5,9 @@ import DiscussionHero from 'flarum/forum/components/DiscussionHero';
import tagsLabel from '../common/helpers/tagsLabel'; import tagsLabel from '../common/helpers/tagsLabel';
import sortTags from '../common/utils/sortTags'; import sortTags from '../common/utils/sortTags';
export default function() { export default function () {
// Add tag labels to each discussion in the discussion list. // Add tag labels to each discussion in the discussion list.
extend(DiscussionListItem.prototype, 'infoItems', function(items) { extend(DiscussionListItem.prototype, 'infoItems', function (items) {
const tags = this.attrs.discussion.tags(); const tags = this.attrs.discussion.tags();
if (tags && tags.length) { if (tags && tags.length) {
@ -16,7 +16,7 @@ export default function() {
}); });
// Restyle a discussion's hero to use its first tag's color. // Restyle a discussion's hero to use its first tag's color.
extend(DiscussionHero.prototype, 'view', function(view) { extend(DiscussionHero.prototype, 'view', function (view) {
const tags = sortTags(this.attrs.discussion.tags()); const tags = sortTags(this.attrs.discussion.tags());
if (tags && tags.length) { if (tags && tags.length) {
@ -30,11 +30,11 @@ export default function() {
// Add a list of a discussion's tags to the discussion hero, displayed // Add a list of a discussion's tags to the discussion hero, displayed
// before the title. Put the title on its own line. // before the title. Put the title on its own line.
extend(DiscussionHero.prototype, 'items', function(items) { extend(DiscussionHero.prototype, 'items', function (items) {
const tags = this.attrs.discussion.tags(); const tags = this.attrs.discussion.tags();
if (tags && tags.length) { if (tags && tags.length) {
items.add('tags', tagsLabel(tags, {link: true}), 5); items.add('tags', tagsLabel(tags, { link: true }), 5);
} }
}); });
} }

View File

@ -7,14 +7,17 @@ import TagLinkButton from './components/TagLinkButton';
import TagsPage from './components/TagsPage'; import TagsPage from './components/TagsPage';
import sortTags from '../common/utils/sortTags'; import sortTags from '../common/utils/sortTags';
export default function() { export default function () {
// Add a link to the tags page, as well as a list of all the tags, // Add a link to the tags page, as well as a list of all the tags,
// to the index page's sidebar. // to the index page's sidebar.
extend(IndexPage.prototype, 'navItems', function (items) { extend(IndexPage.prototype, 'navItems', function (items) {
items.add('tags', <LinkButton icon="fas fa-th-large" href={app.route('tags')}> items.add(
{app.translator.trans('flarum-tags.forum.index.tags_link')} 'tags',
</LinkButton> <LinkButton icon="fas fa-th-large" href={app.route('tags')}>
, -10); {app.translator.trans('flarum-tags.forum.index.tags_link')}
</LinkButton>,
-10
);
if (app.current.matches(TagsPage)) return; if (app.current.matches(TagsPage)) return;
@ -24,7 +27,7 @@ export default function() {
const tags = app.store.all('tags'); const tags = app.store.all('tags');
const currentTag = this.currentTag(); const currentTag = this.currentTag();
const addTag = tag => { const addTag = (tag) => {
let active = currentTag === tag; let active = currentTag === tag;
if (!active && currentTag) { if (!active && currentTag) {
@ -36,23 +39,21 @@ export default function() {
// use its children to populate the dropdown. The problem here is that `view` // use its children to populate the dropdown. The problem here is that `view`
// on TagLinkButton is only called AFTER SelectDropdown, so no children are available // on TagLinkButton is only called AFTER SelectDropdown, so no children are available
// for SelectDropdown to use at the time. // for SelectDropdown to use at the time.
items.add('tag' + tag.id(), TagLinkButton.component({model: tag, params, active}, tag?.name()), -14); items.add('tag' + tag.id(), TagLinkButton.component({ model: tag, params, active }, tag?.name()), -14);
}; };
sortTags(tags) sortTags(tags)
.filter(tag => tag.position() !== null && (!tag.isChild() || (currentTag && (tag.parent() === currentTag || tag.parent() === currentTag.parent())))) .filter(
(tag) => tag.position() !== null && (!tag.isChild() || (currentTag && (tag.parent() === currentTag || tag.parent() === currentTag.parent())))
)
.forEach(addTag); .forEach(addTag);
const more = tags const more = tags.filter((tag) => tag.position() === null).sort((a, b) => b.discussionCount() - a.discussionCount());
.filter(tag => tag.position() === null)
.sort((a, b) => b.discussionCount() - a.discussionCount());
more.splice(0, 3).forEach(addTag); more.splice(0, 3).forEach(addTag);
if (more.length) { if (more.length) {
items.add('moreTags', <LinkButton href={app.route('tags')}> items.add('moreTags', <LinkButton href={app.route('tags')}>{app.translator.trans('flarum-tags.forum.index.more_link')}</LinkButton>, -16);
{app.translator.trans('flarum-tags.forum.index.more_link')}
</LinkButton>, -16)
} }
}); });
} }

View File

@ -9,9 +9,7 @@ export default class DiscussionTaggedPost extends EventPost {
const newTags = attrs.post.content()[1]; const newTags = attrs.post.content()[1];
function diffTags(tags1, tags2) { function diffTags(tags1, tags2) {
return tags1 return tags1.filter((tag) => tags2.indexOf(tag) === -1).map((id) => app.store.getById('tags', id));
.filter(tag => tags2.indexOf(tag) === -1)
.map(id => app.store.getById('tags', id));
} }
attrs.tagsAdded = diffTags(newTags, oldTags); attrs.tagsAdded = diffTags(newTags, oldTags);
@ -39,15 +37,15 @@ export default class DiscussionTaggedPost extends EventPost {
if (this.attrs.tagsAdded.length) { if (this.attrs.tagsAdded.length) {
data.tagsAdded = app.translator.trans('flarum-tags.forum.post_stream.tags_text', { data.tagsAdded = app.translator.trans('flarum-tags.forum.post_stream.tags_text', {
tags: tagsLabel(this.attrs.tagsAdded, {link: true}), tags: tagsLabel(this.attrs.tagsAdded, { link: true }),
count: this.attrs.tagsAdded.length count: this.attrs.tagsAdded.length,
}); });
} }
if (this.attrs.tagsRemoved.length) { if (this.attrs.tagsRemoved.length) {
data.tagsRemoved = app.translator.trans('flarum-tags.forum.post_stream.tags_text', { data.tagsRemoved = app.translator.trans('flarum-tags.forum.post_stream.tags_text', {
tags: tagsLabel(this.attrs.tagsRemoved, {link: true}), tags: tagsLabel(this.attrs.tagsRemoved, { link: true }),
count: this.attrs.tagsRemoved.length count: this.attrs.tagsRemoved.length,
}); });
} }

View File

@ -58,11 +58,11 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
const tags = sortTags(getSelectableTags(this.attrs.discussion)); const tags = sortTags(getSelectableTags(this.attrs.discussion));
this.tags = tags; this.tags = tags;
const discussionTags = this.attrs.discussion?.tags() const discussionTags = this.attrs.discussion?.tags();
if (this.attrs.selectedTags) { if (this.attrs.selectedTags) {
this.attrs.selectedTags.map(this.addTag.bind(this)); this.attrs.selectedTags.map(this.addTag.bind(this));
} else if (discussionTags) { } else if (discussionTags) {
discussionTags.forEach(tag => tag && this.addTag(tag)); discussionTags.forEach((tag) => tag && this.addTag(tag));
} }
this.selectedTag = tags[0]; this.selectedTag = tags[0];
@ -72,11 +72,11 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
} }
primaryCount() { primaryCount() {
return this.selected.filter(tag => tag.isPrimary()).length; return this.selected.filter((tag) => tag.isPrimary()).length;
} }
secondaryCount() { secondaryCount() {
return this.selected.filter(tag => !tag.isPrimary()).length; return this.selected.filter((tag) => !tag.isPrimary()).length;
} }
/** /**
@ -107,9 +107,7 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
// Look through the list of selected tags for any tags which have the tag // Look through the list of selected tags for any tags which have the tag
// we just removed as their parent. We'll need to remove them too. // we just removed as their parent. We'll need to remove them too.
this.selected this.selected.filter((selected) => selected.parent() === tag).forEach(this.removeTag.bind(this));
.filter(selected => selected.parent() === tag)
.forEach(this.removeTag.bind(this));
} }
} }
@ -119,7 +117,7 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
title() { title() {
return this.attrs.discussion return this.attrs.discussion
? app.translator.trans('flarum-tags.forum.choose_tags.edit_title', {title: <em>{this.attrs.discussion.title()}</em>}) ? app.translator.trans('flarum-tags.forum.choose_tags.edit_title', { title: <em>{this.attrs.discussion.title()}</em> })
: app.translator.trans('flarum-tags.forum.choose_tags.title'); : app.translator.trans('flarum-tags.forum.choose_tags.title');
} }
@ -130,10 +128,10 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
if (primaryCount < this.minPrimary) { if (primaryCount < this.minPrimary) {
const remaining = this.minPrimary - primaryCount; const remaining = this.minPrimary - primaryCount;
return app.translator.trans('flarum-tags.forum.choose_tags.choose_primary_placeholder', {count: remaining}); return app.translator.trans('flarum-tags.forum.choose_tags.choose_primary_placeholder', { count: remaining });
} else if (secondaryCount < this.minSecondary) { } else if (secondaryCount < this.minSecondary) {
const remaining = this.minSecondary - secondaryCount; const remaining = this.minSecondary - secondaryCount;
return app.translator.trans('flarum-tags.forum.choose_tags.choose_secondary_placeholder', {count: remaining}); return app.translator.trans('flarum-tags.forum.choose_tags.choose_secondary_placeholder', { count: remaining });
} }
return ''; return '';
@ -151,7 +149,7 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
// Filter out all child tags whose parents have not been selected. This // Filter out all child tags whose parents have not been selected. This
// makes it impossible to select a child if its parent hasn't been selected. // makes it impossible to select a child if its parent hasn't been selected.
tags = tags.filter(tag => { tags = tags.filter((tag) => {
const parent = tag.parent(); const parent = tag.parent();
return parent !== null && (parent === false || this.selected.includes(parent)); return parent !== null && (parent === false || this.selected.includes(parent));
}); });
@ -159,17 +157,17 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
// If the number of selected primary/secondary tags is at the maximum, then // If the number of selected primary/secondary tags is at the maximum, then
// we'll filter out all other tags of that type. // we'll filter out all other tags of that type.
if (primaryCount >= this.maxPrimary && !this.bypassReqs) { if (primaryCount >= this.maxPrimary && !this.bypassReqs) {
tags = tags.filter(tag => !tag.isPrimary() || this.selected.includes(tag)); tags = tags.filter((tag) => !tag.isPrimary() || this.selected.includes(tag));
} }
if (secondaryCount >= this.maxSecondary && !this.bypassReqs) { if (secondaryCount >= this.maxSecondary && !this.bypassReqs) {
tags = tags.filter(tag => tag.isPrimary() || this.selected.includes(tag)); tags = tags.filter((tag) => tag.isPrimary() || this.selected.includes(tag));
} }
// If the user has entered text in the filter input, then filter by tags // If the user has entered text in the filter input, then filter by tags
// whose name matches what they've entered. // whose name matches what they've entered.
if (filter) { if (filter) {
tags = tags.filter(tag => tag.name().substr(0, filter.length).toLowerCase() === filter); tags = tags.filter((tag) => tag.name().substr(0, filter.length).toLowerCase() === filter);
} }
if (!this.selectedTag || !tags.includes(this.selectedTag)) this.selectedTag = tags[0]; if (!this.selectedTag || !tags.includes(this.selectedTag)) this.selectedTag = tags[0];
@ -180,30 +178,38 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
<div className="Modal-body"> <div className="Modal-body">
<div className="TagDiscussionModal-form"> <div className="TagDiscussionModal-form">
<div className="TagDiscussionModal-form-input"> <div className="TagDiscussionModal-form-input">
<div className={'TagsInput FormControl ' + (this.focused ? 'focus' : '')} <div className={'TagsInput FormControl ' + (this.focused ? 'focus' : '')} onclick={() => this.$('.TagsInput input').focus()}>
onclick={() => this.$('.TagsInput input').focus()}
>
<span className="TagsInput-selected"> <span className="TagsInput-selected">
{this.selected.map(tag => {this.selected.map((tag) => (
<span className="TagsInput-tag" onclick={() => { <span
this.removeTag(tag); className="TagsInput-tag"
this.onready(); onclick={() => {
}}> this.removeTag(tag);
this.onready();
}}
>
{tagLabel(tag)} {tagLabel(tag)}
</span> </span>
)} ))}
</span> </span>
<input className="FormControl" <input
className="FormControl"
placeholder={extractText(this.getInstruction(primaryCount, secondaryCount))} placeholder={extractText(this.getInstruction(primaryCount, secondaryCount))}
bidi={this.filter} bidi={this.filter}
style={{ width: inputWidth + 'ch' }} style={{ width: inputWidth + 'ch' }}
onkeydown={this.navigator.navigate.bind(this.navigator)} onkeydown={this.navigator.navigate.bind(this.navigator)}
onfocus={() => this.focused = true} onfocus={() => (this.focused = true)}
onblur={() => this.focused = false}/> onblur={() => (this.focused = false)}
/>
</div> </div>
</div> </div>
<div className="TagDiscussionModal-form-submit App-primaryControl"> <div className="TagDiscussionModal-form-submit App-primaryControl">
<Button type="submit" className="Button Button--primary" disabled={!this.meetsRequirements(primaryCount, secondaryCount)} icon="fas fa-check"> <Button
type="submit"
className="Button Button--primary"
disabled={!this.meetsRequirements(primaryCount, secondaryCount)}
icon="fas fa-check"
>
{app.translator.trans('flarum-tags.forum.choose_tags.submit_button')} {app.translator.trans('flarum-tags.forum.choose_tags.submit_button')}
</Button> </Button>
</div> </div>
@ -213,41 +219,35 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
<div className="Modal-footer"> <div className="Modal-footer">
<ul className="TagDiscussionModal-list SelectTagList"> <ul className="TagDiscussionModal-list SelectTagList">
{tags {tags
.filter(tag => filter || !tag.parent() || this.selected.includes(tag.parent() as Tag)) .filter((tag) => filter || !tag.parent() || this.selected.includes(tag.parent() as Tag))
.map(tag => ( .map((tag) => (
<li data-index={tag.id()} <li
data-index={tag.id()}
className={classList({ className={classList({
pinned: tag.position() !== null, pinned: tag.position() !== null,
child: !!tag.parent(), child: !!tag.parent(),
colored: !!tag.color(), colored: !!tag.color(),
selected: this.selected.includes(tag), selected: this.selected.includes(tag),
active: this.selectedTag === tag active: this.selectedTag === tag,
})} })}
style={{color: tag.color()}} style={{ color: tag.color() }}
onmouseover={() => this.selectedTag = tag} onmouseover={() => (this.selectedTag = tag)}
onclick={this.toggleTag.bind(this, tag)} onclick={this.toggleTag.bind(this, tag)}
> >
{tagIcon(tag)} {tagIcon(tag)}
<span className="SelectTagListItem-name"> <span className="SelectTagListItem-name">{highlight(tag.name(), filter)}</span>
{highlight(tag.name(), filter)} {tag.description() ? <span className="SelectTagListItem-description">{tag.description()}</span> : ''}
</span>
{tag.description()
? (
<span className="SelectTagListItem-description">
{tag.description()}
</span>
) : ''}
</li> </li>
))} ))}
</ul> </ul>
{!!app.forum.attribute('canBypassTagCounts') && ( {!!app.forum.attribute('canBypassTagCounts') && (
<div className="TagDiscussionModal-controls"> <div className="TagDiscussionModal-controls">
<ToggleButton className="Button" onclick={() => this.bypassReqs = !this.bypassReqs} isToggled={this.bypassReqs}> <ToggleButton className="Button" onclick={() => (this.bypassReqs = !this.bypassReqs)} isToggled={this.bypassReqs}>
{app.translator.trans('flarum-tags.forum.choose_tags.bypass_requirements')} {app.translator.trans('flarum-tags.forum.choose_tags.bypass_requirements')}
</ToggleButton> </ToggleButton>
</div> </div>
)} )}
</div> </div>,
]; ];
} }
@ -279,7 +279,7 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
select(e: KeyboardEvent) { select(e: KeyboardEvent) {
// Ctrl + Enter submits the selection, just Enter completes the current entry // Ctrl + Enter submits the selection, just Enter completes the current entry
if (e.metaKey || e.ctrlKey || this.selectedTag && this.selected.includes(this.selectedTag)) { if (e.metaKey || e.ctrlKey || (this.selectedTag && this.selected.includes(this.selectedTag))) {
if (this.selected.length) { if (this.selected.length) {
// The DOM submit method doesn't emit a `submit event, so we // The DOM submit method doesn't emit a `submit event, so we
// simulate a manual submission so our `onsubmit` logic is run. // simulate a manual submission so our `onsubmit` logic is run.
@ -297,9 +297,7 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
getCurrentNumericIndex() { getCurrentNumericIndex() {
if (!this.selectedTag) return -1; if (!this.selectedTag) return -1;
return this.selectableItems().index( return this.selectableItems().index(this.getItem(this.selectedTag));
this.getItem(this.selectedTag)
);
} }
getItem(selectedTag: Tag) { getItem(selectedTag: Tag) {
@ -337,7 +335,7 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
} }
if (typeof scrollTop !== 'undefined') { if (typeof scrollTop !== 'undefined') {
$dropdown.stop(true).animate({scrollTop}, 100); $dropdown.stop(true).animate({ scrollTop }, 100);
} }
} }
} }
@ -349,13 +347,12 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
const tags = this.selected; const tags = this.selected;
if (discussion) { if (discussion) {
discussion.save({relationships: {tags}}) discussion.save({ relationships: { tags } }).then(() => {
.then(() => { if (app.current.matches(DiscussionPage)) {
if (app.current.matches(DiscussionPage)) { app.current.get('stream').update();
app.current.get('stream').update(); }
} m.redraw();
m.redraw(); });
});
} }
if (this.attrs.onsubmit) this.attrs.onsubmit(tags); if (this.attrs.onsubmit) this.attrs.onsubmit(tags);

View File

@ -7,11 +7,12 @@ export default class TagHero extends Component {
const color = tag.color(); const color = tag.color();
return ( return (
<header className={'Hero TagHero' + (color ? ' TagHero--colored' : '')} <header className={'Hero TagHero' + (color ? ' TagHero--colored' : '')} style={color ? { '--hero-bg': color } : ''}>
style={color ? { '--hero-bg': color } : ''}>
<div className="container"> <div className="container">
<div className="containerNarrow"> <div className="containerNarrow">
<h2 className="Hero-title">{tag.icon() && tagIcon(tag, {}, { useColor: false })} {tag.name()}</h2> <h2 className="Hero-title">
{tag.icon() && tagIcon(tag, {}, { useColor: false })} {tag.name()}
</h2>
<div className="Hero-subtitle">{tag.description()}</div> <div className="Hero-subtitle">{tag.description()}</div>
</div> </div>
</div> </div>

View File

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

View File

@ -20,14 +20,14 @@ export default class TagsPage extends Page {
const preloaded = app.preloadedApiDocument(); const preloaded = app.preloadedApiDocument();
if (preloaded) { if (preloaded) {
this.tags = sortTags(preloaded.filter(tag => !tag.isChild())); this.tags = sortTags(preloaded.filter((tag) => !tag.isChild()));
return; return;
} }
this.loading = true; this.loading = true;
app.tagList.load(['children', 'lastPostedDiscussion', 'parent']).then(() => { app.tagList.load(['children', 'lastPostedDiscussion', 'parent']).then(() => {
this.tags = sortTags(app.store.all('tags').filter(tag => !tag.isChild())); this.tags = sortTags(app.store.all('tags').filter((tag) => !tag.isChild()));
this.loading = false; this.loading = false;
@ -40,8 +40,8 @@ export default class TagsPage extends Page {
return <LoadingIndicator />; return <LoadingIndicator />;
} }
const pinned = this.tags.filter(tag => tag.position() !== null); const pinned = this.tags.filter((tag) => tag.position() !== null);
const cloud = this.tags.filter(tag => tag.position() === null); const cloud = this.tags.filter((tag) => tag.position() === null);
return ( return (
<div className="TagsPage"> <div className="TagsPage">
@ -53,53 +53,41 @@ export default class TagsPage extends Page {
<div className="TagsPage-content sideNavOffset"> <div className="TagsPage-content sideNavOffset">
<ul className="TagTiles"> <ul className="TagTiles">
{pinned.map(tag => { {pinned.map((tag) => {
const lastPostedDiscussion = tag.lastPostedDiscussion(); const lastPostedDiscussion = tag.lastPostedDiscussion();
const children = sortTags(tag.children() || []); const children = sortTags(tag.children() || []);
return ( return (
<li className={'TagTile ' + (tag.color() ? 'colored' : '')} <li className={'TagTile ' + (tag.color() ? 'colored' : '')} style={{ '--tag-bg': tag.color() }}>
style={{ '--tag-bg': tag.color() }}>
<Link className="TagTile-info" href={app.route.tag(tag)}> <Link className="TagTile-info" href={app.route.tag(tag)}>
{tag.icon() && tagIcon(tag, {}, { useColor: false })} {tag.icon() && tagIcon(tag, {}, { useColor: false })}
<h3 className="TagTile-name">{tag.name()}</h3> <h3 className="TagTile-name">{tag.name()}</h3>
<p className="TagTile-description">{tag.description()}</p> <p className="TagTile-description">{tag.description()}</p>
{children {children ? (
? ( <div className="TagTile-children">
<div className="TagTile-children"> {children.map((child) => [<Link href={app.route.tag(child)}>{child.name()}</Link>, ' '])}
{children.map(child => [ </div>
<Link href={app.route.tag(child)}>
{child.name()}
</Link>,
' '
])}
</div>
) : ''}
</Link>
{lastPostedDiscussion
? (
<Link className="TagTile-lastPostedDiscussion"
href={app.route.discussion(lastPostedDiscussion, lastPostedDiscussion.lastPostNumber())}
>
<span className="TagTile-lastPostedDiscussion-title">{lastPostedDiscussion.title()}</span>
{humanTime(lastPostedDiscussion.lastPostedAt())}
</Link>
) : ( ) : (
<span className="TagTile-lastPostedDiscussion"/> ''
)} )}
</Link>
{lastPostedDiscussion ? (
<Link
className="TagTile-lastPostedDiscussion"
href={app.route.discussion(lastPostedDiscussion, lastPostedDiscussion.lastPostNumber())}
>
<span className="TagTile-lastPostedDiscussion-title">{lastPostedDiscussion.title()}</span>
{humanTime(lastPostedDiscussion.lastPostedAt())}
</Link>
) : (
<span className="TagTile-lastPostedDiscussion" />
)}
</li> </li>
); );
})} })}
</ul> </ul>
{cloud.length ? ( {cloud.length ? <div className="TagCloud">{cloud.map((tag) => [tagLabel(tag, { link: true }), ' '])}</div> : ''}
<div className="TagCloud">
{cloud.map(tag => [
tagLabel(tag, {link: true}),
' ',
])}
</div>
) : ''}
</div> </div>
</div> </div>
</div> </div>

View File

@ -15,11 +15,11 @@ import addTagLabels from './addTagLabels';
import addTagControl from './addTagControl'; import addTagControl from './addTagControl';
import addTagComposer from './addTagComposer'; import addTagComposer from './addTagComposer';
app.initializers.add('flarum-tags', function() { app.initializers.add('flarum-tags', function () {
app.routes.tags = {path: '/tags', component: TagsPage }; app.routes.tags = { path: '/tags', component: TagsPage };
app.routes.tag = {path: '/t/:tags', component: IndexPage }; app.routes.tag = { path: '/t/:tags', component: IndexPage };
app.route.tag = (tag: Tag) => app.route('tag', {tags: tag.slug()}); app.route.tag = (tag: Tag) => app.route('tag', { tags: tag.slug() });
app.postComponents.discussionTagged = DiscussionTaggedPost; app.postComponents.discussionTagged = DiscussionTaggedPost;
@ -37,7 +37,6 @@ app.initializers.add('flarum-tags', function() {
addTagComposer(); addTagComposer();
}); });
// Expose compat API // Expose compat API
import tagsCompat from './compat'; import tagsCompat from './compat';
import { compat } from '@flarum/core/forum'; import { compat } from '@flarum/core/forum';

View File

@ -1,23 +1,19 @@
import app from "flarum/forum/app"; import app from 'flarum/forum/app';
import type Tag from "../../common/models/Tag"; import type Tag from '../../common/models/Tag';
export default class TagListState { export default class TagListState {
loadedIncludes = new Set(); loadedIncludes = new Set();
async load(includes: string[] = []): Promise<Tag[]> { async load(includes: string[] = []): Promise<Tag[]> {
const unloadedIncludes = includes.filter( const unloadedIncludes = includes.filter((include) => !this.loadedIncludes.has(include));
(include) => !this.loadedIncludes.has(include)
);
if (unloadedIncludes.length === 0) { if (unloadedIncludes.length === 0) {
return Promise.resolve(app.store.all<Tag>("tags")); return Promise.resolve(app.store.all<Tag>('tags'));
} }
return app.store return app.store.find<Tag[]>('tags', { include: unloadedIncludes.join(',') }).then((val) => {
.find<Tag[]>("tags", { include: unloadedIncludes.join(",") }) unloadedIncludes.forEach((include) => this.loadedIncludes.add(include));
.then((val) => { return val;
unloadedIncludes.forEach((include) => this.loadedIncludes.add(include)); });
return val;
});
} }
} }

View File

@ -2,9 +2,9 @@ export default function getSelectableTags(discussion) {
let tags = app.store.all('tags'); let tags = app.store.all('tags');
if (discussion) { if (discussion) {
tags = tags.filter(tag => tag.canAddToDiscussion() || discussion.tags().indexOf(tag) !== -1); tags = tags.filter((tag) => tag.canAddToDiscussion() || discussion.tags().indexOf(tag) !== -1);
} else { } else {
tags = tags.filter(tag => tag.canStartDiscussion()); tags = tags.filter((tag) => tag.canStartDiscussion());
} }
return tags; return tags;

View File

@ -2526,6 +2526,11 @@ prettier@^2.4.1, prettier@^2.5.1:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
prettier@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"