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

This commit is contained in:
David Wheatley 2022-06-20 13:00:01 +01:00 committed by GitHub
parent 4923253fbf
commit 824fb2feff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 501 additions and 450 deletions

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

@ -2,6 +2,7 @@
"private": true,
"name": "@flarum/tags",
"version": "0.0.0",
"prettier": "@flarum/prettier-config",
"dependencies": {
"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",
"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-coverage": "typescript-coverage-report"
"check-typings-coverage": "typescript-coverage-report",
"format": "prettier --write src",
"format-check": "prettier --check src"
},
"devDependencies": {
"flarum-webpack-config": "^2.0.0",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"flarum-tsconfig": "^1.0.2",
"flarum-webpack-config": "^2.0.0",
"prettier": "^2.7.1",
"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"
}
}

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

@ -2,26 +2,30 @@ import { extend } from 'flarum/common/extend';
import PermissionGrid from 'flarum/admin/components/PermissionGrid';
import SettingDropdown from 'flarum/admin/components/SettingDropdown';
export default function() {
extend(PermissionGrid.prototype, 'startItems', items => {
items.add('allowTagChange', {
icon: 'fas fa-tag',
label: app.translator.trans('flarum-tags.admin.permissions.allow_edit_tags_label'),
setting: () => {
const minutes = parseInt(app.data.settings.allow_tag_change, 10);
export default function () {
extend(PermissionGrid.prototype, 'startItems', (items) => {
items.add(
'allowTagChange',
{
icon: 'fas fa-tag',
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({
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: [
{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);
return SettingDropdown.component({
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: [
{ 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
);
});
}

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

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

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

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

@ -1,5 +1,6 @@
import sortable from 'sortablejs';
import app from 'flarum/admin/app';
import ExtensionPage from 'flarum/admin/components/ExtensionPage';
import Button from 'flarum/common/components/Button';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
@ -18,16 +19,18 @@ function tagItem(tag) {
{Button.component({
className: 'Button Button--link',
icon: 'fas fa-pencil-alt',
onclick: () => app.modal.show(EditTagModal, { model: tag })
onclick: () => app.modal.show(EditTagModal, { model: tag }),
})}
</div>
{!tag.isChild() && tag.position() !== null ? (
<ol className="TagListItem-children TagList">
{sortTags(app.store.all('tags'))
.filter(child => child.parent() === tag)
.filter((child) => child.parent() === tag)
.map(tagItem)}
</ol>
) : ''}
) : (
''
)}
</li>
);
}
@ -62,81 +65,78 @@ export default class TagsPage extends ExtensionPage {
const minSecondaryTags = this.setting('flarum-tags.min_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 (
<div className="TagsContent">
<div className="TagsContent-list">
<div className="container" key={this.forcedRefreshKey} oncreate={this.onListOnCreate.bind(this)}><div className="SettingsGroups">
<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')
)}
</div>
<div className="container" key={this.forcedRefreshKey} oncreate={this.onListOnCreate.bind(this)}>
<div className="SettingsGroups">
<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')
)}
</div>
<div className="TagGroup TagGroup--secondary">
<label>{app.translator.trans('flarum-tags.admin.tags.secondary_heading')}</label>
<ul className="TagList">
{tags
.filter(tag => tag.position() === null)
.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')
)}
</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 className="TagGroup TagGroup--secondary">
<label>{app.translator.trans('flarum-tags.admin.tags.secondary_heading')}</label>
<ul className="TagList">
{tags
.filter((tag) => tag.position() === null)
.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')
)}
</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 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 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 className="Form-group">{this.submitButton()}</div>
</div>
</div>
<div className="TagsContent-footer">
<p>{app.translator.trans('flarum-tags.admin.tags.about_tags_text')}</p>
</div>
@ -147,19 +147,21 @@ export default class TagsPage extends ExtensionPage {
}
onListOnCreate(vnode) {
this.$('.TagList').get().map(e => {
sortable.create(e, {
group: 'tags',
delay: 50,
delayOnTouchOnly: true,
touchStartThreshold: 5,
animation: 150,
swapThreshold: 0.65,
dragClass: 'sortable-dragging',
ghostClass: 'sortable-placeholder',
onSort: (e) => this.onSortUpdate(e)
})
});
this.$('.TagList')
.get()
.map((e) => {
sortable.create(e, {
group: 'tags',
delay: 50,
delayOnTouchOnly: true,
touchStartThreshold: 5,
animation: 150,
swapThreshold: 0.65,
dragClass: 'sortable-dragging',
ghostClass: 'sortable-placeholder',
onSort: (e) => this.onSortUpdate(e),
});
});
}
setMinTags(minTags, maxTags, value) {
@ -175,9 +177,9 @@ export default class TagsPage extends ExtensionPage {
app.store.getById('tags', e.item.getAttribute('data-id')).pushData({
attributes: {
position: null,
isChild: false
isChild: false,
},
relationships: { parent: null }
relationships: { parent: null },
});
}
@ -187,12 +189,15 @@ export default class TagsPage extends ExtensionPage {
.map(function () {
return {
id: $(this).data('id'),
children: $(this).find('li')
children: $(this)
.find('li')
.map(function () {
return $(this).data('id');
}).get()
})
.get(),
};
}).get();
})
.get();
// 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
@ -202,18 +207,18 @@ export default class TagsPage extends ExtensionPage {
parent.pushData({
attributes: {
position: i,
isChild: false
isChild: false,
},
relationships: { parent: null }
relationships: { parent: null },
});
tag.children.forEach((child, j) => {
app.store.getById('tags', child).pushData({
attributes: {
position: j,
isChild: true
isChild: true,
},
relationships: { parent }
relationships: { parent },
});
});
});
@ -221,7 +226,7 @@ export default class TagsPage extends ExtensionPage {
app.request({
url: app.forum.attribute('apiUrl') + '/tags/order',
method: 'POST',
body: { order }
body: { order },
});
this.forcedRefreshKey++;

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

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

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

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

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

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

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

@ -4,13 +4,16 @@ import Button from 'flarum/common/components/Button';
import TagDiscussionModal from './components/TagDiscussionModal';
export default function() {
export default function () {
// 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()) {
items.add('tags', <Button icon="fas fa-tag" onclick={() => app.modal.show(TagDiscussionModal, { discussion })}>
{app.translator.trans('flarum-tags.forum.discussion_controls.edit_tags_button')}
</Button>);
items.add(
'tags',
<Button icon="fas fa-tag" onclick={() => app.modal.show(TagDiscussionModal, { discussion })}>
{app.translator.trans('flarum-tags.forum.discussion_controls.edit_tags_button')}
</Button>
);
}
});
}

@ -10,10 +10,10 @@ import TagHero from './components/TagHero';
import Tag from '../common/models/Tag';
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() {
IndexPage.prototype.currentTag = function() {
export default function () {
IndexPage.prototype.currentTag = function () {
if (this.currentActiveTag) {
return this.currentActiveTag;
}
@ -25,7 +25,7 @@ export default function() {
tag = findTag(slug);
}
if (slug && !tag || (tag && !tag.isChild() && !tag.children())) {
if ((slug && !tag) || (tag && !tag.isChild() && !tag.children())) {
if (this.currentTagLoading) {
return;
}
@ -36,13 +36,16 @@ export default function() {
// a child tag page, then either:
// - 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.
app.store.find('tags', slug, { include: 'children,children.parent,parent,state'}).then(() => {
this.currentActiveTag = findTag(slug);
app.store
.find('tags', slug, { include: 'children,children.parent,parent,state' })
.then(() => {
this.currentActiveTag = findTag(slug);
m.redraw();
}).finally(() => {
this.currentTagLoading = false;
});
m.redraw();
})
.finally(() => {
this.currentTagLoading = false;
});
}
if (tag) {
@ -54,7 +57,7 @@ export default function() {
};
// 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();
if (tag) return <TagHero model={tag} />;
@ -62,13 +65,13 @@ export default function() {
return original();
});
extend(IndexPage.prototype, 'view', function(vdom: Mithril.Vnode<ComponentAttrs, {}>) {
extend(IndexPage.prototype, 'view', function (vdom: Mithril.Vnode<ComponentAttrs, {}>) {
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();
if (tag) {
@ -78,7 +81,7 @@ export default function() {
// 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.
extend(IndexPage.prototype, 'sidebarItems', function(items) {
extend(IndexPage.prototype, 'sidebarItems', function (items) {
const tag = this.currentTag();
if (tag) {
@ -92,18 +95,20 @@ export default function() {
}
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
// 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');
});
// 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') {
params.include = [params.include];
} else {
@ -118,7 +123,7 @@ export default function() {
if (q) {
filter.q = `${q} tag:${this.params.tags}`;
}
params.filter = filter
params.filter = filter;
}
});
}

@ -5,9 +5,9 @@ import DiscussionHero from 'flarum/forum/components/DiscussionHero';
import tagsLabel from '../common/helpers/tagsLabel';
import sortTags from '../common/utils/sortTags';
export default function() {
export default function () {
// 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();
if (tags && tags.length) {
@ -16,7 +16,7 @@ export default function() {
});
// 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());
if (tags && tags.length) {
@ -30,11 +30,11 @@ export default function() {
// Add a list of a discussion's tags to the discussion hero, displayed
// 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();
if (tags && tags.length) {
items.add('tags', tagsLabel(tags, {link: true}), 5);
items.add('tags', tagsLabel(tags, { link: true }), 5);
}
});
}

@ -7,14 +7,17 @@ import TagLinkButton from './components/TagLinkButton';
import TagsPage from './components/TagsPage';
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,
// to the index page's sidebar.
extend(IndexPage.prototype, 'navItems', function (items) {
items.add('tags', <LinkButton icon="fas fa-th-large" href={app.route('tags')}>
{app.translator.trans('flarum-tags.forum.index.tags_link')}
</LinkButton>
, -10);
items.add(
'tags',
<LinkButton icon="fas fa-th-large" href={app.route('tags')}>
{app.translator.trans('flarum-tags.forum.index.tags_link')}
</LinkButton>,
-10
);
if (app.current.matches(TagsPage)) return;
@ -24,7 +27,7 @@ export default function() {
const tags = app.store.all('tags');
const currentTag = this.currentTag();
const addTag = tag => {
const addTag = (tag) => {
let active = currentTag === tag;
if (!active && currentTag) {
@ -36,23 +39,21 @@ 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.component({ model: tag, params, active }, tag?.name()), -14);
};
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);
const more = tags
.filter(tag => tag.position() === null)
.sort((a, b) => b.discussionCount() - a.discussionCount());
const more = tags.filter((tag) => tag.position() === null).sort((a, b) => b.discussionCount() - a.discussionCount());
more.splice(0, 3).forEach(addTag);
if (more.length) {
items.add('moreTags', <LinkButton href={app.route('tags')}>
{app.translator.trans('flarum-tags.forum.index.more_link')}
</LinkButton>, -16)
items.add('moreTags', <LinkButton href={app.route('tags')}>{app.translator.trans('flarum-tags.forum.index.more_link')}</LinkButton>, -16);
}
});
}

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

@ -58,11 +58,11 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
const tags = sortTags(getSelectableTags(this.attrs.discussion));
this.tags = tags;
const discussionTags = this.attrs.discussion?.tags()
const discussionTags = this.attrs.discussion?.tags();
if (this.attrs.selectedTags) {
this.attrs.selectedTags.map(this.addTag.bind(this));
} else if (discussionTags) {
discussionTags.forEach(tag => tag && this.addTag(tag));
discussionTags.forEach((tag) => tag && this.addTag(tag));
}
this.selectedTag = tags[0];
@ -72,11 +72,11 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
}
primaryCount() {
return this.selected.filter(tag => tag.isPrimary()).length;
return this.selected.filter((tag) => tag.isPrimary()).length;
}
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
// we just removed as their parent. We'll need to remove them too.
this.selected
.filter(selected => selected.parent() === tag)
.forEach(this.removeTag.bind(this));
this.selected.filter((selected) => selected.parent() === tag).forEach(this.removeTag.bind(this));
}
}
@ -119,7 +117,7 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
title() {
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');
}
@ -130,10 +128,10 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
if (primaryCount < this.minPrimary) {
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) {
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 '';
@ -151,7 +149,7 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
// 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.
tags = tags.filter(tag => {
tags = tags.filter((tag) => {
const parent = tag.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
// we'll filter out all other tags of that type.
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) {
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
// whose name matches what they've entered.
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];
@ -180,30 +178,38 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
<div className="Modal-body">
<div className="TagDiscussionModal-form">
<div className="TagDiscussionModal-form-input">
<div className={'TagsInput FormControl ' + (this.focused ? 'focus' : '')}
onclick={() => this.$('.TagsInput input').focus()}
>
<div className={'TagsInput FormControl ' + (this.focused ? 'focus' : '')} onclick={() => this.$('.TagsInput input').focus()}>
<span className="TagsInput-selected">
{this.selected.map(tag =>
<span className="TagsInput-tag" onclick={() => {
this.removeTag(tag);
this.onready();
}}>
{this.selected.map((tag) => (
<span
className="TagsInput-tag"
onclick={() => {
this.removeTag(tag);
this.onready();
}}
>
{tagLabel(tag)}
</span>
)}
))}
</span>
<input className="FormControl"
<input
className="FormControl"
placeholder={extractText(this.getInstruction(primaryCount, secondaryCount))}
bidi={this.filter}
style={{ width: inputWidth + 'ch' }}
onkeydown={this.navigator.navigate.bind(this.navigator)}
onfocus={() => this.focused = true}
onblur={() => this.focused = false}/>
onfocus={() => (this.focused = true)}
onblur={() => (this.focused = false)}
/>
</div>
</div>
<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')}
</Button>
</div>
@ -213,41 +219,35 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
<div className="Modal-footer">
<ul className="TagDiscussionModal-list SelectTagList">
{tags
.filter(tag => filter || !tag.parent() || this.selected.includes(tag.parent() as Tag))
.map(tag => (
<li data-index={tag.id()}
.filter((tag) => filter || !tag.parent() || this.selected.includes(tag.parent() as Tag))
.map((tag) => (
<li
data-index={tag.id()}
className={classList({
pinned: tag.position() !== null,
child: !!tag.parent(),
colored: !!tag.color(),
selected: this.selected.includes(tag),
active: this.selectedTag === tag
active: this.selectedTag === tag,
})}
style={{color: tag.color()}}
onmouseover={() => this.selectedTag = tag}
style={{ color: tag.color() }}
onmouseover={() => (this.selectedTag = tag)}
onclick={this.toggleTag.bind(this, tag)}
>
{tagIcon(tag)}
<span className="SelectTagListItem-name">
{highlight(tag.name(), filter)}
</span>
{tag.description()
? (
<span className="SelectTagListItem-description">
{tag.description()}
</span>
) : ''}
<span className="SelectTagListItem-name">{highlight(tag.name(), filter)}</span>
{tag.description() ? <span className="SelectTagListItem-description">{tag.description()}</span> : ''}
</li>
))}
</ul>
{!!app.forum.attribute('canBypassTagCounts') && (
<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')}
</ToggleButton>
</div>
)}
</div>
</div>,
];
}
@ -279,7 +279,7 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
select(e: KeyboardEvent) {
// 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) {
// The DOM submit method doesn't emit a `submit event, so we
// simulate a manual submission so our `onsubmit` logic is run.
@ -297,9 +297,7 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
getCurrentNumericIndex() {
if (!this.selectedTag) return -1;
return this.selectableItems().index(
this.getItem(this.selectedTag)
);
return this.selectableItems().index(this.getItem(this.selectedTag));
}
getItem(selectedTag: Tag) {
@ -337,7 +335,7 @@ export default class TagDiscussionModal extends Modal<TagDiscussionModalAttrs> {
}
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;
if (discussion) {
discussion.save({relationships: {tags}})
.then(() => {
if (app.current.matches(DiscussionPage)) {
app.current.get('stream').update();
}
m.redraw();
});
discussion.save({ relationships: { tags } }).then(() => {
if (app.current.matches(DiscussionPage)) {
app.current.get('stream').update();
}
m.redraw();
});
}
if (this.attrs.onsubmit) this.attrs.onsubmit(tags);

@ -7,11 +7,12 @@ export default class TagHero extends Component {
const color = tag.color();
return (
<header className={'Hero TagHero' + (color ? ' TagHero--colored' : '')}
style={color ? { '--hero-bg': color } : ''}>
<header className={'Hero TagHero' + (color ? ' TagHero--colored' : '')} style={color ? { '--hero-bg': color } : ''}>
<div className="container">
<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>
</div>

@ -8,21 +8,12 @@ export default class TagLinkButton extends LinkButton {
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', this.attrs.className, tag.isChild() && 'child']);
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() } : ''} title={description || ''}>
{tagIcon(tag, { className: 'Button-icon' })}
<span className="Button-label">
{tag ? tag.name() : app.translator.trans('flarum-tags.forum.index.untagged_link')}
</span>
<span className="Button-label">{tag ? tag.name() : app.translator.trans('flarum-tags.forum.index.untagged_link')}</span>
</Link>
);
}

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

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

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

@ -2,9 +2,9 @@ export default function getSelectableTags(discussion) {
let tags = app.store.all('tags');
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 {
tags = tags.filter(tag => tag.canStartDiscussion());
tags = tags.filter((tag) => tag.canStartDiscussion());
}
return tags;

@ -2526,6 +2526,11 @@ prettier@^2.4.1, prettier@^2.5.1:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
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:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"