diff --git a/extensions/tags/js/admin/Gulpfile.js b/extensions/tags/js/admin/Gulpfile.js new file mode 100644 index 000000000..0a9f1ed7f --- /dev/null +++ b/extensions/tags/js/admin/Gulpfile.js @@ -0,0 +1,10 @@ +var gulp = require('flarum-gulp'); + +gulp({ + modules: { + 'tags': [ + '../lib/**/*.js', + 'src/**/*.js' + ] + } +}); diff --git a/extensions/tags/js/admin/package.json b/extensions/tags/js/admin/package.json new file mode 100644 index 000000000..3e0ef919d --- /dev/null +++ b/extensions/tags/js/admin/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "devDependencies": { + "gulp": "^3.8.11", + "flarum-gulp": "git+https://github.com/flarum/gulp.git" + } +} diff --git a/extensions/tags/js/admin/src/main.js b/extensions/tags/js/admin/src/main.js new file mode 100644 index 000000000..a7e6dc064 --- /dev/null +++ b/extensions/tags/js/admin/src/main.js @@ -0,0 +1,63 @@ +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'; + +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}) + })) + })); + } + }); +}); diff --git a/extensions/tags/js/forum/Gulpfile.js b/extensions/tags/js/forum/Gulpfile.js index c28a504c2..0a9f1ed7f 100644 --- a/extensions/tags/js/forum/Gulpfile.js +++ b/extensions/tags/js/forum/Gulpfile.js @@ -2,6 +2,9 @@ var gulp = require('flarum-gulp'); gulp({ modules: { - 'tags': 'src/**/*.js' + 'tags': [ + '../lib/**/*.js', + 'src/**/*.js' + ] } }); diff --git a/extensions/tags/js/forum/src/components/TagDiscussionModal.js b/extensions/tags/js/forum/src/components/TagDiscussionModal.js index 41dfdc960..090811a1f 100644 --- a/extensions/tags/js/forum/src/components/TagDiscussionModal.js +++ b/extensions/tags/js/forum/src/components/TagDiscussionModal.js @@ -60,8 +60,8 @@ export default class TagDiscussionModal extends Modal { // 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() && selected.parent() === tag) - .forEach(this.removeTag); + .filter(selected => selected.parent() === tag) + .forEach(this.removeTag.bind(this)); } } diff --git a/extensions/tags/js/forum/src/helpers/tagIcon.js b/extensions/tags/js/lib/helpers/tagIcon.js similarity index 100% rename from extensions/tags/js/forum/src/helpers/tagIcon.js rename to extensions/tags/js/lib/helpers/tagIcon.js diff --git a/extensions/tags/js/forum/src/helpers/tagLabel.js b/extensions/tags/js/lib/helpers/tagLabel.js similarity index 100% rename from extensions/tags/js/forum/src/helpers/tagLabel.js rename to extensions/tags/js/lib/helpers/tagLabel.js diff --git a/extensions/tags/js/forum/src/helpers/tagsLabel.js b/extensions/tags/js/lib/helpers/tagsLabel.js similarity index 100% rename from extensions/tags/js/forum/src/helpers/tagsLabel.js rename to extensions/tags/js/lib/helpers/tagsLabel.js diff --git a/extensions/tags/js/forum/src/models/Tag.js b/extensions/tags/js/lib/models/Tag.js similarity index 94% rename from extensions/tags/js/forum/src/models/Tag.js rename to extensions/tags/js/lib/models/Tag.js index 7e69d31e7..0cfcb74a7 100644 --- a/extensions/tags/js/forum/src/models/Tag.js +++ b/extensions/tags/js/lib/models/Tag.js @@ -20,5 +20,6 @@ export default class Tag extends mixin(Model, { lastTime: Model.attribute('lastTime', Model.transformDate), lastDiscussion: Model.hasOne('lastDiscussion'), + isRestricted: Model.attribute('isRestricted'), canStartDiscussion: Model.attribute('canStartDiscussion') }) {} diff --git a/extensions/tags/js/forum/src/utils/sortTags.js b/extensions/tags/js/lib/utils/sortTags.js similarity index 90% rename from extensions/tags/js/forum/src/utils/sortTags.js rename to extensions/tags/js/lib/utils/sortTags.js index c0e5d2191..1a499f1d3 100644 --- a/extensions/tags/js/forum/src/utils/sortTags.js +++ b/extensions/tags/js/lib/utils/sortTags.js @@ -15,7 +15,7 @@ export default function sortTags(tags) { } else if (aParent === bParent) { return aPos - bPos; } 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(); } diff --git a/extensions/tags/less/admin/extension.less b/extensions/tags/less/admin/extension.less new file mode 100644 index 000000000..70f2babd2 --- /dev/null +++ b/extensions/tags/less/admin/extension.less @@ -0,0 +1,2 @@ +@import "../lib/TagLabel.less"; +@import "../lib/TagIcon.less"; diff --git a/extensions/tags/less/forum/extension.less b/extensions/tags/less/forum/extension.less index 66bd06050..f925014f2 100644 --- a/extensions/tags/less/forum/extension.less +++ b/extensions/tags/less/forum/extension.less @@ -1,7 +1,8 @@ +@import "../lib/TagLabel.less"; +@import "../lib/TagIcon.less"; + @import "TagCloud.less"; @import "TagDiscussionModal.less"; -@import "TagIcon.less"; -@import "TagLabel.less"; @import "TagTiles.less"; .DiscussionHero { diff --git a/extensions/tags/less/forum/TagIcon.less b/extensions/tags/less/lib/TagIcon.less similarity index 100% rename from extensions/tags/less/forum/TagIcon.less rename to extensions/tags/less/lib/TagIcon.less diff --git a/extensions/tags/less/forum/TagLabel.less b/extensions/tags/less/lib/TagLabel.less similarity index 97% rename from extensions/tags/less/forum/TagLabel.less rename to extensions/tags/less/lib/TagLabel.less index 5719fcbea..8136e127e 100644 --- a/extensions/tags/less/forum/TagLabel.less +++ b/extensions/tags/less/lib/TagLabel.less @@ -6,6 +6,7 @@ border-radius: @border-radius; background: @control-bg; color: @control-color; + text-transform: none; &.untagged { background: transparent; diff --git a/extensions/tags/src/TagSerializer.php b/extensions/tags/src/Api/TagSerializer.php similarity index 81% rename from extensions/tags/src/TagSerializer.php rename to extensions/tags/src/Api/TagSerializer.php index 44b0a5f0c..24db8da42 100644 --- a/extensions/tags/src/TagSerializer.php +++ b/extensions/tags/src/Api/TagSerializer.php @@ -1,4 +1,4 @@ - $tag->name, 'description' => $tag->description, 'slug' => $tag->slug, @@ -23,11 +23,17 @@ class TagSerializer extends Serializer 'lastTime' => $tag->last_time ? $tag->last_time->toRFC3339String() : null, 'canStartDiscussion' => $tag->can($this->actor, 'startDiscussion') ]; + + if ($this->actor->isAdmin()) { + $attributes['isRestricted'] = (bool) $tag->is_restricted; + } + + return $attributes; } protected function parent() { - return $this->hasOne('Flarum\Tags\TagSerializer'); + return $this->hasOne('Flarum\Tags\Api\TagSerializer'); } protected function lastDiscussion() diff --git a/extensions/tags/src/Api/UpdateAction.php b/extensions/tags/src/Api/UpdateAction.php new file mode 100644 index 000000000..f3f402e8f --- /dev/null +++ b/extensions/tags/src/Api/UpdateAction.php @@ -0,0 +1,40 @@ +bus = $bus; + } + + /** + * @param JsonApiRequest $request + * @param Document $document + * @return \Flarum\Core\Tags\Tag + */ + protected function data(JsonApiRequest $request, Document $document) + { + return $this->bus->dispatch( + new EditTag($request->get('id'), $request->actor, $request->get('data')) + ); + } +} diff --git a/extensions/tags/src/Commands/EditTag.php b/extensions/tags/src/Commands/EditTag.php new file mode 100644 index 000000000..6407fead3 --- /dev/null +++ b/extensions/tags/src/Commands/EditTag.php @@ -0,0 +1,40 @@ +tagId = $tagId; + $this->actor = $actor; + $this->data = $data; + } +} diff --git a/extensions/tags/src/Commands/EditTagHandler.php b/extensions/tags/src/Commands/EditTagHandler.php new file mode 100644 index 000000000..1254bff58 --- /dev/null +++ b/extensions/tags/src/Commands/EditTagHandler.php @@ -0,0 +1,45 @@ +tags = $tags; + } + + /** + * @param EditTag $command + * @return Tag + * @throws \Flarum\Core\Exceptions\PermissionDeniedException + */ + public function handle(EditTag $command) + { + $actor = $command->actor; + $data = $command->data; + + $tag = $this->tags->findOrFail($command->tagId, $actor); + + $tag->assertCan($actor, 'edit'); + + $attributes = array_get($data, 'attributes', []); + + if (isset($attributes['isRestricted'])) { + $tag->is_restricted = (bool) $attributes['isRestricted']; + } + + $tag->save(); + + return $tag; + } +} diff --git a/extensions/tags/src/Listeners/AddApiAttributes.php b/extensions/tags/src/Listeners/AddApiAttributes.php index 9935eedf4..104827eca 100755 --- a/extensions/tags/src/Listeners/AddApiAttributes.php +++ b/extensions/tags/src/Listeners/AddApiAttributes.php @@ -4,6 +4,7 @@ use Flarum\Events\ApiRelationship; use Flarum\Events\WillSerializeData; use Flarum\Events\BuildApiAction; use Flarum\Events\ApiAttributes; +use Flarum\Events\RegisterApiRoutes; use Flarum\Api\Actions\Forum; use Flarum\Api\Actions\Discussions; use Flarum\Api\Serializers\ForumSerializer; @@ -18,18 +19,19 @@ class AddApiAttributes $events->listen(WillSerializeData::class, [$this, 'loadTagsRelationship']); $events->listen(BuildApiAction::class, [$this, 'includeTagsRelationship']); $events->listen(ApiAttributes::class, [$this, 'addAttributes']); + $events->listen(RegisterApiRoutes::class, [$this, 'addRoutes']); } public function addTagsRelationship(ApiRelationship $event) { if ($event->serializer instanceof ForumSerializer && $event->relationship === 'tags') { - return $event->serializer->hasMany('Flarum\Tags\TagSerializer', 'tags'); + return $event->serializer->hasMany('Flarum\Tags\Api\TagSerializer', 'tags'); } if ($event->serializer instanceof DiscussionSerializer && $event->relationship === 'tags') { - return $event->serializer->hasMany('Flarum\Tags\TagSerializer', 'tags'); + return $event->serializer->hasMany('Flarum\Tags\Api\TagSerializer', 'tags'); } } @@ -69,4 +71,9 @@ class AddApiAttributes $event->attributes['canTag'] = $event->model->can($event->actor, 'tag'); } } + + public function addRoutes(RegisterApiRoutes $event) + { + $event->patch('/tags/{id}', 'tags.update', 'Flarum\Tags\Api\UpdateAction'); + } } diff --git a/extensions/tags/src/Listeners/AddClientAssets.php b/extensions/tags/src/Listeners/AddClientAssets.php index 34f368cfa..242c28fb4 100755 --- a/extensions/tags/src/Listeners/AddClientAssets.php +++ b/extensions/tags/src/Listeners/AddClientAssets.php @@ -41,6 +41,13 @@ class AddClientAssets 'tags.tag_cloud_title', 'tags.deleted' ]); + + $event->adminAssets([ + __DIR__.'/../../js/admin/dist/extension.js', + __DIR__.'/../../less/admin/extension.less' + ]); + + $event->adminBootstrapper('tags/main'); } public function addRoutes(RegisterForumRoutes $event) diff --git a/extensions/tags/src/Tag.php b/extensions/tags/src/Tag.php index 6da62021a..4c63a4415 100644 --- a/extensions/tags/src/Tag.php +++ b/extensions/tags/src/Tag.php @@ -2,9 +2,14 @@ use Flarum\Core\Model; use Flarum\Core\Discussions\Discussion; +use Flarum\Core\Support\VisibleScope; +use Flarum\Core\Support\Locked; class Tag extends Model { + use VisibleScope; + use Locked; + protected $table = 'tags'; protected $dates = ['last_time']; diff --git a/extensions/tags/src/TagRepository.php b/extensions/tags/src/TagRepository.php index 572a44fb1..1093387ab 100644 --- a/extensions/tags/src/TagRepository.php +++ b/extensions/tags/src/TagRepository.php @@ -1,11 +1,28 @@ scopeVisibleTo($query, $actor)->firstOrFail(); + } + /** * Find all tags, optionally making sure they are visible to a * certain user. @@ -13,7 +30,7 @@ class TagRepository * @param User|null $user * @return \Illuminate\Database\Eloquent\Collection */ - public function find(User $user = null) + public function all(User $user = null) { $query = Tag::newQuery();