diff --git a/extensions/tags/bootstrap.php b/extensions/tags/bootstrap.php index 4e5fadac0..c13aa6aaf 100644 --- a/extensions/tags/bootstrap.php +++ b/extensions/tags/bootstrap.php @@ -9,20 +9,26 @@ * file that was distributed with this source code. */ -use Flarum\Core\Application; +use Flarum\Tags\Access; +use Flarum\Tags\Listener; +use Flarum\Tags\Tag; +use Illuminate\Contracts\Events\Dispatcher; -return function (Application $app) { - Flarum\Tags\Tag::setValidator($app->make('validator')); +return function (Dispatcher $events) { + $events->subscribe(Listener\AddClientAssets::class); + $events->subscribe(Listener\AddDiscussionTagsRelationship::class); + $events->subscribe(Listener\AddForumTagsRelationship::class); + $events->subscribe(Listener\AddTagsApi::class); + $events->subscribe(Listener\CreatePostWhenTagsAreChanged::class); + $events->subscribe(Listener\FilterDiscussionListByTags::class); + $events->subscribe(Listener\SaveTagsToDatabase::class); + $events->subscribe(Listener\UpdateTagMetadata::class); - $events = $app->make('events'); + $events->subscribe(Access\DiscussionPolicy::class); + $events->subscribe(Access\TagPolicy::class); + $events->subscribe(Access\FlagPolicy::class); - $events->subscribe('Flarum\Tags\Listeners\AddClientAssets'); - $events->subscribe('Flarum\Tags\Listeners\AddModelRelationship'); - $events->subscribe('Flarum\Tags\Listeners\ConfigureDiscussionPermissions'); - $events->subscribe('Flarum\Tags\Listeners\ConfigureTagPermissions'); - $events->subscribe('Flarum\Tags\Listeners\AddApiAttributes'); - $events->subscribe('Flarum\Tags\Listeners\PersistData'); - $events->subscribe('Flarum\Tags\Listeners\LogDiscussionTagged'); - $events->subscribe('Flarum\Tags\Listeners\UpdateTagMetadata'); - $events->subscribe('Flarum\Tags\Listeners\AddTagGambit'); + Tag::saving(function ($model) { + $this->app->make('Flarum\Tags\TagValidator')->assertValid($model); + }); }; diff --git a/extensions/tags/js/admin/dist/extension.js b/extensions/tags/js/admin/dist/extension.js index 5a2de01ea..f144cf698 100644 --- a/extensions/tags/js/admin/dist/extension.js +++ b/extensions/tags/js/admin/dist/extension.js @@ -1 +1,1335 @@ -!function(t,e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e(require("jquery")):t.sortable=e(t.jQuery)}(this,function(t){"use strict";var e,n,a=t(),r=[],o=function(t){t.off("dragstart.h5s"),t.off("dragend.h5s"),t.off("selectstart.h5s"),t.off("dragover.h5s"),t.off("dragenter.h5s"),t.off("drop.h5s")},i=function(t){t.off("dragover.h5s"),t.off("dragenter.h5s"),t.off("drop.h5s")},s=function(t,e){t.dataTransfer.effectAllowed="move",t.dataTransfer.setData("text",""),t.dataTransfer.setDragImage&&t.dataTransfer.setDragImage(e.item,e.x,e.y)},u=function(t,e){return e.x||(e.x=parseInt(t.pageX-e.draggedItem.offset().left)),e.y||(e.y=parseInt(t.pageY-e.draggedItem.offset().top)),e},l=function(t){return{item:t[0],draggedItem:t}},c=function(t,e){var n=l(e);n=u(t,n),s(t,n)},d=function(t,e){return"undefined"==typeof t?e:t},f=function(t){t.removeData("opts"),t.removeData("connectWith"),t.removeData("items"),t.removeAttr("aria-dropeffect")},m=function(t){t.removeAttr("aria-grabbed"),t.removeAttr("draggable"),t.removeAttr("role")},p=function(t,e){return t[0]===e[0]?!0:void 0!==t.data("connectWith")?t.data("connectWith")===e.data("connectWith"):!1},g=function(t){var e=t.data("opts")||{},n=t.children(e.items),a=e.handle?n.find(e.handle):n;i(t),f(t),a.off("mousedown.h5s"),o(n),m(n)},h=function(e){var n=e.data("opts"),a=e.children(n.items),r=n.handle?a.find(n.handle):a;e.attr("aria-dropeffect","move"),r.attr("draggable","true"),"function"!=typeof document.createElement("span").dragDrop||n.disableIEFix||r.on("mousedown.h5s",function(){-1!==a.index(this)?this.dragDrop():t(this).parents(n.items)[0].dragDrop()})},v=function(t){var e=t.data("opts"),n=t.children(e.items),a=e.handle?n.find(e.handle):n;t.attr("aria-dropeffect","none"),a.attr("draggable",!1),a.off("mousedown.h5s")},b=function(t){var e=t.data("opts"),n=t.children(e.items),a=e.handle?n.find(e.handle):n;o(n),a.off("mousedown.h5s"),i(t)},y=function(o,i){var u=t(o),l=String(i);return i=t.extend({connectWith:!1,placeholder:null,dragImage:null,disableIEFix:!1,placeholderClass:"sortable-placeholder",draggingClass:"sortable-dragging"},i),u.each(function(){var o=t(this);if(/enable|disable|destroy/.test(l))return void y[l](o);i=d(o.data("opts"),i),o.data("opts",i),b(o);var u,f,m,g=o.children(i.items),v=null===i.placeholder?t("<"+(/^ul|ol$/i.test(this.tagName)?"li":"div")+' class="'+i.placeholderClass+'"/>'):t(i.placeholder).addClass(i.placeholderClass);if(!o.attr("data-sortable-id")){var T=r.length;r[T]=o,o.attr("data-sortable-id",T),g.attr("data-item-sortable-id",T)}o.data("items",i.items),a=a.add(v),i.connectWith&&o.data("connectWith",i.connectWith),h(o),g.attr("role","option"),g.attr("aria-grabbed","false"),g.on("dragstart.h5s",function(a){a.stopImmediatePropagation(),i.dragImage?(s(a.originalEvent,{item:i.dragImage,x:0,y:0}),console.log("WARNING: dragImage option is deprecated and will be removed in the future!")):c(a.originalEvent,t(this),i.dragImage),e=t(this),e.addClass(i.draggingClass),e.attr("aria-grabbed","true"),u=e.index(),n=e.height(),f=t(this).parent(),e.parent().triggerHandler("sortstart",{item:e,startparent:f})}),g.on("dragend.h5s",function(){e&&(e.removeClass(i.draggingClass),e.attr("aria-grabbed","false"),e.show(),a.detach(),m=t(this).parent(),e.parent().triggerHandler("sortstop",{item:e,startparent:f}),(u!==e.index()||f.get(0)!==m.get(0))&&e.parent().triggerHandler("sortupdate",{item:e,index:m.children(m.data("items")).index(e),oldindex:g.index(e),elementIndex:e.index(),oldElementIndex:u,startparent:f,endparent:m}),e=null,n=null)}),t(this).add([v]).on("drop.h5s",function(n){return p(o,t(e).parent())?(n.stopPropagation(),a.filter(":visible").after(e),e.trigger("dragend.h5s"),!1):void 0}),g.add([this,v]).on("dragover.h5s dragenter.h5s",function(r){if(p(o,t(e).parent())){if(r.preventDefault(),r.originalEvent.dataTransfer.dropEffect="move",g.is(this)){var s=t(this).height();if(i.forcePlaceholderSize&&v.height(n),s>n){var u=s-n,l=t(this).offset().top;if(v.index()t(this).index()&&r.originalEvent.pageY>l+s-u)return!1}e.hide(),v.index()r;r++)n[r]=arguments[r];l(Object.getPrototypeOf(a.prototype),"constructor",this).apply(this,n),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()||""),this.isHidden=m.prop(this.tag.isHidden()||!1)}return n(a,t),u(a,[{key:"className",value:function(){return"EditTagModal Modal--small"}},{key:"title",value:function(){return this.name()?i({name:this.name,color:this.color}):"Create Tag"}},{key:"content",value:function(){var t=this;return m("div",{className:"Modal-body"},m("div",{className:"Form"},m("div",{className:"Form-group"},m("label",null,"Name"),m("input",{className:"FormControl",placeholder:"Name",value:this.name(),oninput:function(e){t.name(e.target.value),t.slug(o(e.target.value))}})),m("div",{className:"Form-group"},m("label",null,"Slug"),m("input",{className:"FormControl",value:this.slug(),oninput:m.withAttr("value",this.slug)})),m("div",{className:"Form-group"},m("label",null,"Description"),m("textarea",{className:"FormControl",value:this.description(),oninput:m.withAttr("value",this.description)})),m("div",{className:"Form-group"},m("label",null,"Color"),m("input",{className:"FormControl",placeholder:"#aaaaaa",value:this.color(),oninput:m.withAttr("value",this.color)})),m("div",{className:"Form-group"},m("div",null,m("label",{className:"checkbox"},m("input",{type:"checkbox",value:"1",checked:this.isHidden(),onchange:m.withAttr("checked",this.isHidden)}),"Hide from All Discussions"))),m("div",{className:"Form-group"},r.component({type:"submit",className:"Button Button--primary EditTagModal-save",loading:this.loading,children:"Save Changes"}),this.tag.exists?m("button",{type:"button",className:"Button EditTagModal-delete",onclick:this["delete"].bind(this)},"Delete Tag"):"")))}},{key:"onsubmit",value:function(t){var e=this;t.preventDefault(),this.loading=!0,this.tag.save({name:this.name(),slug:this.slug(),description:this.description(),color:this.color(),isHidden:this.isHidden()}).then(function(){return e.hide()},function(){e.loading=!1,m.redraw()})}},{key:"delete",value:function(){confirm("Are you sure you want to delete this tag? The tag's discussions will NOT be deleted.")&&(this.tag["delete"]().then(function(){return m.redraw()}),this.hide())}}]),a}(a),t("default",s)}}}),System.register("flarum/tags/components/TagSettingsModal",["flarum/components/SettingsModal"],function(t){"use strict";function e(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var a,r,o=function(){function t(t,e){for(var n=0;n li").map(function(){return{id:$(this).data("id"),children:$(this).find("li").map(function(){return $(this).data("id")}).get()}}).get();a.forEach(function(t,e){var n=app.store.getById("tags",t.id);n.pushData({attributes:{position:e,isChild:!1},relationships:{parent:null}}),t.children.forEach(function(t,e){app.store.getById("tags",t).pushData({attributes:{position:e,isChild:!0},relationships:{parent:n}})})}),app.request({url:app.forum.attribute("apiUrl")+"/tags/order",method:"POST",data:{order:a}}),m.redraw.strategy("all"),m.redraw()})}}]),r}(r),t("default",c)}}}); \ No newline at end of file +;(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof exports === 'object') { + module.exports = factory(require('jquery')); + } else { + root.sortable = factory(root.jQuery); + } +}(this, function($) { +/* + * HTML5 Sortable jQuery Plugin + * https://github.com/voidberg/html5sortable + * + * Original code copyright 2012 Ali Farhadi. + * This version is mantained by Alexandru Badiu & Lukas Oppermann + * + * + * Released under the MIT license. + */ +'use strict'; +/* + * variables global to the plugin + */ +var dragging; +var draggingHeight; +var placeholders = $(); +var sortables = []; +/* + * remove event handlers from items + * @param [jquery Collection] items + * @info event.h5s (jquery way of namespacing events, to bind multiple handlers to the event) + */ +var _removeItemEvents = function(items) { + items.off('dragstart.h5s'); + items.off('dragend.h5s'); + items.off('selectstart.h5s'); + items.off('dragover.h5s'); + items.off('dragenter.h5s'); + items.off('drop.h5s'); +}; +/* + * remove event handlers from sortable + * @param [jquery Collection] sortable + * @info event.h5s (jquery way of namespacing events, to bind multiple handlers to the event) + */ +var _removeSortableEvents = function(sortable) { + sortable.off('dragover.h5s'); + sortable.off('dragenter.h5s'); + sortable.off('drop.h5s'); +}; +/* + * attache ghost to dataTransfer object + * @param [event] original event + * @param [object] ghost-object with item, x and y coordinates + */ +var _attachGhost = function(event, ghost) { + // this needs to be set for HTML5 drag & drop to work + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text', ''); + + // check if setDragImage method is available + if (event.dataTransfer.setDragImage) { + event.dataTransfer.setDragImage(ghost.item, ghost.x, ghost.y); + } +}; +/** + * _addGhostPos clones the dragged item and adds it as a Ghost item + * @param [object] event - the event fired when dragstart is triggered + * @param [object] ghost - .item = node, draggedItem = jQuery collection + */ +var _addGhostPos = function(e, ghost) { + if (!ghost.x) { + ghost.x = parseInt(e.pageX - ghost.draggedItem.offset().left); + } + if (!ghost.y) { + ghost.y = parseInt(e.pageY - ghost.draggedItem.offset().top); + } + return ghost; +}; +/** + * _makeGhost decides which way to make a ghost and passes it to attachGhost + * @param [jQuery selection] $draggedItem - the item that the user drags + */ +var _makeGhost = function($draggedItem) { + return { + item: $draggedItem[0], + draggedItem: $draggedItem + }; +}; +/** + * _getGhost constructs ghost and attaches it to dataTransfer + * @param [event] event - the original drag event object + * @param [jQuery selection] $draggedItem - the item that the user drags + * @param [object] ghostOpt - the ghost options + */ +// TODO: could $draggedItem be replaced by event.target in all instances +var _getGhost = function(event, $draggedItem) { + // add ghost item & draggedItem to ghost object + var ghost = _makeGhost($draggedItem); + // attach ghost position + ghost = _addGhostPos(event, ghost); + // attach ghost to dataTransfer + _attachGhost(event, ghost); +}; +/* + * return options if not set on sortable already + * @param [object] soptions + * @param [object] options + */ +var _getOptions = function(soptions, options) { + if (typeof soptions === 'undefined') { + return options; + } + return soptions; +}; +/* + * remove data from sortable + * @param [jquery Collection] a single sortable + */ +var _removeSortableData = function(sortable) { + sortable.removeData('opts'); + sortable.removeData('connectWith'); + sortable.removeData('items'); + sortable.removeAttr('aria-dropeffect'); +}; +/* + * remove data from items + * @param [jquery Collection] items + */ +var _removeItemData = function(items) { + items.removeAttr('aria-grabbed'); + items.removeAttr('draggable'); + items.removeAttr('role'); +}; +/* + * check if two lists are connected + * @param [jquery Collection] items + */ +var _listsConnected = function(curList, destList) { + if (curList[0] === destList[0]) { + return true; + } + if (curList.data('connectWith') !== undefined) { + return curList.data('connectWith') === destList.data('connectWith'); + } + return false; +}; +/* + * destroy the sortable + * @param [jquery Collection] a single sortable + */ +var _destroySortable = function(sortable) { + var opts = sortable.data('opts') || {}; + var items = sortable.children(opts.items); + var handles = opts.handle ? items.find(opts.handle) : items; + // remove event handlers & data from sortable + _removeSortableEvents(sortable); + _removeSortableData(sortable); + // remove event handlers & data from items + handles.off('mousedown.h5s'); + _removeItemEvents(items); + _removeItemData(items); +}; +/* + * enable the sortable + * @param [jquery Collection] a single sortable + */ +var _enableSortable = function(sortable) { + var opts = sortable.data('opts'); + var items = sortable.children(opts.items); + var handles = opts.handle ? items.find(opts.handle) : items; + sortable.attr('aria-dropeffect', 'move'); + handles.attr('draggable', 'true'); + // IE FIX for ghost + // can be disabled as it has the side effect that other events + // (e.g. click) will be ignored + if (typeof document.createElement('span').dragDrop === 'function' && !opts.disableIEFix) { + handles.on('mousedown.h5s', function() { + if (items.index(this) !== -1) { + this.dragDrop(); + } else { + $(this).parents(opts.items)[0].dragDrop(); + } + }); + } +}; +/* + * disable the sortable + * @param [jquery Collection] a single sortable + */ +var _disableSortable = function(sortable) { + var opts = sortable.data('opts'); + var items = sortable.children(opts.items); + var handles = opts.handle ? items.find(opts.handle) : items; + sortable.attr('aria-dropeffect', 'none'); + handles.attr('draggable', false); + handles.off('mousedown.h5s'); +}; +/* + * reload the sortable + * @param [jquery Collection] a single sortable + * @description events need to be removed to not be double bound + */ +var _reloadSortable = function(sortable) { + var opts = sortable.data('opts'); + var items = sortable.children(opts.items); + var handles = opts.handle ? items.find(opts.handle) : items; + // remove event handlers from items + _removeItemEvents(items); + handles.off('mousedown.h5s'); + // remove event handlers from sortable + _removeSortableEvents(sortable); +}; +/* + * public sortable object + * @param [object|string] options|method + */ +var sortable = function(selector, options) { + + var $sortables = $(selector); + var method = String(options); + + options = $.extend({ + connectWith: false, + placeholder: null, + // dragImage can be null or a jQuery element + dragImage: null, + disableIEFix: false, + placeholderClass: 'sortable-placeholder', + draggingClass: 'sortable-dragging' + }, options); + + /* TODO: maxstatements should be 25, fix and remove line below */ + /*jshint maxstatements:false */ + return $sortables.each(function() { + + var $sortable = $(this); + + if (/enable|disable|destroy/.test(method)) { + sortable[method]($sortable); + return; + } + + // get options & set options on sortable + options = _getOptions($sortable.data('opts'), options); + $sortable.data('opts', options); + // reset sortable + _reloadSortable($sortable); + // initialize + var items = $sortable.children(options.items); + var index; + var startParent; + var newParent; + var placeholder = (options.placeholder === null) ? $('<' + (/^ul|ol$/i.test(this.tagName) ? 'li' : 'div') + ' class="' + options.placeholderClass + '"/>') : $(options.placeholder).addClass(options.placeholderClass); + + // setup sortable ids + if (!$sortable.attr('data-sortable-id')) { + var id = sortables.length; + sortables[id] = $sortable; + $sortable.attr('data-sortable-id', id); + items.attr('data-item-sortable-id', id); + } + + $sortable.data('items', options.items); + placeholders = placeholders.add(placeholder); + if (options.connectWith) { + $sortable.data('connectWith', options.connectWith); + } + + _enableSortable($sortable); + items.attr('role', 'option'); + items.attr('aria-grabbed', 'false'); + + // Handle drag events on draggable items + items.on('dragstart.h5s', function(e) { + e.stopImmediatePropagation(); + + if (options.dragImage) { + _attachGhost(e.originalEvent, { + item: options.dragImage, + x: 0, + y: 0 + }); + console.log('WARNING: dragImage option is deprecated' + + ' and will be removed in the future!'); + } else { + // add transparent clone or other ghost to cursor + _getGhost(e.originalEvent, $(this), options.dragImage); + } + // cache selsection & add attr for dragging + dragging = $(this); + dragging.addClass(options.draggingClass); + dragging.attr('aria-grabbed', 'true'); + // grab values + index = dragging.index(); + draggingHeight = dragging.height(); + startParent = $(this).parent(); + // trigger sortstar update + dragging.parent().triggerHandler('sortstart', { + item: dragging, + startparent: startParent + }); + }); + // Handle drag events on draggable items + items.on('dragend.h5s', function() { + if (!dragging) { + return; + } + // remove dragging attributes and show item + dragging.removeClass(options.draggingClass); + dragging.attr('aria-grabbed', 'false'); + dragging.show(); + + placeholders.detach(); + newParent = $(this).parent(); + dragging.parent().triggerHandler('sortstop', { + item: dragging, + startparent: startParent, + }); + if (index !== dragging.index() || + startParent.get(0) !== newParent.get(0)) { + dragging.parent().triggerHandler('sortupdate', { + item: dragging, + index: newParent.children(newParent.data('items')).index(dragging), + oldindex: items.index(dragging), + elementIndex: dragging.index(), + oldElementIndex: index, + startparent: startParent, + endparent: newParent + }); + } + dragging = null; + draggingHeight = null; + }); + // Handle drop event on sortable & placeholder + // TODO: REMOVE placeholder????? + $(this).add([placeholder]).on('drop.h5s', function(e) { + if (!_listsConnected($sortable, $(dragging).parent())) { + return; + } + + e.stopPropagation(); + placeholders.filter(':visible').after(dragging); + dragging.trigger('dragend.h5s'); + return false; + }); + + // Handle dragover and dragenter events on draggable items + // TODO: REMOVE placeholder????? + items.add([this, placeholder]).on('dragover.h5s dragenter.h5s', function(e) { + if (!_listsConnected($sortable, $(dragging).parent())) { + return; + } + + e.preventDefault(); + e.originalEvent.dataTransfer.dropEffect = 'move'; + if (items.is(this)) { + var thisHeight = $(this).height(); + if (options.forcePlaceholderSize) { + placeholder.height(draggingHeight); + } + + // Check if $(this) is bigger than the draggable. If it is, we have to define a dead zone to prevent flickering + if (thisHeight > draggingHeight) { + // Dead zone? + var deadZone = thisHeight - draggingHeight; + var offsetTop = $(this).offset().top; + if (placeholder.index() < $(this).index() && + e.originalEvent.pageY < offsetTop + deadZone) { + return false; + } + if (placeholder.index() > $(this).index() && + e.originalEvent.pageY > offsetTop + thisHeight - deadZone) { + return false; + } + } + + dragging.hide(); + if (placeholder.index() < $(this).index()) { + $(this).after(placeholder); + } else { + $(this).before(placeholder); + } + placeholders.not(placeholder).detach(); + } else { + if (!placeholders.is(this) && !$(this).children(options.items).length) { + placeholders.detach(); + $(this).append(placeholder); + } + } + return false; + }); + }); +}; + +sortable.destroy = function(sortable) { + _destroySortable(sortable); +}; + +sortable.enable = function(sortable) { + _enableSortable(sortable); +}; + +sortable.disable = function(sortable) { + _disableSortable(sortable); +}; + +$.fn.sortable = function(options) { + return sortable(this, options); +}; + +return sortable; +})); +;System.register('flarum/tags/helpers/tagIcon', [], function (_export) { + 'use strict'; + + _export('default', tagIcon); + + function tagIcon(tag) { + var attrs = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + attrs.className = 'icon TagIcon ' + (attrs.className || ''); + + if (tag) { + attrs.style = attrs.style || {}; + attrs.style.backgroundColor = tag.color(); + } else { + attrs.className += ' untagged'; + } + + return m('span', attrs); + } + + return { + setters: [], + execute: function () {} + }; +});;System.register('flarum/tags/helpers/tagLabel', ['flarum/utils/extract'], function (_export) { + 'use strict'; + + var extract; + + _export('default', tagLabel); + + function tagLabel(tag) { + var attrs = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + attrs.style = attrs.style || {}; + attrs.className = 'TagLabel ' + (attrs.className || ''); + + var link = extract(attrs, 'link'); + + if (tag) { + var color = tag.color(); + if (color) { + attrs.style.backgroundColor = attrs.style.color = color; + attrs.className += ' colored'; + } + + if (link) { + attrs.title = tag.description() || ''; + attrs.href = app.route('tag', { tags: tag.slug() }); + attrs.config = m.route; + } + } else { + attrs.className += ' untagged'; + } + + return m(link ? 'a' : 'span', attrs, m( + 'span', + { className: 'TagLabel-text' }, + tag ? tag.name() : app.trans('tags.deleted') + )); + } + + return { + setters: [function (_flarumUtilsExtract) { + extract = _flarumUtilsExtract['default']; + }], + execute: function () {} + }; +});;System.register('flarum/tags/helpers/tagsLabel', ['flarum/utils/extract', 'flarum/tags/helpers/tagLabel', 'flarum/tags/utils/sortTags'], function (_export) { + 'use strict'; + + var extract, tagLabel, sortTags; + + _export('default', tagsLabel); + + function tagsLabel(tags) { + var attrs = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + var children = []; + var link = extract(attrs, 'link'); + + attrs.className = 'TagsLabel ' + (attrs.className || ''); + + if (tags) { + sortTags(tags).forEach(function (tag) { + if (tag || tags.length === 1) { + children.push(tagLabel(tag, { link: link })); + } + }); + } else { + children.push(tagLabel()); + } + + return m( + 'span', + attrs, + children + ); + } + + return { + setters: [function (_flarumUtilsExtract) { + extract = _flarumUtilsExtract['default']; + }, function (_flarumTagsHelpersTagLabel) { + tagLabel = _flarumTagsHelpersTagLabel['default']; + }, function (_flarumTagsUtilsSortTags) { + sortTags = _flarumTagsUtilsSortTags['default']; + }], + execute: function () {} + }; +});;System.register('flarum/tags/models/Tag', ['flarum/Model', 'flarum/utils/mixin', 'flarum/utils/computed'], function (_export) { + 'use strict'; + + var Model, mixin, computed, Tag; + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + return { + setters: [function (_flarumModel) { + Model = _flarumModel['default']; + }, function (_flarumUtilsMixin) { + mixin = _flarumUtilsMixin['default']; + }, function (_flarumUtilsComputed) { + computed = _flarumUtilsComputed['default']; + }], + execute: function () { + Tag = (function (_mixin) { + _inherits(Tag, _mixin); + + function Tag() { + _classCallCheck(this, Tag); + + _get(Object.getPrototypeOf(Tag.prototype), 'constructor', this).apply(this, arguments); + } + + return Tag; + })(mixin(Model, { + name: Model.attribute('name'), + slug: Model.attribute('slug'), + description: Model.attribute('description'), + + color: Model.attribute('color'), + backgroundUrl: Model.attribute('backgroundUrl'), + backgroundMode: Model.attribute('backgroundMode'), + + position: Model.attribute('position'), + parent: Model.hasOne('parent'), + defaultSort: Model.attribute('defaultSort'), + isChild: Model.attribute('isChild'), + isHidden: Model.attribute('isHidden'), + + discussionsCount: Model.attribute('discussionsCount'), + lastTime: Model.attribute('lastTime', Model.transformDate), + lastDiscussion: Model.hasOne('lastDiscussion'), + + isRestricted: Model.attribute('isRestricted'), + canStartDiscussion: Model.attribute('canStartDiscussion'), + + isPrimary: computed('position', 'parent', function (position, parent) { + return position !== null && parent === false; + }) + })); + + _export('default', Tag); + } + }; +});;System.register("flarum/tags/utils/sortTags", [], function (_export) { + "use strict"; + + _export("default", sortTags); + + function sortTags(tags) { + return tags.slice(0).sort(function (a, b) { + var aPos = a.position(); + var bPos = b.position(); + + var aParent = a.parent(); + var bParent = b.parent(); + + if (aPos === null && bPos === null) { + return b.discussionsCount() - a.discussionsCount(); + } else if (bPos === null) { + return -1; + } else if (aPos === null) { + return 1; + } else if (aParent === bParent) { + return aPos - bPos; + } else if (aParent) { + return aParent === b ? 1 : aParent.position() - bPos; + } else if (bParent) { + return bParent === a ? -1 : aPos - bParent.position(); + } + + return 0; + }); + } + + return { + setters: [], + execute: function () {} + }; +});;System.register('flarum/tags/addTagPermission', ['flarum/extend', 'flarum/components/PermissionGrid'], function (_export) { + 'use strict'; + + var extend, PermissionGrid; + return { + setters: [function (_flarumExtend) { + extend = _flarumExtend.extend; + }, function (_flarumComponentsPermissionGrid) { + PermissionGrid = _flarumComponentsPermissionGrid['default']; + }], + execute: function () { + _export('default', function () { + extend(PermissionGrid.prototype, 'moderateItems', function (items) { + items.add('tag', { + icon: 'tag', + label: 'Tag discussions', + permission: 'discussion.tag' + }, 95); + }); + }); + } + }; +});;System.register('flarum/tags/addTagsHomePageOption', ['flarum/extend', 'flarum/components/BasicsPage'], function (_export) { + 'use strict'; + + var extend, BasicsPage; + return { + setters: [function (_flarumExtend) { + extend = _flarumExtend.extend; + }, function (_flarumComponentsBasicsPage) { + BasicsPage = _flarumComponentsBasicsPage['default']; + }], + execute: function () { + _export('default', function () { + extend(BasicsPage.prototype, 'homePageItems', function (items) { + items.add('tags', { + path: '/tags', + label: 'Tags' + }); + }); + }); + } + }; +});;System.register('flarum/tags/addTagsPane', ['flarum/extend', 'flarum/components/AdminNav', 'flarum/components/AdminLinkButton', 'flarum/tags/components/TagsPage'], function (_export) { + 'use strict'; + + var extend, AdminNav, AdminLinkButton, TagsPage; + return { + setters: [function (_flarumExtend) { + extend = _flarumExtend.extend; + }, function (_flarumComponentsAdminNav) { + AdminNav = _flarumComponentsAdminNav['default']; + }, function (_flarumComponentsAdminLinkButton) { + AdminLinkButton = _flarumComponentsAdminLinkButton['default']; + }, function (_flarumTagsComponentsTagsPage) { + TagsPage = _flarumTagsComponentsTagsPage['default']; + }], + execute: function () { + _export('default', function () { + app.routes.tags = { path: '/tags', component: TagsPage.component() }; + + app.extensionSettings.tags = function () { + return m.route(app.route('tags')); + }; + + extend(AdminNav.prototype, 'items', function (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.' + })); + }); + }); + } + }; +});;System.register('flarum/tags/addTagsPermissionScope', ['flarum/extend', 'flarum/components/PermissionGrid', 'flarum/components/PermissionDropdown', 'flarum/components/Dropdown', 'flarum/components/Button', 'flarum/tags/helpers/tagLabel', 'flarum/tags/helpers/tagIcon', 'flarum/tags/utils/sortTags'], function (_export) { + 'use strict'; + + var extend, PermissionGrid, PermissionDropdown, Dropdown, Button, tagLabel, tagIcon, sortTags; + return { + setters: [function (_flarumExtend) { + extend = _flarumExtend.extend; + }, function (_flarumComponentsPermissionGrid) { + PermissionGrid = _flarumComponentsPermissionGrid['default']; + }, function (_flarumComponentsPermissionDropdown) { + PermissionDropdown = _flarumComponentsPermissionDropdown['default']; + }, function (_flarumComponentsDropdown) { + Dropdown = _flarumComponentsDropdown['default']; + }, function (_flarumComponentsButton) { + Button = _flarumComponentsButton['default']; + }, function (_flarumTagsHelpersTagLabel) { + tagLabel = _flarumTagsHelpersTagLabel['default']; + }, function (_flarumTagsHelpersTagIcon) { + tagIcon = _flarumTagsHelpersTagIcon['default']; + }, function (_flarumTagsUtilsSortTags) { + sortTags = _flarumTagsUtilsSortTags['default']; + }], + execute: function () { + _export('default', function () { + extend(PermissionGrid.prototype, 'scopeItems', function (items) { + sortTags(app.store.all('tags')).filter(function (tag) { + return tag.isRestricted(); + }).forEach(function (tag) { + return items.add('tag' + tag.id(), { + label: tagLabel(tag), + onremove: function onremove() { + return tag.save({ isRestricted: false }); + }, + render: function render(item) { + if (item.permission) { + var permission = undefined; + + 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) { + return PermissionDropdown.component({ + permission: 'tag' + tag.id() + '.' + permission, + allowGuest: item.allowGuest + }); + } + } + + return ''; + } + }); + }); + }); + + extend(PermissionGrid.prototype, 'scopeControlItems', function (items) { + var tags = sortTags(app.store.all('tags').filter(function (tag) { + return !tag.isRestricted(); + })); + + if (tags.length) { + items.add('tag', Dropdown.component({ + className: 'Dropdown--restrictByTag', + buttonClassName: 'Button Button--text', + label: 'Restrict by Tag', + icon: 'plus', + caretIcon: null, + children: tags.map(function (tag) { + return Button.component({ + icon: true, + children: [tagIcon(tag, { className: 'Button-icon' }), ' ', tag.name()], + onclick: function onclick() { + return tag.save({ isRestricted: true }); + } + }); + }) + })); + } + }); + }); + } + }; +});;System.register('flarum/tags/main', ['flarum/tags/models/Tag', 'flarum/tags/addTagsPermissionScope', 'flarum/tags/addTagPermission', 'flarum/tags/addTagsPane', 'flarum/tags/addTagsHomePageOption'], function (_export) { + 'use strict'; + + var Tag, addTagsPermissionScope, addTagPermission, addTagsPane, addTagsHomePageOption; + return { + setters: [function (_flarumTagsModelsTag) { + Tag = _flarumTagsModelsTag['default']; + }, function (_flarumTagsAddTagsPermissionScope) { + addTagsPermissionScope = _flarumTagsAddTagsPermissionScope['default']; + }, function (_flarumTagsAddTagPermission) { + addTagPermission = _flarumTagsAddTagPermission['default']; + }, function (_flarumTagsAddTagsPane) { + addTagsPane = _flarumTagsAddTagsPane['default']; + }, function (_flarumTagsAddTagsHomePageOption) { + addTagsHomePageOption = _flarumTagsAddTagsHomePageOption['default']; + }], + execute: function () { + + app.initializers.add('tags', function (app) { + app.store.models.tags = Tag; + + addTagsPermissionScope(); + addTagPermission(); + addTagsPane(); + addTagsHomePageOption(); + }); + } + }; +});;System.register('flarum/tags/components/EditTagModal', ['flarum/components/Modal', 'flarum/components/Button', 'flarum/utils/string', 'flarum/tags/helpers/tagLabel'], function (_export) { + + /** + * The `EditTagModal` component shows a modal dialog which allows the user + * to create or edit a tag. + */ + 'use strict'; + + var Modal, Button, slug, tagLabel, EditTagModal; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + return { + setters: [function (_flarumComponentsModal) { + Modal = _flarumComponentsModal['default']; + }, function (_flarumComponentsButton) { + Button = _flarumComponentsButton['default']; + }, function (_flarumUtilsString) { + slug = _flarumUtilsString.slug; + }, function (_flarumTagsHelpersTagLabel) { + tagLabel = _flarumTagsHelpersTagLabel['default']; + }], + execute: function () { + EditTagModal = (function (_Modal) { + _inherits(EditTagModal, _Modal); + + function EditTagModal() { + _classCallCheck(this, EditTagModal); + + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + _get(Object.getPrototypeOf(EditTagModal.prototype), 'constructor', this).apply(this, 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() || ''); + this.isHidden = m.prop(this.tag.isHidden() || false); + } + + _createClass(EditTagModal, [{ + key: 'className', + value: function className() { + return 'EditTagModal Modal--small'; + } + }, { + key: 'title', + value: function title() { + return this.name() ? tagLabel({ + name: this.name, + color: this.color + }) : 'Create Tag'; + } + }, { + key: 'content', + value: function content() { + var _this = this; + + return m( + 'div', + { className: 'Modal-body' }, + m( + 'div', + { className: 'Form' }, + m( + 'div', + { className: 'Form-group' }, + m( + 'label', + null, + 'Name' + ), + m('input', { className: 'FormControl', placeholder: 'Name', value: this.name(), oninput: function (e) { + _this.name(e.target.value); + _this.slug(slug(e.target.value)); + } }) + ), + m( + 'div', + { className: 'Form-group' }, + m( + 'label', + null, + 'Slug' + ), + m('input', { className: 'FormControl', value: this.slug(), oninput: m.withAttr('value', this.slug) }) + ), + m( + 'div', + { className: 'Form-group' }, + m( + 'label', + null, + 'Description' + ), + m('textarea', { className: 'FormControl', value: this.description(), oninput: m.withAttr('value', this.description) }) + ), + m( + 'div', + { className: 'Form-group' }, + m( + 'label', + null, + 'Color' + ), + m('input', { className: 'FormControl', placeholder: '#aaaaaa', value: this.color(), oninput: m.withAttr('value', this.color) }) + ), + m( + 'div', + { className: 'Form-group' }, + m( + 'div', + null, + m( + 'label', + { className: 'checkbox' }, + m('input', { type: 'checkbox', value: '1', checked: this.isHidden(), onchange: m.withAttr('checked', this.isHidden) }), + 'Hide from All Discussions' + ) + ) + ), + m( + 'div', + { className: 'Form-group' }, + Button.component({ + type: 'submit', + className: 'Button Button--primary EditTagModal-save', + loading: this.loading, + children: 'Save Changes' + }), + this.tag.exists ? m( + 'button', + { type: 'button', className: 'Button EditTagModal-delete', onclick: this['delete'].bind(this) }, + 'Delete Tag' + ) : '' + ) + ) + ); + } + }, { + key: 'onsubmit', + value: function onsubmit(e) { + var _this2 = this; + + e.preventDefault(); + + this.loading = true; + + this.tag.save({ + name: this.name(), + slug: this.slug(), + description: this.description(), + color: this.color(), + isHidden: this.isHidden() + }).then(function () { + return _this2.hide(); + }, function () { + _this2.loading = false; + m.redraw(); + }); + } + }, { + key: 'delete', + value: function _delete() { + if (confirm('Are you sure you want to delete this tag? The tag\'s discussions will NOT be deleted.')) { + this.tag['delete']().then(function () { + return m.redraw(); + }); + this.hide(); + } + } + }]); + + return EditTagModal; + })(Modal); + + _export('default', EditTagModal); + } + }; +});;System.register('flarum/tags/components/TagSettingsModal', ['flarum/components/SettingsModal'], function (_export) { + 'use strict'; + + var SettingsModal, TagSettingsModal; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + return { + setters: [function (_flarumComponentsSettingsModal) { + SettingsModal = _flarumComponentsSettingsModal['default']; + }], + execute: function () { + TagSettingsModal = (function (_SettingsModal) { + _inherits(TagSettingsModal, _SettingsModal); + + function TagSettingsModal() { + _classCallCheck(this, TagSettingsModal); + + _get(Object.getPrototypeOf(TagSettingsModal.prototype), 'constructor', this).apply(this, arguments); + } + + _createClass(TagSettingsModal, [{ + key: 'setMinTags', + value: function setMinTags(minTags, maxTags, value) { + minTags(value); + maxTags(Math.max(value, maxTags())); + } + }, { + key: 'className', + value: function className() { + return 'TagSettingsModal Modal--small'; + } + }, { + key: 'title', + value: function title() { + return 'Tag Settings'; + } + }, { + key: 'form', + value: function form() { + var minPrimaryTags = this.setting('tags.min_primary_tags', 0); + var maxPrimaryTags = this.setting('tags.max_primary_tags', 0); + + var minSecondaryTags = this.setting('tags.min_secondary_tags', 0); + var maxSecondaryTags = this.setting('tags.max_secondary_tags', 0); + + return [m( + 'div', + { className: 'Form-group' }, + m( + 'label', + null, + 'Required Number of Primary Tags' + ), + m( + 'div', + { className: 'helpText' }, + 'Enter the minimum and maximum number of primary tags that may be applied to a discussion.' + ), + m( + 'div', + { className: 'TagSettingsModal-rangeInput' }, + m('input', { className: 'FormControl', + type: 'number', + min: '0', + value: minPrimaryTags(), + oninput: m.withAttr('value', this.setMinTags.bind(this, minPrimaryTags, maxPrimaryTags)) }), + ' to ', + m('input', { className: 'FormControl', + type: 'number', + min: minPrimaryTags(), + bidi: maxPrimaryTags }) + ) + ), m( + 'div', + { className: 'Form-group' }, + m( + 'label', + null, + 'Required Number of Secondary Tags' + ), + m( + 'div', + { className: 'helpText' }, + 'Enter the minimum and maximum number of secondary tags that may be applied to a discussion.' + ), + m( + 'div', + { className: 'TagSettingsModal-rangeInput' }, + m('input', { className: 'FormControl', + type: 'number', + min: '0', + value: minSecondaryTags(), + oninput: m.withAttr('value', this.setMinTags.bind(this, minSecondaryTags, maxSecondaryTags)) }), + ' to ', + m('input', { className: 'FormControl', + type: 'number', + min: minSecondaryTags(), + bidi: maxSecondaryTags }) + ) + )]; + } + }]); + + return TagSettingsModal; + })(SettingsModal); + + _export('default', TagSettingsModal); + } + }; +});;System.register('flarum/tags/components/TagsPage', ['flarum/Component', 'flarum/components/Button', 'flarum/tags/components/EditTagModal', 'flarum/tags/components/TagSettingsModal', 'flarum/tags/helpers/tagIcon', 'flarum/tags/utils/sortTags'], function (_export) { + 'use strict'; + + var Component, Button, EditTagModal, TagSettingsModal, tagIcon, sortTags, TagsPage; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + function tagItem(tag) { + return m( + 'li', + { 'data-id': tag.id(), style: { color: tag.color() } }, + m( + 'div', + { className: 'TagListItem-info' }, + tagIcon(tag), + m( + 'span', + { className: 'TagListItem-name' }, + tag.name() + ), + Button.component({ + className: 'Button Button--link', + icon: 'pencil', + onclick: function onclick() { + return app.modal.show(new EditTagModal({ tag: tag })); + } + }) + ), + !tag.isChild() && tag.position() !== null ? m( + 'ol', + { className: 'TagListItem-children' }, + sortTags(app.store.all('tags')).filter(function (child) { + return child.parent() === tag; + }).map(tagItem) + ) : '' + ); + } + + return { + setters: [function (_flarumComponent) { + Component = _flarumComponent['default']; + }, function (_flarumComponentsButton) { + Button = _flarumComponentsButton['default']; + }, function (_flarumTagsComponentsEditTagModal) { + EditTagModal = _flarumTagsComponentsEditTagModal['default']; + }, function (_flarumTagsComponentsTagSettingsModal) { + TagSettingsModal = _flarumTagsComponentsTagSettingsModal['default']; + }, function (_flarumTagsHelpersTagIcon) { + tagIcon = _flarumTagsHelpersTagIcon['default']; + }, function (_flarumTagsUtilsSortTags) { + sortTags = _flarumTagsUtilsSortTags['default']; + }], + execute: function () { + TagsPage = (function (_Component) { + _inherits(TagsPage, _Component); + + function TagsPage() { + _classCallCheck(this, TagsPage); + + _get(Object.getPrototypeOf(TagsPage.prototype), 'constructor', this).apply(this, arguments); + } + + _createClass(TagsPage, [{ + key: 'view', + value: function view() { + return m( + 'div', + { className: 'TagsPage' }, + m( + 'div', + { className: 'TagsPage-header' }, + m( + 'div', + { className: 'container' }, + m( + 'p', + null, + '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: function onclick() { + return app.modal.show(new EditTagModal()); + } + }), + Button.component({ + className: 'Button', + children: 'Settings', + onclick: function onclick() { + return app.modal.show(new TagSettingsModal()); + } + }) + ) + ), + m( + 'div', + { className: 'TagsPage-list' }, + m( + 'div', + { className: 'container' }, + m( + 'div', + { className: 'TagGroup' }, + m( + 'label', + null, + 'Primary Tags' + ), + m( + 'ol', + { className: 'TagList TagList--primary' }, + sortTags(app.store.all('tags')).filter(function (tag) { + return tag.position() !== null && !tag.isChild(); + }).map(tagItem) + ) + ), + m( + 'div', + { className: 'TagGroup' }, + m( + 'label', + null, + 'Secondary Tags' + ), + m( + 'ul', + { className: 'TagList' }, + app.store.all('tags').filter(function (tag) { + return tag.position() === null; + }).sort(function (a, b) { + return a.name().localeCompare(b.name()); + }).map(tagItem) + ) + ) + ) + ) + ); + } + }, { + key: 'config', + value: function config() { + var _this = this; + + this.$('ol, ul').sortable({ connectWith: 'primary' }).on('sortupdate', function (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. + var 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(function (tag, i) { + var parent = app.store.getById('tags', tag.id); + parent.pushData({ + attributes: { + position: i, + isChild: false + }, + relationships: { parent: null } + }); + + tag.children.forEach(function (child, j) { + app.store.getById('tags', child).pushData({ + attributes: { + position: j, + isChild: true + }, + relationships: { parent: parent } + }); + }); + }); + + app.request({ + url: app.forum.attribute('apiUrl') + '/tags/order', + method: 'POST', + data: { order: 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(); + }); + } + }]); + + return TagsPage; + })(Component); + + _export('default', TagsPage); + } + }; +}); \ No newline at end of file diff --git a/extensions/tags/js/forum/dist/extension.js b/extensions/tags/js/forum/dist/extension.js index 39bde0a9b..0057c89d6 100644 --- a/extensions/tags/js/forum/dist/extension.js +++ b/extensions/tags/js/forum/dist/extension.js @@ -1 +1,1355 @@ -System.register("flarum/tags/helpers/tagIcon",[],function(t){"use strict";function e(t){var e=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];return e.className="icon TagIcon "+(e.className||""),t?(e.style=e.style||{},e.style.backgroundColor=t.color()):e.className+=" untagged",m("span",e)}return t("default",e),{setters:[],execute:function(){}}}),System.register("flarum/tags/helpers/tagLabel",["flarum/utils/extract"],function(t){"use strict";function e(t){var e=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];e.style=e.style||{},e.className="TagLabel "+(e.className||"");var a=n(e,"link");if(t){var r=t.color();r&&(e.style.backgroundColor=e.style.color=r,e.className+=" colored"),a&&(e.title=t.description()||"",e.href=app.route("tag",{tags:t.slug()}),e.config=m.route)}else e.className+=" untagged";return m(a?"a":"span",e,m("span",{className:"TagLabel-text"},t?t.name():app.trans("tags.deleted")))}var n;return t("default",e),{setters:[function(t){n=t["default"]}],execute:function(){}}}),System.register("flarum/tags/helpers/tagsLabel",["flarum/utils/extract","flarum/tags/helpers/tagLabel","flarum/tags/utils/sortTags"],function(t){"use strict";function e(t){var e=arguments.length<=1||void 0===arguments[1]?{}:arguments[1],o=[],s=n(e,"link");return e.className="TagsLabel "+(e.className||""),t?r(t).forEach(function(e){(e||1===t.length)&&o.push(a(e,{link:s}))}):o.push(a()),m("span",e,o)}var n,a,r;return t("default",e),{setters:[function(t){n=t["default"]},function(t){a=t["default"]},function(t){r=t["default"]}],execute:function(){}}}),System.register("flarum/tags/models/Tag",["flarum/Model","flarum/utils/mixin","flarum/utils/computed"],function(t){"use strict";function e(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var a,r,o,s,i=function(t,e,n){for(var a=!0;a;){var r=t,o=e,s=n;i=c=u=void 0,a=!1,null===r&&(r=Function.prototype);var i=Object.getOwnPropertyDescriptor(r,o);if(void 0!==i){if("value"in i)return i.value;var u=i.get;return void 0===u?void 0:u.call(s)}var c=Object.getPrototypeOf(r);if(null===c)return void 0;t=c,e=o,n=s,a=!0}};return{setters:[function(t){a=t["default"]},function(t){r=t["default"]},function(t){o=t["default"]}],execute:function(){s=function(t){function a(){e(this,a),i(Object.getPrototypeOf(a.prototype),"constructor",this).apply(this,arguments)}return n(a,t),a}(r(a,{name:a.attribute("name"),slug:a.attribute("slug"),description:a.attribute("description"),color:a.attribute("color"),backgroundUrl:a.attribute("backgroundUrl"),backgroundMode:a.attribute("backgroundMode"),position:a.attribute("position"),parent:a.hasOne("parent"),defaultSort:a.attribute("defaultSort"),isChild:a.attribute("isChild"),isHidden:a.attribute("isHidden"),discussionsCount:a.attribute("discussionsCount"),lastTime:a.attribute("lastTime",a.transformDate),lastDiscussion:a.hasOne("lastDiscussion"),isRestricted:a.attribute("isRestricted"),canStartDiscussion:a.attribute("canStartDiscussion"),isPrimary:o("position","parent",function(t,e){return null!==t&&e===!1})})),t("default",s)}}}),System.register("flarum/tags/utils/sortTags",[],function(t){"use strict";function e(t){return t.slice(0).sort(function(t,e){var n=t.position(),a=e.position(),r=t.parent(),o=e.parent();return null===n&&null===a?e.discussionsCount()-t.discussionsCount():null===a?-1:null===n?1:r===o?n-a:r?r===e?1:r.position()-a:o?o===t?-1:n-o.position():0})}return t("default",e),{setters:[],execute:function(){}}}),System.register("flarum/tags/addTagComposer",["flarum/extend","flarum/components/IndexPage","flarum/components/DiscussionComposer","flarum/tags/components/TagDiscussionModal","flarum/tags/helpers/tagsLabel"],function(t){"use strict";var e,n,a,r,o,s;return{setters:[function(t){e=t.extend,n=t.override},function(t){a=t["default"]},function(t){r=t["default"]},function(t){o=t["default"]},function(t){s=t["default"]}],execute:function(){t("default",function(){e(a.prototype,"composeNewDiscussion",function(t){var e=app.store.getBy("tags","slug",this.params().tags);e&&t.then(function(t){return t.tags=[e]})}),r.prototype.tags=[],r.prototype.chooseTags=function(){var t=this;app.modal.show(new o({selectedTags:this.tags.slice(0),onsubmit:function(e){t.tags=e,t.$("textarea").focus()}}))},e(r.prototype,"headerItems",function(t){t.add("tags",m("a",{className:"DiscussionComposer-changeTags",onclick:this.chooseTags.bind(this)},this.tags.length?s(this.tags):m("span",{className:"TagLabel untagged"},app.trans("tags.tag_new_discussion_link"))),10)}),n(r.prototype,"onsubmit",function(t){var e=this;this.tags.length?t():app.modal.show(new o({selectedTags:[],onsubmit:function(n){e.tags=n,t()}}))}),e(r.prototype,"data",function(t){t.relationships=t.relationships||{},t.relationships.tags=this.tags})})}}}),System.register("flarum/tags/addTagControl",["flarum/extend","flarum/utils/DiscussionControls","flarum/components/Button","flarum/tags/components/TagDiscussionModal"],function(t){"use strict";var e,n,a,r;return{setters:[function(t){e=t.extend},function(t){n=t["default"]},function(t){a=t["default"]},function(t){r=t["default"]}],execute:function(){t("default",function(){e(n,"moderationControls",function(t,e){e.canTag()&&t.add("tags",a.component({children:app.trans("tags.edit_discussion_tags_link"),icon:"tag",onclick:function(){return app.modal.show(new r({discussion:e}))}}))})})}}}),System.register("flarum/tags/addTagFilter",["flarum/extend","flarum/components/IndexPage","flarum/components/DiscussionList","flarum/tags/components/TagHero"],function(t){"use strict";var e,n,a,r,o;return{setters:[function(t){e=t.extend,n=t.override},function(t){a=t["default"]},function(t){r=t["default"]},function(t){o=t["default"]}],execute:function(){t("default",function(){a.prototype.currentTag=function(){var t=this.params().tags;return t?app.store.getBy("tags","slug",t):void 0},n(a.prototype,"hero",function(t){var e=this.currentTag();return e?o.component({tag:e}):t()}),e(a.prototype,"sidebarItems",function(t){var e=this.currentTag();if(e){var n=e.color();n&&(t.newDiscussion.content.props.style={backgroundColor:n})}}),e(a.prototype,"params",function(t){t.tags=m.route.param("tags")}),e(r.prototype,"requestParams",function(t){t.include.push("tags"),this.props.params.tags&&(t.filter.q=(t.filter.q||"")+" tag:"+this.props.params.tags)})})}}}),System.register("flarum/tags/addTagLabels",["flarum/extend","flarum/components/DiscussionListItem","flarum/components/DiscussionPage","flarum/components/DiscussionHero","flarum/tags/helpers/tagsLabel","flarum/tags/utils/sortTags"],function(t){"use strict";var e,n,a,r,o,s;return{setters:[function(t){e=t.extend},function(t){n=t["default"]},function(t){a=t["default"]},function(t){r=t["default"]},function(t){o=t["default"]},function(t){s=t["default"]}],execute:function(){t("default",function(){e(n.prototype,"infoItems",function(t){var e=this.props.discussion.tags();e&&e.length&&t.add("tags",o(e),10)}),e(a.prototype,"params",function(t){t.include.push("tags")}),e(r.prototype,"view",function(t){var e=s(this.props.discussion.tags());if(e&&e.length){var n=e[0].color();n&&(t.attrs.style={backgroundColor:n},t.attrs.className+=" DiscussionHero--colored")}}),e(r.prototype,"items",function(t){var e=this.props.discussion.tags();e&&e.length&&t.add("tags",o(e,{link:!0}),5)})})}}}),System.register("flarum/tags/addTagList",["flarum/extend","flarum/components/IndexPage","flarum/components/Separator","flarum/components/LinkButton","flarum/tags/components/TagLinkButton","flarum/tags/components/TagsPage","flarum/tags/utils/sortTags"],function(t){"use strict";var e,n,a,r,o,s,i;return{setters:[function(t){e=t.extend},function(t){n=t["default"]},function(t){a=t["default"]},function(t){r=t["default"]},function(t){o=t["default"]},function(t){s=t["default"]},function(t){i=t["default"]}],execute:function(){t("default",function(){e(n.prototype,"navItems",function(t){if(t.add("tags",r.component({icon:"th-large",children:app.trans("tags.tags"),href:app.route("tags")}),-10),!(app.current instanceof s)){t.add("separator",a.component(),-10);var e=this.stickyParams(),n=app.store.all("tags"),u=this.currentTag(),c=function(n){var a=u===n;!a&&u&&(a=u.parent()===n),t.add("tag"+n.id(),o.component({tag:n,params:e,active:a}),-10)};i(n).filter(function(t){return null!==t.position()&&(!t.isChild()||u&&(t.parent()===u||t.parent()===u.parent()))}).forEach(c);var l=n.filter(function(t){return null===t.position()}).sort(function(t,e){return e.discussionsCount()-t.discussionsCount()});l.splice(0,3).forEach(c),l.length&&t.add("moreTags",r.component({children:app.trans("tags.more"),href:app.route("tags")}),-10)}})})}}}),System.register("flarum/tags/main",["flarum/Model","flarum/models/Discussion","flarum/components/IndexPage","flarum/tags/models/Tag","flarum/tags/components/TagsPage","flarum/tags/components/DiscussionTaggedPost","flarum/tags/addTagList","flarum/tags/addTagFilter","flarum/tags/addTagLabels","flarum/tags/addTagControl","flarum/tags/addTagComposer"],function(t){"use strict";var e,n,a,r,o,s,i,u,c,l,f;return{setters:[function(t){e=t["default"]},function(t){n=t["default"]},function(t){a=t["default"]},function(t){r=t["default"]},function(t){o=t["default"]},function(t){s=t["default"]},function(t){i=t["default"]},function(t){u=t["default"]},function(t){c=t["default"]},function(t){l=t["default"]},function(t){f=t["default"]}],execute:function(){app.initializers.add("tags",function(t){t.routes.tags={path:"/tags",component:o.component()},t.routes.tag={path:"/t/:tags",component:a.component()},t.route.tag=function(e){return t.route("tag",{tags:e.slug()})},t.postComponents.discussionTagged=s,t.store.models.tags=r,n.prototype.tags=e.hasMany("tags"),n.prototype.canTag=e.attribute("canTag"),i(),u(),c(),l(),f()})}}}),System.register("flarum/tags/components/DiscussionTaggedPost",["flarum/components/EventPost","flarum/helpers/punctuate","flarum/tags/helpers/tagsLabel"],function(t){"use strict";function e(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var a,r,o,s,i=function(){function t(t,e){for(var n=0;nr;r++)n[r]=arguments[r];g(Object.getPrototypeOf(a.prototype),"constructor",this).apply(this,n),this.tags=f(app.store.all("tags").filter(function(t){return t.canStartDiscussion()})),this.selected=[],this.filter=m.prop(""),this.index=this.tags[0].id(),this.focused=!1,this.props.selectedTags?this.props.selectedTags.map(this.addTag.bind(this)):this.props.discussion&&this.props.discussion.tags().map(this.addTag.bind(this)),this.minPrimary=app.forum.attribute("minPrimaryTags"),this.maxPrimary=app.forum.attribute("maxPrimaryTags"),this.minSecondary=app.forum.attribute("minSecondaryTags"),this.maxSecondary=app.forum.attribute("maxSecondaryTags")}return n(a,t),d(a,[{key:"primaryCount",value:function(){return this.selected.filter(function(t){return t.isPrimary()}).length}},{key:"secondaryCount",value:function(){return this.selected.filter(function(t){return!t.isPrimary()}).length}},{key:"addTag",value:function(t){if(t.canStartDiscussion()){var e=t.parent();if(e){var n=this.selected.indexOf(e);-1===n&&this.selected.push(e)}this.selected.push(t)}}},{key:"removeTag",value:function(t){var e=this.selected.indexOf(t);-1!==e&&(this.selected.splice(e,1),this.selected.filter(function(e){return e.parent()===t}).forEach(this.removeTag.bind(this)))}},{key:"className",value:function(){return"TagDiscussionModal"}},{key:"title",value:function(){return this.props.discussion?app.trans("tags.edit_discussion_tags_title",{title:m("em",null,this.props.discussion.title())}):app.trans("tags.tag_new_discussion_title")}},{key:"getInstruction",value:function(t,e){return t=app.forum.attribute("maxPrimaryTags")&&(e=e.filter(function(e){return!e.isPrimary()||-1!==t.selected.indexOf(e)})),r>=app.forum.attribute("maxSecondaryTags")&&(e=e.filter(function(e){return e.isPrimary()||-1!==t.selected.indexOf(e)})),n&&(e=e.filter(function(t){return t.name().substr(0,n.length).toLowerCase()===n})),-1===e.indexOf(this.index)&&(this.index=e[0]),[m("div",{className:"Modal-body"},m("div",{className:"TagDiscussionModal-form"},m("div",{className:"TagDiscussionModal-form-input"},m("div",{className:"TagsInput FormControl "+(this.focused?"focus":"")},m("span",{className:"TagsInput-selected"},this.selected.map(function(e){return m("span",{className:"TagsInput-tag",onclick:function(){t.removeTag(e),t.onready()}},c(e))})),m("input",{className:"FormControl",placeholder:u(this.getInstruction(a,r)),value:this.filter(),oninput:m.withAttr("value",this.filter),onkeydown:this.onkeydown.bind(this),onfocus:function(){return t.focused=!0},onblur:function(){return t.focused=!1}}))),m("div",{className:"TagDiscussionModal-form-submit App-primaryControl"},o.component({type:"submit",className:"Button Button--primary",disabled:a li")}},{key:"getCurrentNumericIndex",value:function(){return this.selectableItems().index(this.getItem(this.index))}},{key:"getItem",value:function(t){return this.selectableItems().filter('[data-index="'+t.id()+'"]')}},{key:"setIndex",value:function(t,e){var n=this.selectableItems(),a=n.parent();0>t?t=n.length-1:t>=n.length&&(t=0);var r=n.eq(t);if(this.index=app.store.getById("tags",r.attr("data-index")),m.redraw(),e){var o=a.scrollTop(),s=a.offset().top,i=s+a.outerHeight(),u=r.offset().top,c=u+r.outerHeight(),l=void 0;s>u?l=o-s+u-parseInt(a.css("padding-top"),10):c>i&&(l=o-i+c+parseInt(a.css("padding-bottom"),10)),"undefined"!=typeof l&&a.stop(!0).animate({scrollTop:l},100)}}},{key:"onsubmit",value:function(t){t.preventDefault();var e=this.props.discussion,n=this.selected;e&&e.save({relationships:{tags:n}}).then(function(){app.current instanceof r&&app.current.stream.update(),m.redraw()}),this.props.onsubmit&&this.props.onsubmit(n),app.modal.close(),m.redraw.strategy("none")}}]),a}(a),t("default",p)}}}),System.register("flarum/tags/components/TagHero",["flarum/Component"],function(t){"use strict";function e(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var a,r,o=function(){function t(t,e){for(var n=0;nr;r++)n[r]=arguments[r];f(Object.getPrototypeOf(a.prototype),"constructor",this).apply(this,n),this.tags=u(app.store.all("tags").filter(function(t){return!t.parent()})),app.current=this,app.history.push("tags"),app.drawer.hide(),app.modal.close()}return n(a,t),l(a,[{key:"view",value:function(){var t=this.tags.filter(function(t){return null!==t.position()}),e=this.tags.filter(function(t){return null===t.position()});return m("div",{className:"TagsPage"},r.prototype.hero(),m("div",{className:"container"},m("nav",{className:"TagsPage-nav IndexPage-nav sideNav",config:r.prototype.affixSidebar},m("ul",null,o(r.prototype.sidebarItems().toArray()))),m("div",{className:"TagsPage-content sideNavOffset"},m("ul",{className:"TagTiles"},t.map(function(t){var e=t.lastDiscussion(),n=app.store.all("tags").filter(function(e){return e.parent()===t});return m("li",{className:"TagTile "+(t.color()?"colored":""),style:{backgroundColor:t.color()}},m("a",{className:"TagTile-info",href:app.route.tag(t),config:m.route},m("h3",{className:"TagTile-name"},t.name()),m("p",{className:"TagTile-description"},t.description()),n?m("div",{className:"TagTile-children"},n.map(function(t){return m("a",{href:app.route.tag(t),config:function(t,e){e||($(t).on("click",function(t){return t.stopPropagation()}),m.route.apply(this,arguments))}},t.name())})):""),e?m("a",{className:"TagTile-lastDiscussion",href:app.route.discussion(e,e.lastPostNumber()),config:m.route},m("span",{className:"TagTile-lastDiscussion-title"},e.title()),s(e.lastTime())):m("span",{className:"TagTile-lastDiscussion"}))})),e.length?m("div",{className:"TagCloud"},e.map(function(t){t.color();return[i(t,{link:!0})," "]})):"")))}}]),a}(a),t("default",c)}}}); \ No newline at end of file +System.register('flarum/tags/models/Tag', ['flarum/Model', 'flarum/utils/mixin', 'flarum/utils/computed'], function (_export) { + 'use strict'; + + var Model, mixin, computed, Tag; + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + return { + setters: [function (_flarumModel) { + Model = _flarumModel['default']; + }, function (_flarumUtilsMixin) { + mixin = _flarumUtilsMixin['default']; + }, function (_flarumUtilsComputed) { + computed = _flarumUtilsComputed['default']; + }], + execute: function () { + Tag = (function (_mixin) { + _inherits(Tag, _mixin); + + function Tag() { + _classCallCheck(this, Tag); + + _get(Object.getPrototypeOf(Tag.prototype), 'constructor', this).apply(this, arguments); + } + + return Tag; + })(mixin(Model, { + name: Model.attribute('name'), + slug: Model.attribute('slug'), + description: Model.attribute('description'), + + color: Model.attribute('color'), + backgroundUrl: Model.attribute('backgroundUrl'), + backgroundMode: Model.attribute('backgroundMode'), + + position: Model.attribute('position'), + parent: Model.hasOne('parent'), + defaultSort: Model.attribute('defaultSort'), + isChild: Model.attribute('isChild'), + isHidden: Model.attribute('isHidden'), + + discussionsCount: Model.attribute('discussionsCount'), + lastTime: Model.attribute('lastTime', Model.transformDate), + lastDiscussion: Model.hasOne('lastDiscussion'), + + isRestricted: Model.attribute('isRestricted'), + canStartDiscussion: Model.attribute('canStartDiscussion'), + + isPrimary: computed('position', 'parent', function (position, parent) { + return position !== null && parent === false; + }) + })); + + _export('default', Tag); + } + }; +});;System.register('flarum/tags/helpers/tagIcon', [], function (_export) { + 'use strict'; + + _export('default', tagIcon); + + function tagIcon(tag) { + var attrs = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + attrs.className = 'icon TagIcon ' + (attrs.className || ''); + + if (tag) { + attrs.style = attrs.style || {}; + attrs.style.backgroundColor = tag.color(); + } else { + attrs.className += ' untagged'; + } + + return m('span', attrs); + } + + return { + setters: [], + execute: function () {} + }; +});;System.register('flarum/tags/helpers/tagLabel', ['flarum/utils/extract'], function (_export) { + 'use strict'; + + var extract; + + _export('default', tagLabel); + + function tagLabel(tag) { + var attrs = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + attrs.style = attrs.style || {}; + attrs.className = 'TagLabel ' + (attrs.className || ''); + + var link = extract(attrs, 'link'); + + if (tag) { + var color = tag.color(); + if (color) { + attrs.style.backgroundColor = attrs.style.color = color; + attrs.className += ' colored'; + } + + if (link) { + attrs.title = tag.description() || ''; + attrs.href = app.route('tag', { tags: tag.slug() }); + attrs.config = m.route; + } + } else { + attrs.className += ' untagged'; + } + + return m(link ? 'a' : 'span', attrs, m( + 'span', + { className: 'TagLabel-text' }, + tag ? tag.name() : app.trans('tags.deleted') + )); + } + + return { + setters: [function (_flarumUtilsExtract) { + extract = _flarumUtilsExtract['default']; + }], + execute: function () {} + }; +});;System.register('flarum/tags/helpers/tagsLabel', ['flarum/utils/extract', 'flarum/tags/helpers/tagLabel', 'flarum/tags/utils/sortTags'], function (_export) { + 'use strict'; + + var extract, tagLabel, sortTags; + + _export('default', tagsLabel); + + function tagsLabel(tags) { + var attrs = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + var children = []; + var link = extract(attrs, 'link'); + + attrs.className = 'TagsLabel ' + (attrs.className || ''); + + if (tags) { + sortTags(tags).forEach(function (tag) { + if (tag || tags.length === 1) { + children.push(tagLabel(tag, { link: link })); + } + }); + } else { + children.push(tagLabel()); + } + + return m( + 'span', + attrs, + children + ); + } + + return { + setters: [function (_flarumUtilsExtract) { + extract = _flarumUtilsExtract['default']; + }, function (_flarumTagsHelpersTagLabel) { + tagLabel = _flarumTagsHelpersTagLabel['default']; + }, function (_flarumTagsUtilsSortTags) { + sortTags = _flarumTagsUtilsSortTags['default']; + }], + execute: function () {} + }; +});;System.register("flarum/tags/utils/sortTags", [], function (_export) { + "use strict"; + + _export("default", sortTags); + + function sortTags(tags) { + return tags.slice(0).sort(function (a, b) { + var aPos = a.position(); + var bPos = b.position(); + + var aParent = a.parent(); + var bParent = b.parent(); + + if (aPos === null && bPos === null) { + return b.discussionsCount() - a.discussionsCount(); + } else if (bPos === null) { + return -1; + } else if (aPos === null) { + return 1; + } else if (aParent === bParent) { + return aPos - bPos; + } else if (aParent) { + return aParent === b ? 1 : aParent.position() - bPos; + } else if (bParent) { + return bParent === a ? -1 : aPos - bParent.position(); + } + + return 0; + }); + } + + return { + setters: [], + execute: function () {} + }; +});;System.register('flarum/tags/addTagComposer', ['flarum/extend', 'flarum/components/IndexPage', 'flarum/components/DiscussionComposer', 'flarum/tags/components/TagDiscussionModal', 'flarum/tags/helpers/tagsLabel'], function (_export) { + 'use strict'; + + var extend, override, IndexPage, DiscussionComposer, TagDiscussionModal, tagsLabel; + return { + setters: [function (_flarumExtend) { + extend = _flarumExtend.extend; + override = _flarumExtend.override; + }, function (_flarumComponentsIndexPage) { + IndexPage = _flarumComponentsIndexPage['default']; + }, function (_flarumComponentsDiscussionComposer) { + DiscussionComposer = _flarumComponentsDiscussionComposer['default']; + }, function (_flarumTagsComponentsTagDiscussionModal) { + TagDiscussionModal = _flarumTagsComponentsTagDiscussionModal['default']; + }, function (_flarumTagsHelpersTagsLabel) { + tagsLabel = _flarumTagsHelpersTagsLabel['default']; + }], + execute: function () { + _export('default', function () { + extend(IndexPage.prototype, 'composeNewDiscussion', function (promise) { + var tag = app.store.getBy('tags', 'slug', this.params().tags); + + if (tag) { + promise.then(function (component) { + return component.tags = [tag]; + }); + } + }); + + // Add tag-selection abilities to the discussion composer. + DiscussionComposer.prototype.tags = []; + DiscussionComposer.prototype.chooseTags = function () { + var _this = this; + + app.modal.show(new TagDiscussionModal({ + selectedTags: this.tags.slice(0), + onsubmit: function onsubmit(tags) { + _this.tags = tags; + _this.$('textarea').focus(); + } + })); + }; + + // Add a tag-selection menu to the discussion composer's header, after the + // title. + extend(DiscussionComposer.prototype, 'headerItems', function (items) { + items.add('tags', m( + 'a', + { className: 'DiscussionComposer-changeTags', onclick: this.chooseTags.bind(this) }, + this.tags.length ? tagsLabel(this.tags) : m( + 'span', + { className: 'TagLabel untagged' }, + app.trans('tags.tag_new_discussion_link') + ) + ), 10); + }); + + override(DiscussionComposer.prototype, 'onsubmit', function (original) { + var _this2 = this; + + if (!this.tags.length) { + app.modal.show(new TagDiscussionModal({ + selectedTags: [], + onsubmit: function onsubmit(tags) { + _this2.tags = tags; + original(); + } + })); + } else { + original(); + } + }); + + // Add the selected tags as data to submit to the server. + extend(DiscussionComposer.prototype, 'data', function (data) { + data.relationships = data.relationships || {}; + data.relationships.tags = this.tags; + }); + }); + } + }; +});;System.register('flarum/tags/addTagControl', ['flarum/extend', 'flarum/utils/DiscussionControls', 'flarum/components/Button', 'flarum/tags/components/TagDiscussionModal'], function (_export) { + 'use strict'; + + var extend, DiscussionControls, Button, TagDiscussionModal; + return { + setters: [function (_flarumExtend) { + extend = _flarumExtend.extend; + }, function (_flarumUtilsDiscussionControls) { + DiscussionControls = _flarumUtilsDiscussionControls['default']; + }, function (_flarumComponentsButton) { + Button = _flarumComponentsButton['default']; + }, function (_flarumTagsComponentsTagDiscussionModal) { + TagDiscussionModal = _flarumTagsComponentsTagDiscussionModal['default']; + }], + execute: function () { + _export('default', function () { + // Add a control allowing the discussion to be moved to another category. + extend(DiscussionControls, 'moderationControls', function (items, discussion) { + if (discussion.canTag()) { + items.add('tags', Button.component({ + children: app.trans('tags.edit_discussion_tags_link'), + icon: 'tag', + onclick: function onclick() { + return app.modal.show(new TagDiscussionModal({ discussion: discussion })); + } + })); + } + }); + }); + } + }; +});;System.register('flarum/tags/addTagFilter', ['flarum/extend', 'flarum/components/IndexPage', 'flarum/components/DiscussionList', 'flarum/tags/components/TagHero'], function (_export) { + 'use strict'; + + var extend, override, IndexPage, DiscussionList, TagHero; + return { + setters: [function (_flarumExtend) { + extend = _flarumExtend.extend; + override = _flarumExtend.override; + }, function (_flarumComponentsIndexPage) { + IndexPage = _flarumComponentsIndexPage['default']; + }, function (_flarumComponentsDiscussionList) { + DiscussionList = _flarumComponentsDiscussionList['default']; + }, function (_flarumTagsComponentsTagHero) { + TagHero = _flarumTagsComponentsTagHero['default']; + }], + execute: function () { + _export('default', function () { + IndexPage.prototype.currentTag = function () { + var slug = this.params().tags; + + if (slug) return app.store.getBy('tags', 'slug', slug); + }; + + // If currently viewing a tag, insert a tag hero at the top of the view. + override(IndexPage.prototype, 'hero', function (original) { + var tag = this.currentTag(); + + if (tag) return TagHero.component({ tag: tag }); + + return original(); + }); + + // If currently viewing a tag, restyle the 'new discussion' button to use + // the tag's color. + extend(IndexPage.prototype, 'sidebarItems', function (items) { + var tag = this.currentTag(); + + if (tag) { + var color = tag.color(); + + if (color) { + items.newDiscussion.content.props.style = { backgroundColor: color }; + } + } + }); + + // Add a parameter for the IndexPage to pass on to the DiscussionList that + // will let us filter discussions by tag. + extend(IndexPage.prototype, 'params', function (params) { + params.tags = m.route.param('tags'); + }); + + // Translate that parameter into a gambit appended to the search query. + extend(DiscussionList.prototype, 'requestParams', function (params) { + params.include.push('tags'); + + if (this.props.params.tags) { + params.filter.q = (params.filter.q || '') + ' tag:' + this.props.params.tags; + } + }); + }); + } + }; +});;System.register('flarum/tags/addTagLabels', ['flarum/extend', 'flarum/components/DiscussionListItem', 'flarum/components/DiscussionPage', 'flarum/components/DiscussionHero', 'flarum/tags/helpers/tagsLabel', 'flarum/tags/utils/sortTags'], function (_export) { + 'use strict'; + + var extend, DiscussionListItem, DiscussionPage, DiscussionHero, tagsLabel, sortTags; + return { + setters: [function (_flarumExtend) { + extend = _flarumExtend.extend; + }, function (_flarumComponentsDiscussionListItem) { + DiscussionListItem = _flarumComponentsDiscussionListItem['default']; + }, function (_flarumComponentsDiscussionPage) { + DiscussionPage = _flarumComponentsDiscussionPage['default']; + }, function (_flarumComponentsDiscussionHero) { + DiscussionHero = _flarumComponentsDiscussionHero['default']; + }, function (_flarumTagsHelpersTagsLabel) { + tagsLabel = _flarumTagsHelpersTagsLabel['default']; + }, function (_flarumTagsUtilsSortTags) { + sortTags = _flarumTagsUtilsSortTags['default']; + }], + execute: function () { + _export('default', function () { + // Add tag labels to each discussion in the discussion list. + extend(DiscussionListItem.prototype, 'infoItems', function (items) { + var tags = this.props.discussion.tags(); + + if (tags && tags.length) { + items.add('tags', tagsLabel(tags), 10); + } + }); + + // Include a discussion's tags when fetching it. + extend(DiscussionPage.prototype, 'params', function (params) { + params.include.push('tags'); + }); + + // Restyle a discussion's hero to use its first tag's color. + extend(DiscussionHero.prototype, 'view', function (view) { + var tags = sortTags(this.props.discussion.tags()); + + if (tags && tags.length) { + var color = tags[0].color(); + if (color) { + view.attrs.style = { backgroundColor: color }; + view.attrs.className += ' DiscussionHero--colored'; + } + } + }); + + // 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) { + var tags = this.props.discussion.tags(); + + if (tags && tags.length) { + items.add('tags', tagsLabel(tags, { link: true }), 5); + } + }); + }); + } + }; +});;System.register('flarum/tags/addTagList', ['flarum/extend', 'flarum/components/IndexPage', 'flarum/components/Separator', 'flarum/components/LinkButton', 'flarum/tags/components/TagLinkButton', 'flarum/tags/components/TagsPage', 'flarum/tags/utils/sortTags'], function (_export) { + 'use strict'; + + var extend, IndexPage, Separator, LinkButton, TagLinkButton, TagsPage, sortTags; + return { + setters: [function (_flarumExtend) { + extend = _flarumExtend.extend; + }, function (_flarumComponentsIndexPage) { + IndexPage = _flarumComponentsIndexPage['default']; + }, function (_flarumComponentsSeparator) { + Separator = _flarumComponentsSeparator['default']; + }, function (_flarumComponentsLinkButton) { + LinkButton = _flarumComponentsLinkButton['default']; + }, function (_flarumTagsComponentsTagLinkButton) { + TagLinkButton = _flarumTagsComponentsTagLinkButton['default']; + }, function (_flarumTagsComponentsTagsPage) { + TagsPage = _flarumTagsComponentsTagsPage['default']; + }, function (_flarumTagsUtilsSortTags) { + sortTags = _flarumTagsUtilsSortTags['default']; + }], + execute: 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.component({ + icon: 'th-large', + children: app.trans('tags.tags'), + href: app.route('tags') + }), -10); + + if (app.current instanceof TagsPage) return; + + items.add('separator', Separator.component(), -10); + + var params = this.stickyParams(); + var tags = app.store.all('tags'); + var currentTag = this.currentTag(); + + var addTag = function addTag(tag) { + var active = currentTag === tag; + + if (!active && currentTag) { + active = currentTag.parent() === tag; + } + + items.add('tag' + tag.id(), TagLinkButton.component({ tag: tag, params: params, active: active }), -10); + }; + + sortTags(tags).filter(function (tag) { + return tag.position() !== null && (!tag.isChild() || currentTag && (tag.parent() === currentTag || tag.parent() === currentTag.parent())); + }).forEach(addTag); + + var more = tags.filter(function (tag) { + return tag.position() === null; + }).sort(function (a, b) { + return b.discussionsCount() - a.discussionsCount(); + }); + + more.splice(0, 3).forEach(addTag); + + if (more.length) { + items.add('moreTags', LinkButton.component({ + children: app.trans('tags.more'), + href: app.route('tags') + }), -10); + } + }); + }); + } + }; +});;System.register('flarum/tags/main', ['flarum/Model', 'flarum/models/Discussion', 'flarum/components/IndexPage', 'flarum/tags/models/Tag', 'flarum/tags/components/TagsPage', 'flarum/tags/components/DiscussionTaggedPost', 'flarum/tags/addTagList', 'flarum/tags/addTagFilter', 'flarum/tags/addTagLabels', 'flarum/tags/addTagControl', 'flarum/tags/addTagComposer'], function (_export) { + 'use strict'; + + var Model, Discussion, IndexPage, Tag, TagsPage, DiscussionTaggedPost, addTagList, addTagFilter, addTagLabels, addTagControl, addTagComposer; + return { + setters: [function (_flarumModel) { + Model = _flarumModel['default']; + }, function (_flarumModelsDiscussion) { + Discussion = _flarumModelsDiscussion['default']; + }, function (_flarumComponentsIndexPage) { + IndexPage = _flarumComponentsIndexPage['default']; + }, function (_flarumTagsModelsTag) { + Tag = _flarumTagsModelsTag['default']; + }, function (_flarumTagsComponentsTagsPage) { + TagsPage = _flarumTagsComponentsTagsPage['default']; + }, function (_flarumTagsComponentsDiscussionTaggedPost) { + DiscussionTaggedPost = _flarumTagsComponentsDiscussionTaggedPost['default']; + }, function (_flarumTagsAddTagList) { + addTagList = _flarumTagsAddTagList['default']; + }, function (_flarumTagsAddTagFilter) { + addTagFilter = _flarumTagsAddTagFilter['default']; + }, function (_flarumTagsAddTagLabels) { + addTagLabels = _flarumTagsAddTagLabels['default']; + }, function (_flarumTagsAddTagControl) { + addTagControl = _flarumTagsAddTagControl['default']; + }, function (_flarumTagsAddTagComposer) { + addTagComposer = _flarumTagsAddTagComposer['default']; + }], + execute: function () { + + app.initializers.add('tags', function (app) { + app.routes.tags = { path: '/tags', component: TagsPage.component() }; + app.routes.tag = { path: '/t/:tags', component: IndexPage.component() }; + + app.route.tag = function (tag) { + return app.route('tag', { tags: tag.slug() }); + }; + + app.postComponents.discussionTagged = DiscussionTaggedPost; + + app.store.models.tags = Tag; + + Discussion.prototype.tags = Model.hasMany('tags'); + Discussion.prototype.canTag = Model.attribute('canTag'); + + addTagList(); + addTagFilter(); + addTagLabels(); + addTagControl(); + addTagComposer(); + }); + } + }; +});;System.register('flarum/tags/components/DiscussionTaggedPost', ['flarum/components/EventPost', 'flarum/helpers/punctuateSeries', 'flarum/tags/helpers/tagsLabel'], function (_export) { + 'use strict'; + + var EventPost, punctuateSeries, tagsLabel, DiscussionTaggedPost; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + return { + setters: [function (_flarumComponentsEventPost) { + EventPost = _flarumComponentsEventPost['default']; + }, function (_flarumHelpersPunctuateSeries) { + punctuateSeries = _flarumHelpersPunctuateSeries['default']; + }, function (_flarumTagsHelpersTagsLabel) { + tagsLabel = _flarumTagsHelpersTagsLabel['default']; + }], + execute: function () { + DiscussionTaggedPost = (function (_EventPost) { + _inherits(DiscussionTaggedPost, _EventPost); + + function DiscussionTaggedPost() { + _classCallCheck(this, DiscussionTaggedPost); + + _get(Object.getPrototypeOf(DiscussionTaggedPost.prototype), 'constructor', this).apply(this, arguments); + } + + _createClass(DiscussionTaggedPost, [{ + key: 'icon', + value: function icon() { + return 'tag'; + } + }, { + key: 'descriptionKey', + value: function descriptionKey() { + return 'tags.discussion_tagged_post'; + } + }, { + key: 'descriptionData', + value: function descriptionData() { + var post = this.props.post; + var oldTags = post.content()[0]; + var newTags = post.content()[1]; + + function diffTags(tags1, tags2) { + return tags1.filter(function (tag) { + return tags2.indexOf(tag) === -1; + }).map(function (id) { + return app.store.getById('tags', id); + }); + } + + var added = diffTags(newTags, oldTags); + var removed = diffTags(oldTags, newTags); + var actions = []; + + if (added.length) { + actions.push(app.trans('tags.added_tags', { + tags: tagsLabel(added, { link: true }), + count: added + })); + } + + if (removed.length) { + actions.push(app.trans('tags.removed_tags', { + tags: tagsLabel(removed, { link: true }), + count: removed + })); + } + + return { + action: punctuateSeries(actions), + count: added.length + removed.length + }; + } + }]); + + return DiscussionTaggedPost; + })(EventPost); + + _export('default', DiscussionTaggedPost); + } + }; +});;System.register('flarum/tags/components/TagDiscussionModal', ['flarum/components/Modal', 'flarum/components/DiscussionPage', 'flarum/components/Button', 'flarum/helpers/highlight', 'flarum/utils/classList', 'flarum/utils/extractText', 'flarum/tags/helpers/tagLabel', 'flarum/tags/helpers/tagIcon', 'flarum/tags/utils/sortTags'], function (_export) { + 'use strict'; + + var Modal, DiscussionPage, Button, highlight, classList, extractText, tagLabel, tagIcon, sortTags, TagDiscussionModal; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + return { + setters: [function (_flarumComponentsModal) { + Modal = _flarumComponentsModal['default']; + }, function (_flarumComponentsDiscussionPage) { + DiscussionPage = _flarumComponentsDiscussionPage['default']; + }, function (_flarumComponentsButton) { + Button = _flarumComponentsButton['default']; + }, function (_flarumHelpersHighlight) { + highlight = _flarumHelpersHighlight['default']; + }, function (_flarumUtilsClassList) { + classList = _flarumUtilsClassList['default']; + }, function (_flarumUtilsExtractText) { + extractText = _flarumUtilsExtractText['default']; + }, function (_flarumTagsHelpersTagLabel) { + tagLabel = _flarumTagsHelpersTagLabel['default']; + }, function (_flarumTagsHelpersTagIcon) { + tagIcon = _flarumTagsHelpersTagIcon['default']; + }, function (_flarumTagsUtilsSortTags) { + sortTags = _flarumTagsUtilsSortTags['default']; + }], + execute: function () { + TagDiscussionModal = (function (_Modal) { + _inherits(TagDiscussionModal, _Modal); + + function TagDiscussionModal() { + _classCallCheck(this, TagDiscussionModal); + + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + _get(Object.getPrototypeOf(TagDiscussionModal.prototype), 'constructor', this).apply(this, args); + + this.tags = sortTags(app.store.all('tags').filter(function (tag) { + return tag.canStartDiscussion(); + })); + + this.selected = []; + this.filter = m.prop(''); + this.index = this.tags[0].id(); + this.focused = false; + + if (this.props.selectedTags) { + this.props.selectedTags.map(this.addTag.bind(this)); + } else if (this.props.discussion) { + this.props.discussion.tags().map(this.addTag.bind(this)); + } + + this.minPrimary = app.forum.attribute('minPrimaryTags'); + this.maxPrimary = app.forum.attribute('maxPrimaryTags'); + this.minSecondary = app.forum.attribute('minSecondaryTags'); + this.maxSecondary = app.forum.attribute('maxSecondaryTags'); + } + + _createClass(TagDiscussionModal, [{ + key: 'primaryCount', + value: function primaryCount() { + return this.selected.filter(function (tag) { + return tag.isPrimary(); + }).length; + } + }, { + key: 'secondaryCount', + value: function secondaryCount() { + return this.selected.filter(function (tag) { + return !tag.isPrimary(); + }).length; + } + + /** + * Add the given tag to the list of selected tags. + * + * @param {Tag} tag + */ + }, { + key: 'addTag', + value: function addTag(tag) { + if (!tag.canStartDiscussion()) return; + + // If this tag has a parent, we'll also need to add the parent tag to the + // selected list if it's not already in there. + var parent = tag.parent(); + if (parent) { + var index = this.selected.indexOf(parent); + if (index === -1) { + this.selected.push(parent); + } + } + + this.selected.push(tag); + } + + /** + * Remove the given tag from the list of selected tags. + * + * @param {Tag} tag + */ + }, { + key: 'removeTag', + value: function removeTag(tag) { + var index = this.selected.indexOf(tag); + if (index !== -1) { + this.selected.splice(index, 1); + + // 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(function (selected) { + return selected.parent() === tag; + }).forEach(this.removeTag.bind(this)); + } + } + }, { + key: 'className', + value: function className() { + return 'TagDiscussionModal'; + } + }, { + key: 'title', + value: function title() { + return this.props.discussion ? app.trans('tags.edit_discussion_tags_title', { title: m( + 'em', + null, + this.props.discussion.title() + ) }) : app.trans('tags.tag_new_discussion_title'); + } + }, { + key: 'getInstruction', + value: function getInstruction(primaryCount, secondaryCount) { + if (primaryCount < this.minPrimary) { + return app.trans('tags.choose_primary_tags', { count: this.minPrimary - primaryCount }); + } else if (secondaryCount < this.minSecondary) { + return app.trans('tags.choose_secondary_tags', { count: this.minSecondary - secondaryCount }); + } + + return ''; + } + }, { + key: 'content', + value: function content() { + var _this = this; + + var tags = this.tags; + var filter = this.filter().toLowerCase(); + var primaryCount = this.primaryCount(); + var secondaryCount = this.secondaryCount(); + + // 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(function (tag) { + var parent = tag.parent(); + return parent === false || _this.selected.indexOf(parent) !== -1; + }); + + // 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 >= app.forum.attribute('maxPrimaryTags')) { + tags = tags.filter(function (tag) { + return !tag.isPrimary() || _this.selected.indexOf(tag) !== -1; + }); + } + + if (secondaryCount >= app.forum.attribute('maxSecondaryTags')) { + tags = tags.filter(function (tag) { + return tag.isPrimary() || _this.selected.indexOf(tag) !== -1; + }); + } + + // 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(function (tag) { + return tag.name().substr(0, filter.length).toLowerCase() === filter; + }); + } + + if (tags.indexOf(this.index) === -1) this.index = tags[0]; + + return [m( + 'div', + { className: 'Modal-body' }, + m( + 'div', + { className: 'TagDiscussionModal-form' }, + m( + 'div', + { className: 'TagDiscussionModal-form-input' }, + m( + 'div', + { className: 'TagsInput FormControl ' + (this.focused ? 'focus' : '') }, + m( + 'span', + { className: 'TagsInput-selected' }, + this.selected.map(function (tag) { + return m( + 'span', + { className: 'TagsInput-tag', onclick: function () { + _this.removeTag(tag); + _this.onready(); + } }, + tagLabel(tag) + ); + }) + ), + m('input', { className: 'FormControl', + placeholder: extractText(this.getInstruction(primaryCount, secondaryCount)), + value: this.filter(), + oninput: m.withAttr('value', this.filter), + onkeydown: this.onkeydown.bind(this), + onfocus: function () { + return _this.focused = true; + }, + onblur: function () { + return _this.focused = false; + } }) + ) + ), + m( + 'div', + { className: 'TagDiscussionModal-form-submit App-primaryControl' }, + Button.component({ + type: 'submit', + className: 'Button Button--primary', + disabled: primaryCount < this.minPrimary || secondaryCount < this.minSecondary, + icon: 'check', + children: app.trans('tags.confirm') + }) + ) + ) + ), m( + 'div', + { className: 'Modal-footer' }, + m( + 'ul', + { className: 'TagDiscussionModal-list SelectTagList' }, + tags.filter(function (tag) { + return filter || !tag.parent() || _this.selected.indexOf(tag.parent()) !== -1; + }).map(function (tag) { + return m( + 'li', + { 'data-index': tag.id(), + className: classList({ + pinned: tag.position() !== null, + child: !!tag.parent(), + colored: !!tag.color(), + selected: _this.selected.indexOf(tag) !== -1, + active: _this.index === tag + }), + style: { color: tag.color() }, + onmouseover: function () { + return _this.index = tag; + }, + onclick: _this.toggleTag.bind(_this, tag) + }, + tagIcon(tag), + m( + 'span', + { className: 'SelectTagListItem-name' }, + highlight(tag.name(), filter) + ), + tag.description() ? m( + 'span', + { className: 'SelectTagListItem-description' }, + tag.description() + ) : '' + ); + }) + ) + )]; + } + }, { + key: 'toggleTag', + value: function toggleTag(tag) { + var index = this.selected.indexOf(tag); + + if (index !== -1) { + this.removeTag(tag); + } else { + this.addTag(tag); + } + + if (this.filter()) { + this.filter(''); + this.index = this.tags[0]; + } + + this.onready(); + } + }, { + key: 'onkeydown', + value: function onkeydown(e) { + switch (e.which) { + case 40: + case 38: + // Down/Up + e.preventDefault(); + this.setIndex(this.getCurrentNumericIndex() + (e.which === 40 ? 1 : -1), true); + break; + + case 13: + // Return + e.preventDefault(); + if (e.metaKey || e.ctrlKey || this.selected.indexOf(this.index) !== -1) { + if (this.selected.length) { + this.$('form').submit(); + } + } else { + this.getItem(this.index)[0].dispatchEvent(new Event('click')); + } + break; + + case 8: + // Backspace + if (e.target.selectionStart === 0 && e.target.selectionEnd === 0) { + e.preventDefault(); + this.selected.splice(this.selected.length - 1, 1); + } + break; + + default: + // no default + } + } + }, { + key: 'selectableItems', + value: function selectableItems() { + return this.$('.TagDiscussionModal-list > li'); + } + }, { + key: 'getCurrentNumericIndex', + value: function getCurrentNumericIndex() { + return this.selectableItems().index(this.getItem(this.index)); + } + }, { + key: 'getItem', + value: function getItem(index) { + return this.selectableItems().filter('[data-index="' + index.id() + '"]'); + } + }, { + key: 'setIndex', + value: function setIndex(index, scrollToItem) { + var $items = this.selectableItems(); + var $dropdown = $items.parent(); + + if (index < 0) { + index = $items.length - 1; + } else if (index >= $items.length) { + index = 0; + } + + var $item = $items.eq(index); + + this.index = app.store.getById('tags', $item.attr('data-index')); + + m.redraw(); + + if (scrollToItem) { + var dropdownScroll = $dropdown.scrollTop(); + var dropdownTop = $dropdown.offset().top; + var dropdownBottom = dropdownTop + $dropdown.outerHeight(); + var itemTop = $item.offset().top; + var itemBottom = itemTop + $item.outerHeight(); + + var scrollTop = undefined; + if (itemTop < dropdownTop) { + scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'), 10); + } else if (itemBottom > dropdownBottom) { + scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'), 10); + } + + if (typeof scrollTop !== 'undefined') { + $dropdown.stop(true).animate({ scrollTop: scrollTop }, 100); + } + } + } + }, { + key: 'onsubmit', + value: function onsubmit(e) { + e.preventDefault(); + + var discussion = this.props.discussion; + var tags = this.selected; + + if (discussion) { + discussion.save({ relationships: { tags: tags } }).then(function () { + if (app.current instanceof DiscussionPage) { + app.current.stream.update(); + } + m.redraw(); + }); + } + + if (this.props.onsubmit) this.props.onsubmit(tags); + + app.modal.close(); + + m.redraw.strategy('none'); + } + }]); + + return TagDiscussionModal; + })(Modal); + + _export('default', TagDiscussionModal); + } + }; +});;System.register('flarum/tags/components/TagHero', ['flarum/Component'], function (_export) { + 'use strict'; + + var Component, TagHero; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + return { + setters: [function (_flarumComponent) { + Component = _flarumComponent['default']; + }], + execute: function () { + TagHero = (function (_Component) { + _inherits(TagHero, _Component); + + function TagHero() { + _classCallCheck(this, TagHero); + + _get(Object.getPrototypeOf(TagHero.prototype), 'constructor', this).apply(this, arguments); + } + + _createClass(TagHero, [{ + key: 'view', + value: function view() { + var tag = this.props.tag; + var color = tag.color(); + + return m( + 'header', + { className: 'Hero TagHero', + style: color ? { color: '#fff', backgroundColor: color } : '' }, + m( + 'div', + { className: 'container' }, + m( + 'div', + { className: 'containerNarrow' }, + m( + 'h2', + { className: 'Hero-title' }, + tag.name() + ), + m( + 'div', + { className: 'Hero-subtitle' }, + tag.description() + ) + ) + ) + ); + } + }]); + + return TagHero; + })(Component); + + _export('default', TagHero); + } + }; +});;System.register('flarum/tags/components/TagLinkButton', ['flarum/components/LinkButton', 'flarum/tags/helpers/tagIcon'], function (_export) { + 'use strict'; + + var LinkButton, tagIcon, TagLinkButton; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + return { + setters: [function (_flarumComponentsLinkButton) { + LinkButton = _flarumComponentsLinkButton['default']; + }, function (_flarumTagsHelpersTagIcon) { + tagIcon = _flarumTagsHelpersTagIcon['default']; + }], + execute: function () { + TagLinkButton = (function (_LinkButton) { + _inherits(TagLinkButton, _LinkButton); + + function TagLinkButton() { + _classCallCheck(this, TagLinkButton); + + _get(Object.getPrototypeOf(TagLinkButton.prototype), 'constructor', this).apply(this, arguments); + } + + _createClass(TagLinkButton, [{ + key: 'view', + value: function view() { + var tag = this.props.tag; + var active = this.constructor.isActive(this.props); + var description = tag && tag.description(); + + return m( + 'a', + { className: 'TagLinkButton hasIcon ' + (tag.isChild() ? 'child' : ''), href: this.props.href, config: m.route, + style: active && tag ? { color: tag.color() } : '', + title: description || '' }, + tagIcon(tag, { className: 'Button-icon' }), + this.props.children + ); + } + }], [{ + key: 'initProps', + value: function initProps(props) { + var tag = props.tag; + + props.params.tags = tag ? tag.slug() : 'untagged'; + props.href = app.route('tag', props.params); + props.children = tag ? tag.name() : app.trans('tags.untagged'); + } + }]); + + return TagLinkButton; + })(LinkButton); + + _export('default', TagLinkButton); + } + }; +});;System.register('flarum/tags/components/TagsPage', ['flarum/Component', 'flarum/components/IndexPage', 'flarum/helpers/listItems', 'flarum/helpers/humanTime', 'flarum/tags/helpers/tagLabel', 'flarum/tags/utils/sortTags'], function (_export) { + 'use strict'; + + var Component, IndexPage, listItems, humanTime, tagLabel, sortTags, TagsPage; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + return { + setters: [function (_flarumComponent) { + Component = _flarumComponent['default']; + }, function (_flarumComponentsIndexPage) { + IndexPage = _flarumComponentsIndexPage['default']; + }, function (_flarumHelpersListItems) { + listItems = _flarumHelpersListItems['default']; + }, function (_flarumHelpersHumanTime) { + humanTime = _flarumHelpersHumanTime['default']; + }, function (_flarumTagsHelpersTagLabel) { + tagLabel = _flarumTagsHelpersTagLabel['default']; + }, function (_flarumTagsUtilsSortTags) { + sortTags = _flarumTagsUtilsSortTags['default']; + }], + execute: function () { + TagsPage = (function (_Component) { + _inherits(TagsPage, _Component); + + function TagsPage() { + _classCallCheck(this, TagsPage); + + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + _get(Object.getPrototypeOf(TagsPage.prototype), 'constructor', this).apply(this, args); + + this.tags = sortTags(app.store.all('tags').filter(function (tag) { + return !tag.parent(); + })); + + app.current = this; + app.history.push('tags'); + app.drawer.hide(); + app.modal.close(); + } + + _createClass(TagsPage, [{ + key: 'view', + value: function view() { + var pinned = this.tags.filter(function (tag) { + return tag.position() !== null; + }); + var cloud = this.tags.filter(function (tag) { + return tag.position() === null; + }); + + return m( + 'div', + { className: 'TagsPage' }, + IndexPage.prototype.hero(), + m( + 'div', + { className: 'container' }, + m( + 'nav', + { className: 'TagsPage-nav IndexPage-nav sideNav', config: IndexPage.prototype.affixSidebar }, + m( + 'ul', + null, + listItems(IndexPage.prototype.sidebarItems().toArray()) + ) + ), + m( + 'div', + { className: 'TagsPage-content sideNavOffset' }, + m( + 'ul', + { className: 'TagTiles' }, + pinned.map(function (tag) { + var lastDiscussion = tag.lastDiscussion(); + var children = app.store.all('tags').filter(function (child) { + return child.parent() === tag; + }); + + return m( + 'li', + { className: 'TagTile ' + (tag.color() ? 'colored' : ''), + style: { backgroundColor: tag.color() } }, + m( + 'a', + { className: 'TagTile-info', href: app.route.tag(tag), config: m.route }, + m( + 'h3', + { className: 'TagTile-name' }, + tag.name() + ), + m( + 'p', + { className: 'TagTile-description' }, + tag.description() + ), + children ? m( + 'div', + { className: 'TagTile-children' }, + children.map(function (child) { + return m( + 'a', + { href: app.route.tag(child), config: function (element, isInitialized) { + if (isInitialized) return; + $(element).on('click', function (e) { + return e.stopPropagation(); + }); + m.route.apply(this, arguments); + } }, + child.name() + ); + }) + ) : '' + ), + lastDiscussion ? m( + 'a', + { className: 'TagTile-lastDiscussion', + href: app.route.discussion(lastDiscussion, lastDiscussion.lastPostNumber()), + config: m.route }, + m( + 'span', + { className: 'TagTile-lastDiscussion-title' }, + lastDiscussion.title() + ), + humanTime(lastDiscussion.lastTime()) + ) : m('span', { className: 'TagTile-lastDiscussion' }) + ); + }) + ), + cloud.length ? m( + 'div', + { className: 'TagCloud' }, + cloud.map(function (tag) { + var color = tag.color(); + + return [tagLabel(tag, { link: true }), ' ']; + }) + ) : '' + ) + ) + ); + } + }]); + + return TagsPage; + })(Component); + + _export('default', TagsPage); + } + }; +}); \ No newline at end of file diff --git a/extensions/tags/js/forum/src/components/DiscussionTaggedPost.js b/extensions/tags/js/forum/src/components/DiscussionTaggedPost.js index 14b7a6051..093a2b282 100644 --- a/extensions/tags/js/forum/src/components/DiscussionTaggedPost.js +++ b/extensions/tags/js/forum/src/components/DiscussionTaggedPost.js @@ -1,5 +1,5 @@ import EventPost from 'flarum/components/EventPost'; -import punctuate from 'flarum/helpers/punctuate'; +import punctuateSeries from 'flarum/helpers/punctuateSeries'; import tagsLabel from 'flarum/tags/helpers/tagsLabel'; export default class DiscussionTaggedPost extends EventPost { @@ -41,7 +41,7 @@ export default class DiscussionTaggedPost extends EventPost { } return { - action: punctuate(actions), + action: punctuateSeries(actions), count: added.length + removed.length }; } diff --git a/extensions/tags/migrations/2015_02_24_000000_create_discussions_tags_table.php b/extensions/tags/migrations/2015_02_24_000000_create_discussions_tags_table.php index 1b7f6f16a..73d98ce44 100644 --- a/extensions/tags/migrations/2015_02_24_000000_create_discussions_tags_table.php +++ b/extensions/tags/migrations/2015_02_24_000000_create_discussions_tags_table.php @@ -10,16 +10,11 @@ namespace Flarum\Tags\Migrations; +use Flarum\Database\AbstractMigration; use Illuminate\Database\Schema\Blueprint; -use Flarum\Migrations\Migration; -class CreateDiscussionsTagsTable extends Migration +class CreateDiscussionsTagsTable extends AbstractMigration { - /** - * Run the migrations. - * - * @return void - */ public function up() { $this->schema->create('discussions_tags', function (Blueprint $table) { @@ -29,11 +24,6 @@ class CreateDiscussionsTagsTable extends Migration }); } - /** - * Reverse the migrations. - * - * @return void - */ public function down() { $this->schema->drop('discussions_tags'); diff --git a/extensions/tags/migrations/2015_02_24_000000_create_tags_table.php b/extensions/tags/migrations/2015_02_24_000000_create_tags_table.php index a44b7482c..0fbabbf91 100644 --- a/extensions/tags/migrations/2015_02_24_000000_create_tags_table.php +++ b/extensions/tags/migrations/2015_02_24_000000_create_tags_table.php @@ -10,17 +10,12 @@ namespace Flarum\Tags\Migrations; -use Illuminate\Database\Schema\Blueprint; -use Flarum\Migrations\Migration; +use Flarum\Database\AbstractMigration; use Flarum\Tags\Tag; +use Illuminate\Database\Schema\Blueprint; -class CreateTagsTable extends Migration +class CreateTagsTable extends AbstractMigration { - /** - * Run the migrations. - * - * @return void - */ public function up() { $this->schema->create('tags', function (Blueprint $table) { @@ -53,11 +48,6 @@ class CreateTagsTable extends Migration ]); } - /** - * Reverse the migrations. - * - * @return void - */ public function down() { $this->schema->drop('tags'); diff --git a/extensions/tags/migrations/2015_02_24_000000_create_users_tags_table.php b/extensions/tags/migrations/2015_02_24_000000_create_users_tags_table.php index 34182f186..e8851af72 100644 --- a/extensions/tags/migrations/2015_02_24_000000_create_users_tags_table.php +++ b/extensions/tags/migrations/2015_02_24_000000_create_users_tags_table.php @@ -10,16 +10,11 @@ namespace Flarum\Tags\Migrations; +use Flarum\Database\AbstractMigration; use Illuminate\Database\Schema\Blueprint; -use Flarum\Migrations\Migration; -class CreateUsersTagsTable extends Migration +class CreateUsersTagsTable extends AbstractMigration { - /** - * Run the migrations. - * - * @return void - */ public function up() { $this->schema->create('users_tags', function (Blueprint $table) { @@ -31,11 +26,6 @@ class CreateUsersTagsTable extends Migration }); } - /** - * Reverse the migrations. - * - * @return void - */ public function down() { $this->schema->drop('users_tags'); diff --git a/extensions/tags/migrations/2015_02_24_000000_set_default_settings.php b/extensions/tags/migrations/2015_02_24_000000_set_default_settings.php index 6737d8252..cc0f46cbe 100644 --- a/extensions/tags/migrations/2015_02_24_000000_set_default_settings.php +++ b/extensions/tags/migrations/2015_02_24_000000_set_default_settings.php @@ -10,33 +10,23 @@ namespace Flarum\Tags\Migrations; -use Flarum\Migrations\Migration; +use Flarum\Database\AbstractMigration; -class SetDefaultSettings extends Migration +class SetDefaultSettings extends AbstractMigration { - /** - * Run the migrations. - * - * @return void - */ public function up() { - $this->settings->set('tags.max_primary_tags', '1'); - $this->settings->set('tags.min_primary_tags', '1'); - $this->settings->set('tags.max_secondary_tags', '3'); - $this->settings->set('tags.min_secondary_tags', '0'); + $this->settings->set('flarum-tags.max_primary_tags', '1'); + $this->settings->set('flarum-tags.min_primary_tags', '1'); + $this->settings->set('flarum-tags.max_secondary_tags', '3'); + $this->settings->set('flarum-tags.min_secondary_tags', '0'); } - /** - * Reverse the migrations. - * - * @return void - */ public function down() { - $this->settings->delete('tags.max_primary_tags'); - $this->settings->delete('tags.max_secondary_tags'); - $this->settings->delete('tags.min_primary_tags'); - $this->settings->delete('tags.min_secondary_tags'); + $this->settings->delete('flarum-tags.max_primary_tags'); + $this->settings->delete('flarum-tags.max_secondary_tags'); + $this->settings->delete('flarum-tags.min_primary_tags'); + $this->settings->delete('flarum-tags.min_secondary_tags'); } } diff --git a/extensions/tags/src/Access/DiscussionPolicy.php b/extensions/tags/src/Access/DiscussionPolicy.php new file mode 100755 index 000000000..4d36ae5b3 --- /dev/null +++ b/extensions/tags/src/Access/DiscussionPolicy.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Tags\Access; + +use Flarum\Core\Access\AbstractPolicy; +use Flarum\Core\Discussion; +use Flarum\Core\User; +use Flarum\Event\ScopeHiddenDiscussionVisibility; +use Flarum\Tags\Tag; +use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Query\Expression; + +class DiscussionPolicy extends AbstractPolicy +{ + /** + * {@inheritdoc} + */ + protected $model = Discussion::class; + + /** + * {@inheritdoc} + */ + public function subscribe(Dispatcher $events) + { + parent::subscribe($events); + + $events->listen(ScopeHiddenDiscussionVisibility::class, [$this, 'scopeHiddenDiscussionVisibility']); + } + + /** + * @param User $actor + * @param string $ability + * @param Discussion $discussion + * @return bool + */ + public function before(User $actor, $ability, Discussion $discussion) + { + // Wrap all discussion permission checks with some logic pertaining to + // the discussion's tags. If the discussion has a tag that has been + // restricted, and the user has this permission for that tag, then they + // are allowed. If the discussion only has tags that have been + // restricted, then the user *must* have permission for at least one of + // them. + $tags = $discussion->tags; + + if (count($tags)) { + $restricted = true; + + foreach ($tags as $tag) { + if ($tag->is_restricted) { + if ($actor->hasPermission('tag' . $tag->id . '.discussion.' . $ability)) { + return true; + } + } else { + $restricted = false; + } + } + + if ($restricted) { + return false; + } + } + } + + /** + * @param User $actor + * @param Builder $query + */ + public function find(User $actor, Builder $query) + { + // Hide discussions which have tags that the user is not allowed to see. + $query->whereNotExists(function ($query) use ($actor) { + return $query->select(new Expression(1)) + ->from('discussions_tags') + ->whereIn('tag_id', Tag::getIdsWhereCannot($actor, 'view')) + ->where('discussions.id', new Expression('discussion_id')); + }); + } + + /** + * @param ScopeHiddenDiscussionVisibility $event + */ + public function scopeHiddenDiscussionVisibility(ScopeHiddenDiscussionVisibility $event) + { + // By default, discussions are not visible to the public if they are + // hidden or contain zero comments - unless the actor has a certain + // permission. Since we grant permissions per-tag, we will make + // discussions visible in the tags for which the user has that + // permission. + $event->query->orWhereExists(function ($query) use ($event) { + return $query->select(new Expression(1)) + ->from('discussions_tags') + ->whereIn('tag_id', Tag::getIdsWhereCan($event->actor, $event->permission)) + ->where('discussions.id', new Expression('discussion_id')); + }); + } +} diff --git a/extensions/tags/src/Access/FlagPolicy.php b/extensions/tags/src/Access/FlagPolicy.php new file mode 100755 index 000000000..ee0ffcbed --- /dev/null +++ b/extensions/tags/src/Access/FlagPolicy.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Tags\Access; + +use Flarum\Core\Access\AbstractPolicy; +use Flarum\Core\User; +use Flarum\Flags\Flag; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Query\Expression; + +class FlagPolicy extends AbstractPolicy +{ + /** + * {@inheritdoc} + */ + protected $model = Flag::class; + + /** + * @param User $actor + * @param Builder $query + */ + public function find(User $actor, Builder $query) + { + $query + ->select('flags.*') + ->leftJoin('posts', 'posts.id', '=', 'flags.post_id') + ->leftJoin('discussions', 'discussions.id', '=', 'posts.discussion_id') + ->whereNotExists(function ($query) use ($actor) { + return $query->select(new Expression(1)) + ->from('discussions_tags') + ->whereIn('tag_id', Tag::getIdsWhereCannot($actor, 'discussion.viewFlags')) + ->where('discussions.id', new Expression('discussion_id')); + }); + } +} diff --git a/extensions/tags/src/Access/TagPolicy.php b/extensions/tags/src/Access/TagPolicy.php new file mode 100755 index 000000000..055c395dc --- /dev/null +++ b/extensions/tags/src/Access/TagPolicy.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Tags\Access; + +use Flarum\Core\Access\AbstractPolicy; +use Flarum\Core\User; +use Flarum\Tags\Tag; +use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Database\Eloquent\Builder; + +class TagPolicy extends AbstractPolicy +{ + /** + * {@inheritdoc} + */ + protected $model = Tag::class; + + /** + * @param User $actor + * @param Builder $query + */ + public function find(User $actor, Builder $query) + { + $query->whereNotIn('id', Tag::getIdsWhereCannot($actor, 'view')); + } + + /** + * @param User $actor + * @param Tag $tag + * @return bool|null + */ + public function startDiscussion(User $actor, Tag $tag) + { + if (! $tag->is_restricted + || $actor->hasPermission('tag' . $tag->id . '.startDiscussion')) { + return true; + } + } +} diff --git a/extensions/tags/src/Api/UpdateAction.php b/extensions/tags/src/Api/Controller/CreateTagController.php similarity index 53% rename from extensions/tags/src/Api/UpdateAction.php rename to extensions/tags/src/Api/Controller/CreateTagController.php index 4ef864444..98f1b0d67 100644 --- a/extensions/tags/src/Api/UpdateAction.php +++ b/extensions/tags/src/Api/Controller/CreateTagController.php @@ -8,26 +8,27 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Api; +namespace Flarum\Tags\Api\Controller; -use Flarum\Tags\Commands\EditTag; -use Flarum\Api\Actions\SerializeResourceAction; -use Flarum\Api\JsonApiRequest; +use Flarum\Api\Controller\AbstractCreateController; +use Flarum\Tags\Api\Serializer\TagSerializer; +use Flarum\Tags\Command\CreateTag; use Illuminate\Contracts\Bus\Dispatcher; +use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; -class UpdateAction extends SerializeResourceAction +class CreateTagController extends AbstractCreateController { + /** + * @inheritdoc + */ + public $serializer = TagSerializer::class; + /** * @var Dispatcher */ protected $bus; - /** - * @inheritdoc - */ - public $serializer = 'Flarum\Tags\Api\TagSerializer'; - /** * @param Dispatcher $bus */ @@ -37,14 +38,12 @@ class UpdateAction extends SerializeResourceAction } /** - * @param JsonApiRequest $request - * @param Document $document - * @return \Flarum\Core\Tags\Tag + * {@inheritdoc} */ - protected function data(JsonApiRequest $request, Document $document) + protected function data(ServerRequestInterface $request, Document $document) { return $this->bus->dispatch( - new EditTag($request->get('id'), $request->actor, $request->get('data')) + new CreateTag($request->getAttribute('actor'), array_get($request->getParsedBody(), 'data')) ); } } diff --git a/extensions/tags/src/Api/DeleteAction.php b/extensions/tags/src/Api/Controller/DeleteTagController.php similarity index 56% rename from extensions/tags/src/Api/DeleteAction.php rename to extensions/tags/src/Api/Controller/DeleteTagController.php index 1767c82e2..856d2da3c 100644 --- a/extensions/tags/src/Api/DeleteAction.php +++ b/extensions/tags/src/Api/Controller/DeleteTagController.php @@ -8,14 +8,14 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Api; +namespace Flarum\Tags\Api\Controller; -use Flarum\Tags\Commands\DeleteTag; -use Flarum\Api\Actions\DeleteAction as BaseDeleteAction; -use Flarum\Api\Request; +use Flarum\Api\Controller\AbstractDeleteController; +use Flarum\Tags\Command\DeleteTag; use Illuminate\Contracts\Bus\Dispatcher; +use Psr\Http\Message\ServerRequestInterface; -class DeleteAction extends BaseDeleteAction +class DeleteTagController extends AbstractDeleteController { /** * @var Dispatcher @@ -31,14 +31,12 @@ class DeleteAction extends BaseDeleteAction } /** - * Delete a tag. - * - * @param Request $request + * {@inheritdoc} */ - protected function delete(Request $request) + protected function delete(ServerRequestInterface $request) { $this->bus->dispatch( - new DeleteTag($request->get('id'), $request->actor) + new DeleteTag(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor')) ); } } diff --git a/extensions/tags/src/Api/OrderAction.php b/extensions/tags/src/Api/Controller/OrderTagsController.php similarity index 66% rename from extensions/tags/src/Api/OrderAction.php rename to extensions/tags/src/Api/Controller/OrderTagsController.php index d7b5c87c6..6fd341a36 100644 --- a/extensions/tags/src/Api/OrderAction.php +++ b/extensions/tags/src/Api/Controller/OrderTagsController.php @@ -8,23 +8,26 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Api; +namespace Flarum\Tags\Api\Controller; -use Flarum\Api\Actions\Action; -use Flarum\Api\Request; -use Zend\Diactoros\Response\EmptyResponse; +use Flarum\Core\Access\AssertPermissionTrait; +use Flarum\Http\Controller\ControllerInterface; use Flarum\Tags\Tag; -use Flarum\Core\Exceptions\PermissionDeniedException; +use Psr\Http\Message\ServerRequestInterface; +use Zend\Diactoros\Response\EmptyResponse; -class OrderAction implements Action +class OrderTagsController implements ControllerInterface { - public function handle(Request $request) - { - if (! $request->actor->isAdmin()) { - throw new PermissionDeniedException; - } + use AssertPermissionTrait; - $order = $request->get('order'); + /** + * {@inheritdoc} + */ + public function handle(ServerRequestInterface $request) + { + $this->assertAdmin($request->getAttribute('actor')); + + $order = array_get($request->getParsedBody(), 'order'); Tag::query()->update([ 'position' => null, diff --git a/extensions/tags/src/Api/Controller/UpdateTagController.php b/extensions/tags/src/Api/Controller/UpdateTagController.php new file mode 100644 index 000000000..6166e8856 --- /dev/null +++ b/extensions/tags/src/Api/Controller/UpdateTagController.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Tags\Api\Controller; + +use Flarum\Api\Controller\AbstractResourceController; +use Flarum\Tags\Api\Serializer\TagSerializer; +use Flarum\Tags\Command\EditTag; +use Illuminate\Contracts\Bus\Dispatcher; +use Psr\Http\Message\ServerRequestInterface; +use Tobscure\JsonApi\Document; + +class UpdateTagController extends AbstractResourceController +{ + /** + * @inheritdoc + */ + public $serializer = TagSerializer::class; + + /** + * @var Dispatcher + */ + protected $bus; + + /** + * @param Dispatcher $bus + */ + public function __construct(Dispatcher $bus) + { + $this->bus = $bus; + } + + /** + * {@inheritdoc} + */ + protected function data(ServerRequestInterface $request, Document $document) + { + $id = array_get($request->getQueryParams(), 'id'); + $actor = $request->getAttribute('actor'); + $data = array_get($request->getParsedBody(), 'data'); + + return $this->bus->dispatch( + new EditTag($id, $actor, $data) + ); + } +} diff --git a/extensions/tags/src/Api/CreateAction.php b/extensions/tags/src/Api/CreateAction.php deleted file mode 100644 index 819ca81f5..000000000 --- a/extensions/tags/src/Api/CreateAction.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Tags\Api; - -use Flarum\Tags\Commands\CreateTag; -use Flarum\Api\Actions\CreateAction as BaseCreateAction; -use Flarum\Api\JsonApiRequest; -use Illuminate\Contracts\Bus\Dispatcher; - -class CreateAction extends BaseCreateAction -{ - /** - * @var Dispatcher - */ - protected $bus; - - /** - * @inheritdoc - */ - public $serializer = 'Flarum\Tags\Api\TagSerializer'; - - /** - * @param Dispatcher $bus - */ - public function __construct(Dispatcher $bus) - { - $this->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/TagSerializer.php b/extensions/tags/src/Api/Serializer/TagSerializer.php similarity index 66% rename from extensions/tags/src/Api/TagSerializer.php rename to extensions/tags/src/Api/Serializer/TagSerializer.php index 3d5a51b5c..b51ee97c2 100644 --- a/extensions/tags/src/Api/TagSerializer.php +++ b/extensions/tags/src/Api/Serializer/TagSerializer.php @@ -8,14 +8,21 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Api; +namespace Flarum\Tags\Api\Serializer; -use Flarum\Api\Serializers\Serializer; +use Flarum\Api\Serializer\AbstractSerializer; +use Flarum\Api\Serializer\DiscussionSerializer; -class TagSerializer extends Serializer +class TagSerializer extends AbstractSerializer { + /** + * {@inheritdoc} + */ protected $type = 'tags'; + /** + * {@inheritdoc} + */ protected function getDefaultAttributes($tag) { $attributes = [ @@ -31,8 +38,8 @@ class TagSerializer extends Serializer 'defaultSort' => $tag->default_sort, 'isChild' => (bool) $tag->parent_id, 'isHidden' => (bool) $tag->is_hidden, - 'lastTime' => $tag->last_time ? $tag->last_time->toRFC3339String() : null, - 'canStartDiscussion' => $tag->can($this->actor, 'startDiscussion') + 'lastTime' => $this->formatDate($tag->last_time), + 'canStartDiscussion' => $this->actor->can('startDiscussion', $tag) ]; if ($this->actor->isAdmin()) { @@ -42,13 +49,19 @@ class TagSerializer extends Serializer return $attributes; } + /** + * @return \Flarum\Api\Relationship\HasOneBuilder + */ protected function parent() { - return $this->hasOne('Flarum\Tags\Api\TagSerializer'); + return $this->hasOne(TagSerializer::class); } + /** + * @return \Flarum\Api\Relationship\HasOneBuilder + */ protected function lastDiscussion() { - return $this->hasOne('Flarum\Api\Serializers\DiscussionSerializer'); + return $this->hasOne(DiscussionSerializer::class); } } diff --git a/extensions/tags/src/Commands/CreateTag.php b/extensions/tags/src/Command/CreateTag.php similarity index 92% rename from extensions/tags/src/Commands/CreateTag.php rename to extensions/tags/src/Command/CreateTag.php index 327c9709f..2109a63ef 100644 --- a/extensions/tags/src/Commands/CreateTag.php +++ b/extensions/tags/src/Command/CreateTag.php @@ -8,9 +8,9 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Commands; +namespace Flarum\Tags\Command; -use Flarum\Core\Users\User; +use Flarum\Core\User; class CreateTag { diff --git a/extensions/tags/src/Commands/CreateTagHandler.php b/extensions/tags/src/Command/CreateTagHandler.php similarity index 70% rename from extensions/tags/src/Commands/CreateTagHandler.php rename to extensions/tags/src/Command/CreateTagHandler.php index 94e4d2f9c..2de29769b 100644 --- a/extensions/tags/src/Commands/CreateTagHandler.php +++ b/extensions/tags/src/Command/CreateTagHandler.php @@ -8,26 +8,14 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Commands; +namespace Flarum\Tags\Command; +use Flarum\Core\Access\AssertPermissionTrait; use Flarum\Tags\Tag; -use Flarum\Core\Forum; -use Flarum\Events\TagWillBeSaved; class CreateTagHandler { - /** - * @var Forum - */ - protected $forum; - - /** - * @param Forum $forum - */ - public function __construct(Forum $forum) - { - $this->forum = $forum; - } + use AssertPermissionTrait; /** * @param CreateTag $command @@ -38,7 +26,7 @@ class CreateTagHandler $actor = $command->actor; $data = $command->data; - $this->forum->assertCan($actor, 'createTag'); + $this->assertCan($actor, 'createTag'); $tag = Tag::build( array_get($data, 'attributes.name'), diff --git a/extensions/tags/src/Commands/DeleteTag.php b/extensions/tags/src/Command/DeleteTag.php similarity index 92% rename from extensions/tags/src/Commands/DeleteTag.php rename to extensions/tags/src/Command/DeleteTag.php index d6a8dc0d3..f1f6f43d5 100644 --- a/extensions/tags/src/Commands/DeleteTag.php +++ b/extensions/tags/src/Command/DeleteTag.php @@ -8,10 +8,9 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Commands; +namespace Flarum\Tags\Command; -use Flarum\Tags\Tag; -use Flarum\Core\Users\User; +use Flarum\Core\User; class DeleteTag { diff --git a/extensions/tags/src/Commands/DeleteTagHandler.php b/extensions/tags/src/Command/DeleteTagHandler.php similarity index 74% rename from extensions/tags/src/Commands/DeleteTagHandler.php rename to extensions/tags/src/Command/DeleteTagHandler.php index 0be93a7bb..40c1de517 100644 --- a/extensions/tags/src/Commands/DeleteTagHandler.php +++ b/extensions/tags/src/Command/DeleteTagHandler.php @@ -8,13 +8,15 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Commands; +namespace Flarum\Tags\Command; -use Flarum\Tags\Tag; +use Flarum\Core\Access\AssertPermissionTrait; use Flarum\Tags\TagRepository; class DeleteTagHandler { + use AssertPermissionTrait; + /** * @var TagRepository */ @@ -30,8 +32,8 @@ class DeleteTagHandler /** * @param DeleteTag $command - * @return Tag - * @throws \Flarum\Core\Exceptions\PermissionDeniedException + * @return \Flarum\Tags\Tag + * @throws \Flarum\Core\Exception\PermissionDeniedException */ public function handle(DeleteTag $command) { @@ -39,7 +41,7 @@ class DeleteTagHandler $tag = $this->tags->findOrFail($command->tagId, $actor); - $tag->assertCan($actor, 'delete'); + $this->assertCan($actor, 'delete', $tag); $tag->delete(); diff --git a/extensions/tags/src/Commands/EditTag.php b/extensions/tags/src/Command/EditTag.php similarity index 91% rename from extensions/tags/src/Commands/EditTag.php rename to extensions/tags/src/Command/EditTag.php index cc4801387..aa51c311e 100644 --- a/extensions/tags/src/Commands/EditTag.php +++ b/extensions/tags/src/Command/EditTag.php @@ -8,10 +8,9 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Commands; +namespace Flarum\Tags\Command; -use Flarum\Core\Tags\Tag; -use Flarum\Core\Users\User; +use Flarum\Core\User; class EditTag { diff --git a/extensions/tags/src/Commands/EditTagHandler.php b/extensions/tags/src/Command/EditTagHandler.php similarity index 85% rename from extensions/tags/src/Commands/EditTagHandler.php rename to extensions/tags/src/Command/EditTagHandler.php index 59b8fd3e9..b1ea2ef28 100644 --- a/extensions/tags/src/Commands/EditTagHandler.php +++ b/extensions/tags/src/Command/EditTagHandler.php @@ -8,13 +8,15 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Commands; +namespace Flarum\Tags\Command; -use Flarum\Tags\Tag; +use Flarum\Core\Access\AssertPermissionTrait; use Flarum\Tags\TagRepository; class EditTagHandler { + use AssertPermissionTrait; + /** * @var TagRepository */ @@ -30,8 +32,8 @@ class EditTagHandler /** * @param EditTag $command - * @return Tag - * @throws \Flarum\Core\Exceptions\PermissionDeniedException + * @return \Flarum\Tags\Tag + * @throws \Flarum\Core\Exception\PermissionDeniedException */ public function handle(EditTag $command) { @@ -40,7 +42,7 @@ class EditTagHandler $tag = $this->tags->findOrFail($command->tagId, $actor); - $tag->assertCan($actor, 'edit'); + $this->assertCan($actor, 'edit', $tag); $attributes = array_get($data, 'attributes', []); diff --git a/extensions/tags/src/Events/DiscussionWasTagged.php b/extensions/tags/src/Event/DiscussionWasTagged.php similarity index 88% rename from extensions/tags/src/Events/DiscussionWasTagged.php rename to extensions/tags/src/Event/DiscussionWasTagged.php index f269af105..055373ce6 100644 --- a/extensions/tags/src/Events/DiscussionWasTagged.php +++ b/extensions/tags/src/Event/DiscussionWasTagged.php @@ -8,10 +8,10 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Events; +namespace Flarum\Tags\Event; -use Flarum\Core\Discussions\Discussion; -use Flarum\Core\Users\User; +use Flarum\Core\Discussion; +use Flarum\Core\User; class DiscussionWasTagged { diff --git a/extensions/tags/src/Gambits/TagGambit.php b/extensions/tags/src/Gambit/TagGambit.php similarity index 74% rename from extensions/tags/src/Gambits/TagGambit.php rename to extensions/tags/src/Gambit/TagGambit.php index 6b5f775cd..5dda52b32 100644 --- a/extensions/tags/src/Gambits/TagGambit.php +++ b/extensions/tags/src/Gambit/TagGambit.php @@ -8,31 +8,37 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Gambits; +namespace Flarum\Tags\Gambit; +use Flarum\Core\Search\AbstractRegexGambit; +use Flarum\Core\Search\AbstractSearch; use Flarum\Tags\TagRepository; -use Flarum\Core\Search\Search; -use Flarum\Core\Search\RegexGambit; use Illuminate\Database\Query\Expression; -class TagGambit extends RegexGambit +class TagGambit extends AbstractRegexGambit { + /** + * {@inheritdoc} + */ protected $pattern = 'tag:(.+)'; /** - * @var \Flarum\Tags\TagRepository + * @var TagRepository */ protected $tags; /** - * @param \Flarum\Tags\TagRepository $tags + * @param TagRepository $tags */ public function __construct(TagRepository $tags) { $this->tags = $tags; } - protected function conditions(Search $search, array $matches, $negate) + /** + * {@inheritdoc} + */ + protected function conditions(AbstractSearch $search, array $matches, $negate) { $slugs = explode(',', trim($matches[1], '"')); @@ -41,7 +47,7 @@ class TagGambit extends RegexGambit foreach ($slugs as $slug) { if ($slug === 'untagged') { $query->orWhereNotExists(function ($query) { - $query->select(app('flarum.db')->raw(1)) + $query->select(new Expression(1)) ->from('discussions_tags') ->where('discussions.id', new Expression('discussion_id')); }); @@ -49,7 +55,7 @@ class TagGambit extends RegexGambit $id = $this->tags->getIdForSlug($slug); $query->orWhereExists(function ($query) use ($id) { - $query->select(app('flarum.db')->raw(1)) + $query->select(new Expression(1)) ->from('discussions_tags') ->where('discussions.id', new Expression('discussion_id')) ->where('tag_id', $id); diff --git a/extensions/tags/src/Listeners/AddClientAssets.php b/extensions/tags/src/Listener/AddClientAssets.php similarity index 57% rename from extensions/tags/src/Listeners/AddClientAssets.php rename to extensions/tags/src/Listener/AddClientAssets.php index 14726cab1..70d0703f3 100755 --- a/extensions/tags/src/Listeners/AddClientAssets.php +++ b/extensions/tags/src/Listener/AddClientAssets.php @@ -8,32 +8,35 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Listeners; +namespace Flarum\Tags\Listener; -use Flarum\Events\RegisterLocales; -use Flarum\Events\BuildClientView; -use Flarum\Events\RegisterForumRoutes; +use Flarum\Event\ConfigureClientView; +use Flarum\Event\ConfigureForumRoutes; use Illuminate\Contracts\Events\Dispatcher; class AddClientAssets { + /** + * @param Dispatcher $events + */ public function subscribe(Dispatcher $events) { - $events->listen(BuildClientView::class, [$this, 'addAssets']); - $events->listen(RegisterForumRoutes::class, [$this, 'addRoutes']); + $events->listen(ConfigureClientView::class, [$this, 'addAssets']); + $events->listen(ConfigureForumRoutes::class, [$this, 'addRoutes']); } - public function addAssets(BuildClientView $event) + /** + * @param ConfigureClientView $event + */ + public function addAssets(ConfigureClientView $event) { if ($event->isForum()) { $event->addAssets([ __DIR__.'/../../js/forum/dist/extension.js', __DIR__.'/../../less/forum/extension.less' ]); - $event->addBootstrapper('flarum/tags/main'); - - $event->addTranslations(['flarum-tags.forum']); + $event->addTranslations('flarum-tags.forum'); } if ($event->isAdmin()) { @@ -41,14 +44,16 @@ class AddClientAssets __DIR__.'/../../js/admin/dist/extension.js', __DIR__.'/../../less/admin/extension.less' ]); - $event->addBootstrapper('flarum/tags/main'); } } - public function addRoutes(RegisterForumRoutes $event) + /** + * @param ConfigureForumRoutes $event + */ + public function addRoutes(ConfigureForumRoutes $event) { - $event->get('/t/{slug}', 'tags.forum.tag'); - $event->get('/tags', 'tags.forum.tags'); + $event->get('/t/{slug}', 'tag'); + $event->get('/tags', 'tags'); } } diff --git a/extensions/tags/src/Listener/AddDiscussionTagsRelationship.php b/extensions/tags/src/Listener/AddDiscussionTagsRelationship.php new file mode 100755 index 000000000..5bffa4b1e --- /dev/null +++ b/extensions/tags/src/Listener/AddDiscussionTagsRelationship.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Tags\Listener; + +use Flarum\Api\Controller; +use Flarum\Api\Serializer\DiscussionSerializer; +use Flarum\Core\Discussion; +use Flarum\Event\ConfigureApiController; +use Flarum\Event\GetApiRelationship; +use Flarum\Event\GetModelRelationship; +use Flarum\Event\PrepareApiAttributes; +use Flarum\Tags\Tag; +use Illuminate\Contracts\Events\Dispatcher; + +class AddDiscussionTagsRelationship +{ + /** + * @param Dispatcher $events + */ + public function subscribe(Dispatcher $events) + { + $events->listen(GetModelRelationship::class, [$this, 'getModelRelationship']); + $events->listen(GetApiRelationship::class, [$this, 'getApiRelationship']); + $events->listen(ConfigureApiController::class, [$this, 'includeTagsRelationship']); + $events->listen(PrepareApiAttributes::class, [$this, 'prepareApiAttributes']); + } + + /** + * @param GetModelRelationship $event + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany|null + */ + public function getModelRelationship(GetModelRelationship $event) + { + if ($event->isRelationship(Discussion::class, 'tags')) { + return $event->model->belongsToMany(Tag::class, 'discussions_tags', null, null, 'tags'); + } + } + + /** + * @param GetApiRelationship $event + * @return \Flarum\Api\Relationship\HasManyBuilder|null + */ + public function getApiRelationship(GetApiRelationship $event) + { + if ($event->isRelationship(DiscussionSerializer::class, 'tags')) { + return $event->serializer->hasMany('Flarum\Tags\Api\Serializer\TagSerializer', 'tags'); + } + } + + /** + * @param ConfigureApiController $event + */ + public function includeTagsRelationship(ConfigureApiController $event) + { + if ($event->isController(Controller\ListDiscussionsController::class) + || $event->isController(Controller\ShowDiscussionController::class) + || $event->isController(Controller\CreateDiscussionController::class)) { + $event->addInclude('tags'); + } + + if ($event->isController(Controller\CreateDiscussionController::class)) { + $event->addInclude('tags.lastDiscussion'); + } + } + + /** + * @param PrepareApiAttributes $event + */ + public function prepareApiAttributes(PrepareApiAttributes $event) + { + if ($event->isSerializer(DiscussionSerializer::class)) { + $event->attributes['canTag'] = $event->actor->can('tag', $event->model); + } + } +} diff --git a/extensions/tags/src/Listener/AddForumTagsRelationship.php b/extensions/tags/src/Listener/AddForumTagsRelationship.php new file mode 100755 index 000000000..9d9bdb4a0 --- /dev/null +++ b/extensions/tags/src/Listener/AddForumTagsRelationship.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Tags\Listener; + +use Flarum\Api\Controller\ShowForumController; +use Flarum\Api\Serializer\ForumSerializer; +use Flarum\Event\ConfigureApiController; +use Flarum\Event\GetApiRelationship; +use Flarum\Event\PrepareApiAttributes; +use Flarum\Event\PrepareApiData; +use Flarum\Settings\SettingsRepository; +use Flarum\Tags\Tag; +use Illuminate\Contracts\Events\Dispatcher; + +class AddForumTagsRelationship +{ + /** + * @var SettingsRepository + */ + protected $settings; + + /** + * @param SettingsRepository $settings + */ + public function __construct(SettingsRepository $settings) + { + $this->settings = $settings; + } + + /** + * @param Dispatcher $events + */ + public function subscribe(Dispatcher $events) + { + $events->listen(GetApiRelationship::class, [$this, 'getApiRelationship']); + $events->listen(PrepareApiData::class, [$this, 'loadTagsRelationship']); + $events->listen(ConfigureApiController::class, [$this, 'includeTagsRelationship']); + $events->listen(PrepareApiAttributes::class, [$this, 'prepareApiAttributes']); + } + + /** + * @param GetApiRelationship $event + * @return \Flarum\Api\Relationship\HasManyBuilder|null + */ + public function getApiRelationship(GetApiRelationship $event) + { + if ($event->isRelationship(ForumSerializer::class, 'tags')) { + return $event->serializer->hasMany('Flarum\Tags\Api\Serializer\TagSerializer', 'tags'); + } + } + + /** + * @param PrepareApiData $event + */ + public function loadTagsRelationship(PrepareApiData $event) + { + // Expose the complete tag list to clients by adding it as a + // relationship to the /api/forum endpoint. Since the Forum model + // doesn't actually have a tags relationship, we will manually load and + // assign the tags data to it using an event listener. + if ($event->isController(ShowForumController::class)) { + $event->data['tags'] = Tag::whereVisibleTo($event->actor)->with('lastDiscussion')->get(); + } + } + + /** + * @param ConfigureApiController $event + */ + public function includeTagsRelationship(ConfigureApiController $event) + { + if ($event->isController(ShowForumController::class)) { + $event->addInclude(['tags', 'tags.lastDiscussion', 'tags.parent']); + } + } + + /** + * @param PrepareApiAttributes $event + */ + public function prepareApiAttributes(PrepareApiAttributes $event) + { + if ($event->isSerializer(ForumSerializer::class)) { + $event->attributes['minPrimaryTags'] = $this->settings->get('flarum-tags.min_primary_tags'); + $event->attributes['maxPrimaryTags'] = $this->settings->get('flarum-tags.max_primary_tags'); + $event->attributes['minSecondaryTags'] = $this->settings->get('flarum-tags.min_secondary_tags'); + $event->attributes['maxSecondaryTags'] = $this->settings->get('flarum-tags.max_secondary_tags'); + } + } +} diff --git a/extensions/tags/src/Listener/AddTagsApi.php b/extensions/tags/src/Listener/AddTagsApi.php new file mode 100755 index 000000000..c5986ece1 --- /dev/null +++ b/extensions/tags/src/Listener/AddTagsApi.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Tags\Listener; + +use Flarum\Tags\Api\Controller; +use Flarum\Event\ConfigureApiRoutes; +use Illuminate\Contracts\Events\Dispatcher; + +class AddTagsApi +{ + public function subscribe(Dispatcher $events) + { + $events->listen(ConfigureApiRoutes::class, [$this, 'configureApiRoutes']); + } + + public function configureApiRoutes(ConfigureApiRoutes $event) + { + $event->post('/tags', 'tags.create', Controller\CreateTagController::class); + $event->post('/tags/order', 'tags.order', Controller\OrderTagsController::class); + $event->patch('/tags/{id}', 'tags.update', Controller\UpdateTagController::class); + $event->delete('/tags/{id}', 'tags.delete', Controller\DeleteTagController::class); + } +} diff --git a/extensions/tags/src/Listeners/LogDiscussionTagged.php b/extensions/tags/src/Listener/CreatePostWhenTagsAreChanged.php similarity index 59% rename from extensions/tags/src/Listeners/LogDiscussionTagged.php rename to extensions/tags/src/Listener/CreatePostWhenTagsAreChanged.php index b44ba20dc..5ea3a6470 100755 --- a/extensions/tags/src/Listeners/LogDiscussionTagged.php +++ b/extensions/tags/src/Listener/CreatePostWhenTagsAreChanged.php @@ -8,26 +8,35 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Listeners; +namespace Flarum\Tags\Listener; -use Flarum\Events\RegisterPostTypes; -use Flarum\Tags\Posts\DiscussionTaggedPost; -use Flarum\Tags\Events\DiscussionWasTagged; +use Flarum\Event\ConfigurePostTypes; +use Flarum\Tags\Event\DiscussionWasTagged; +use Flarum\Tags\Post\DiscussionTaggedPost; use Illuminate\Contracts\Events\Dispatcher; -class LogDiscussionTagged +class CreatePostWhenTagsAreChanged { + /** + * @param Dispatcher $events + */ public function subscribe(Dispatcher $events) { - $events->listen(RegisterPostTypes::class, [$this, 'registerPostType']); + $events->listen(ConfigurePostTypes::class, [$this, 'addPostType']); $events->listen(DiscussionWasTagged::class, [$this, 'whenDiscussionWasTagged']); } - public function registerPostType(RegisterPostTypes $event) + /** + * @param ConfigurePostTypes $event + */ + public function addPostType(ConfigurePostTypes $event) { - $event->register(DiscussionTaggedPost::class); + $event->add(DiscussionTaggedPost::class); } + /** + * @param DiscussionWasTagged $event + */ public function whenDiscussionWasTagged(DiscussionWasTagged $event) { $post = DiscussionTaggedPost::reply( diff --git a/extensions/tags/src/Listeners/AddTagGambit.php b/extensions/tags/src/Listener/FilterDiscussionListByTags.php similarity index 55% rename from extensions/tags/src/Listeners/AddTagGambit.php rename to extensions/tags/src/Listener/FilterDiscussionListByTags.php index 886d27151..01e838506 100755 --- a/extensions/tags/src/Listeners/AddTagGambit.php +++ b/extensions/tags/src/Listener/FilterDiscussionListByTags.php @@ -8,29 +8,38 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Listeners; +namespace Flarum\Tags\Listener; -use Flarum\Events\RegisterDiscussionGambits; -use Flarum\Events\DiscussionSearchWillBePerformed; -use Illuminate\Contracts\Events\Dispatcher; -use Flarum\Tags\Gambits\TagGambit; +use Flarum\Event\ConfigureDiscussionGambits; +use Flarum\Event\DiscussionSearchWillBePerformed; +use Flarum\Tags\Gambit\TagGambit; use Flarum\Tags\Tag; +use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Query\Expression; -class AddTagGambit +class FilterDiscussionListByTags { + /** + * @param Dispatcher $events + */ public function subscribe(Dispatcher $events) { - $events->listen(RegisterDiscussionGambits::class, [$this, 'registerTagGambit']); - $events->listen(DiscussionSearchWillBePerformed::class, [$this, 'hideTags']); + $events->listen(ConfigureDiscussionGambits::class, [$this, 'addTagGambit']); + $events->listen(DiscussionSearchWillBePerformed::class, [$this, 'hideTagsFromDiscussionList']); } - public function registerTagGambit(RegisterDiscussionGambits $event) + /** + * @param ConfigureDiscussionGambits $event + */ + public function addTagGambit(ConfigureDiscussionGambits $event) { - $event->gambits->add('Flarum\Tags\Gambits\TagGambit'); + $event->gambits->add(TagGambit::class); } - public function hideTags(DiscussionSearchWillBePerformed $event) + /** + * @param DiscussionSearchWillBePerformed $event + */ + public function hideTagsFromDiscussionList(DiscussionSearchWillBePerformed $event) { $query = $event->search->getQuery(); @@ -41,7 +50,7 @@ class AddTagGambit } $query->whereNotExists(function ($query) { - return $query->select(app('flarum.db')->raw(1)) + return $query->select(new Expression(1)) ->from('discussions_tags') ->whereIn('tag_id', Tag::where('is_hidden', 1)->lists('id')) ->where('discussions.id', new Expression('discussion_id')); diff --git a/extensions/tags/src/Listeners/PersistData.php b/extensions/tags/src/Listener/SaveTagsToDatabase.php similarity index 54% rename from extensions/tags/src/Listeners/PersistData.php rename to extensions/tags/src/Listener/SaveTagsToDatabase.php index cfee7142a..5469e9934 100755 --- a/extensions/tags/src/Listeners/PersistData.php +++ b/extensions/tags/src/Listener/SaveTagsToDatabase.php @@ -8,32 +8,47 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Listeners; +namespace Flarum\Tags\Listener; +use Flarum\Core\Exception\PermissionDeniedException; +use Flarum\Core\Exception\ValidationException; +use Flarum\Event\DiscussionWillBeSaved; +use Flarum\Settings\SettingsRepository; +use Flarum\Tags\Event\DiscussionWasTagged; use Flarum\Tags\Tag; -use Flarum\Tags\Events\DiscussionWasTagged; -use Flarum\Events\DiscussionWillBeSaved; -use Flarum\Core\Discussions\Discussion; -use Flarum\Core\Exceptions\PermissionDeniedException; -use Flarum\Core\Settings\SettingsRepository; -use Flarum\Tags\TagCountException; +use Illuminate\Contracts\Events\Dispatcher; -class PersistData +class SaveTagsToDatabase { + /** + * @var SettingsRepository + */ protected $settings; + /** + * @param SettingsRepository $settings + */ public function __construct(SettingsRepository $settings) { $this->settings = $settings; } - public function subscribe($events) + /** + * @param Dispatcher $events + */ + public function subscribe(Dispatcher $events) { $events->listen(DiscussionWillBeSaved::class, [$this, 'whenDiscussionWillBeSaved']); } + /** + * @param DiscussionWillBeSaved $event + * @throws PermissionDeniedException + * @throws ValidationException + */ public function whenDiscussionWillBeSaved(DiscussionWillBeSaved $event) { + // TODO: clean up, prevent discussion from being created without tags if (isset($event->data['relationships']['tags']['data'])) { $discussion = $event->discussion; $actor = $event->actor; @@ -49,7 +64,7 @@ class PersistData $secondaryCount = 0; foreach ($newTags as $tag) { - if (! $tag->can($actor, 'startDiscussion')) { + if ($actor->cannot('startDiscussion', $tag)) { throw new PermissionDeniedException; } @@ -63,8 +78,6 @@ class PersistData $this->validatePrimaryTagCount($primaryCount); $this->validateSecondaryTagCount($secondaryCount); - $oldTags = []; - if ($discussion->exists) { $oldTags = $discussion->tags()->get(); $oldTagIds = $oldTags->lists('id'); @@ -73,32 +86,46 @@ class PersistData return; } - $discussion->raise(new DiscussionWasTagged($discussion, $actor, $oldTags->all())); + $discussion->raise( + new DiscussionWasTagged($discussion, $actor, $oldTags->all()) + ); } - Discussion::saved(function ($discussion) use ($newTagIds) { + $discussion->afterSave(function ($discussion) use ($newTagIds) { $discussion->tags()->sync($newTagIds); }); } } + /** + * @param $count + * @throws ValidationException + */ protected function validatePrimaryTagCount($count) { - $min = $this->settings->get('tags.min_primary_tags'); - $max = $this->settings->get('tags.max_primary_tags'); + $min = $this->settings->get('flarum-tags.min_primary_tags'); + $max = $this->settings->get('flarum-tags.max_primary_tags'); if ($count < $min || $count > $max) { - throw new TagCountException(['tags' => sprintf('Discussion must have between %d and %d primary tags.', $min, $max)]); + throw new ValidationException([ + 'tags' => sprintf('Discussion must have between %d and %d primary tags.', $min, $max) + ]); } } + /** + * @param $count + * @throws ValidationException + */ protected function validateSecondaryTagCount($count) { - $min = $this->settings->get('tags.min_secondary_tags'); - $max = $this->settings->get('tags.max_secondary_tags'); + $min = $this->settings->get('flarum-tags.min_secondary_tags'); + $max = $this->settings->get('flarum-tags.max_secondary_tags'); if ($count < $min || $count > $max) { - throw new TagCountException(['tags' => sprintf('Discussion must have between %d and %d secondary tags.', $min, $max)]); + throw new ValidationException([ + 'tags' => sprintf('Discussion must have between %d and %d secondary tags.', $min, $max) + ]); } } } diff --git a/extensions/tags/src/Listeners/UpdateTagMetadata.php b/extensions/tags/src/Listener/UpdateTagMetadata.php similarity index 72% rename from extensions/tags/src/Listeners/UpdateTagMetadata.php rename to extensions/tags/src/Listener/UpdateTagMetadata.php index 635bc45ad..0b3a686c9 100755 --- a/extensions/tags/src/Listeners/UpdateTagMetadata.php +++ b/extensions/tags/src/Listener/UpdateTagMetadata.php @@ -8,22 +8,25 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Listeners; +namespace Flarum\Tags\Listener; +use Flarum\Core\Post; +use Flarum\Event\DiscussionWasDeleted; +use Flarum\Event\DiscussionWasStarted; +use Flarum\Event\PostWasDeleted; +use Flarum\Event\PostWasHidden; +use Flarum\Event\PostWasPosted; +use Flarum\Event\PostWasRestored; +use Flarum\Tags\Event\DiscussionWasTagged; use Flarum\Tags\Tag; -use Flarum\Tags\Events\DiscussionWasTagged; -use Flarum\Events\DiscussionWasStarted; -use Flarum\Events\DiscussionWasDeleted; -use Flarum\Core\Discussions\Discussion; -use Flarum\Core\Posts\Post; -use Flarum\Events\PostWasPosted; -use Flarum\Events\PostWasDeleted; -use Flarum\Events\PostWasHidden; -use Flarum\Events\PostWasRestored; +use Illuminate\Contracts\Events\Dispatcher; class UpdateTagMetadata { - public function subscribe($events) + /** + * @param Dispatcher $events + */ + public function subscribe(Dispatcher $events) { $events->listen(DiscussionWasStarted::class, [$this, 'whenDiscussionWasStarted']); $events->listen(DiscussionWasTagged::class, [$this, 'whenDiscussionWasTagged']); @@ -35,20 +38,28 @@ class UpdateTagMetadata $events->listen(PostWasRestored::class, [$this, 'whenPostWasRestored']); } + /** + * @param DiscussionWasStarted $event + */ public function whenDiscussionWasStarted(DiscussionWasStarted $event) { $this->updateTags($event->discussion, 1); } + /** + * @param DiscussionWasTagged $event + */ public function whenDiscussionWasTagged(DiscussionWasTagged $event) { $oldTags = Tag::whereIn('id', array_pluck($event->oldTags, 'id')); $this->updateTags($event->discussion, -1, $oldTags); - $this->updateTags($event->discussion, 1); } + /** + * @param DiscussionWasDeleted $event + */ public function whenDiscussionWasDeleted(DiscussionWasDeleted $event) { $this->updateTags($event->discussion, -1); @@ -56,26 +67,43 @@ class UpdateTagMetadata $event->discussion->tags()->detach(); } + /** + * @param PostWasPosted $event + */ public function whenPostWasPosted(PostWasPosted $event) { $this->updateTags($event->post->discussion); } + /** + * @param PostWasDeleted $event + */ public function whenPostWasDeleted(PostWasDeleted $event) { $this->updateTags($event->post->discussion); } + /** + * @param PostWasHidden $event + */ public function whenPostWasHidden(PostWasHidden $event) { $this->updateTags($event->post->discussion); } + /** + * @param PostWasRestored $event + */ public function whenPostWasRestored(PostWasRestored $event) { $this->updateTags($event->post->discussion); } + /** + * @param \Flarum\Core\Discussion $discussion + * @param int $delta + * @param Tag[]|null $tags + */ protected function updateTags($discussion, $delta = 0, $tags = null) { if (! $tags) { diff --git a/extensions/tags/src/Listeners/AddApiAttributes.php b/extensions/tags/src/Listeners/AddApiAttributes.php deleted file mode 100755 index 2e320c1f5..000000000 --- a/extensions/tags/src/Listeners/AddApiAttributes.php +++ /dev/null @@ -1,106 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Tags\Listeners; - -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; -use Flarum\Api\Serializers\DiscussionSerializer; -use Flarum\Tags\Tag; - -class AddApiAttributes -{ - public function subscribe($events) - { - $events->listen(ApiRelationship::class, [$this, 'addTagsRelationship']); - $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\Api\TagSerializer', 'tags'); - } - - if ($event->serializer instanceof DiscussionSerializer && - $event->relationship === 'tags') { - return $event->serializer->hasMany('Flarum\Tags\Api\TagSerializer', 'tags'); - } - } - - public function loadTagsRelationship(WillSerializeData $event) - { - // Expose the complete tag list to clients by adding it as a - // relationship to the /api/forum endpoint. Since the Forum model - // doesn't actually have a tags relationship, we will manually load and - // assign the tags data to it using an event listener. - if ($event->action instanceof Forum\ShowAction) { - $forum = $event->data; - - $query = Tag::whereVisibleTo($event->request->actor); - - $forum->tags = $query->with('lastDiscussion')->get(); - $forum->tags_ids = $forum->tags->lists('id'); - } - } - - public function includeTagsRelationship(BuildApiAction $event) - { - if ($event->action instanceof Forum\ShowAction) { - $event->addInclude('tags'); - $event->addInclude('tags.lastDiscussion'); - $event->addLink('tags.parent'); - } - - if ($event->action instanceof Discussions\IndexAction || - $event->action instanceof Discussions\ShowAction || - $event->action instanceof Discussions\CreateAction) { - $event->addInclude('tags'); - } - - if ($event->action instanceof Discussions\CreateAction) { - $event->addInclude('tags.lastDiscussion'); - } - } - - public function addAttributes(ApiAttributes $event) - { - if ($event->serializer instanceof DiscussionSerializer) { - $event->attributes['canTag'] = $event->model->can($event->actor, 'tag'); - } - - if ($event->serializer instanceof ForumSerializer) { - $settings = app('Flarum\Core\Settings\SettingsRepository'); - - $event->attributes['minPrimaryTags'] = $settings->get('tags.min_primary_tags'); - $event->attributes['maxPrimaryTags'] = $settings->get('tags.max_primary_tags'); - $event->attributes['minSecondaryTags'] = $settings->get('tags.min_secondary_tags'); - $event->attributes['maxSecondaryTags'] = $settings->get('tags.max_secondary_tags'); - } - } - - 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/Listeners/AddModelRelationship.php b/extensions/tags/src/Listeners/AddModelRelationship.php deleted file mode 100755 index 12d3d7c35..000000000 --- a/extensions/tags/src/Listeners/AddModelRelationship.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Tags\Listeners; - -use Flarum\Events\ModelRelationship; -use Flarum\Core\Discussions\Discussion; -use Flarum\Tags\Tag; - -class AddModelRelationship -{ - public function subscribe($events) - { - $events->listen(ModelRelationship::class, [$this, 'addTagsRelationship']); - } - - public function addTagsRelationship(ModelRelationship $event) - { - if ($event->model instanceof Discussion && - $event->relationship === 'tags') { - return $event->model->belongsToMany('Flarum\Tags\Tag', 'discussions_tags', null, null, 'tags'); - } - } -} diff --git a/extensions/tags/src/Listeners/ConfigureDiscussionPermissions.php b/extensions/tags/src/Listeners/ConfigureDiscussionPermissions.php deleted file mode 100755 index 2cae5cb32..000000000 --- a/extensions/tags/src/Listeners/ConfigureDiscussionPermissions.php +++ /dev/null @@ -1,101 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Tags\Listeners; - -use Flarum\Events\ScopeModelVisibility; -use Flarum\Events\ScopeHiddenDiscussionVisibility; -use Flarum\Events\ModelAllow; -use Flarum\Core\Discussions\Discussion; -use Flarum\Tags\Tag; -use Flarum\Reports\Report; -use Illuminate\Database\Query\Expression; - -class ConfigureDiscussionPermissions -{ - public function subscribe($events) - { - $events->listen(ScopeModelVisibility::class, [$this, 'scopeDiscussionVisibility']); - $events->listen(ScopeHiddenDiscussionVisibility::class, [$this, 'scopeHiddenDiscussionVisibility']); - $events->listen(ModelAllow::class, [$this, 'allowDiscussionPermissions']); - } - - public function scopeDiscussionVisibility(ScopeModelVisibility $event) - { - // Hide discussions which have tags that the user is not allowed to see. - if ($event->model instanceof Discussion) { - $event->query->whereNotExists(function ($query) use ($event) { - return $query->select(new Expression(1)) - ->from('discussions_tags') - ->whereIn('tag_id', Tag::getIdsWhereCannot($event->actor, 'view')) - ->where('discussions.id', new Expression('discussion_id')); - }); - } - - if ($event->model instanceof Flag) { - $event->query - ->select('flags.*') - ->leftJoin('posts', 'posts.id', '=', 'flags.post_id') - ->leftJoin('discussions', 'discussions.id', '=', 'posts.discussion_id') - ->whereNotExists(function ($query) use ($event) { - return $query->select(new Expression(1)) - ->from('discussions_tags') - ->whereIn('tag_id', Tag::getIdsWhereCannot($event->actor, 'discussion.viewFlags')) - ->where('discussions.id', new Expression('discussion_id')); - }); - } - } - - public function scopeHiddenDiscussionVisibility(ScopeHiddenDiscussionVisibility $event) - { - // By default, discussions are not visible to the public if they are - // hidden or contain zero comments - unless the actor has a certain - // permission. Since we grant permissions per-tag, we will make - // discussions visible in the tags for which the user has that - // permission. - $event->query->orWhereExists(function ($query) use ($event) { - return $query->select(new Expression(1)) - ->from('discussions_tags') - ->whereIn('tag_id', Tag::getIdsWhereCan($event->actor, $event->permission)) - ->where('discussions.id', new Expression('discussion_id')); - }); - } - - public function allowDiscussionPermissions(ModelAllow $event) - { - // Wrap all discussion permission checks with some logic pertaining to - // the discussion's tags. If the discussion has a tag that has been - // restricted, and the user has this permission for that tag, then they - // are allowed. If the discussion only has tags that have been - // restricted, then the user *must* have permission for at least one of - // them. - if ($event->model instanceof Discussion) { - $tags = $event->model->tags; - - if (count($tags)) { - $restricted = true; - - foreach ($tags as $tag) { - if ($tag->is_restricted) { - if ($event->actor->hasPermission('tag' . $tag->id . '.discussion.' . $event->action)) { - return true; - } - } else { - $restricted = false; - } - } - - if ($restricted) { - return false; - } - } - } - } -} diff --git a/extensions/tags/src/Listeners/ConfigureTagPermissions.php b/extensions/tags/src/Listeners/ConfigureTagPermissions.php deleted file mode 100755 index a4e9e36bd..000000000 --- a/extensions/tags/src/Listeners/ConfigureTagPermissions.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Tags\Listeners; - -use Flarum\Events\ScopeModelVisibility; -use Flarum\Events\ModelAllow; -use Flarum\Tags\Tag; - -class ConfigureTagPermissions -{ - public function subscribe($events) - { - $events->listen(ScopeModelVisibility::class, [$this, 'scopeTagVisibility']); - $events->listen(ModelAllow::class, [$this, 'allowStartDiscussion']); - } - - public function scopeTagVisibility(ScopeModelVisibility $event) - { - if ($event->model instanceof Tag) { - $event->query->whereNotIn('id', Tag::getIdsWhereCannot($event->actor, 'view')); - } - } - - public function allowStartDiscussion(ModelAllow $event) - { - if ($event->model instanceof Tag) { - if (! $event->model->is_restricted || - $event->actor->hasPermission('tag' . $event->model->id . '.startDiscussion')) { - return true; - } - } - } -} diff --git a/extensions/tags/src/Posts/DiscussionTaggedPost.php b/extensions/tags/src/Post/DiscussionTaggedPost.php similarity index 87% rename from extensions/tags/src/Posts/DiscussionTaggedPost.php rename to extensions/tags/src/Post/DiscussionTaggedPost.php index 35870caae..3970a1e12 100755 --- a/extensions/tags/src/Posts/DiscussionTaggedPost.php +++ b/extensions/tags/src/Post/DiscussionTaggedPost.php @@ -8,16 +8,22 @@ * file that was distributed with this source code. */ -namespace Flarum\Tags\Posts; +namespace Flarum\Tags\Post; -use Flarum\Core\Posts\Post; -use Flarum\Core\Posts\EventPost; -use Flarum\Core\Posts\MergeablePost; +use Flarum\Core\Post; +use Flarum\Core\Post\AbstractEventPost; +use Flarum\Core\Post\MergeableInterface; -class DiscussionTaggedPost extends EventPost implements MergeablePost +class DiscussionTaggedPost extends AbstractEventPost implements MergeableInterface { + /** + * {@inheritdoc} + */ public static $type = 'discussionTagged'; + /** + * {@inheritdoc} + */ public function saveAfter(Post $previous) { // If the previous post is another 'discussion tagged' post, and it's diff --git a/extensions/tags/src/Tag.php b/extensions/tags/src/Tag.php index f8d378007..37470b8db 100644 --- a/extensions/tags/src/Tag.php +++ b/extensions/tags/src/Tag.php @@ -10,32 +10,27 @@ namespace Flarum\Tags; -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; +use Flarum\Core\Discussion; +use Flarum\Core\Permission; +use Flarum\Core\Support\ScopeVisibilityTrait; +use Flarum\Database\AbstractModel; -class Tag extends Model +class Tag extends AbstractModel { - use ValidatesBeforeSave; - use VisibleScope; - use Locked; - - protected $table = 'tags'; - - protected $dates = ['last_time']; - - protected $rules = [ - 'name' => 'required', - 'slug' => ['required', 'unique:slug'] - ]; + use ScopeVisibilityTrait; /** - * Boot the model. - * - * @return void + * {@inheritdoc} + */ + protected $table = 'tags'; + + /** + * {@inheritdoc} + */ + protected $dates = ['last_time']; + + /** + * {@inheritdoc} */ public static function boot() { @@ -71,19 +66,28 @@ class Tag extends Model return $tag; } + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function parent() { return $this->belongsTo('Flarum\Tags\Tag', 'parent_id'); } + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function lastDiscussion() { - return $this->belongsTo('Flarum\Core\Discussions\Discussion', 'last_discussion_id'); + return $this->belongsTo('Flarum\Core\Discussion', 'last_discussion_id'); } + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ public function discussions() { - return $this->belongsToMany('Flarum\Core\Discussions\Discussion', 'discussions_tags'); + return $this->belongsToMany('Flarum\Core\Discussion', 'discussions_tags'); } /** @@ -114,6 +118,11 @@ class Tag extends Model return $this; } + /** + * @param $user + * @param $permission + * @return array + */ public static function getIdsWhereCan($user, $permission) { static $tags; @@ -134,6 +143,11 @@ class Tag extends Model return $ids; } + /** + * @param $user + * @param $permission + * @return array + */ public static function getIdsWhereCannot($user, $permission) { static $tags; diff --git a/extensions/tags/src/TagRepository.php b/extensions/tags/src/TagRepository.php index b104d0b5d..755139035 100644 --- a/extensions/tags/src/TagRepository.php +++ b/extensions/tags/src/TagRepository.php @@ -10,9 +10,8 @@ namespace Flarum\Tags; +use Flarum\Core\User; use Illuminate\Database\Eloquent\Builder; -use Flarum\Core\Users\User; -use Flarum\Tags\Tag; class TagRepository { @@ -23,7 +22,6 @@ class TagRepository * @param int $id * @param User $actor * @return Tag - * * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ public function findOrFail($id, User $actor = null) diff --git a/extensions/tags/src/TagCountException.php b/extensions/tags/src/TagValidator.php similarity index 51% rename from extensions/tags/src/TagCountException.php rename to extensions/tags/src/TagValidator.php index 12c1fd148..3411f5d17 100644 --- a/extensions/tags/src/TagCountException.php +++ b/extensions/tags/src/TagValidator.php @@ -10,8 +10,15 @@ namespace Flarum\Tags; -use Flarum\Core\Exceptions\ValidationException; +use Flarum\Core\Validator\AbstractValidator; -class TagCountException extends ValidationException +class TagValidator extends AbstractValidator { + /** + * {@inheritdoc} + */ + protected $rules = [ + 'name' => ['required'], + 'slug' => ['required', 'unique:slug'] + ]; }