diff --git a/extensions/tags/js/admin/Gulpfile.js b/extensions/tags/js/admin/Gulpfile.js
index 0a9f1ed7f..53a9f303b 100644
--- a/extensions/tags/js/admin/Gulpfile.js
+++ b/extensions/tags/js/admin/Gulpfile.js
@@ -1,6 +1,9 @@
var gulp = require('flarum-gulp');
gulp({
+ files: [
+ 'bower_components/html.sortable/dist/html.sortable.js'
+ ],
modules: {
'tags': [
'../lib/**/*.js',
diff --git a/extensions/tags/js/admin/src/addTagsPane.js b/extensions/tags/js/admin/src/addTagsPane.js
new file mode 100644
index 000000000..f3674cc81
--- /dev/null
+++ b/extensions/tags/js/admin/src/addTagsPane.js
@@ -0,0 +1,18 @@
+import { extend } from 'flarum/extend';
+import AdminNav from 'flarum/components/AdminNav';
+import AdminLinkButton from 'flarum/components/AdminLinkButton';
+
+import TagsPage from 'tags/components/TagsPage';
+
+export default function() {
+ app.routes.tags = {path: '/tags', component: TagsPage.component()};
+
+ extend(AdminNav.prototype, 'items', items => {
+ items.add('tags', AdminLinkButton.component({
+ href: app.route('tags'),
+ icon: 'tags',
+ children: 'Tags',
+ description: 'Manage the list of tags available to organise discussions with.'
+ }));
+ });
+}
diff --git a/extensions/tags/js/admin/src/addTagsPermissionScope.js b/extensions/tags/js/admin/src/addTagsPermissionScope.js
new file mode 100644
index 000000000..b79dfd506
--- /dev/null
+++ b/extensions/tags/js/admin/src/addTagsPermissionScope.js
@@ -0,0 +1,60 @@
+import { extend } from 'flarum/extend';
+import PermissionGrid from 'flarum/components/PermissionGrid';
+import PermissionDropdown from 'flarum/components/PermissionDropdown';
+import Dropdown from 'flarum/components/Dropdown';
+import Button from 'flarum/components/Button';
+
+import tagLabel from 'tags/helpers/tagLabel';
+import tagIcon from 'tags/helpers/tagIcon';
+import sortTags from 'tags/utils/sortTags';
+
+export default function() {
+ 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 (item.permission) {
+ let permission;
+
+ if (item.permission === 'forum.view') {
+ permission = 'view';
+ } else if (item.permission === 'forum.startDiscussion') {
+ permission = 'startDiscussion';
+ } else if (item.permission.indexOf('discussion.') === 0) {
+ permission = item.permission;
+ }
+
+ if (permission) {
+ const props = Object.assign({}, item);
+ props.permission = 'tag' + tag.id() + '.' + permission;
+
+ return PermissionDropdown.component(props);
+ }
+ }
+
+ return '';
+ }
+ }));
+ });
+
+ extend(PermissionGrid.prototype, 'scopeControlItems', items => {
+ const tags = sortTags(app.store.all('tags').filter(tag => !tag.isRestricted()));
+
+ if (tags.length) {
+ items.add('tag', Dropdown.component({
+ buttonClassName: 'Button Button--text',
+ label: 'Restrict by Tag',
+ icon: 'plus',
+ caretIcon: null,
+ children: tags.map(tag => Button.component({
+ icon: true,
+ children: [tagIcon(tag, {className: 'Button-icon'}), ' ', tag.name()],
+ onclick: () => tag.save({isRestricted: true})
+ }))
+ }));
+ }
+ });
+}
diff --git a/extensions/tags/js/admin/src/components/EditTagModal.js b/extensions/tags/js/admin/src/components/EditTagModal.js
new file mode 100644
index 000000000..97d6fa3ad
--- /dev/null
+++ b/extensions/tags/js/admin/src/components/EditTagModal.js
@@ -0,0 +1,106 @@
+import Modal from 'flarum/components/Modal';
+import Button from 'flarum/components/Button';
+import { slug } from 'flarum/utils/string';
+
+import tagLabel from 'tags/helpers/tagLabel';
+
+/**
+ * The `EditTagModal` component shows a modal dialog which allows the user
+ * to create or edit a tag.
+ */
+export default class EditTagModal extends Modal {
+ constructor(...args) {
+ super(...args);
+
+ this.tag = this.props.tag || app.store.createRecord('tags');
+
+ this.name = m.prop(this.tag.name() || '');
+ this.slug = m.prop(this.tag.slug() || '');
+ this.description = m.prop(this.tag.description() || '');
+ this.color = m.prop(this.tag.color() || '');
+ }
+
+ className() {
+ return 'EditTagModal Modal--small';
+ }
+
+ title() {
+ return this.name()
+ ? tagLabel({
+ name: this.name,
+ color: this.color
+ })
+ : 'Create Tag';
+ }
+
+ content() {
+ return (
+
+
+
+ Name
+ {
+ this.name(e.target.value);
+ this.slug(slug(e.target.value));
+ }}/>
+
+
+
+ Slug
+
+
+
+
+ Description
+
+
+
+
+ Color
+
+
+
+
+ {Button.component({
+ type: 'submit',
+ className: 'Button Button--primary EditTagModal-save',
+ loading: this._loading,
+ children: 'Save Changes'
+ })}
+ {this.tag.exists ? (
+
+ Delete Tag
+
+ ) : ''}
+
+
+
+ );
+ }
+
+ onsubmit(e) {
+ e.preventDefault();
+
+ this._loading = true;
+
+ this.tag.save({
+ name: this.name(),
+ slug: this.slug(),
+ description: this.description(),
+ color: this.color()
+ }).then(
+ () => this.hide(),
+ () => {
+ this._loading = false;
+ m.redraw();
+ }
+ );
+ }
+
+ delete() {
+ if (confirm('Are you sure you want to delete this tag? The tag\'s discussions will NOT be deleted.')) {
+ this.tag.delete().then(() => m.redraw());
+ this.hide();
+ }
+ }
+}
diff --git a/extensions/tags/js/admin/src/components/TagSettingsModal.js b/extensions/tags/js/admin/src/components/TagSettingsModal.js
new file mode 100644
index 000000000..961c73f3f
--- /dev/null
+++ b/extensions/tags/js/admin/src/components/TagSettingsModal.js
@@ -0,0 +1,107 @@
+import Modal from 'flarum/components/Modal';
+import Button from 'flarum/components/Button';
+import saveConfig from 'flarum/utils/saveConfig';
+
+export default class TagSettingsModal extends Modal {
+ constructor(...args) {
+ super(...args);
+
+ this.minPrimaryTags = m.prop(app.config['tags.min_primary_tags'] || 0);
+ this.maxPrimaryTags = m.prop(app.config['tags.max_primary_tags'] || 0);
+ this.minSecondaryTags = m.prop(app.config['tags.min_secondary_tags'] || 0);
+ this.maxSecondaryTags = m.prop(app.config['tags.max_secondary_tags'] || 0);
+ }
+
+ setMinTags(minTags, maxTags, value) {
+ minTags(value);
+ maxTags(Math.max(value, maxTags()));
+ }
+
+ className() {
+ return 'TagSettingsModal Modal--small';
+ }
+
+ title() {
+ return 'Tag Settings';
+ }
+
+ content() {
+ return (
+
+
+
+
Required Number of Primary Tags
+
+ Enter the minimum and maximum number of primary tags that may be applied to a discussion.
+
+
+
+ {' to '}
+
+
+
+
+
+
Required Number of Secondary Tags
+
+ Enter the minimum and maximum number of secondary tags that may be applied to a discussion.
+
+
+
+ {' to '}
+
+
+
+
+
+ {Button.component({
+ type: 'submit',
+ className: 'Button Button--primary TagSettingsModal-save',
+ loading: this.loading,
+ children: 'Save Changes'
+ })}
+
+
+
+ );
+ }
+
+ onsubmit(e) {
+ e.preventDefault();
+
+ this.loading = true;
+
+ saveConfig({
+ 'tags.min_primary_tags': this.minPrimaryTags(),
+ 'tags.max_primary_tags': this.maxPrimaryTags(),
+ 'tags.min_secondary_tags': this.minSecondaryTags(),
+ 'tags.max_secondary_tags': this.maxSecondaryTags()
+ }).then(
+ () => this.hide(),
+ () => {
+ this.loading = false;
+ m.redraw();
+ }
+ );
+ }
+}
diff --git a/extensions/tags/js/admin/src/components/TagsPage.js b/extensions/tags/js/admin/src/components/TagsPage.js
new file mode 100644
index 000000000..11624acf9
--- /dev/null
+++ b/extensions/tags/js/admin/src/components/TagsPage.js
@@ -0,0 +1,149 @@
+import Component from 'flarum/Component';
+import Button from 'flarum/components/Button';
+
+import EditTagModal from 'tags/components/EditTagModal';
+import TagSettingsModal from 'tags/components/TagSettingsModal';
+import tagIcon from 'tags/helpers/tagIcon';
+import sortTags from 'tags/utils/sortTags';
+
+function tagItem(tag) {
+ return (
+
+
+ {tagIcon(tag)}
+ {tag.name()}
+ {Button.component({
+ className: 'Button Button--link',
+ icon: 'pencil',
+ onclick: () => app.modal.show(new EditTagModal({tag}))
+ })}
+
+ {!tag.isChild() && tag.position() !== null ? (
+
+ {sortTags(app.store.all('tags'))
+ .filter(child => child.parent() === tag)
+ .map(tagItem)}
+
+ ) : ''}
+
+ );
+}
+
+export default class TagsPage extends Component {
+ view() {
+ return (
+
+
+
+
+ Tags are used to categorize discussions. Primary tags are like traditional forum categories: They can be arranged in a two-level hierarchy. Secondary tags do not have hierarchy or order, and are useful for micro-categorization.
+
+ {Button.component({
+ className: 'Button Button--primary',
+ icon: 'plus',
+ children: 'Create Tag',
+ onclick: () => app.modal.show(new EditTagModal())
+ })}
+ {Button.component({
+ className: 'Button',
+ children: 'Settings',
+ onclick: () => app.modal.show(new TagSettingsModal())
+ })}
+
+
+
+
+
+
Primary Tags
+
+ {sortTags(app.store.all('tags'))
+ .filter(tag => tag.position() !== null && !tag.isChild())
+ .map(tagItem)}
+
+
+
+
+
Secondary Tags
+
+ {app.store.all('tags')
+ .filter(tag => tag.position() === null)
+ .sort((a, b) => a.name().localeCompare(b.name()))
+ .map(tagItem)}
+
+
+
+
+
+ );
+ }
+
+ config(isInitialized) {
+ if (isInitialized) return;
+
+ this.$('ol, ul')
+ .sortable({connectWith: 'primary'})
+ .on('sortupdate', (e, ui) => {
+ // If we've moved a tag from 'primary' to 'secondary', then we'll update
+ // its attributes in our local store so that when we redraw the change
+ // will be made.
+ if (ui.startparent.is('ol') && ui.endparent.is('ul')) {
+ app.store.getById('tags', ui.item.data('id')).pushData({
+ attributes: {
+ position: null,
+ isChild: false
+ },
+ relationships: {parent: null}
+ });
+ }
+
+ // Construct an array of primary tag IDs and their children, in the same
+ // order that they have been arranged in.
+ const order = this.$('.TagList--primary > li')
+ .map(function() {
+ return {
+ id: $(this).data('id'),
+ children: $(this).find('li')
+ .map(function() {
+ return $(this).data('id');
+ }).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
+ // store to reflect this order.
+ order.forEach((tag, i) => {
+ const parent = app.store.getById('tags', tag.id);
+ parent.pushData({
+ attributes: {
+ position: i,
+ isChild: false
+ },
+ relationships: {parent: null}
+ });
+
+ tag.children.forEach((child, j) => {
+ app.store.getById('tags', child).pushData({
+ attributes: {
+ position: j,
+ isChild: true
+ },
+ relationships: {parent}
+ });
+ });
+ });
+
+ app.request({
+ url: app.forum.attribute('apiUrl') + '/tags/order',
+ method: 'POST',
+ data: {order}
+ });
+
+ // A diff redraw won't work here, because sortable has mucked around
+ // with the DOM which will confuse Mithril's diffing algorithm. Instead
+ // we force a full reconstruction of the DOM.
+ m.redraw.strategy('all');
+ m.redraw();
+ });
+ }
+}
diff --git a/extensions/tags/js/admin/src/main.js b/extensions/tags/js/admin/src/main.js
index a7e6dc064..c7f47901d 100644
--- a/extensions/tags/js/admin/src/main.js
+++ b/extensions/tags/js/admin/src/main.js
@@ -1,63 +1,10 @@
-import { extend } from 'flarum/extend';
-import PermissionGrid from 'flarum/components/PermissionGrid';
-import PermissionDropdown from 'flarum/components/PermissionDropdown';
-import Dropdown from 'flarum/components/Dropdown';
-import Button from 'flarum/components/Button';
-
import Tag from 'tags/models/Tag';
-import tagLabel from 'tags/helpers/tagLabel';
-import tagIcon from 'tags/helpers/tagIcon';
-import sortTags from 'tags/utils/sortTags';
+import addTagsPermissionScope from 'tags/addTagsPermissionScope';
+import addTagsPane from 'tags/addTagsPane';
app.initializers.add('tags', app => {
app.store.models.tags = Tag;
- 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 (item.permission) {
- let permission;
-
- if (item.permission === 'forum.view') {
- permission = 'view';
- } else if (item.permission === 'forum.startDiscussion') {
- permission = 'startDiscussion';
- } else if (item.permission.indexOf('discussion.') === 0) {
- permission = item.permission;
- }
-
- if (permission) {
- const props = Object.assign({}, item);
- props.permission = 'tag' + tag.id() + '.' + permission;
-
- return PermissionDropdown.component(props);
- }
- }
-
- return '';
- }
- }));
- });
-
- extend(PermissionGrid.prototype, 'scopeControlItems', items => {
- const tags = sortTags(app.store.all('tags').filter(tag => !tag.isRestricted()));
-
- if (tags.length) {
- items.add('tag', Dropdown.component({
- buttonClassName: 'Button Button--text',
- label: 'Restrict by Tag',
- icon: 'plus',
- caretIcon: null,
- children: tags.map(tag => Button.component({
- icon: true,
- children: [tagIcon(tag, {className: 'Button-icon'}), ' ', tag.name()],
- onclick: () => tag.save({isRestricted: true})
- }))
- }));
- }
- });
+ addTagsPermissionScope();
+ addTagsPane();
});
diff --git a/extensions/tags/js/forum/src/components/TagsPage.js b/extensions/tags/js/forum/src/components/TagsPage.js
index cc5efbaf5..02e1cbd3e 100644
--- a/extensions/tags/js/forum/src/components/TagsPage.js
+++ b/extensions/tags/js/forum/src/components/TagsPage.js
@@ -46,6 +46,7 @@ export default class TagsPage extends Component {
{children.map(child =>
e.stopPropagation());
m.route.apply(this, arguments);
}}>
diff --git a/extensions/tags/less/admin/EditTagModal.less b/extensions/tags/less/admin/EditTagModal.less
new file mode 100644
index 000000000..49150870a
--- /dev/null
+++ b/extensions/tags/less/admin/EditTagModal.less
@@ -0,0 +1,8 @@
+.EditTagModal {
+ .Form-group:not(:last-child) {
+ margin-bottom: 30px;
+ }
+}
+.EditTagModal-delete {
+ float: right;
+}
diff --git a/extensions/tags/less/admin/TagSettingsModal.less b/extensions/tags/less/admin/TagSettingsModal.less
new file mode 100644
index 000000000..6522257c6
--- /dev/null
+++ b/extensions/tags/less/admin/TagSettingsModal.less
@@ -0,0 +1,16 @@
+.TagSettingsModal {
+ .Form-group:not(:last-child) {
+ margin-bottom: 30px;
+ }
+}
+.TagSettingsModal-rangeInput {
+ input {
+ width: 80px;
+ display: inline;
+ margin: 0 5px;
+
+ &:first-child {
+ margin-left: 0;
+ }
+ }
+}
diff --git a/extensions/tags/less/admin/TagsPage.less b/extensions/tags/less/admin/TagsPage.less
new file mode 100644
index 000000000..3ef3cc3a8
--- /dev/null
+++ b/extensions/tags/less/admin/TagsPage.less
@@ -0,0 +1,90 @@
+.TagsPage-header {
+ background: @control-bg;
+ color: @control-color;
+ padding: 20px 0;
+
+ .container {
+ max-width: 600px;
+ }
+ p {
+ margin-bottom: 20px;
+ }
+ .Button {
+ margin-right: 10px;
+ }
+}
+.TagsPage-list {
+ padding: 20px 0;
+
+ .container {
+ max-width: 600px;
+ }
+}
+.TagList, .TagList ol {
+ list-style: none;
+ padding: 10px 0;
+ margin: 0;
+ color: @muted-color;
+ font-size: 13px;
+
+ > li {
+ cursor: move;
+ }
+
+ .TagIcon {
+ margin-right: 10px;
+ }
+}
+.TagListItem-info {
+ padding: 5px 10px;
+ border-radius: @border-radius;
+
+ &:hover {
+ background: @control-bg;
+ }
+
+ .Button {
+ float: right;
+ visibility: hidden;
+ margin: -8px -10px -8px 10px;
+ }
+}
+li:not(.sortable-dragging) > .TagListItem-info:hover > .Button {
+ visibility: visible;
+}
+.TagList--primary {
+ font-size: 16px;
+
+ > .sortable-placeholder {
+ height: 34px;
+ margin-bottom: 10px;
+ }
+}
+.TagList ol {
+ margin-left: 27px;
+ min-height: 10px;
+ padding: 0;
+
+ & > :last-child {
+ margin-bottom: 10px;
+ }
+}
+.sortable-placeholder {
+ border: 2px dashed @control-bg;
+ border-radius: @border-radius;
+ height: 29px;
+}
+
+.TagGroup {
+ padding-left: 150px;
+
+ &:first-child {
+ border-bottom: 2px solid @control-bg;
+ }
+ > label {
+ margin-left: -150px;
+ float: left;
+ font-weight: bold;
+ margin-top: 14px;
+ }
+}
diff --git a/extensions/tags/less/admin/extension.less b/extensions/tags/less/admin/extension.less
index 70f2babd2..2252a5dcf 100644
--- a/extensions/tags/less/admin/extension.less
+++ b/extensions/tags/less/admin/extension.less
@@ -1,2 +1,6 @@
@import "../lib/TagLabel.less";
@import "../lib/TagIcon.less";
+
+@import "TagsPage.less";
+@import "EditTagModal.less";
+@import "TagSettingsModal.less";
diff --git a/extensions/tags/src/Api/CreateAction.php b/extensions/tags/src/Api/CreateAction.php
new file mode 100644
index 000000000..f4044fba9
--- /dev/null
+++ b/extensions/tags/src/Api/CreateAction.php
@@ -0,0 +1,40 @@
+bus = $bus;
+ }
+
+ /**
+ * Create a tag according to input from the API request.
+ *
+ * @param JsonApiRequest $request
+ * @return \Flarum\Core\Tags\Tag
+ */
+ protected function create(JsonApiRequest $request)
+ {
+ return $this->bus->dispatch(
+ new CreateTag($request->actor, $request->get('data'))
+ );
+ }
+}
diff --git a/extensions/tags/src/Api/DeleteAction.php b/extensions/tags/src/Api/DeleteAction.php
new file mode 100644
index 000000000..b086f72f7
--- /dev/null
+++ b/extensions/tags/src/Api/DeleteAction.php
@@ -0,0 +1,34 @@
+bus = $bus;
+ }
+
+ /**
+ * Delete a tag.
+ *
+ * @param Request $request
+ */
+ protected function delete(Request $request)
+ {
+ $this->bus->dispatch(
+ new DeleteTag($request->get('id'), $request->actor)
+ );
+ }
+}
diff --git a/extensions/tags/src/Api/OrderAction.php b/extensions/tags/src/Api/OrderAction.php
new file mode 100644
index 000000000..0404fef16
--- /dev/null
+++ b/extensions/tags/src/Api/OrderAction.php
@@ -0,0 +1,41 @@
+actor->isAdmin()) {
+ throw new PermissionDeniedException;
+ }
+
+ $order = $request->get('order');
+
+ Tag::query()->update([
+ 'position' => null,
+ 'parent_id' => null
+ ]);
+
+ foreach ($order as $i => $parent) {
+ $parentId = array_get($parent, 'id');
+
+ Tag::where('id', $parentId)->update(['position' => $i]);
+
+ if (isset($parent['children']) && is_array($parent['children'])) {
+ foreach ($parent['children'] as $j => $childId) {
+ Tag::where('id', $childId)->update([
+ 'position' => $j,
+ 'parent_id' => $parentId
+ ]);
+ }
+ }
+ }
+
+ return new EmptyResponse(204);
+ }
+}
diff --git a/extensions/tags/src/Commands/CreateTag.php b/extensions/tags/src/Commands/CreateTag.php
new file mode 100644
index 000000000..e986e5966
--- /dev/null
+++ b/extensions/tags/src/Commands/CreateTag.php
@@ -0,0 +1,30 @@
+actor = $actor;
+ $this->data = $data;
+ }
+}
diff --git a/extensions/tags/src/Commands/CreateTagHandler.php b/extensions/tags/src/Commands/CreateTagHandler.php
new file mode 100644
index 000000000..869acc20a
--- /dev/null
+++ b/extensions/tags/src/Commands/CreateTagHandler.php
@@ -0,0 +1,44 @@
+forum = $forum;
+ }
+
+ /**
+ * @param CreateTag $command
+ * @return Tag
+ */
+ public function handle(CreateTag $command)
+ {
+ $actor = $command->actor;
+ $data = $command->data;
+
+ $this->forum->assertCan($actor, 'createTag');
+
+ $tag = Tag::build(
+ array_get($data, 'attributes.name'),
+ array_get($data, 'attributes.slug'),
+ array_get($data, 'attributes.description'),
+ array_get($data, 'attributes.color')
+ );
+
+ $tag->save();
+
+ return $tag;
+ }
+}
diff --git a/extensions/tags/src/Commands/DeleteTag.php b/extensions/tags/src/Commands/DeleteTag.php
new file mode 100644
index 000000000..fe73bf392
--- /dev/null
+++ b/extensions/tags/src/Commands/DeleteTag.php
@@ -0,0 +1,42 @@
+tagId = $tagId;
+ $this->actor = $actor;
+ $this->data = $data;
+ }
+}
diff --git a/extensions/tags/src/Commands/DeleteTagHandler.php b/extensions/tags/src/Commands/DeleteTagHandler.php
new file mode 100644
index 000000000..d3d441c92
--- /dev/null
+++ b/extensions/tags/src/Commands/DeleteTagHandler.php
@@ -0,0 +1,38 @@
+tags = $tags;
+ }
+
+ /**
+ * @param DeleteTag $command
+ * @return Tag
+ * @throws \Flarum\Core\Exceptions\PermissionDeniedException
+ */
+ public function handle(DeleteTag $command)
+ {
+ $actor = $command->actor;
+
+ $tag = $this->tags->findOrFail($command->tagId, $actor);
+
+ $tag->assertCan($actor, 'delete');
+
+ $tag->delete();
+
+ return $tag;
+ }
+}
diff --git a/extensions/tags/src/Commands/EditTagHandler.php b/extensions/tags/src/Commands/EditTagHandler.php
index 1254bff58..a14422e13 100644
--- a/extensions/tags/src/Commands/EditTagHandler.php
+++ b/extensions/tags/src/Commands/EditTagHandler.php
@@ -34,6 +34,22 @@ class EditTagHandler
$attributes = array_get($data, 'attributes', []);
+ if (isset($attributes['name'])) {
+ $tag->name = $attributes['name'];
+ }
+
+ if (isset($attributes['slug'])) {
+ $tag->slug = $attributes['slug'];
+ }
+
+ if (isset($attributes['description'])) {
+ $tag->description = $attributes['description'];
+ }
+
+ if (isset($attributes['color'])) {
+ $tag->color = $attributes['color'];
+ }
+
if (isset($attributes['isRestricted'])) {
$tag->is_restricted = (bool) $attributes['isRestricted'];
}
diff --git a/extensions/tags/src/Extension.php b/extensions/tags/src/Extension.php
index 914420472..4c27affdd 100644
--- a/extensions/tags/src/Extension.php
+++ b/extensions/tags/src/Extension.php
@@ -7,6 +7,8 @@ class Extension extends BaseExtension
{
public function boot(Dispatcher $events)
{
+ Tag::setValidator($this->app->make('validator'));
+
$events->subscribe('Flarum\Tags\Listeners\AddClientAssets');
$events->subscribe('Flarum\Tags\Listeners\AddModelRelationship');
$events->subscribe('Flarum\Tags\Listeners\ConfigureDiscussionPermissions');
diff --git a/extensions/tags/src/Listeners/AddApiAttributes.php b/extensions/tags/src/Listeners/AddApiAttributes.php
index 104827eca..33ddaf64e 100755
--- a/extensions/tags/src/Listeners/AddApiAttributes.php
+++ b/extensions/tags/src/Listeners/AddApiAttributes.php
@@ -74,6 +74,9 @@ class AddApiAttributes
public function addRoutes(RegisterApiRoutes $event)
{
+ $event->post('/tags', 'tags.create', 'Flarum\Tags\Api\CreateAction');
+ $event->post('/tags/order', 'tags.order', 'Flarum\Tags\Api\OrderAction');
$event->patch('/tags/{id}', 'tags.update', 'Flarum\Tags\Api\UpdateAction');
+ $event->delete('/tags/{id}', 'tags.delete', 'Flarum\Tags\Api\DeleteAction');
}
}
diff --git a/extensions/tags/src/Tag.php b/extensions/tags/src/Tag.php
index 4c63a4415..2a5c6a971 100644
--- a/extensions/tags/src/Tag.php
+++ b/extensions/tags/src/Tag.php
@@ -2,11 +2,14 @@
use Flarum\Core\Model;
use Flarum\Core\Discussions\Discussion;
+use Flarum\Core\Groups\Permission;
use Flarum\Core\Support\VisibleScope;
use Flarum\Core\Support\Locked;
+use Flarum\Core\Support\ValidatesBeforeSave;
class Tag extends Model
{
+ use ValidatesBeforeSave;
use VisibleScope;
use Locked;
@@ -14,6 +17,48 @@ class Tag extends Model
protected $dates = ['last_time'];
+ protected $rules = [
+ 'name' => 'required',
+ 'slug' => 'required'
+ ];
+
+ /**
+ * Boot the model.
+ *
+ * @return void
+ */
+ public static function boot()
+ {
+ parent::boot();
+
+ static::deleted(function ($tag) {
+ $tag->discussions()->detach();
+
+ Permission::where('permission', 'like', "tag{$tag->id}.%")->delete();
+ });
+ }
+
+ /**
+ * Create a new tag.
+ *
+ * @param string $name
+ * @param string $slug
+ * @param string $description
+ * @param string $color
+ * @return static
+ */
+ public static function build($name, $slug, $description, $color)
+ {
+ $tag = new static;
+
+ $tag->name = $name;
+ $tag->slug = $slug;
+ $tag->description = $description;
+ $tag->color = $color;
+
+ return $tag;
+ }
+
public function parent()
{
return $this->belongsTo('Flarum\Tags\Tag', 'parent_id');