mirror of
https://github.com/flarum/framework.git
synced 2025-04-26 22:54:03 +08:00
Rename extension to Tags. Allow multiple tags per discussion.
WIP!
This commit is contained in:
parent
f569d00314
commit
c9a03d9d8a
@ -6,4 +6,4 @@ require __DIR__.'/vendor/autoload.php';
|
|||||||
|
|
||||||
// Register our service provider with the Flarum application. In here we can
|
// Register our service provider with the Flarum application. In here we can
|
||||||
// register bindings and execute code when the application boots.
|
// register bindings and execute code when the application boots.
|
||||||
return $this->app->register('Flarum\Categories\CategoriesServiceProvider');
|
return $this->app->register('Flarum\Tags\TagsServiceProvider');
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Flarum\\Categories\\": "src/"
|
"Flarum\\Tags\\": "src/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "flarum-categories",
|
"name": "flarum-tags",
|
||||||
"title": "Categories",
|
"title": "Tags",
|
||||||
"description": "Organise discussions into a heirarchy of categories.",
|
"description": "Organise discussions into a heirarchy of tags and categories.",
|
||||||
"tags": [
|
"tags": [],
|
||||||
"discussions"
|
|
||||||
],
|
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Toby Zerner",
|
"name": "Toby Zerner",
|
||||||
"email": "toby@flarum.org",
|
"email": "toby.zerner@gmail.com"
|
||||||
"homepage": "http://tobyzerner.com"
|
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.4.0",
|
"php": ">=5.4.0",
|
||||||
"flarum": ">0.1.0"
|
"flarum": ">0.1.0"
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"github": "https://github.com/flarum/categories",
|
|
||||||
"issues": "https://github.com/flarum/categories/issues"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
var gulp = require('flarum-gulp');
|
var gulp = require('flarum-gulp');
|
||||||
|
|
||||||
gulp({
|
gulp({
|
||||||
modulePrefix: 'flarum-categories'
|
modulePrefix: 'flarum-tags'
|
||||||
});
|
});
|
||||||
|
241
extensions/tags/js/bootstrap.js
vendored
241
extensions/tags/js/bootstrap.js
vendored
@ -1,237 +1,34 @@
|
|||||||
import { extend, override } from 'flarum/extension-utils';
|
import app from 'flarum/app';
|
||||||
import Model from 'flarum/model';
|
import Model from 'flarum/model';
|
||||||
import Discussion from 'flarum/models/discussion';
|
import Discussion from 'flarum/models/discussion';
|
||||||
import IndexPage from 'flarum/components/index-page';
|
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 ActionButton from 'flarum/components/action-button';
|
|
||||||
import NavItem from 'flarum/components/nav-item';
|
|
||||||
import DiscussionComposer from 'flarum/components/discussion-composer';
|
|
||||||
import SettingsPage from 'flarum/components/settings-page';
|
|
||||||
import PostedActivity from 'flarum/components/posted-activity';
|
|
||||||
import icon from 'flarum/helpers/icon';
|
|
||||||
import app from 'flarum/app';
|
|
||||||
|
|
||||||
import Category from 'flarum-categories/models/category';
|
import Tag from 'flarum-tags/models/tag';
|
||||||
import CategoriesPage from 'flarum-categories/components/categories-page';
|
import TagsPage from 'flarum-tags/components/tags-page';
|
||||||
import CategoryHero from 'flarum-categories/components/category-hero';
|
import addTagList from 'flarum-tags/add-tag-list';
|
||||||
import CategoryNavItem from 'flarum-categories/components/category-nav-item';
|
import addTagFilter from 'flarum-tags/add-tag-filter';
|
||||||
import MoveDiscussionModal from 'flarum-categories/components/move-discussion-modal';
|
import addTagLabels from 'flarum-tags/add-tag-labels';
|
||||||
import DiscussionMovedNotification from 'flarum-categories/components/discussion-moved-notification';
|
|
||||||
import DiscussionMovedPost from 'flarum-categories/components/discussion-moved-post';
|
|
||||||
import categoryLabel from 'flarum-categories/helpers/category-label';
|
|
||||||
import categoryIcon from 'flarum-categories/helpers/category-icon';
|
|
||||||
|
|
||||||
app.initializers.add('flarum-categories', function() {
|
app.initializers.add('flarum-tags', function() {
|
||||||
// Register routes.
|
// Register routes.
|
||||||
app.routes['categories'] = ['/categories', CategoriesPage.component()];
|
app.routes['tags'] = ['/tags', TagsPage.component()];
|
||||||
app.routes['category'] = ['/c/:categories', IndexPage.component()];
|
app.routes['tag'] = ['/t/:tags', IndexPage.component()];
|
||||||
|
|
||||||
// @todo support combination with filters
|
|
||||||
// app.routes['category.filter'] = ['/c/:slug/:filter', IndexPage.component({category: true})];
|
|
||||||
|
|
||||||
// Register models.
|
// Register models.
|
||||||
app.store.models['categories'] = Category;
|
app.store.models['tags'] = Tag;
|
||||||
Discussion.prototype.category = Model.one('category');
|
Discussion.prototype.tags = Model.many('tags');
|
||||||
Discussion.prototype.canMove = Model.prop('canMove');
|
Discussion.prototype.canMove = Model.prop('canMove');
|
||||||
|
|
||||||
// Register components.
|
// Add a list of tags to the index navigation.
|
||||||
app.postComponentRegistry['discussionMoved'] = DiscussionMovedPost;
|
addTagList();
|
||||||
app.notificationComponentRegistry['discussionMoved'] = DiscussionMovedNotification;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// When a tag is selected, filter the discussion list by that tag.
|
||||||
// INDEX PAGE
|
addTagFilter();
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Add a category label to each discussion in the discussion list.
|
// Add tags to the discussion list and discussion hero.
|
||||||
extend(DiscussionList.prototype, 'infoItems', function(items, discussion) {
|
addTagLabels();
|
||||||
var category = discussion.category();
|
|
||||||
if (category && category.slug() !== this.props.params.categories) {
|
|
||||||
items.add('category', categoryLabel(category), {first: true});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a link to the categories page, as well as a list of all the categories,
|
// addMoveDiscussionControl();
|
||||||
// to the index page's sidebar.
|
|
||||||
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});
|
// addDiscussionComposer();
|
||||||
|
|
||||||
items.add('uncategorized', CategoryNavItem.component({params: this.stickyParams()}), {last: true});
|
|
||||||
|
|
||||||
app.store.all('categories').sort((a, b) => a.position() - b.position()).forEach(category => {
|
|
||||||
items.add('category'+category.id(), CategoryNavItem.component({category, params: this.stickyParams()}), {last: true});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
IndexPage.prototype.currentCategory = function() {
|
|
||||||
var slug = this.params().categories;
|
|
||||||
if (slug) {
|
|
||||||
return app.store.getBy('categories', 'slug', slug);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// If currently viewing a category, insert a category hero at the top of the
|
|
||||||
// view.
|
|
||||||
extend(IndexPage.prototype, 'view', function(view) {
|
|
||||||
var category = this.currentCategory();
|
|
||||||
if (category) {
|
|
||||||
view.children[0] = CategoryHero.component({category});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If currently viewing a category, restyle the 'new discussion' button to use
|
|
||||||
// the category's color.
|
|
||||||
extend(IndexPage.prototype, 'sidebarItems', function(items) {
|
|
||||||
var category = this.currentCategory();
|
|
||||||
if (category) {
|
|
||||||
items.newDiscussion.content.props.style = 'background-color: '+category.color();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a parameter for the IndexPage to pass on to the DiscussionList that
|
|
||||||
// will let us filter discussions by category.
|
|
||||||
extend(IndexPage.prototype, 'params', function(params) {
|
|
||||||
params.categories = m.route.param('categories');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Translate that parameter into a gambit appended to the search query.
|
|
||||||
extend(DiscussionList.prototype, 'params', function(params) {
|
|
||||||
params.include.push('category');
|
|
||||||
if (params.categories) {
|
|
||||||
params.q = (params.q || '')+' category:'+params.categories;
|
|
||||||
delete params.categories;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// DISCUSSION PAGE
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Include a discussion's category when fetching it.
|
|
||||||
extend(DiscussionPage.prototype, 'params', function(params) {
|
|
||||||
params.include.push('category');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restyle a discussion's hero to use its category color.
|
|
||||||
extend(DiscussionHero.prototype, 'view', function(view) {
|
|
||||||
var category = this.props.discussion.category();
|
|
||||||
if (category) {
|
|
||||||
view.attrs.style = 'color: #fff; background-color: '+category.color();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add the name of a discussion's category to the discussion hero, displayed
|
|
||||||
// before the title. Put the title on its own line.
|
|
||||||
extend(DiscussionHero.prototype, 'items', function(items) {
|
|
||||||
var category = this.props.discussion.category();
|
|
||||||
if (category) {
|
|
||||||
items.add('category', m('a', {
|
|
||||||
href: app.route('category', {categories: category.slug()}),
|
|
||||||
config: m.route
|
|
||||||
}, categoryLabel(category)), {before: 'title'});
|
|
||||||
|
|
||||||
items.title.content.wrapperClass = 'block-item';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a control allowing the discussion to be moved to another category.
|
|
||||||
extend(Discussion.prototype, 'controls', function(items) {
|
|
||||||
if (this.canMove()) {
|
|
||||||
items.add('move', ActionButton.component({
|
|
||||||
label: 'Move',
|
|
||||||
icon: 'arrow-right',
|
|
||||||
onclick: () => app.modal.show(new MoveDiscussionModal({discussion: this}))
|
|
||||||
}), {after: 'rename'});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// COMPOSER
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// When the 'new discussion' button is clicked...
|
|
||||||
override(IndexPage.prototype, 'newDiscussion', function(original) {
|
|
||||||
var slug = this.params().categories;
|
|
||||||
|
|
||||||
// If we're currently viewing a specific category, or if the user isn't
|
|
||||||
// logged in, then we'll let the core code proceed. If that results in the
|
|
||||||
// composer appearing, we'll set the composer's current category to the one
|
|
||||||
// we're viewing.
|
|
||||||
if (slug || !app.session.user()) {
|
|
||||||
if (original()) {
|
|
||||||
var category = app.store.getBy('categories', 'slug', slug);
|
|
||||||
app.composer.component.category(category);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If we're logged in and we're viewing All Discussions, we'll present the
|
|
||||||
// user with a category selection dialog before proceeding to show the
|
|
||||||
// composer.
|
|
||||||
var modal = new MoveDiscussionModal({
|
|
||||||
onchange: category => {
|
|
||||||
original();
|
|
||||||
app.composer.component.category(category);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.modal.show(modal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add category-selection abilities to the discussion composer.
|
|
||||||
DiscussionComposer.prototype.category = m.prop();
|
|
||||||
DiscussionComposer.prototype.chooseCategory = function() {
|
|
||||||
var modal = new MoveDiscussionModal({
|
|
||||||
onchange: category => {
|
|
||||||
this.category(category);
|
|
||||||
this.$('textarea').focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.modal.show(modal);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add a category-selection menu to the discussion composer's header, after
|
|
||||||
// the title.
|
|
||||||
extend(DiscussionComposer.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)}, [
|
|
||||||
categoryIcon(category), ' ',
|
|
||||||
m('span.label', category ? category.title() : 'Uncategorized'),
|
|
||||||
icon('sort')
|
|
||||||
]));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add the selected category as data to submit to the server.
|
|
||||||
extend(DiscussionComposer.prototype, 'data', function(data) {
|
|
||||||
data.links = data.links || {};
|
|
||||||
data.links.category = this.category();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// USER PROFILE
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Add a category label next to the discussion title in post activity items.
|
|
||||||
extend(PostedActivity.prototype, 'headerItems', function(items) {
|
|
||||||
var category = this.props.activity.subject().discussion().category();
|
|
||||||
if (category) {
|
|
||||||
items.add('category', categoryLabel(category));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a notification preference.
|
|
||||||
extend(SettingsPage.prototype, 'notificationTypes', function(items) {
|
|
||||||
items.add('discussionMoved', {
|
|
||||||
name: 'discussionMoved',
|
|
||||||
label: [icon('arrow-right'), ' Someone moves a discussion I started']
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
50
extensions/tags/js/src/add-tag-filter.js
Normal file
50
extensions/tags/js/src/add-tag-filter.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { extend } from 'flarum/extension-utils';
|
||||||
|
import IndexPage from 'flarum/components/index-page';
|
||||||
|
import DiscussionList from 'flarum/components/discussion-list';
|
||||||
|
|
||||||
|
import TagHero from 'flarum-tags/components/tag-hero';
|
||||||
|
|
||||||
|
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.
|
||||||
|
extend(IndexPage.prototype, 'view', function(view) {
|
||||||
|
var tag = this.currentTag();
|
||||||
|
if (tag) {
|
||||||
|
view.children[0] = TagHero.component({tag});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 = 'background-color: '+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, 'params', function(params) {
|
||||||
|
params.include.push('tags');
|
||||||
|
if (params.tags) {
|
||||||
|
params.q = (params.q || '')+' tag:'+params.tags;
|
||||||
|
delete params.tags;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
40
extensions/tags/js/src/add-tag-labels.js
Normal file
40
extensions/tags/js/src/add-tag-labels.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { extend } from 'flarum/extension-utils';
|
||||||
|
import DiscussionList from 'flarum/components/discussion-list';
|
||||||
|
import DiscussionPage from 'flarum/components/discussion-page';
|
||||||
|
import DiscussionHero from 'flarum/components/discussion-hero';
|
||||||
|
|
||||||
|
import tagsLabel from 'flarum-tags/helpers/tags-label';
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
// Add tag labels to each discussion in the discussion list.
|
||||||
|
extend(DiscussionList.prototype, 'infoItems', function(items, discussion) {
|
||||||
|
var tags = discussion.tags();
|
||||||
|
if (tags) {
|
||||||
|
items.add('tags', tagsLabel(tags.filter(tag => tag.slug() !== this.props.params.tags)), {first: true});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 = this.props.discussion.tags();
|
||||||
|
if (tags) {
|
||||||
|
view.attrs.style = 'color: #fff; background-color: '+tags[0].color();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
items.add('tags', tagsLabel(tags, {link: true}), {before: 'title'});
|
||||||
|
|
||||||
|
items.title.content.wrapperClass = 'block-item';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
50
extensions/tags/js/src/add-tag-list.js
Normal file
50
extensions/tags/js/src/add-tag-list.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { extend } from 'flarum/extension-utils';
|
||||||
|
import IndexPage from 'flarum/components/index-page';
|
||||||
|
import NavItem from 'flarum/components/nav-item';
|
||||||
|
import Separator from 'flarum/components/separator';
|
||||||
|
|
||||||
|
import TagNavItem from 'flarum-tags/components/tag-nav-item';
|
||||||
|
|
||||||
|
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', NavItem.component({
|
||||||
|
icon: 'reorder',
|
||||||
|
label: 'Tags',
|
||||||
|
href: app.route('tags'),
|
||||||
|
config: m.route
|
||||||
|
}), {last: true});
|
||||||
|
|
||||||
|
items.add('separator', Separator.component(), {last: true});
|
||||||
|
|
||||||
|
var params = this.stickyParams();
|
||||||
|
var tags = app.store.all('tags');
|
||||||
|
|
||||||
|
items.add('untagged', TagNavItem.component({params}), {last: true});
|
||||||
|
|
||||||
|
var addTag = tag => {
|
||||||
|
var currentTag = this.currentTag();
|
||||||
|
var active = currentTag === tag;
|
||||||
|
if (!active && currentTag) {
|
||||||
|
currentTag = currentTag.parent();
|
||||||
|
active = currentTag === tag;
|
||||||
|
}
|
||||||
|
items.add('tag'+tag.id(), TagNavItem.component({tag, params, active}), {last: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.filter(tag => tag.position() !== null && !tag.isChild()).sort((a, b) => a.position() - b.position()).forEach(addTag);
|
||||||
|
|
||||||
|
var more = tags.filter(tag => tag.position() === null).sort((a, b) => b.discussionsCount() - a.discussionsCount());
|
||||||
|
|
||||||
|
more.splice(0, 3).forEach(addTag);
|
||||||
|
|
||||||
|
if (more.length) {
|
||||||
|
items.add('moreTags', NavItem.component({
|
||||||
|
label: 'More...',
|
||||||
|
href: app.route('tags'),
|
||||||
|
config: m.route
|
||||||
|
}), {last: true});;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -1,16 +0,0 @@
|
|||||||
import Component from 'flarum/component';
|
|
||||||
|
|
||||||
export default class CategoryHero extends Component {
|
|
||||||
view() {
|
|
||||||
var category = this.props.category;
|
|
||||||
|
|
||||||
return m('header.hero.category-hero', {style: 'color: #fff; background-color: '+category.color()}, [
|
|
||||||
m('div.container', [
|
|
||||||
m('div.container-narrow', [
|
|
||||||
m('h2', category.title()),
|
|
||||||
m('div.subtitle', category.description())
|
|
||||||
])
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import NavItem from 'flarum/components/nav-item';
|
|
||||||
import categoryIcon from 'flarum-categories/helpers/category-icon';
|
|
||||||
|
|
||||||
export default 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 && category) ? 'color: '+category.color() : '', title: category ? category.description() : ''}, [
|
|
||||||
categoryIcon(category, {className: 'icon'}),
|
|
||||||
this.props.label
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
static props(props) {
|
|
||||||
var category = props.category;
|
|
||||||
props.params.categories = category ? category.slug() : 'uncategorized';
|
|
||||||
props.href = app.route('category', props.params);
|
|
||||||
props.label = category ? category.title() : 'Uncategorized';
|
|
||||||
}
|
|
||||||
}
|
|
17
extensions/tags/js/src/components/tag-hero.js
Normal file
17
extensions/tags/js/src/components/tag-hero.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import Component from 'flarum/component';
|
||||||
|
|
||||||
|
export default class TagHero extends Component {
|
||||||
|
view() {
|
||||||
|
var tag = this.props.tag;
|
||||||
|
var color = tag.color();
|
||||||
|
|
||||||
|
return m('header.hero.tag-hero', {style: color ? 'color: #fff; background-color: '+tag.color() : ''}, [
|
||||||
|
m('div.container', [
|
||||||
|
m('div.container-narrow', [
|
||||||
|
m('h2', tag.name()),
|
||||||
|
m('div.subtitle', tag.description())
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
39
extensions/tags/js/src/components/tag-nav-item.js
Normal file
39
extensions/tags/js/src/components/tag-nav-item.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import NavItem from 'flarum/components/nav-item';
|
||||||
|
import tagIcon from 'flarum-tags/helpers/tag-icon';
|
||||||
|
|
||||||
|
export default class TagNavItem extends NavItem {
|
||||||
|
view() {
|
||||||
|
var tag = this.props.tag;
|
||||||
|
var active = this.constructor.active(this.props);
|
||||||
|
var description = tag && tag.description();
|
||||||
|
var children;
|
||||||
|
|
||||||
|
if (active && tag) {
|
||||||
|
children = app.store.all('tags').filter(child => {
|
||||||
|
var parent = child.parent();
|
||||||
|
return parent && parent.id() == tag.id();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 && tag) ? 'color: '+tag.color() : '',
|
||||||
|
title: description || ''
|
||||||
|
}, [
|
||||||
|
tagIcon(tag, {className: 'icon'}),
|
||||||
|
this.props.label
|
||||||
|
]),
|
||||||
|
children && children.length ? m('ul.dropdown-menu', children.map(tag => TagNavItem.component({tag, params: this.props.params}))) : ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static props(props) {
|
||||||
|
var tag = props.tag;
|
||||||
|
props.params.tags = tag ? tag.slug() : 'untagged';
|
||||||
|
props.href = app.route('tag', props.params);
|
||||||
|
props.label = tag ? tag.name() : 'Untagged';
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ import Component from 'flarum/component';
|
|||||||
import WelcomeHero from 'flarum/components/welcome-hero';
|
import WelcomeHero from 'flarum/components/welcome-hero';
|
||||||
import icon from 'flarum/helpers/icon';
|
import icon from 'flarum/helpers/icon';
|
||||||
|
|
||||||
export default class CategoriesPage extends Component {
|
export default class TagsPage extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
@ -1,12 +0,0 @@
|
|||||||
export default function categoryIcon(category, attrs) {
|
|
||||||
attrs = attrs || {};
|
|
||||||
|
|
||||||
if (category) {
|
|
||||||
attrs.style = attrs.style || {};
|
|
||||||
attrs.style.backgroundColor = category.color();
|
|
||||||
} else {
|
|
||||||
attrs.className = (attrs.className || '')+' uncategorized';
|
|
||||||
}
|
|
||||||
|
|
||||||
return m('span.icon.category-icon', attrs);
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
export default function categoryLabel(category, attrs) {
|
|
||||||
attrs = attrs || {};
|
|
||||||
|
|
||||||
if (category) {
|
|
||||||
attrs.style = attrs.style || {};
|
|
||||||
attrs.style.backgroundColor = attrs.style.color = category.color();
|
|
||||||
} else {
|
|
||||||
attrs.className = (attrs.className || '')+' uncategorized';
|
|
||||||
}
|
|
||||||
|
|
||||||
return m('span.category-label', attrs, m('span.category-label-text', category ? category.title() : 'Uncategorized'));
|
|
||||||
}
|
|
12
extensions/tags/js/src/helpers/tag-icon.js
Normal file
12
extensions/tags/js/src/helpers/tag-icon.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export default function tagIcon(tag, attrs) {
|
||||||
|
attrs = attrs || {};
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
attrs.style = attrs.style || {};
|
||||||
|
attrs.style.backgroundColor = tag.color();
|
||||||
|
} else {
|
||||||
|
attrs.className = (attrs.className || '')+' untagged';
|
||||||
|
}
|
||||||
|
|
||||||
|
return m('span.icon.tag-icon', attrs);
|
||||||
|
}
|
24
extensions/tags/js/src/helpers/tag-label.js
Normal file
24
extensions/tags/js/src/helpers/tag-label.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export default function tagsLabel(tag, attrs) {
|
||||||
|
attrs = attrs || {};
|
||||||
|
attrs.style = attrs.style || {};
|
||||||
|
attrs.className = attrs.className || '';
|
||||||
|
|
||||||
|
var link = attrs.link;
|
||||||
|
delete attrs.link;
|
||||||
|
if (link) {
|
||||||
|
attrs.href = app.route('tag', {tags: tag.slug()});
|
||||||
|
attrs.config = m.route;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
var color = tag.color();
|
||||||
|
if (color) {
|
||||||
|
attrs.style.backgroundColor = attrs.style.color = color;
|
||||||
|
attrs.className += ' colored';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attrs.className += ' untagged';
|
||||||
|
}
|
||||||
|
|
||||||
|
return m((link ? 'a' : 'span')+'.tag-label', attrs, m('span.tag-label-text', tag ? tag.name() : 'Untagged'));
|
||||||
|
}
|
19
extensions/tags/js/src/helpers/tags-label.js
Normal file
19
extensions/tags/js/src/helpers/tags-label.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import tagLabel from 'flarum-tags/helpers/tag-label';
|
||||||
|
|
||||||
|
export default function tagsLabel(tags, attrs) {
|
||||||
|
attrs = attrs || {};
|
||||||
|
var children = [];
|
||||||
|
|
||||||
|
var link = attrs.link;
|
||||||
|
delete attrs.link;
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
tags.forEach(tag => {
|
||||||
|
children.push(tagLabel(tag, {link}));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
children.push(tagLabel());
|
||||||
|
}
|
||||||
|
|
||||||
|
return m('span.tags-label', attrs, children);
|
||||||
|
}
|
@ -1,13 +0,0 @@
|
|||||||
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');
|
|
||||||
Category.prototype.position = Model.prop('position');
|
|
||||||
|
|
||||||
export default Category;
|
|
18
extensions/tags/js/src/models/tag.js
Normal file
18
extensions/tags/js/src/models/tag.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import Model from 'flarum/model';
|
||||||
|
|
||||||
|
class Tag extends Model {}
|
||||||
|
|
||||||
|
Tag.prototype.id = Model.prop('id');
|
||||||
|
Tag.prototype.name = Model.prop('name');
|
||||||
|
Tag.prototype.slug = Model.prop('slug');
|
||||||
|
Tag.prototype.description = Model.prop('description');
|
||||||
|
Tag.prototype.color = Model.prop('color');
|
||||||
|
Tag.prototype.backgroundUrl = Model.prop('backgroundUrl');
|
||||||
|
Tag.prototype.iconUrl = Model.prop('iconUrl');
|
||||||
|
Tag.prototype.discussionsCount = Model.prop('discussionsCount');
|
||||||
|
Tag.prototype.position = Model.prop('position');
|
||||||
|
Tag.prototype.parent = Model.one('parent');
|
||||||
|
Tag.prototype.defaultSort = Model.prop('defaultSort');
|
||||||
|
Tag.prototype.isChild = Model.prop('isChild');
|
||||||
|
|
||||||
|
export default Tag;
|
93
extensions/tags/less/extension.less
Normal file
93
extensions/tags/less/extension.less
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
.tag-label {
|
||||||
|
font-size: 85%;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2em 0.55em;
|
||||||
|
border-radius: @border-radius-base;
|
||||||
|
background: @fl-body-secondary-color;
|
||||||
|
|
||||||
|
&.untagged {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px dotted @fl-body-muted-color;
|
||||||
|
color: @fl-body-muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.colored {
|
||||||
|
& .tag-label-text {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.discussion-hero .tags-label & {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
|
||||||
|
&.colored {
|
||||||
|
margin-right: 5px;
|
||||||
|
background: #fff !important;
|
||||||
|
color: @fl-body-muted-color;
|
||||||
|
|
||||||
|
& .tag-label-text {
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.discussion-moved-post & {
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tags-label {
|
||||||
|
.discussion-summary & {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .tag-label {
|
||||||
|
border-radius: 0;
|
||||||
|
margin-right: 1px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-radius: @border-radius-base 0 0 @border-radius-base;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
border-radius: 0 @border-radius-base @border-radius-base 0;
|
||||||
|
}
|
||||||
|
&:first-child:last-child {
|
||||||
|
border-radius: @border-radius-base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo give all <li>s a class in core, get rid of block-item
|
||||||
|
.discussion-hero {
|
||||||
|
& .block-item {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-icon {
|
||||||
|
border-radius: @border-radius-base;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: -3px;
|
||||||
|
margin-left: 1px;
|
||||||
|
background: @fl-body-secondary-color;
|
||||||
|
|
||||||
|
&.untagged {
|
||||||
|
border: 1px dotted @fl-body-muted-color;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.side-nav .dropdown-menu > li > .dropdown-menu {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
& .tag-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
& > li > a {
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
|
|
||||||
class CreateCategoriesTable extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function up()
|
|
||||||
{
|
|
||||||
Schema::create('categories', function (Blueprint $table) {
|
|
||||||
$table->increments('id');
|
|
||||||
$table->string('title');
|
|
||||||
$table->string('slug');
|
|
||||||
$table->text('description');
|
|
||||||
$table->string('color');
|
|
||||||
$table->integer('discussions_count')->unsigned()->default(0);
|
|
||||||
$table->integer('position')->nullable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function down()
|
|
||||||
{
|
|
||||||
Schema::drop('categories');
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,7 +3,7 @@
|
|||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
class AddCategoryToDiscussions extends Migration
|
class CreateDiscussionsTagsTable extends Migration
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Run the migrations.
|
* Run the migrations.
|
||||||
@ -12,8 +12,10 @@ class AddCategoryToDiscussions extends Migration
|
|||||||
*/
|
*/
|
||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
Schema::table('discussions', function (Blueprint $table) {
|
Schema::create('discussions_tags', function (Blueprint $table) {
|
||||||
$table->integer('category_id')->unsigned()->nullable();
|
$table->integer('discussion_id')->unsigned();
|
||||||
|
$table->integer('tag_id')->unsigned();
|
||||||
|
$table->primary(['discussion_id', 'tag_id']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,8 +26,6 @@ class AddCategoryToDiscussions extends Migration
|
|||||||
*/
|
*/
|
||||||
public function down()
|
public function down()
|
||||||
{
|
{
|
||||||
Schema::table('discussions', function (Blueprint $table) {
|
Schema::drop('discussions_tags');
|
||||||
$table->dropColumn('category_id');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class CreateTagsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('tags', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->string('name', 100);
|
||||||
|
$table->string('slug', 100);
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->string('color', 50)->nullable();
|
||||||
|
$table->string('background_path', 100)->nullable();
|
||||||
|
$table->string('icon_path', 100)->nullable();
|
||||||
|
$table->integer('discussions_count')->unsigned()->default(0);
|
||||||
|
$table->integer('position')->nullable();
|
||||||
|
$table->integer('parent_id')->unsigned()->nullable();
|
||||||
|
$table->string('default_sort', 50)->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::drop('tags');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class CreateUsersTagsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('users_tags', function (Blueprint $table) {
|
||||||
|
$table->integer('user_id')->unsigned();
|
||||||
|
$table->integer('tag_id')->unsigned();
|
||||||
|
$table->dateTime('read_time')->nullable();
|
||||||
|
$table->boolean('is_hidden')->default(0);
|
||||||
|
$table->primary(['user_id', 'tag_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::drop('users_tags');
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +0,0 @@
|
|||||||
<?php namespace Flarum\Categories;
|
|
||||||
|
|
||||||
use Flarum\Core\Models\Model;
|
|
||||||
|
|
||||||
class Category extends Model
|
|
||||||
{
|
|
||||||
protected $table = 'categories';
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
<?php namespace Flarum\Categories;
|
|
||||||
|
|
||||||
use Flarum\Core\Repositories\UserRepositoryInterface as UserRepository;
|
|
||||||
use Flarum\Core\Search\SearcherInterface;
|
|
||||||
use Flarum\Core\Search\GambitAbstract;
|
|
||||||
|
|
||||||
class CategoryGambit extends GambitAbstract
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The gambit's regex pattern.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $pattern = 'category:(.+)';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Flarum\Categories\CategoryRepositoryInterface
|
|
||||||
*/
|
|
||||||
protected $categories;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate the gambit.
|
|
||||||
*
|
|
||||||
* @param \Flarum\Categories\CategoryRepositoryInterface $categories
|
|
||||||
*/
|
|
||||||
public function __construct(CategoryRepositoryInterface $categories)
|
|
||||||
{
|
|
||||||
$this->categories = $categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply conditions to the searcher, given matches from the gambit's
|
|
||||||
* regex.
|
|
||||||
*
|
|
||||||
* @param array $matches The matches from the gambit's regex.
|
|
||||||
* @param \Flarum\Core\Search\SearcherInterface $searcher
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function conditions($matches, SearcherInterface $searcher)
|
|
||||||
{
|
|
||||||
$slugs = explode(',', trim($matches[1], '"'));
|
|
||||||
|
|
||||||
$searcher->query()->where(function ($query) use ($slugs) {
|
|
||||||
foreach ($slugs as $slug) {
|
|
||||||
if ($slug === 'uncategorized') {
|
|
||||||
$query->orWhereNull('category_id');
|
|
||||||
} else {
|
|
||||||
$id = $this->categories->getIdForSlug($slug);
|
|
||||||
$query->orWhere('category_id', $id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
<?php namespace Flarum\Categories;
|
|
||||||
|
|
||||||
use Flarum\Api\Serializers\BaseSerializer;
|
|
||||||
|
|
||||||
class CategorySerializer extends BaseSerializer
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The resource type.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $type = 'categories';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize category attributes to be exposed in the API.
|
|
||||||
*
|
|
||||||
* @param \Flarum\Categories\Category $category
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function attributes($category)
|
|
||||||
{
|
|
||||||
$attributes = [
|
|
||||||
'title' => $category->title,
|
|
||||||
'description' => $category->description,
|
|
||||||
'slug' => $category->slug,
|
|
||||||
'color' => $category->color,
|
|
||||||
'discussionsCount' => (int) $category->discussions_count,
|
|
||||||
'position' => (int) $category->position
|
|
||||||
];
|
|
||||||
|
|
||||||
return $this->extendAttributes($category, $attributes);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +1,13 @@
|
|||||||
<?php namespace Flarum\Categories;
|
<?php namespace Flarum\Tags;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Flarum\Core\Models\User;
|
use Flarum\Core\Models\User;
|
||||||
use Flarum\Categories\Category;
|
use Flarum\Tags\Tag;
|
||||||
|
|
||||||
class EloquentCategoryRepository implements CategoryRepositoryInterface
|
class EloquentTagRepository implements TagRepositoryInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Find all categories, optionally making sure they are visible to a
|
* Find all tags, optionally making sure they are visible to a
|
||||||
* certain user.
|
* certain user.
|
||||||
*
|
*
|
||||||
* @param \Flarum\Core\Models\User|null $user
|
* @param \Flarum\Core\Models\User|null $user
|
||||||
@ -15,13 +15,13 @@ class EloquentCategoryRepository implements CategoryRepositoryInterface
|
|||||||
*/
|
*/
|
||||||
public function find(User $user = null)
|
public function find(User $user = null)
|
||||||
{
|
{
|
||||||
$query = Category::newQuery();
|
$query = Tag::newQuery();
|
||||||
|
|
||||||
return $this->scopeVisibleForUser($query, $user)->get();
|
return $this->scopeVisibleForUser($query, $user)->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the ID of a category with the given slug.
|
* Get the ID of a tag with the given slug.
|
||||||
*
|
*
|
||||||
* @param string $slug
|
* @param string $slug
|
||||||
* @param \Flarum\Core\Models\User|null $user
|
* @param \Flarum\Core\Models\User|null $user
|
||||||
@ -29,7 +29,7 @@ class EloquentCategoryRepository implements CategoryRepositoryInterface
|
|||||||
*/
|
*/
|
||||||
public function getIdForSlug($slug, User $user = null)
|
public function getIdForSlug($slug, User $user = null)
|
||||||
{
|
{
|
||||||
$query = Category::where('slug', 'like', $slug);
|
$query = Tag::where('slug', 'like', $slug);
|
||||||
|
|
||||||
return $this->scopeVisibleForUser($query, $user)->pluck('id');
|
return $this->scopeVisibleForUser($query, $user)->pluck('id');
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
<?php namespace Flarum\Categories\Handlers;
|
<?php namespace Flarum\Tags\Handlers;
|
||||||
|
|
||||||
use Flarum\Categories\Category;
|
use Flarum\Tags\Tag;
|
||||||
use Flarum\Categories\CategorySerializer;
|
use Flarum\Tags\TagSerializer;
|
||||||
use Flarum\Forum\Events\RenderView;
|
use Flarum\Forum\Events\RenderView;
|
||||||
|
|
||||||
class CategoryPreloader
|
class TagPreloader
|
||||||
{
|
{
|
||||||
public function subscribe($events)
|
public function subscribe($events)
|
||||||
{
|
{
|
||||||
@ -13,7 +13,7 @@ class CategoryPreloader
|
|||||||
|
|
||||||
public function renderForum(RenderView $event)
|
public function renderForum(RenderView $event)
|
||||||
{
|
{
|
||||||
$serializer = new CategorySerializer($event->action->actor);
|
$serializer = new TagSerializer($event->action->actor, null, ['parent']);
|
||||||
$event->view->data = array_merge($event->view->data, $serializer->collection(Category::orderBy('position')->get())->toArray());
|
$event->view->data = array_merge($event->view->data, $serializer->collection(Tag::orderBy('position')->get())->toArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
8
extensions/tags/src/Tag.php
Normal file
8
extensions/tags/src/Tag.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php namespace Flarum\Tags;
|
||||||
|
|
||||||
|
use Flarum\Core\Models\Model;
|
||||||
|
|
||||||
|
class Tag extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'tags';
|
||||||
|
}
|
63
extensions/tags/src/TagGambit.php
Normal file
63
extensions/tags/src/TagGambit.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?php namespace Flarum\Tags;
|
||||||
|
|
||||||
|
use Flarum\Core\Repositories\UserRepositoryInterface as UserRepository;
|
||||||
|
use Flarum\Core\Search\SearcherInterface;
|
||||||
|
use Flarum\Core\Search\GambitAbstract;
|
||||||
|
|
||||||
|
class TagGambit extends GambitAbstract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The gambit's regex pattern.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $pattern = 'tag:(.+)';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Flarum\Tags\TagRepositoryInterface
|
||||||
|
*/
|
||||||
|
protected $tags;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate the gambit.
|
||||||
|
*
|
||||||
|
* @param \Flarum\Tags\TagRepositoryInterface $categories
|
||||||
|
*/
|
||||||
|
public function __construct(TagRepositoryInterface $tags)
|
||||||
|
{
|
||||||
|
$this->tags = $tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply conditions to the searcher, given matches from the gambit's
|
||||||
|
* regex.
|
||||||
|
*
|
||||||
|
* @param array $matches The matches from the gambit's regex.
|
||||||
|
* @param \Flarum\Core\Search\SearcherInterface $searcher
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function conditions($matches, SearcherInterface $searcher)
|
||||||
|
{
|
||||||
|
$slugs = explode(',', trim($matches[1], '"'));
|
||||||
|
|
||||||
|
$searcher->query()->where(function ($query) use ($slugs) {
|
||||||
|
foreach ($slugs as $slug) {
|
||||||
|
if ($slug === 'uncategorized') {
|
||||||
|
$query->orWhereNotExists(function ($query) {
|
||||||
|
$query->select(app('db')->raw(1))
|
||||||
|
->from('discussions_tags')
|
||||||
|
->whereRaw('discussion_id = discussions.id');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$id = $this->tags->getIdForSlug($slug);
|
||||||
|
|
||||||
|
$query->orWhereExists(function ($query) use ($id) {
|
||||||
|
$query->select(app('db')->raw(1))
|
||||||
|
->from('discussions_tags')
|
||||||
|
->whereRaw('discussion_id = discussions.id AND tag_id = ?', [$id]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
<?php namespace Flarum\Categories;
|
<?php namespace Flarum\Tags;
|
||||||
|
|
||||||
use Flarum\Core\Models\User;
|
use Flarum\Core\Models\User;
|
||||||
|
|
||||||
interface CategoryRepositoryInterface
|
interface TagRepositoryInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Find all categories, optionally making sure they are visible to a
|
* Find all tags, optionally making sure they are visible to a
|
||||||
* certain user.
|
* certain user.
|
||||||
*
|
*
|
||||||
* @param \Flarum\Core\Models\User|null $user
|
* @param \Flarum\Core\Models\User|null $user
|
||||||
@ -14,7 +14,7 @@ interface CategoryRepositoryInterface
|
|||||||
public function find(User $user = null);
|
public function find(User $user = null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the ID of a category with the given slug.
|
* Get the ID of a tag with the given slug.
|
||||||
*
|
*
|
||||||
* @param string $slug
|
* @param string $slug
|
||||||
* @param \Flarum\Core\Models\User|null $user
|
* @param \Flarum\Core\Models\User|null $user
|
42
extensions/tags/src/TagSerializer.php
Normal file
42
extensions/tags/src/TagSerializer.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php namespace Flarum\Tags;
|
||||||
|
|
||||||
|
use Flarum\Api\Serializers\BaseSerializer;
|
||||||
|
|
||||||
|
class TagSerializer extends BaseSerializer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The resource type.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $type = 'tags';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize tag attributes to be exposed in the API.
|
||||||
|
*
|
||||||
|
* @param \Flarum\Tags\Tag $tag
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function attributes($tag)
|
||||||
|
{
|
||||||
|
$attributes = [
|
||||||
|
'name' => $tag->name,
|
||||||
|
'description' => $tag->description,
|
||||||
|
'slug' => $tag->slug,
|
||||||
|
'color' => $tag->color,
|
||||||
|
'backgroundUrl' => $tag->background_path,
|
||||||
|
'iconUrl' => $tag->icon_path,
|
||||||
|
'discussionsCount' => (int) $tag->discussions_count,
|
||||||
|
'position' => $tag->position === null ? null : (int) $tag->position,
|
||||||
|
'defaultSort' => $tag->default_sort,
|
||||||
|
'isChild' => (bool) $tag->parent_id
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->extendAttributes($tag, $attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function parent()
|
||||||
|
{
|
||||||
|
return $this->hasOne('Flarum\Tags\TagSerializer');
|
||||||
|
}
|
||||||
|
}
|
64
extensions/tags/src/TagsServiceProvider.php
Normal file
64
extensions/tags/src/TagsServiceProvider.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?php namespace Flarum\Tags;
|
||||||
|
|
||||||
|
use Flarum\Support\ServiceProvider;
|
||||||
|
use Flarum\Extend\ForumAssets;
|
||||||
|
use Flarum\Extend\EventSubscribers;
|
||||||
|
use Flarum\Extend\Relationship;
|
||||||
|
use Flarum\Extend\SerializeRelationship;
|
||||||
|
use Flarum\Extend\ApiInclude;
|
||||||
|
use Flarum\Extend\Permission;
|
||||||
|
use Flarum\Extend\DiscussionGambit;
|
||||||
|
|
||||||
|
class TagsServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bootstrap the application events.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
$this->extend(
|
||||||
|
new ForumAssets([
|
||||||
|
__DIR__.'/../js/dist/extension.js',
|
||||||
|
__DIR__.'/../less/extension.less'
|
||||||
|
]),
|
||||||
|
|
||||||
|
new EventSubscribers([
|
||||||
|
// 'Flarum\Categories\Handlers\DiscussionMovedNotifier',
|
||||||
|
'Flarum\Tags\Handlers\TagPreloader',
|
||||||
|
// 'Flarum\Categories\Handlers\CategorySaver'
|
||||||
|
]),
|
||||||
|
|
||||||
|
new Relationship('Flarum\Core\Models\Discussion', 'tags', function ($model) {
|
||||||
|
return $model->belongsToMany('Flarum\Tags\Tag', 'discussions_tags');
|
||||||
|
}),
|
||||||
|
|
||||||
|
new SerializeRelationship('Flarum\Api\Serializers\DiscussionBasicSerializer', 'hasMany', 'tags', 'Flarum\Tags\TagSerializer'),
|
||||||
|
|
||||||
|
new ApiInclude(['discussions.index', 'discussions.show'], 'tags', true),
|
||||||
|
|
||||||
|
(new Permission('discussion.editTags'))
|
||||||
|
->serialize()
|
||||||
|
->grant(function ($grant, $user) {
|
||||||
|
$grant->where('start_user_id', $user->id);
|
||||||
|
// @todo add limitations to time etc. according to a config setting
|
||||||
|
}),
|
||||||
|
|
||||||
|
new DiscussionGambit('Flarum\Tags\TagGambit')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the service provider.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
$this->app->bind(
|
||||||
|
'Flarum\Tags\TagRepositoryInterface',
|
||||||
|
'Flarum\Tags\EloquentTagRepository'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user