From 5919d16edfcbbc8a473ad70d1b4e93a6bd9d3d62 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 5 May 2015 08:39:24 +0930 Subject: [PATCH] Initial commit --- extensions/tags/.gitignore | 4 + extensions/tags/bootstrap.php | 5 + extensions/tags/composer.json | 11 + extensions/tags/extension.json | 15 + extensions/tags/js/.gitignore | 3 + extensions/tags/js/Gulpfile.js | 45 +++ extensions/tags/js/bootstrap.js | 274 ++++++++++++++++++ extensions/tags/js/package.json | 18 ++ extensions/tags/js/src/category.js | 12 + .../tags/js/src/components/categories-page.js | 38 +++ .../src/components/post-discussion-moved.js | 59 ++++ extensions/tags/less/categories.less | 206 +++++++++++++ ..._24_000000_add_category_to_discussions.php | 31 ++ ...5_02_24_000000_create_categories_table.php | 32 ++ .../tags/src/CategoriesServiceProvider.php | 36 +++ extensions/tags/src/Category.php | 8 + extensions/tags/src/CategoryGambit.php | 25 ++ extensions/tags/src/CategorySerializer.php | 26 ++ extensions/tags/src/DiscussionMovedPost.php | 49 ++++ extensions/tags/src/Handlers/Handler.php | 100 +++++++ 20 files changed, 997 insertions(+) create mode 100644 extensions/tags/.gitignore create mode 100644 extensions/tags/bootstrap.php create mode 100644 extensions/tags/composer.json create mode 100644 extensions/tags/extension.json create mode 100644 extensions/tags/js/.gitignore create mode 100644 extensions/tags/js/Gulpfile.js create mode 100644 extensions/tags/js/bootstrap.js create mode 100644 extensions/tags/js/package.json create mode 100644 extensions/tags/js/src/category.js create mode 100644 extensions/tags/js/src/components/categories-page.js create mode 100644 extensions/tags/js/src/components/post-discussion-moved.js create mode 100644 extensions/tags/less/categories.less create mode 100644 extensions/tags/migrations/2015_02_24_000000_add_category_to_discussions.php create mode 100644 extensions/tags/migrations/2015_02_24_000000_create_categories_table.php create mode 100644 extensions/tags/src/CategoriesServiceProvider.php create mode 100644 extensions/tags/src/Category.php create mode 100644 extensions/tags/src/CategoryGambit.php create mode 100644 extensions/tags/src/CategorySerializer.php create mode 100755 extensions/tags/src/DiscussionMovedPost.php create mode 100755 extensions/tags/src/Handlers/Handler.php diff --git a/extensions/tags/.gitignore b/extensions/tags/.gitignore new file mode 100644 index 000000000..a4f3b125e --- /dev/null +++ b/extensions/tags/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.phar +.DS_Store +Thumbs.db diff --git a/extensions/tags/bootstrap.php b/extensions/tags/bootstrap.php new file mode 100644 index 000000000..bcde602ce --- /dev/null +++ b/extensions/tags/bootstrap.php @@ -0,0 +1,5 @@ +app->register('Flarum\Categories\CategoriesServiceProvider'); diff --git a/extensions/tags/composer.json b/extensions/tags/composer.json new file mode 100644 index 000000000..f95f058d8 --- /dev/null +++ b/extensions/tags/composer.json @@ -0,0 +1,11 @@ +{ + "require": { + "php": ">=5.4.0" + }, + "autoload": { + "psr-4": { + "Flarum\\Categories\\": "src/" + } + }, + "minimum-stability": "dev" +} diff --git a/extensions/tags/extension.json b/extensions/tags/extension.json new file mode 100644 index 000000000..be33153e4 --- /dev/null +++ b/extensions/tags/extension.json @@ -0,0 +1,15 @@ +{ + "name": "categories", + "description": "Organise discussions into a heirarchy of categories.", + "version": "0.1.0", + "author": { + "name": "Toby Zerner", + "email": "toby@flarum.org", + "website": "http://tobyzerner.com" + }, + "license": "MIT", + "require": { + "php": ">=5.4.0", + "flarum": ">1.0.0" + } +} diff --git a/extensions/tags/js/.gitignore b/extensions/tags/js/.gitignore new file mode 100644 index 000000000..372e20a51 --- /dev/null +++ b/extensions/tags/js/.gitignore @@ -0,0 +1,3 @@ +bower_components +node_modules +dist diff --git a/extensions/tags/js/Gulpfile.js b/extensions/tags/js/Gulpfile.js new file mode 100644 index 000000000..baa47a7ac --- /dev/null +++ b/extensions/tags/js/Gulpfile.js @@ -0,0 +1,45 @@ +var gulp = require('gulp'); +var livereload = require('gulp-livereload'); +var concat = require('gulp-concat'); +var argv = require('yargs').argv; +var uglify = require('gulp-uglify'); +var gulpif = require('gulp-if'); +var babel = require('gulp-babel'); +var cached = require('gulp-cached'); +var remember = require('gulp-remember'); +var merge = require('merge-stream'); +var streamqueue = require('streamqueue'); + +var staticFiles = [ + 'bootstrap.js' +]; +var moduleFiles = [ + 'src/**/*.js' +]; +var modulePrefix = 'categories'; + +gulp.task('default', function() { + return streamqueue({objectMode: true}, + gulp.src(moduleFiles) + .pipe(cached('scripts')) + .pipe(babel({ modules: 'amd', moduleIds: true, moduleRoot: modulePrefix })) + .pipe(remember('scripts')), + gulp.src(staticFiles) + .pipe(babel()) + ) + .pipe(concat('extension.js')) + .pipe(gulpif(argv.production, uglify())) + .pipe(gulp.dest('dist')) + .pipe(livereload()); +}); + +gulp.task('watch', ['default'], function () { + livereload.listen(); + var watcher = gulp.watch(moduleFiles.concat(staticFiles), ['default']); + watcher.on('change', function (event) { + if (event.type === 'deleted') { + delete cached.caches.scripts[event.path]; + remember.forget('scripts', event.path); + } + }); +}); diff --git a/extensions/tags/js/bootstrap.js b/extensions/tags/js/bootstrap.js new file mode 100644 index 000000000..00de45748 --- /dev/null +++ b/extensions/tags/js/bootstrap.js @@ -0,0 +1,274 @@ +import { extend, override } from 'flarum/extension-utils'; +import Model from 'flarum/model'; +import Component from 'flarum/component'; +import Discussion from 'flarum/models/discussion'; +import IndexPage from 'flarum/components/index-page'; +import DiscussionPage from 'flarum/components/discussion-page'; +import DiscussionList from 'flarum/components/discussion-list'; +import DiscussionHero from 'flarum/components/discussion-hero'; +import Separator from 'flarum/components/separator'; +import NavItem from 'flarum/components/nav-item'; +import ActionButton from 'flarum/components/action-button'; +import ComposerDiscussion from 'flarum/components/composer-discussion'; +import ActivityPost from 'flarum/components/activity-post'; +import icon from 'flarum/helpers/icon'; + +import CategoriesPage from 'categories/components/categories-page'; +import Category from 'categories/category'; +import PostDiscussionMoved from 'categories/components/post-discussion-moved'; + +import app from 'flarum/app'; + +Discussion.prototype.category = Model.one('category'); + +app.initializers.add('categories', function() { + app.routes['categories'] = ['/categories', CategoriesPage.component()]; + + app.routes['category'] = ['/c/:categories', IndexPage.component({category: true})]; + + + // @todo support combination with filters + // app.routes['category.filter'] = ['/c/:slug/:filter', IndexPage.component({category: true})]; + + app.postComponentRegistry['discussionMoved'] = PostDiscussionMoved; + app.store.model('categories', Category); + + extend(DiscussionList.prototype, 'infoItems', function(items, discussion) { + var category = discussion.category(); + if (category && category.slug() !== this.props.params.categories) { + items.add('category', m('span.category', {style: 'color: '+category.color()}, category.title()), {first: true}); + } + + return items; + }); + + class CategoryNavItem extends NavItem { + view() { + var category = this.props.category; + var active = this.constructor.active(this.props); + return m('li'+(active ? '.active' : ''), m('a', {href: this.props.href, config: m.route, onclick: () => {app.cache.discussionList = null; m.redraw.strategy('none')}, style: active ? 'color: '+category.color() : ''}, [ + m('span.icon.category-icon', {style: 'background-color: '+category.color()}), + category.title() + ])); + } + + static props(props) { + var category = props.category; + props.params.categories = category.slug(); + props.href = app.route('category', props.params); + props.label = category.title(); + + return props; + } + } + + extend(IndexPage.prototype, 'navItems', function(items) { + items.add('categories', NavItem.component({ + icon: 'reorder', + label: 'Categories', + href: app.route('categories'), + config: m.route + }), {last: true}); + + items.add('separator', Separator.component(), {last: true}); + + app.store.all('categories').forEach(category => { + items.add('category'+category.id(), CategoryNavItem.component({category, params: this.stickyParams()}), {last: true}); + }); + + return items; + }); + + extend(IndexPage.prototype, 'params', function(params) { + params.categories = this.props.category ? m.route.param('categories') : undefined; + return params; + }); + + class CategoryHero extends Component { + view() { + var category = this.props.category; + + return m('header.hero.category-hero', {style: 'background-color: '+category.color()}, [ + m('div.container', [ + m('div.container-narrow', [ + m('h2', category.title()), + m('div.subtitle', category.description()) + ]) + ]) + ]); + } + } + + extend(IndexPage.prototype, 'view', function(view) { + if (this.props.category) { + var slug = this.params().categories; + var category = app.store.all('categories').filter(category => category.slug() == slug)[0]; + view.children[0] = CategoryHero.component({category}); + } + return view; + }); + + extend(IndexPage.prototype, 'sidebarItems', function(items) { + var slug = this.params().categories; + var category = app.store.all('categories').filter(category => category.slug() == slug)[0]; + if (category) { + items.newDiscussion.content.props.style = 'background-color: '+category.color(); + } + return items; + }); + + extend(DiscussionList.prototype, 'params', function(params) { + if (params.categories) { + params.q = (params.q || '')+' category:'+params.categories; + delete params.categories; + } + return params; + }); + + extend(DiscussionPage.prototype, 'params', function(params) { + params.include += ',category'; + return params; + }); + + extend(DiscussionHero.prototype, 'view', function(view) { + var category = this.props.discussion.category(); + if (category) { + view.attrs.style = 'background-color: '+category.color(); + } + return view; + }); + + extend(DiscussionHero.prototype, 'items', function(items) { + var category = this.props.discussion.category(); + if (category) { + items.add('category', m('span.category', category.title()), {before: 'title'}); + items.title.content.wrapperClass = 'block-item'; + } + return items; + }); + + class MoveDiscussionModal extends Component { + constructor(props) { + super(props); + + this.categories = m.prop(app.store.all('categories')); + } + + view() { + var discussion = this.props.discussion; + + return m('div.modal-dialog.modal-move-discussion', [ + m('div.modal-content', [ + m('button.btn.btn-icon.btn-link.close.back-control', {onclick: app.modal.close.bind(app.modal)}, icon('times')), + m('div.modal-header', m('h3.title-control', discussion + ? ['Move ', m('em', discussion.title()), ' from ', m('span.category', {style: 'color: '+discussion.category().color()}, discussion.category().title()), ' to...'] + : ['Start a Discussion In...'])), + m('div', [ + m('ul.category-list', [ + this.categories().map(category => + (discussion && category.id() === discussion.category().id()) ? '' : m('li.category-tile', {style: 'background-color: '+category.color()}, [ + m('a[href=javascript:;]', {onclick: this.save.bind(this, category)}, [ + m('h3.title', category.title()), + m('p.description', category.description()), + m('span.count', category.discussionsCount()+' discussions'), + ]) + ]) + ) + ]) + ]) + ]) + ]); + } + + save(category) { + var discussion = this.props.discussion; + + if (discussion) { + discussion.save({links: {category}}).then(discussion => { + if (app.current instanceof DiscussionPage) { + app.current.stream().sync(); + } + m.redraw(); + }); + } + + this.props.onchange && this.props.onchange(category); + + app.modal.close(); + + m.redraw.strategy('none'); + } + } + + function move() { + app.modal.show(new MoveDiscussionModal({discussion: this})); + } + + extend(Discussion.prototype, 'controls', function(items) { + if (this.canEdit()) { + items.add('move', ActionButton.component({ + label: 'Move', + icon: 'arrow-right', + onclick: move.bind(this) + }), {after: 'rename'}); + } + + return items; + }); + + override(IndexPage.prototype, 'newDiscussion', function(parent) { + var categories = app.store.all('categories'); + var slug = this.params().categories; + var category; + if (slug || !app.session.user()) { + parent(); + if (app.composer.component) { + category = categories.filter(category => category.slug() == slug)[0]; + app.composer.component.category(category); + } + } else { + var modal = new MoveDiscussionModal({onchange: category => { + parent(); + app.composer.component.category(category); + }}); + app.modal.show(modal); + } + }); + + ComposerDiscussion.prototype.chooseCategory = function() { + var modal = new MoveDiscussionModal({onchange: category => { + this.category(category); + this.$('textarea').focus(); + }}); + app.modal.show(modal); + }; + + ComposerDiscussion.prototype.category = m.prop(); + extend(ComposerDiscussion.prototype, 'headerItems', function(items) { + var category = this.category(); + + items.add('category', m('a[href=javascript:;][tabindex=-1].btn.btn-link.control-change-category', { + onclick: this.chooseCategory.bind(this) + }, [ + category ? m('span.category-icon', {style: 'background-color: '+category.color()}) : '', ' ', + m('span.label', category ? category.title() : 'Uncategorized'), + icon('sort') + ])); + + return items; + }); + + extend(ComposerDiscussion.prototype, 'data', function(data) { + data.links = data.links || {}; + data.links.category = this.category(); + return data; + }) + + extend(ActivityPost.prototype, 'headerItems', function(items) { + var category = this.props.activity.post().discussion().category(); + if (category) { + items.add('category', m('span.category', {style: {color: category.color()}}, category.title())); + } + return items; + }) +}); diff --git a/extensions/tags/js/package.json b/extensions/tags/js/package.json new file mode 100644 index 000000000..b83b01cc5 --- /dev/null +++ b/extensions/tags/js/package.json @@ -0,0 +1,18 @@ +{ + "name": "flarum-sticky", + "devDependencies": { + "gulp": "^3.8.11", + "gulp-babel": "^5.1.0", + "gulp-cached": "^1.0.4", + "gulp-concat": "^2.5.2", + "gulp-if": "^1.2.5", + "gulp-livereload": "^3.8.0", + "gulp-remember": "^0.3.0", + "gulp-uglify": "^1.2.0", + "merge-stream": "^0.1.7", + "yargs": "^3.7.2" + }, + "dependencies": { + "streamqueue": "^0.1.3" + } +} diff --git a/extensions/tags/js/src/category.js b/extensions/tags/js/src/category.js new file mode 100644 index 000000000..4de836019 --- /dev/null +++ b/extensions/tags/js/src/category.js @@ -0,0 +1,12 @@ +import Model from 'flarum/model'; + +class Category extends Model {} + +Category.prototype.id = Model.prop('id'); +Category.prototype.title = Model.prop('title'); +Category.prototype.slug = Model.prop('slug'); +Category.prototype.description = Model.prop('description'); +Category.prototype.color = Model.prop('color'); +Category.prototype.discussionsCount = Model.prop('discussionsCount'); + +export default Category; diff --git a/extensions/tags/js/src/components/categories-page.js b/extensions/tags/js/src/components/categories-page.js new file mode 100644 index 000000000..4bf407de6 --- /dev/null +++ b/extensions/tags/js/src/components/categories-page.js @@ -0,0 +1,38 @@ +import Component from 'flarum/component'; +import WelcomeHero from 'flarum/components/welcome-hero'; +import icon from 'flarum/helpers/icon'; + +export default class CategoriesPage extends Component { + constructor(props) { + super(props); + + this.categories = m.prop(app.store.all('categories')); + } + + view() { + return m('div.categories-area', [ + m('div.title-control.categories-forum-title', app.config.forum_title), + WelcomeHero.component(), + m('div.container', [ + m('ul.category-list.category-list-tiles', [ + m('li.filter-tile', [ + m('a', {href: app.route('index'), config: m.route}, 'All Discussions'), + // m('ul.filter-list', [ + // m('li', m('a', {href: app.route('index'), config: m.route}, m('span', [icon('star'), ' Following']))), + // m('li', m('a', {href: app.route('index'), config: m.route}, m('span', [icon('envelope-o'), ' Inbox']))) + // ]) + ]), + this.categories().map(category => + m('li.category-tile', {style: 'background-color: '+category.color()}, [ + m('a', {href: app.route('category', {categories: category.slug()}), config: m.route}, [ + m('h3.title', category.title()), + m('p.description', category.description()), + m('span.count', category.discussionsCount()+' discussions'), + ]) + ]) + ) + ]) + ]) + ]); + } +} diff --git a/extensions/tags/js/src/components/post-discussion-moved.js b/extensions/tags/js/src/components/post-discussion-moved.js new file mode 100644 index 000000000..1168b42a8 --- /dev/null +++ b/extensions/tags/js/src/components/post-discussion-moved.js @@ -0,0 +1,59 @@ +import Component from 'flarum/component'; +import icon from 'flarum/helpers/icon'; +import username from 'flarum/helpers/username'; +import humanTime from 'flarum/utils/human-time'; +import SubtreeRetainer from 'flarum/utils/subtree-retainer'; +import ItemList from 'flarum/utils/item-list'; +import ActionButton from 'flarum/components/action-button'; +import DropdownButton from 'flarum/components/dropdown-button'; + +export default class PostDiscussionMoved extends Component { + constructor(props) { + super(props); + + this.subtree = new SubtreeRetainer( + () => this.props.post.freshness, + () => this.props.post.user().freshness + ); + } + + view(ctrl) { + var controls = this.controlItems().toArray(); + + var post = this.props.post; + var oldCategory = app.store.getById('categories', post.content()[0]); + var newCategory = app.store.getById('categories', post.content()[1]); + + return m('article.post.post-activity.post-discussion-moved', this.subtree.retain() || m('div', [ + controls.length ? DropdownButton.component({ + items: controls, + className: 'contextual-controls', + buttonClass: 'btn btn-default btn-icon btn-sm btn-naked', + menuClass: 'pull-right' + }) : '', + icon('arrow-right post-icon'), + m('div.post-activity-info', [ + m('a.post-user', {href: app.route('user', {username: post.user().username()}), config: m.route}, username(post.user())), + ' moved the discussion from ', m('span.category', {style: {color: oldCategory.color()}}, oldCategory.title()), ' to ', m('span.category', {style: {color: newCategory.color()}}, newCategory.title()), '.' + ]), + m('div.post-activity-time', humanTime(post.time())) + ])); + } + + controlItems() { + var items = new ItemList(); + var post = this.props.post; + + if (post.canDelete()) { + items.add('delete', ActionButton.component({ icon: 'times', label: 'Delete', onclick: this.delete.bind(this) })); + } + + return items; + } + + delete() { + var post = this.props.post; + post.delete(); + this.props.ondelete && this.props.ondelete(post); + } +} diff --git a/extensions/tags/less/categories.less b/extensions/tags/less/categories.less new file mode 100644 index 000000000..5b1b9aeee --- /dev/null +++ b/extensions/tags/less/categories.less @@ -0,0 +1,206 @@ +.category { + text-transform: uppercase; + font-size: 80%; + font-weight: bold; + display: inline-block; + + .discussion-summary & { + margin-right: 10px; + font-size: 11px; + } + + .discussion-hero & { + font-size: 14px; + } + + .post-discussion-moved & { + margin: 0 2px; + } +} +.discussion-hero { + & .block-item { + margin-top: 10px; + } +} + +.category-icon { + border-radius: @border-radius-base; + width: 16px; + height: 16px; + display: inline-block; + vertical-align: -3px; + margin-left: 1px; +} + +.categories-area .container { + width: 100%; + max-width: none; + padding: 0; +} + +.control-change-category { + vertical-align: 1px; + margin: -10px 0; + + & .label { + margin: 0 2px 0 5px; + } + + .minimized & { + display: none; + } +} + +.modal-move-discussion { + & .modal-header { + padding: 20px; + text-align: left; + + & h3 { + font-size: 16px; + } + } + & .modal-content { + overflow: hidden; + } + & .category-tile .title { + margin-bottom: 10px; + font-size: 18px; + } + & .category-tile .description { + margin-bottom: 0; + font-size: 13px; + } + & .count { + display: none; + } + & .category-tile > a { + padding: 20px; + } +} + + +.category-list { + margin: 0; + padding: 0; + list-style: none; + background: @fl-body-control-bg; + color: @fl-body-control-color; + overflow: hidden; +} + +@media @tablet { + .category-list-tiles { + & > li { + float: left; + width: 50%; + height: 175px; + + & > a { + height: 100%; + } + } + } +} +@media @tablet, @desktop, @desktop-hd { + .categories-forum-title { + display: none; + } +} +@media @desktop, @desktop-hd { + .category-list-tiles { + & > li { + float: left; + width: 33.333%; + height: 175px; + + & > a { + height: 100%; + } + } + } +} +.category-tile { + position: relative; + + &, & > a { + color: #fff; + } + & > a { + display: block; + padding: 25px; + transition: background 0.1s; + text-decoration: none; + } + & > a:hover { + background: rgba(0, 0, 0, 0.1); + } + & > a:active { + background: rgba(0, 0, 0, 0.2); + } + & .title { + font-size: 20px; + margin: 0 0 15px; + } + & .description { + font-size: 14px; + line-height: 1.5em; + color: rgba(255, 255, 255, 0.6); + } + & .count { + text-transform: uppercase; + font-weight: bold; + font-size: 11px; + color: rgba(255, 255, 255, 0.6); + } +} +.filter-tile a { + display: block; + padding: 15px 25px; + font-size: 18px; + color: @fl-body-control-color; +} +@media @tablet, @desktop, @desktop-hd { + .filter-tile { + & > a { + float: left; + width: 50%; + border-right: 1px solid #fff; + + &:first-child:last-child { + width: 100%; + border-right: 0; + } + } + & a { + display: block; + height: 100%; + padding: 20px; + font-size: 18px; + color: @fl-body-control-color; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + } + } + .filter-list { + float: left; + margin: 0; + padding: 0; + list-style: none; + height: 100%; + display: table; + width: 50%; + table-layout: fixed; + + & > li { + display: table-row; + height: 1%; + + &:not(:last-child) a { + border-bottom: 1px solid #fff; + } + } + } +} diff --git a/extensions/tags/migrations/2015_02_24_000000_add_category_to_discussions.php b/extensions/tags/migrations/2015_02_24_000000_add_category_to_discussions.php new file mode 100644 index 000000000..68c1b285a --- /dev/null +++ b/extensions/tags/migrations/2015_02_24_000000_add_category_to_discussions.php @@ -0,0 +1,31 @@ +integer('category_id')->unsigned(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('discussions', function (Blueprint $table) { + $table->dropColumn('category_id'); + }); + } +} diff --git a/extensions/tags/migrations/2015_02_24_000000_create_categories_table.php b/extensions/tags/migrations/2015_02_24_000000_create_categories_table.php new file mode 100644 index 000000000..dac755568 --- /dev/null +++ b/extensions/tags/migrations/2015_02_24_000000_create_categories_table.php @@ -0,0 +1,32 @@ +increments('id'); + $table->string('title'); + $table->text('description'); + $table->string('color'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('categories'); + } +} diff --git a/extensions/tags/src/CategoriesServiceProvider.php b/extensions/tags/src/CategoriesServiceProvider.php new file mode 100644 index 000000000..bd99b994a --- /dev/null +++ b/extensions/tags/src/CategoriesServiceProvider.php @@ -0,0 +1,36 @@ +subscribe('Flarum\Categories\Handlers\Handler'); + + IndexAction::$include['category'] = true; + + ShowAction::$include['category'] = true; + + Post::addType('discussionMoved', 'Flarum\Categories\DiscussionMovedPost'); + + Discussion::addRelationship('category', function ($model) { + return $model->belongsTo('Flarum\Categories\Category', null, null, 'category'); + }); + } + + public function register() + { + // + } +} diff --git a/extensions/tags/src/Category.php b/extensions/tags/src/Category.php new file mode 100644 index 000000000..65bb38817 --- /dev/null +++ b/extensions/tags/src/Category.php @@ -0,0 +1,8 @@ +categories->getIdForSlug($slug); + $id = Category::whereSlug($slug)->pluck('id'); + + $searcher->query()->where('category_id', $id); + } +} diff --git a/extensions/tags/src/CategorySerializer.php b/extensions/tags/src/CategorySerializer.php new file mode 100644 index 000000000..48ee398e4 --- /dev/null +++ b/extensions/tags/src/CategorySerializer.php @@ -0,0 +1,26 @@ + $category->title, + 'description' => $category->description, + 'slug' => $category->slug, + 'color' => $category->color, + 'discussionsCount' => (int) $category->discussions_count + ]; + + return $this->extendAttributes($category, $attributes); + } +} diff --git a/extensions/tags/src/DiscussionMovedPost.php b/extensions/tags/src/DiscussionMovedPost.php new file mode 100755 index 000000000..a5cd59639 --- /dev/null +++ b/extensions/tags/src/DiscussionMovedPost.php @@ -0,0 +1,49 @@ +content = [$oldCategoryId, $newCategoryId]; + $post->time = time(); + $post->discussion_id = $discussionId; + $post->user_id = $userId; + $post->type = 'discussionMoved'; + + return $post; + } + + /** + * Unserialize the content attribute. + * + * @param string $value + * @return string + */ + public function getContentAttribute($value) + { + return json_decode($value); + } + + /** + * Serialize the content attribute. + * + * @param string $value + */ + public function setContentAttribute($value) + { + $this->attributes['content'] = json_encode($value); + } +} diff --git a/extensions/tags/src/Handlers/Handler.php b/extensions/tags/src/Handlers/Handler.php new file mode 100755 index 000000000..fae68dd60 --- /dev/null +++ b/extensions/tags/src/Handlers/Handler.php @@ -0,0 +1,100 @@ +actor = $actor; + } + + public function subscribe($events) + { + $events->listen('Flarum\Forum\Events\RenderView', __CLASS__.'@renderForum'); + $events->listen('Flarum\Api\Events\SerializeRelationship', __CLASS__.'@serializeRelationship'); + $events->listen('Flarum\Core\Events\RegisterDiscussionGambits', __CLASS__.'@registerGambits'); + $events->listen('Flarum\Core\Events\DiscussionWillBeSaved', __CLASS__.'@saveCategoryToDiscussion'); + } + + public function renderForum(RenderView $event) + { + $root = __DIR__.'/../..'; + + $event->assets->addFile([ + $root.'/js/dist/extension.js', + $root.'/less/categories.less' + ]); + + $serializer = new CategorySerializer($event->action->actor); + $event->view->data = array_merge($event->view->data, $serializer->collection(Category::orderBy('position')->get())->toArray()); + } + + public function saveCategoryToDiscussion(DiscussionWillBeSaved $event) + { + if (isset($event->command->data['links']['category']['linkage'])) { + $linkage = $event->command->data['links']['category']['linkage']; + + $categoryId = (int) $linkage['id']; + $discussion = $event->discussion; + $user = $event->command->user; + + $oldCategoryId = (int) $discussion->category_id; + + if ($oldCategoryId === $categoryId) { + return; + } + + $discussion->category_id = $categoryId; + + if ($discussion->exists) { + $lastPost = $discussion->posts()->orderBy('time', 'desc')->first(); + if ($lastPost instanceof DiscussionMovedPost) { + if ($lastPost->content[0] == $categoryId) { + $lastPost->delete(); + $discussion->postWasRemoved($lastPost); + } else { + $newContent = $lastPost->content; + $newContent[1] = $categoryId; + $lastPost->content = $newContent; + $lastPost->save(); + $discussion->postWasAdded($lastPost); + } + } else { + $post = DiscussionMovedPost::reply( + $discussion->id, + $user->id, + $oldCategoryId, + $categoryId + ); + + $post->save(); + + $discussion->postWasAdded($post); + } + } + } + } + + public function registerGambits(RegisterDiscussionGambits $event) + { + $event->gambits->add('Flarum\Categories\CategoryGambit'); + } + + public function serializeRelationship(SerializeRelationship $event) + { + if ($event->serializer instanceof DiscussionSerializer && $event->name === 'category') { + return $event->serializer->hasOne('Flarum\Categories\CategorySerializer', 'category'); + } + } +}