mirror of
https://github.com/flarum/framework.git
synced 2025-02-17 02:02:47 +08:00
Merge remote-tracking branch 'extensions_tags/REWRITE'
This commit is contained in:
commit
deea028dab
19
extensions/tags/.editorconfig
Normal file
19
extensions/tags/.editorconfig
Normal file
|
@ -0,0 +1,19 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{php,xml,json}]
|
||||
indent_size = 4
|
18
extensions/tags/.gitattributes
vendored
Normal file
18
extensions/tags/.gitattributes
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.gitmodules export-ignore
|
||||
.github export-ignore
|
||||
.travis export-ignore
|
||||
.travis.yml export-ignore
|
||||
.editorconfig export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
||||
phpunit.xml export-ignore
|
||||
tests export-ignore
|
||||
|
||||
js/dist/* -diff
|
||||
js/dist/* linguist-generated
|
||||
js/dist-typings/* linguist-generated
|
||||
js/yarn.lock -diff
|
||||
|
||||
* text=auto eol=lf
|
15
extensions/tags/.github/workflows/backend.yml
vendored
Normal file
15
extensions/tags/.github/workflows/backend.yml
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
name: Tags PHP
|
||||
|
||||
on: [workflow_dispatch, push, pull_request]
|
||||
|
||||
# The reusable workflow definitions will be moved to the `flarum/framework` repo soon.
|
||||
# This will break your current script.
|
||||
# When this happens, run `flarum-cli audit infra --fix` to update your infrastructure.
|
||||
|
||||
jobs:
|
||||
run:
|
||||
uses: flarum/.github/.github/workflows/REUSABLE_backend.yml@main
|
||||
with:
|
||||
enable_backend_testing: true
|
||||
|
||||
backend_directory: .
|
21
extensions/tags/.github/workflows/frontend.yml
vendored
Normal file
21
extensions/tags/.github/workflows/frontend.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
name: Tags JS
|
||||
|
||||
on: [workflow_dispatch, push, pull_request]
|
||||
|
||||
# The reusable workflow definitions will be moved to the `flarum/framework` repo soon.
|
||||
# This will break your current script.
|
||||
# When this happens, run `flarum-cli audit infra --fix` to update your infrastructure.
|
||||
|
||||
jobs:
|
||||
run:
|
||||
uses: flarum/.github/.github/workflows/REUSABLE_frontend.yml@main
|
||||
with:
|
||||
enable_bundlewatch: false
|
||||
enable_prettier: false
|
||||
enable_typescript: false
|
||||
|
||||
frontend_directory: ./js
|
||||
main_git_branch: master
|
||||
|
||||
secrets:
|
||||
bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
|
12
extensions/tags/.gitignore
vendored
Normal file
12
extensions/tags/.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
/vendor
|
||||
composer.lock
|
||||
composer.phar
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
tests/.phpunit.result.cache
|
||||
/tests/integration/tmp
|
||||
.vagrant
|
||||
.idea/*
|
||||
.vscode
|
||||
js/coverage-ts
|
14
extensions/tags/.styleci.yml
Normal file
14
extensions/tags/.styleci.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
preset: recommended
|
||||
|
||||
enabled:
|
||||
- logical_not_operators_with_successor_space
|
||||
|
||||
disabled:
|
||||
- align_double_arrow
|
||||
- blank_line_after_opening_tag
|
||||
- multiline_array_trailing_comma
|
||||
- new_with_braces
|
||||
- phpdoc_align
|
||||
- phpdoc_order
|
||||
- phpdoc_separation
|
||||
- phpdoc_types
|
171
extensions/tags/CHANGELOG.md
Normal file
171
extensions/tags/CHANGELOG.md
Normal file
|
@ -0,0 +1,171 @@
|
|||
# Changelog
|
||||
|
||||
## [1.2.0](https://github.com/flarum/tags/compare/v1.1.0...v1.2.0)
|
||||
|
||||
### Added
|
||||
- Bypass tag requirements toggle button for able actors (https://github.com/flarum/tags/pull/153).
|
||||
|
||||
### Changed
|
||||
- Eager load tags state with actor id (https://github.com/flarum/tags/pull/149, https://github.com/flarum/tags/pull/151).
|
||||
- Slashes in tag slug break routing (https://github.com/flarum/tags/pull/150).
|
||||
- Stop loading tag last posted discussion relation on admin side (https://github.com/flarum/tags/pull/152).
|
||||
|
||||
### Fixed
|
||||
- UI does not reflect bypass tag requirements permission (https://github.com/flarum/tags/pull/148).
|
||||
- Occassional errors when deleting flagged posts (https://github.com/flarum/tags/pull/154)
|
||||
- Tag discussion count doesn't adjust when deleting first/only post of the discussion (https://github.com/flarum/tags/pull/154)
|
||||
|
||||
## [1.1.0](https://github.com/flarum/tags/compare/v1.0.3...v1.1.0)
|
||||
|
||||
### Added
|
||||
- Custom colorising with CSS Custom Properties (https://github.com/flarum/tags/pulls/139)
|
||||
|
||||
### Changed
|
||||
- Update nojs view to use slug driver (https://github.com/flarum/tags/pulls/142)
|
||||
- Pass filter params (https://github.com/flarum/tags/pulls/141)
|
||||
- Eager load actor tag states (https://github.com/flarum/tags/pulls/143)
|
||||
- Export `getSelectableTags` util (https://github.com/flarum/tags/pulls/144)
|
||||
|
||||
### Fixed
|
||||
- Broken side nav tag listing (https://github.com/flarum/tags/pulls/137)
|
||||
- Discussions hidden from all users including admins (https://github.com/flarum/tags/pulls/140)
|
||||
- Unauthorized view of restricted tags (https://github.com/flarum/tags/pulls/145)
|
||||
- Make clicking edit tag button easier on mobile (https://github.com/flarum/core/issues/3098)
|
||||
|
||||
|
||||
## [1.0.3](https://github.com/flarum/tags/compare/v1.0.2...v1.0.3)
|
||||
|
||||
### Fixed
|
||||
- Sub tags that were previously loaded are visible when visiting the index from another page (https://github.com/flarum/tags/pull/135)
|
||||
- Discussion pages are showing parent tags after child tags
|
||||
|
||||
## [1.0.2](https://github.com/flarum/tags/compare/v1.0.1...v1.0.2)
|
||||
|
||||
### Fixed
|
||||
- All sub tags are open for each primary tag (https://github.com/flarum/tags/pull/134)
|
||||
|
||||
## [1.0.1](https://github.com/flarum/tags/compare/v1.0.0...v1.0.1)
|
||||
|
||||
### Fixed
|
||||
- Permission grid does not lazy load secondary tags (https://github.com/flarum/tags/pull/133)
|
||||
|
||||
## [1.0.0](https://github.com/flarum/tags/compare/v0.1.0-beta.16...v1.0.0)
|
||||
|
||||
### Changed
|
||||
- Compatibility with Flarum v1.0.0.
|
||||
- Eager loading additional relations to improve performance (https://github.com/flarum/tags/pull/125)
|
||||
- Optimize tag permissions querying to improve performance (https://github.com/flarum/tags/pull/126)
|
||||
- Remove loading all tags on request to improve performance (https://github.com/flarum/tags/pull/87)
|
||||
|
||||
### Fixed
|
||||
- Tags page uses incorrect canonical URL due to reusing the request object (https://github.com/flarum/tags/pull/122)
|
||||
- Searching while on a tag page causes the search to affect everything and not the subset (https://github.com/flarum/tags/pull/129)
|
||||
- Sometimes tag pages show the wrong tag information if it had children
|
||||
- The API returns outdated tag information when saving discussions (https://github.com/flarum/tags/pull/131)
|
||||
|
||||
## [0.1.0-beta.16](https://github.com/flarum/tags/compare/v0.1.0-beta.15...v0.1.0-beta.16)
|
||||
|
||||
### Added
|
||||
- Permission to allow bypassing tag requirements (https://github.com/flarum/tags/pull/111)
|
||||
- `.Taglabel--child` added to tagLabel to allow styling (https://github.com/flarum/tags/pull/114)
|
||||
|
||||
### Changed
|
||||
- Updated admin category from discussion to feature (https://github.com/flarum/tags/pull/118)
|
||||
- Moved locale files from translation pack to extension (https://github.com/flarum/tags/pull/99)
|
||||
- Compatibility with Illuminate 8 (https://github.com/flarum/tags/pull/121)
|
||||
- Eager load relations lastPostedDiscussion requires (https://github.com/flarum/tags/pull/120)
|
||||
|
||||
### Fixed
|
||||
- Prevent page creep with long list of tags (https://github.com/flarum/tags/pull/116)
|
||||
- Enter key does not submit tag selection modal ([617fc4d](https://github.com/flarum/tags/commit/617fc4d4419fe4d3ef7b388d14965acc83b319ce))
|
||||
- Editing a tag does not work (https://github.com/flarum/tags/pull/117)
|
||||
- Tags link not wrapped inside `Button-label` (https://github.com/flarum/tags/pull/113)
|
||||
- Without selectable tags the tag selection modal errors (https://github.com/flarum/tags/pull/112)
|
||||
|
||||
## [0.1.0-beta.15](https://github.com/flarum/tags/compare/v0.1.0-beta.14...v0.1.0-beta.15)
|
||||
|
||||
### Added
|
||||
- Tag tiles have icons (#104).
|
||||
|
||||
### Changed
|
||||
- Updated composer.json and admin javascript for new admin area.
|
||||
- Updated to use newest extenders.
|
||||
- Implement new authorization layer ([c3eff74](https://github.com/flarum/tags/commit/c3eff74289d3461e55d7320556b1e5a5ca08e0ac)).
|
||||
|
||||
### Fixed
|
||||
- Guests do not see "new discussion" and get the log in modal when clicked (#98).
|
||||
- The tag hidden property is not a bidi and is not saved ([3f54b70](https://github.com/flarum/tags/commit/3f54b70733bb94f7f100580f50f6503a0c387ad6)).
|
||||
|
||||
### Removed
|
||||
- TagWillBeSaved event is removed ([05837de](https://github.com/flarum/tags/commit/05837de8bbe11ca094c7ac63f1a23d7aeceb28d2)).
|
||||
|
||||
## [0.1.0-beta.14](https://github.com/flarum/tags/compare/v0.1.0-beta.13...v0.1.0-beta.14)
|
||||
|
||||
### Added
|
||||
- Introduced Creating and Deleting events (#86)
|
||||
|
||||
### Changed
|
||||
- Updated mithril to version 2
|
||||
- Load language strings correctly on en-/disable
|
||||
- Updated JS dependencies
|
||||
- Allow tag visibility override with an event listener (#79)
|
||||
- TagWillBeSaved event renamed to Saving (#92)
|
||||
|
||||
### Fixed
|
||||
- Sorting tag structure on mobile hardly worked (#82)
|
||||
- Discussion count and visibility incorrectly included hidden or private discussions (#78)
|
||||
- Negated tag filtering does not work (#88)
|
||||
- Call to non existing method handleErrors (#94)
|
||||
- Changing tags of discussions by other users is possible (#95)
|
||||
- Tag modal shows duplicate tags ()
|
||||
|
||||
## [0.1.0-beta.13](https://github.com/flarum/tags/compare/v0.1.0-beta.12...v0.1.0-beta.13)
|
||||
|
||||
### Added
|
||||
- Add title and description meta tags (#72)
|
||||
|
||||
|
||||
### Changed
|
||||
- Updated JS dependencies
|
||||
- Improved performance of subqueries (#75)
|
||||
- Allow menu items between listed tags and link to tags page (#70)
|
||||
- Using new model extender
|
||||
|
||||
## [0.1.0-beta.12](https://github.com/flarum/tags/compare/v0.1.0-beta.11...v0.1.0-beta.12)
|
||||
|
||||
### Fixed
|
||||
- Icons misaligned in tag selection modal (#73, #76)
|
||||
- Selected tags indiscernible when they have no icon (#68, #76)
|
||||
- Group permissions weren't really deleted when a tag was opened up again to the public (#65)
|
||||
|
||||
## [0.1.0-beta.11](https://github.com/flarum/tags/compare/v0.1.0-beta.10...v0.1.0-beta.11)
|
||||
|
||||
### Fixed
|
||||
- Tag change events triggered errors for deleted tags ([e5694e5](https://github.com/flarum/tags/pull/66/commits/e5694e51ef7851523ac6e467b4d7d98d471fd997))
|
||||
|
||||
## [0.1.0-beta.10](https://github.com/flarum/tags/compare/v0.1.0-beta.9...v0.1.0-beta.10)
|
||||
|
||||
### Added
|
||||
- SEO: The tags page now has a `rel="canonical"` meta tag, preventing duplicate content (#64)
|
||||
- SEO: The tags page now has server-rendered content, for better indexing by search engines (#64)
|
||||
|
||||
## [0.1.0-beta.9](https://github.com/flarum/tags/compare/v0.1.0-beta.8.2...v0.1.0-beta.9)
|
||||
|
||||
### Added
|
||||
- Allow configuration of icon per tag ([e1a0ff8](https://github.com/flarum/tags/commit/e1a0ff8e0f726fbfe26fa47aea4a0555b109aad0))
|
||||
|
||||
### Changed
|
||||
- Replace event subscribers (that resolve services too early) with listeners ([73c0626](https://github.com/flarum/tags/commit/73c0626e722d2be2b82804eec5746646b64b0c44))
|
||||
- Compatibility with Laravel 5.7 ([cb683f3](https://github.com/flarum/tags/commit/cb683f37e689a03b25e43e47447025de8e127a56))
|
||||
- Update html5sortable library ([e8104a6](https://github.com/flarum/tags/commit/e8104a623edff6560c544972b2171faf050ec2ab))
|
||||
|
||||
### Fixed
|
||||
- JS: Vulnerable lodash dependency ([c80cbe8](https://github.com/flarum/tags/commit/c80cbe8ae7063d1c18784e983e9789554dbe4e03))
|
||||
- Search crashed when searched tag did not exist ([3d6921b](https://github.com/flarum/tags/commit/3d6921bdd257c0f17ea36bd8c1f352670fef66e8))
|
||||
- Discussions from hidden tags weren't showing when gambits were used ([7275c39](https://github.com/flarum/tags/commit/7275c395799dac0f420aa14afccb1f125622af08))
|
||||
|
||||
## [0.1.0-beta.8.2](https://github.com/flarum/tags/compare/v0.1.0-beta.8.1...v0.1.0-beta.8.2)
|
||||
|
||||
### Fixed
|
||||
- Fix dropping foreign keys in `down` migrations ([cad9741](https://github.com/flarum/tags/commit/cad97410e53854d58fefd01916ba3a1c3bd5ba3d))
|
||||
- Fix `INVALID DATE` errors on tags page ([8d4d01c](https://github.com/flarum/tags/commit/8d4d01c61079fecf84608dda1c64d112f5d9be34))
|
22
extensions/tags/LICENSE
Normal file
22
extensions/tags/LICENSE
Normal file
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
84
extensions/tags/composer.json
Normal file
84
extensions/tags/composer.json
Normal file
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"name": "flarum/tags",
|
||||
"description": "Organize discussions into a hierarchy of tags and categories.",
|
||||
"type": "flarum-extension",
|
||||
"keywords": [
|
||||
"discussion"
|
||||
],
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"issues": "https://github.com/flarum/core/issues",
|
||||
"source": "https://github.com/flarum/tags",
|
||||
"forum": "https://discuss.flarum.org"
|
||||
},
|
||||
"homepage": "https://flarum.org",
|
||||
"funding": [
|
||||
{
|
||||
"type": "website",
|
||||
"url": "https://flarum.org/donate/"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"flarum/core": "^1.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Flarum\\Tags\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Flarum\\Tags\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.x-dev"
|
||||
},
|
||||
"flarum-extension": {
|
||||
"title": "Tags",
|
||||
"category": "feature",
|
||||
"icon": {
|
||||
"name": "fas fa-tags",
|
||||
"backgroundColor": "#F28326",
|
||||
"color": "#fff"
|
||||
}
|
||||
},
|
||||
"flarum-cli": {
|
||||
"modules": {
|
||||
"admin": true,
|
||||
"forum": true,
|
||||
"js": true,
|
||||
"jsCommon": true,
|
||||
"css": true,
|
||||
"gitConf": true,
|
||||
"githubActions": true,
|
||||
"prettier": false,
|
||||
"typescript": false,
|
||||
"bundlewatch": false,
|
||||
"backendTesting": true,
|
||||
"editorConfig": true,
|
||||
"styleci": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": [
|
||||
"@test:unit",
|
||||
"@test:integration"
|
||||
],
|
||||
"test:unit": "phpunit -c tests/phpunit.unit.xml",
|
||||
"test:integration": "phpunit -c tests/phpunit.integration.xml",
|
||||
"test:setup": "@php tests/integration/setup.php"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"test": "Runs all tests.",
|
||||
"test:unit": "Runs all unit tests.",
|
||||
"test:integration": "Runs all integration tests.",
|
||||
"test:setup": "Sets up a database for use with integration tests. Execute this only once."
|
||||
},
|
||||
"require-dev": {
|
||||
"flarum/core": "*@dev",
|
||||
"flarum/testing": "^1.0.0"
|
||||
}
|
||||
}
|
136
extensions/tags/extend.php
Normal file
136
extensions/tags/extend.php
Normal file
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Api\Controller as FlarumController;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Api\Serializer\ForumSerializer;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Discussion\Filter\DiscussionFilterer;
|
||||
use Flarum\Discussion\Search\DiscussionSearcher;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Flags\Api\Controller\ListFlagsController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Filter\PostFilterer;
|
||||
use Flarum\Tags\Access;
|
||||
use Flarum\Tags\Api\Controller;
|
||||
use Flarum\Tags\Api\Serializer\TagSerializer;
|
||||
use Flarum\Tags\Content;
|
||||
use Flarum\Tags\Event\DiscussionWasTagged;
|
||||
use Flarum\Tags\Filter\HideHiddenTagsFromAllDiscussionsPage;
|
||||
use Flarum\Tags\Filter\PostTagFilter;
|
||||
use Flarum\Tags\Listener;
|
||||
use Flarum\Tags\LoadForumTagsRelationship;
|
||||
use Flarum\Tags\Post\DiscussionTaggedPost;
|
||||
use Flarum\Tags\Query\TagFilterGambit;
|
||||
use Flarum\Tags\Tag;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
$eagerLoadTagState = function ($query, ?ServerRequestInterface $request, array $relations) {
|
||||
if ($request && in_array('tags.state', $relations, true)) {
|
||||
$query->withStateFor(RequestUtil::getActor($request));
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
(new Extend\Frontend('forum'))
|
||||
->js(__DIR__.'/js/dist/forum.js')
|
||||
->css(__DIR__.'/less/forum.less')
|
||||
->route('/t/{slug}', 'tag', Content\Tag::class)
|
||||
->route('/tags', 'tags', Content\Tags::class),
|
||||
|
||||
(new Extend\Frontend('admin'))
|
||||
->js(__DIR__.'/js/dist/admin.js')
|
||||
->css(__DIR__.'/less/admin.less'),
|
||||
|
||||
(new Extend\Routes('api'))
|
||||
->get('/tags', 'tags.index', Controller\ListTagsController::class)
|
||||
->post('/tags', 'tags.create', Controller\CreateTagController::class)
|
||||
->post('/tags/order', 'tags.order', Controller\OrderTagsController::class)
|
||||
->get('/tags/{slug}', 'tags.show', Controller\ShowTagController::class)
|
||||
->patch('/tags/{id}', 'tags.update', Controller\UpdateTagController::class)
|
||||
->delete('/tags/{id}', 'tags.delete', Controller\DeleteTagController::class),
|
||||
|
||||
(new Extend\Model(Discussion::class))
|
||||
->belongsToMany('tags', Tag::class, 'discussion_tag'),
|
||||
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->hasMany('tags', TagSerializer::class)
|
||||
->attribute('canBypassTagCounts', function (ForumSerializer $serializer) {
|
||||
return $serializer->getActor()->can('bypassTagCounts');
|
||||
}),
|
||||
|
||||
(new Extend\ApiSerializer(DiscussionSerializer::class))
|
||||
->hasMany('tags', TagSerializer::class)
|
||||
->attribute('canTag', function (DiscussionSerializer $serializer, $model) {
|
||||
return $serializer->getActor()->can('tag', $model);
|
||||
}),
|
||||
|
||||
(new Extend\ApiController(FlarumController\ListPostsController::class))
|
||||
->load('discussion.tags'),
|
||||
|
||||
(new Extend\ApiController(ListFlagsController::class))
|
||||
->load('post.discussion.tags'),
|
||||
|
||||
(new Extend\ApiController(FlarumController\ListDiscussionsController::class))
|
||||
->addInclude(['tags', 'tags.state', 'tags.parent'])
|
||||
->loadWhere('tags', $eagerLoadTagState),
|
||||
|
||||
(new Extend\ApiController(FlarumController\ShowDiscussionController::class))
|
||||
->addInclude(['tags', 'tags.state', 'tags.parent'])
|
||||
->loadWhere('tags', $eagerLoadTagState),
|
||||
|
||||
(new Extend\ApiController(FlarumController\CreateDiscussionController::class))
|
||||
->addInclude(['tags', 'tags.state', 'tags.parent'])
|
||||
->loadWhere('tags', $eagerLoadTagState),
|
||||
|
||||
(new Extend\ApiController(FlarumController\ShowForumController::class))
|
||||
->addInclude(['tags', 'tags.parent'])
|
||||
->prepareDataForSerialization(LoadForumTagsRelationship::class),
|
||||
|
||||
(new Extend\Settings())
|
||||
->serializeToForum('minPrimaryTags', 'flarum-tags.min_primary_tags')
|
||||
->serializeToForum('maxPrimaryTags', 'flarum-tags.max_primary_tags')
|
||||
->serializeToForum('minSecondaryTags', 'flarum-tags.min_secondary_tags')
|
||||
->serializeToForum('maxSecondaryTags', 'flarum-tags.max_secondary_tags'),
|
||||
|
||||
(new Extend\Policy())
|
||||
->modelPolicy(Discussion::class, Access\DiscussionPolicy::class)
|
||||
->modelPolicy(Tag::class, Access\TagPolicy::class)
|
||||
->globalPolicy(Access\GlobalPolicy::class),
|
||||
|
||||
(new Extend\ModelVisibility(Discussion::class))
|
||||
->scopeAll(Access\ScopeDiscussionVisibilityForAbility::class),
|
||||
|
||||
(new Extend\ModelVisibility(Tag::class))
|
||||
->scope(Access\ScopeTagVisibility::class),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\View)
|
||||
->namespace('tags', __DIR__.'/views'),
|
||||
|
||||
(new Extend\Post)
|
||||
->type(DiscussionTaggedPost::class),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(Saving::class, Listener\SaveTagsToDatabase::class)
|
||||
->listen(DiscussionWasTagged::class, Listener\CreatePostWhenTagsAreChanged::class)
|
||||
->subscribe(Listener\UpdateTagMetadata::class),
|
||||
|
||||
(new Extend\Filter(PostFilterer::class))
|
||||
->addFilter(PostTagFilter::class),
|
||||
|
||||
(new Extend\Filter(DiscussionFilterer::class))
|
||||
->addFilter(TagFilterGambit::class)
|
||||
->addFilterMutator(HideHiddenTagsFromAllDiscussionsPage::class),
|
||||
|
||||
(new Extend\SimpleFlarumSearch(DiscussionSearcher::class))
|
||||
->addGambit(TagFilterGambit::class),
|
||||
];
|
9
extensions/tags/js/.gitignore
vendored
Normal file
9
extensions/tags/js/.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
node_modules
|
2
extensions/tags/js/admin.js
Normal file
2
extensions/tags/js/admin.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './src/common';
|
||||
export * from './src/admin';
|
3
extensions/tags/js/dist/admin.js
generated
vendored
Normal file
3
extensions/tags/js/dist/admin.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
6
extensions/tags/js/dist/admin.js.LICENSE.txt
generated
vendored
Normal file
6
extensions/tags/js/dist/admin.js.LICENSE.txt
generated
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
/**!
|
||||
* Sortable 1.14.0
|
||||
* @author RubaXa <trash@rubaxa.org>
|
||||
* @author owenm <owen23355@gmail.com>
|
||||
* @license MIT
|
||||
*/
|
1
extensions/tags/js/dist/admin.js.map
generated
vendored
Normal file
1
extensions/tags/js/dist/admin.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2
extensions/tags/js/dist/forum.js
generated
vendored
Normal file
2
extensions/tags/js/dist/forum.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
extensions/tags/js/dist/forum.js.map
generated
vendored
Normal file
1
extensions/tags/js/dist/forum.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2
extensions/tags/js/forum.js
Normal file
2
extensions/tags/js/forum.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './src/common';
|
||||
export * from './src/forum';
|
17
extensions/tags/js/package.json
Normal file
17
extensions/tags/js/package.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"private": true,
|
||||
"name": "@flarum/tags",
|
||||
"dependencies": {
|
||||
"sortablejs": "^1.14.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "webpack --mode development --watch",
|
||||
"build": "webpack --mode production",
|
||||
"analyze": "cross-env ANALYZER=true yarn build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"flarum-webpack-config": "^2.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack-cli": "^4.9.1"
|
||||
}
|
||||
}
|
27
extensions/tags/js/src/admin/addTagChangePermission.js
Normal file
27
extensions/tags/js/src/admin/addTagChangePermission.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { extend } from 'flarum/extend';
|
||||
import PermissionGrid from 'flarum/components/PermissionGrid';
|
||||
import SettingDropdown from 'flarum/components/SettingDropdown';
|
||||
|
||||
export default function() {
|
||||
extend(PermissionGrid.prototype, 'startItems', items => {
|
||||
items.add('allowTagChange', {
|
||||
icon: 'fas fa-tag',
|
||||
label: app.translator.trans('flarum-tags.admin.permissions.allow_edit_tags_label'),
|
||||
setting: () => {
|
||||
const minutes = parseInt(app.data.settings.allow_tag_change, 10);
|
||||
|
||||
return SettingDropdown.component({
|
||||
defaultLabel: minutes
|
||||
? app.translator.trans('core.admin.permissions_controls.allow_some_minutes_button', {count: minutes})
|
||||
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
|
||||
key: 'allow_tag_change',
|
||||
options: [
|
||||
{value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')},
|
||||
{value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')},
|
||||
{value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button')}
|
||||
]
|
||||
});
|
||||
}
|
||||
}, 90);
|
||||
});
|
||||
}
|
14
extensions/tags/js/src/admin/addTagPermission.js
Normal file
14
extensions/tags/js/src/admin/addTagPermission.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
export default function () {
|
||||
app.extensionData
|
||||
.for('flarum-tags')
|
||||
.registerPermission({
|
||||
icon: 'fas fa-tag',
|
||||
label: app.translator.trans('flarum-tags.admin.permissions.tag_discussions_label'),
|
||||
permission: 'discussion.tag',
|
||||
}, 'moderate', 95)
|
||||
.registerPermission({
|
||||
icon: 'fas fa-tags',
|
||||
label: app.translator.trans('flarum-tags.admin.permissions.bypass_tag_counts_label'),
|
||||
permission: 'bypassTagCounts',
|
||||
}, 'start', 89);
|
||||
}
|
11
extensions/tags/js/src/admin/addTagsHomePageOption.js
Normal file
11
extensions/tags/js/src/admin/addTagsHomePageOption.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { extend } from 'flarum/extend';
|
||||
import BasicsPage from 'flarum/components/BasicsPage';
|
||||
|
||||
export default function() {
|
||||
extend(BasicsPage.prototype, 'homePageItems', items => {
|
||||
items.add('tags', {
|
||||
path: '/tags',
|
||||
label: app.translator.trans('flarum-tags.admin.basics.tags_label')
|
||||
});
|
||||
});
|
||||
}
|
80
extensions/tags/js/src/admin/addTagsPermissionScope.js
Normal file
80
extensions/tags/js/src/admin/addTagsPermissionScope.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { extend, override } from 'flarum/extend';
|
||||
import PermissionGrid from 'flarum/components/PermissionGrid';
|
||||
import PermissionDropdown from 'flarum/components/PermissionDropdown';
|
||||
import Dropdown from 'flarum/components/Dropdown';
|
||||
import Button from 'flarum/components/Button';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
|
||||
import tagLabel from '../common/helpers/tagLabel';
|
||||
import tagIcon from '../common/helpers/tagIcon';
|
||||
import sortTags from '../common/utils/sortTags';
|
||||
|
||||
export default function() {
|
||||
extend(PermissionGrid.prototype, 'oninit', function () {
|
||||
this.loading = true;
|
||||
})
|
||||
|
||||
extend(PermissionGrid.prototype, 'oncreate', function () {
|
||||
app.store.find('tags').then(() => {
|
||||
this.loading = false;
|
||||
|
||||
m.redraw();
|
||||
});
|
||||
});
|
||||
|
||||
override(PermissionGrid.prototype, 'view', function (original, vnode) {
|
||||
if (this.loading) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
return original(vnode);
|
||||
})
|
||||
|
||||
override(app, 'getRequiredPermissions', (original, permission) => {
|
||||
const tagPrefix = permission.match(/^tag\d+\./);
|
||||
|
||||
if (tagPrefix) {
|
||||
const globalPermission = permission.substr(tagPrefix[0].length);
|
||||
|
||||
const required = original(globalPermission);
|
||||
|
||||
return required.map(required => tagPrefix[0] + required);
|
||||
}
|
||||
|
||||
return original(permission);
|
||||
});
|
||||
|
||||
extend(PermissionGrid.prototype, 'scopeItems', items => {
|
||||
sortTags(app.store.all('tags'))
|
||||
.filter(tag => tag.isRestricted())
|
||||
.forEach(tag => items.add('tag' + tag.id(), {
|
||||
label: tagLabel(tag),
|
||||
onremove: () => tag.save({isRestricted: false}),
|
||||
render: item => {
|
||||
if (item.permission === 'viewForum'
|
||||
|| item.permission === 'startDiscussion'
|
||||
|| (item.permission && item.permission.indexOf('discussion.') === 0 && item.tagScoped !== false)
|
||||
|| item.tagScoped) {
|
||||
return PermissionDropdown.component({
|
||||
permission: 'tag' + tag.id() + '.' + item.permission,
|
||||
allowGuest: item.allowGuest
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
extend(PermissionGrid.prototype, 'scopeControlItems', items => {
|
||||
const tags = sortTags(app.store.all('tags').filter(tag => !tag.isRestricted()));
|
||||
|
||||
if (tags.length) {
|
||||
items.add('tag', <Dropdown className='Dropdown--restrictByTag' buttonClassName='Button Button--text' label={app.translator.trans('flarum-tags.admin.permissions.restrict_by_tag_heading')} icon='fas fa-plus' caretIcon={null}>
|
||||
{tags.map(tag => <Button icon={true} onclick={() => tag.save({ isRestricted: true })}>
|
||||
{[tagIcon(tag, { className: 'Button-icon' }), ' ', tag.name()]}
|
||||
</Button>)}
|
||||
</Dropdown>);
|
||||
}
|
||||
});
|
||||
}
|
17
extensions/tags/js/src/admin/compat.js
Normal file
17
extensions/tags/js/src/admin/compat.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import compat from '../common/compat';
|
||||
|
||||
import addTagsHomePageOption from './addTagsHomePageOption';
|
||||
import addTagChangePermission from './addTagChangePermission';
|
||||
import TagsPage from './components/TagsPage';
|
||||
import EditTagModal from './components/EditTagModal';
|
||||
import addTagPermission from './addTagPermission';
|
||||
import addTagsPermissionScope from './addTagsPermissionScope';
|
||||
|
||||
export default Object.assign(compat, {
|
||||
'tags/addTagsHomePageOption': addTagsHomePageOption,
|
||||
'tags/addTagChangePermission': addTagChangePermission,
|
||||
'tags/components/TagsPage': TagsPage,
|
||||
'tags/components/EditTagModal': EditTagModal,
|
||||
'tags/addTagPermission': addTagPermission,
|
||||
'tags/addTagsPermissionScope': addTagsPermissionScope,
|
||||
});
|
149
extensions/tags/js/src/admin/components/EditTagModal.js
Normal file
149
extensions/tags/js/src/admin/components/EditTagModal.js
Normal file
|
@ -0,0 +1,149 @@
|
|||
import app from 'flarum/admin/app';
|
||||
import Modal from 'flarum/common/components/Modal';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import ColorPreviewInput from 'flarum/common/components/ColorPreviewInput';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
import { slug } from 'flarum/common/utils/string';
|
||||
import Stream from 'flarum/common/utils/Stream';
|
||||
|
||||
import tagLabel from '../../common/helpers/tagLabel';
|
||||
|
||||
/**
|
||||
* The `EditTagModal` component shows a modal dialog which allows the user
|
||||
* to create or edit a tag.
|
||||
*/
|
||||
export default class EditTagModal extends Modal {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.tag = this.attrs.model || app.store.createRecord('tags');
|
||||
|
||||
this.name = Stream(this.tag.name() || '');
|
||||
this.slug = Stream(this.tag.slug() || '');
|
||||
this.description = Stream(this.tag.description() || '');
|
||||
this.color = Stream(this.tag.color() || '');
|
||||
this.icon = Stream(this.tag.icon() || '');
|
||||
this.isHidden = Stream(this.tag.isHidden() || false);
|
||||
this.primary = Stream(this.attrs.primary || false);
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'EditTagModal Modal--small';
|
||||
}
|
||||
|
||||
title() {
|
||||
return this.name()
|
||||
? tagLabel(app.store.createRecord('tags', { attributes: this.submitData() }))
|
||||
: app.translator.trans('flarum-tags.admin.edit_tag.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form">
|
||||
{this.fields().toArray()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fields() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('name', <div className="Form-group">
|
||||
<label>{app.translator.trans('flarum-tags.admin.edit_tag.name_label')}</label>
|
||||
<input className="FormControl" placeholder={app.translator.trans('flarum-tags.admin.edit_tag.name_placeholder')} value={this.name()} oninput={e => {
|
||||
this.name(e.target.value);
|
||||
this.slug(slug(e.target.value));
|
||||
}} />
|
||||
</div>, 50);
|
||||
|
||||
items.add('slug', <div className="Form-group">
|
||||
<label>{app.translator.trans('flarum-tags.admin.edit_tag.slug_label')}</label>
|
||||
<input className="FormControl" bidi={this.slug} />
|
||||
</div>, 40);
|
||||
|
||||
items.add('description', <div className="Form-group">
|
||||
<label>{app.translator.trans('flarum-tags.admin.edit_tag.description_label')}</label>
|
||||
<textarea className="FormControl" bidi={this.description} />
|
||||
</div>, 30);
|
||||
|
||||
items.add('color', <div className="Form-group">
|
||||
<label>{app.translator.trans('flarum-tags.admin.edit_tag.color_label')}</label>
|
||||
<ColorPreviewInput className="FormControl" placeholder="#aaaaaa" bidi={this.color} />
|
||||
</div>, 20);
|
||||
|
||||
items.add('icon', <div className="Form-group">
|
||||
<label>{app.translator.trans('flarum-tags.admin.edit_tag.icon_label')}</label>
|
||||
<div className="helpText">
|
||||
{app.translator.trans('flarum-tags.admin.edit_tag.icon_text', { a: <a href="https://fontawesome.com/icons?m=free" tabindex="-1" /> })}
|
||||
</div>
|
||||
<input className="FormControl" placeholder="fas fa-bolt" bidi={this.icon} />
|
||||
</div>, 10);
|
||||
|
||||
items.add('hidden', <div className="Form-group">
|
||||
<div>
|
||||
<label className="checkbox">
|
||||
<input type="checkbox" bidi={this.isHidden} />
|
||||
{app.translator.trans('flarum-tags.admin.edit_tag.hide_label')}
|
||||
</label>
|
||||
</div>
|
||||
</div>, 10);
|
||||
|
||||
items.add('submit', <div className="Form-group">
|
||||
{Button.component({
|
||||
type: 'submit',
|
||||
className: 'Button Button--primary EditTagModal-save',
|
||||
loading: this.loading,
|
||||
}, app.translator.trans('flarum-tags.admin.edit_tag.submit_button'))}
|
||||
{this.tag.exists ? (
|
||||
<button type="button" className="Button EditTagModal-delete" onclick={this.delete.bind(this)}>
|
||||
{app.translator.trans('flarum-tags.admin.edit_tag.delete_tag_button')}
|
||||
</button>
|
||||
) : ''}
|
||||
</div>, -10);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
submitData() {
|
||||
return {
|
||||
name: this.name(),
|
||||
slug: this.slug(),
|
||||
description: this.description(),
|
||||
color: this.color(),
|
||||
icon: this.icon(),
|
||||
isHidden: this.isHidden(),
|
||||
primary: this.primary(),
|
||||
};
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
// Errors aren't passed to the modal onerror handler here.
|
||||
// This is done for better error visibility on smaller screen heights.
|
||||
this.tag.save(this.submitData()).then(
|
||||
() => this.hide(),
|
||||
() => this.loading = false
|
||||
);
|
||||
}
|
||||
|
||||
delete() {
|
||||
if (confirm(app.translator.trans('flarum-tags.admin.edit_tag.delete_tag_confirmation'))) {
|
||||
const children = app.store.all('tags').filter(tag => tag.parent() === this.tag);
|
||||
|
||||
this.tag.delete().then(() => {
|
||||
children.forEach(tag => tag.pushData({
|
||||
attributes: { isChild: false },
|
||||
relationships: { parent: null }
|
||||
}));
|
||||
m.redraw();
|
||||
});
|
||||
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
230
extensions/tags/js/src/admin/components/TagsPage.js
Normal file
230
extensions/tags/js/src/admin/components/TagsPage.js
Normal file
|
@ -0,0 +1,230 @@
|
|||
import sortable from 'sortablejs';
|
||||
|
||||
import ExtensionPage from 'flarum/components/ExtensionPage';
|
||||
import Button from 'flarum/components/Button';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
import withAttr from 'flarum/utils/withAttr';
|
||||
|
||||
import EditTagModal from './EditTagModal';
|
||||
import tagIcon from '../../common/helpers/tagIcon';
|
||||
import sortTags from '../../common/utils/sortTags';
|
||||
|
||||
function tagItem(tag) {
|
||||
return (
|
||||
<li data-id={tag.id()} style={{ color: tag.color() }}>
|
||||
<div className="TagListItem-info">
|
||||
{tagIcon(tag)}
|
||||
<span className="TagListItem-name">{tag.name()}</span>
|
||||
{Button.component({
|
||||
className: 'Button Button--link',
|
||||
icon: 'fas fa-pencil-alt',
|
||||
onclick: () => app.modal.show(EditTagModal, { model: tag })
|
||||
})}
|
||||
</div>
|
||||
{!tag.isChild() && tag.position() !== null ? (
|
||||
<ol className="TagListItem-children TagList">
|
||||
{sortTags(app.store.all('tags'))
|
||||
.filter(child => child.parent() === tag)
|
||||
.map(tagItem)}
|
||||
</ol>
|
||||
) : ''}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default class TagsPage extends ExtensionPage {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
// A regular 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 by changing the key, which
|
||||
// makes mithril completely re-render the component on redraw.
|
||||
this.forcedRefreshKey = 0;
|
||||
|
||||
this.loading = true;
|
||||
|
||||
app.store.find('tags', { include: 'parent' }).then(() => {
|
||||
this.loading = false;
|
||||
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
content() {
|
||||
if (this.loading) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
const minPrimaryTags = this.setting('flarum-tags.min_primary_tags', 0);
|
||||
const maxPrimaryTags = this.setting('flarum-tags.max_primary_tags', 0);
|
||||
|
||||
const minSecondaryTags = this.setting('flarum-tags.min_secondary_tags', 0);
|
||||
const maxSecondaryTags = this.setting('flarum-tags.max_secondary_tags', 0);
|
||||
|
||||
const tags = sortTags(app.store.all('tags').filter(tag => !tag.parent()));
|
||||
|
||||
return (
|
||||
<div className="TagsContent">
|
||||
<div className="TagsContent-list">
|
||||
<div className="container" key={this.forcedRefreshKey} oncreate={this.onListOnCreate.bind(this)}><div className="SettingsGroups">
|
||||
<div className="TagGroup">
|
||||
<label>{app.translator.trans('flarum-tags.admin.tags.primary_heading')}</label>
|
||||
<ol className="TagList TagList--primary">
|
||||
{tags
|
||||
.filter(tag => tag.position() !== null && !tag.isChild())
|
||||
.map(tagItem)}
|
||||
</ol>
|
||||
{Button.component(
|
||||
{
|
||||
className: 'Button TagList-button',
|
||||
icon: 'fas fa-plus',
|
||||
onclick: () => app.modal.show(EditTagModal, { primary: true }),
|
||||
},
|
||||
app.translator.trans('flarum-tags.admin.tags.create_primary_tag_button')
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="TagGroup TagGroup--secondary">
|
||||
<label>{app.translator.trans('flarum-tags.admin.tags.secondary_heading')}</label>
|
||||
<ul className="TagList">
|
||||
{tags
|
||||
.filter(tag => tag.position() === null)
|
||||
.sort((a, b) => a.name().localeCompare(b.name()))
|
||||
.map(tagItem)}
|
||||
</ul>
|
||||
{Button.component(
|
||||
{
|
||||
className: 'Button TagList-button',
|
||||
icon: 'fas fa-plus',
|
||||
onclick: () => app.modal.show(EditTagModal, { primary: false }),
|
||||
},
|
||||
app.translator.trans('flarum-tags.admin.tags.create_secondary_tag_button')
|
||||
)}
|
||||
</div>
|
||||
<div className="Form">
|
||||
<label>{app.translator.trans('flarum-tags.admin.tags.settings_heading')}</label>
|
||||
<div className="Form-group">
|
||||
<label>{app.translator.trans('flarum-tags.admin.tag_settings.required_primary_heading')}</label>
|
||||
<div className="helpText">{app.translator.trans('flarum-tags.admin.tag_settings.required_primary_text')}</div>
|
||||
<div className="TagSettings-rangeInput">
|
||||
<input
|
||||
className="FormControl"
|
||||
type="number"
|
||||
min="0"
|
||||
value={minPrimaryTags()}
|
||||
oninput={withAttr('value', this.setMinTags.bind(this, minPrimaryTags, maxPrimaryTags))}
|
||||
/>
|
||||
{app.translator.trans('flarum-tags.admin.tag_settings.range_separator_text')}
|
||||
<input className="FormControl" type="number" min={minPrimaryTags()} bidi={maxPrimaryTags} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="Form-group">
|
||||
<label>{app.translator.trans('flarum-tags.admin.tag_settings.required_secondary_heading')}</label>
|
||||
<div className="helpText">{app.translator.trans('flarum-tags.admin.tag_settings.required_secondary_text')}</div>
|
||||
<div className="TagSettings-rangeInput">
|
||||
<input
|
||||
className="FormControl"
|
||||
type="number"
|
||||
min="0"
|
||||
value={minSecondaryTags()}
|
||||
oninput={withAttr('value', this.setMinTags.bind(this, minSecondaryTags, maxSecondaryTags))}
|
||||
/>
|
||||
{app.translator.trans('flarum-tags.admin.tag_settings.range_separator_text')}
|
||||
<input className="FormControl" type="number" min={minSecondaryTags()} bidi={maxSecondaryTags} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="Form-group">{this.submitButton()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="TagsContent-footer">
|
||||
<p>{app.translator.trans('flarum-tags.admin.tags.about_tags_text')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onListOnCreate(vnode) {
|
||||
this.$('.TagList').get().map(e => {
|
||||
sortable.create(e, {
|
||||
group: 'tags',
|
||||
delay: 50,
|
||||
delayOnTouchOnly: true,
|
||||
touchStartThreshold: 5,
|
||||
animation: 150,
|
||||
swapThreshold: 0.65,
|
||||
dragClass: 'sortable-dragging',
|
||||
ghostClass: 'sortable-placeholder',
|
||||
onSort: (e) => this.onSortUpdate(e)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
setMinTags(minTags, maxTags, value) {
|
||||
minTags(value);
|
||||
maxTags(Math.max(value, maxTags()));
|
||||
}
|
||||
|
||||
onSortUpdate(e) {
|
||||
// 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 (e.from instanceof HTMLOListElement && e.to instanceof HTMLUListElement) {
|
||||
app.store.getById('tags', e.item.getAttribute('data-id')).pushData({
|
||||
attributes: {
|
||||
position: null,
|
||||
isChild: false
|
||||
},
|
||||
relationships: { parent: null }
|
||||
});
|
||||
}
|
||||
|
||||
// Construct an array of primary tag IDs and their children, in the same
|
||||
// order that they have been arranged in.
|
||||
const order = this.$('.TagList--primary > li')
|
||||
.map(function () {
|
||||
return {
|
||||
id: $(this).data('id'),
|
||||
children: $(this).find('li')
|
||||
.map(function () {
|
||||
return $(this).data('id');
|
||||
}).get()
|
||||
};
|
||||
}).get();
|
||||
|
||||
// Now that we have an accurate representation of the order which the
|
||||
// primary tags are in, we will update the tag attributes in our local
|
||||
// store to reflect this order.
|
||||
order.forEach((tag, i) => {
|
||||
const parent = app.store.getById('tags', tag.id);
|
||||
parent.pushData({
|
||||
attributes: {
|
||||
position: i,
|
||||
isChild: false
|
||||
},
|
||||
relationships: { parent: null }
|
||||
});
|
||||
|
||||
tag.children.forEach((child, j) => {
|
||||
app.store.getById('tags', child).pushData({
|
||||
attributes: {
|
||||
position: j,
|
||||
isChild: true
|
||||
},
|
||||
relationships: { parent }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.request({
|
||||
url: app.forum.attribute('apiUrl') + '/tags/order',
|
||||
method: 'POST',
|
||||
body: { order }
|
||||
});
|
||||
|
||||
this.forcedRefreshKey++;
|
||||
m.redraw();
|
||||
}
|
||||
}
|
24
extensions/tags/js/src/admin/index.js
Normal file
24
extensions/tags/js/src/admin/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Tag from '../common/models/Tag';
|
||||
import addTagsPermissionScope from './addTagsPermissionScope';
|
||||
import addTagPermission from './addTagPermission';
|
||||
import addTagsHomePageOption from './addTagsHomePageOption';
|
||||
import addTagChangePermission from './addTagChangePermission';
|
||||
import TagsPage from './components/TagsPage';
|
||||
|
||||
app.initializers.add('flarum-tags', app => {
|
||||
app.store.models.tags = Tag;
|
||||
|
||||
app.extensionData.for('flarum-tags').registerPage(TagsPage);
|
||||
|
||||
addTagsPermissionScope();
|
||||
addTagPermission();
|
||||
addTagsHomePageOption();
|
||||
addTagChangePermission();
|
||||
});
|
||||
|
||||
|
||||
// Expose compat API
|
||||
import tagsCompat from './compat';
|
||||
import { compat } from '@flarum/core/admin';
|
||||
|
||||
Object.assign(compat, tagsCompat);
|
13
extensions/tags/js/src/common/compat.js
Normal file
13
extensions/tags/js/src/common/compat.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import sortTags from './utils/sortTags';
|
||||
import Tag from './models/Tag';
|
||||
import tagsLabel from './helpers/tagsLabel';
|
||||
import tagIcon from './helpers/tagIcon';
|
||||
import tagLabel from './helpers/tagLabel';
|
||||
|
||||
export default {
|
||||
'tags/utils/sortTags': sortTags,
|
||||
'tags/models/Tag': Tag,
|
||||
'tags/helpers/tagsLabel': tagsLabel,
|
||||
'tags/helpers/tagIcon': tagIcon,
|
||||
'tags/helpers/tagLabel': tagLabel
|
||||
};
|
25
extensions/tags/js/src/common/helpers/tagIcon.js
Normal file
25
extensions/tags/js/src/common/helpers/tagIcon.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import classList from 'flarum/utils/classList';
|
||||
|
||||
export default function tagIcon(tag, attrs = {}, settings = {}) {
|
||||
const hasIcon = tag && tag.icon();
|
||||
const { useColor = true } = settings;
|
||||
|
||||
attrs.className = classList([
|
||||
attrs.className,
|
||||
'icon',
|
||||
hasIcon ? tag.icon() : 'TagIcon'
|
||||
]);
|
||||
|
||||
if (tag && useColor) {
|
||||
attrs.style = attrs.style || {};
|
||||
attrs.style['--color'] = tag.color();
|
||||
|
||||
if (hasIcon) {
|
||||
attrs.style.color = tag.color();
|
||||
}
|
||||
} else if (!tag) {
|
||||
attrs.className += ' untagged';
|
||||
}
|
||||
|
||||
return hasIcon ? <i {...attrs}/> : <span {...attrs}/>;
|
||||
}
|
38
extensions/tags/js/src/common/helpers/tagLabel.js
Normal file
38
extensions/tags/js/src/common/helpers/tagLabel.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import extract from 'flarum/utils/extract';
|
||||
import Link from 'flarum/components/Link';
|
||||
import tagIcon from './tagIcon';
|
||||
|
||||
export default function tagLabel(tag, attrs = {}) {
|
||||
attrs.style = attrs.style || {};
|
||||
attrs.className = 'TagLabel ' + (attrs.className || '');
|
||||
|
||||
const link = extract(attrs, 'link');
|
||||
const tagText = tag ? tag.name() : app.translator.trans('flarum-tags.lib.deleted_tag_text');
|
||||
|
||||
if (tag) {
|
||||
const color = tag.color();
|
||||
if (color) {
|
||||
attrs.style['--tag-bg'] = color;
|
||||
attrs.className += ' colored';
|
||||
}
|
||||
|
||||
if (link) {
|
||||
attrs.title = tag.description() || '';
|
||||
attrs.href = app.route('tag', {tags: tag.slug()});
|
||||
}
|
||||
|
||||
if (tag.isChild()) {
|
||||
attrs.className += ' TagLabel--child';
|
||||
}
|
||||
} else {
|
||||
attrs.className += ' untagged';
|
||||
}
|
||||
|
||||
return (
|
||||
m((link ? Link : 'span'), attrs,
|
||||
<span className="TagLabel-text">
|
||||
{tag && tag.icon() && tagIcon(tag, {}, {useColor: false})} {tagText}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
22
extensions/tags/js/src/common/helpers/tagsLabel.js
Normal file
22
extensions/tags/js/src/common/helpers/tagsLabel.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import extract from 'flarum/utils/extract';
|
||||
import tagLabel from './tagLabel';
|
||||
import sortTags from '../utils/sortTags';
|
||||
|
||||
export default function tagsLabel(tags, attrs = {}) {
|
||||
const children = [];
|
||||
const link = extract(attrs, 'link');
|
||||
|
||||
attrs.className = 'TagsLabel ' + (attrs.className || '');
|
||||
|
||||
if (tags) {
|
||||
sortTags(tags).forEach(tag => {
|
||||
if (tag || tags.length === 1) {
|
||||
children.push(tagLabel(tag, {link}));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
children.push(tagLabel());
|
||||
}
|
||||
|
||||
return <span {...attrs}>{children}</span>;
|
||||
}
|
0
extensions/tags/js/src/common/index.js
Normal file
0
extensions/tags/js/src/common/index.js
Normal file
31
extensions/tags/js/src/common/models/Tag.js
Normal file
31
extensions/tags/js/src/common/models/Tag.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import Model from 'flarum/Model';
|
||||
import mixin from 'flarum/utils/mixin';
|
||||
import computed from 'flarum/utils/computed';
|
||||
|
||||
export default class Tag extends 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'),
|
||||
icon: Model.attribute('icon'),
|
||||
|
||||
position: Model.attribute('position'),
|
||||
parent: Model.hasOne('parent'),
|
||||
children: Model.hasMany('children'),
|
||||
defaultSort: Model.attribute('defaultSort'),
|
||||
isChild: Model.attribute('isChild'),
|
||||
isHidden: Model.attribute('isHidden'),
|
||||
|
||||
discussionCount: Model.attribute('discussionCount'),
|
||||
lastPostedAt: Model.attribute('lastPostedAt', Model.transformDate),
|
||||
lastPostedDiscussion: Model.hasOne('lastPostedDiscussion'),
|
||||
|
||||
isRestricted: Model.attribute('isRestricted'),
|
||||
canStartDiscussion: Model.attribute('canStartDiscussion'),
|
||||
canAddToDiscussion: Model.attribute('canAddToDiscussion'),
|
||||
|
||||
isPrimary: computed('position', 'parent', (position, parent) => position !== null && parent === false)
|
||||
}) {}
|
41
extensions/tags/js/src/common/utils/sortTags.js
Normal file
41
extensions/tags/js/src/common/utils/sortTags.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
export default function sortTags(tags) {
|
||||
return tags.slice(0).sort((a, b) => {
|
||||
const aPos = a.position();
|
||||
const bPos = b.position();
|
||||
|
||||
// If they're both secondary tags, sort them by their discussions count,
|
||||
// descending.
|
||||
if (aPos === null && bPos === null)
|
||||
return b.discussionCount() - a.discussionCount();
|
||||
|
||||
// If just one is a secondary tag, then the primary tag should
|
||||
// come first.
|
||||
if (bPos === null) return -1;
|
||||
if (aPos === null) return 1;
|
||||
|
||||
// If we've made it this far, we know they're both primary tags. So we'll
|
||||
// need to see if they have parents.
|
||||
const aParent = a.parent();
|
||||
const bParent = b.parent();
|
||||
|
||||
// If they both have the same parent, then their positions are local,
|
||||
// so we can compare them directly.
|
||||
if (aParent === bParent) return aPos - bPos;
|
||||
|
||||
// If they are both child tags, then we will compare the positions of their
|
||||
// parents.
|
||||
else if (aParent && bParent)
|
||||
return aParent.position() - bParent.position();
|
||||
|
||||
// If we are comparing a child tag with its parent, then we let the parent
|
||||
// come first. If we are comparing an unrelated parent/child, then we
|
||||
// compare both of the parents.
|
||||
else if (aParent)
|
||||
return aParent === b ? 1 : aParent.position() - bPos;
|
||||
|
||||
else if (bParent)
|
||||
return bParent === a ? -1 : aPos - bParent.position();
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
86
extensions/tags/js/src/forum/addTagComposer.js
Normal file
86
extensions/tags/js/src/forum/addTagComposer.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { extend, override } from 'flarum/extend';
|
||||
import IndexPage from 'flarum/components/IndexPage';
|
||||
import DiscussionComposer from 'flarum/components/DiscussionComposer';
|
||||
import classList from 'flarum/utils/classList';
|
||||
|
||||
import TagDiscussionModal from './components/TagDiscussionModal';
|
||||
import tagsLabel from '../common/helpers/tagsLabel';
|
||||
import getSelectableTags from './utils/getSelectableTags';
|
||||
|
||||
export default function () {
|
||||
extend(IndexPage.prototype, 'newDiscussionAction', function (promise) {
|
||||
// From `addTagFilter
|
||||
const tag = this.currentTag();
|
||||
|
||||
if (tag) {
|
||||
const parent = tag.parent();
|
||||
const tags = parent ? [parent, tag] : [tag];
|
||||
promise.then(composer => composer.fields.tags = tags);
|
||||
} else {
|
||||
app.composer.fields.tags = [];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
extend(DiscussionComposer.prototype, 'oninit', function () {
|
||||
app.tagList.load(['parent']).then(() => m.redraw())
|
||||
});
|
||||
|
||||
// Add tag-selection abilities to the discussion composer.
|
||||
DiscussionComposer.prototype.chooseTags = function () {
|
||||
const selectableTags = getSelectableTags();
|
||||
|
||||
if (!selectableTags.length) return;
|
||||
|
||||
app.modal.show(TagDiscussionModal, {
|
||||
selectedTags: (this.composer.fields.tags || []).slice(0),
|
||||
onsubmit: tags => {
|
||||
this.composer.fields.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) {
|
||||
const tags = this.composer.fields.tags || [];
|
||||
const selectableTags = getSelectableTags();
|
||||
|
||||
items.add('tags', (
|
||||
<a className={classList(['DiscussionComposer-changeTags', !selectableTags.length && 'disabled'])} onclick={this.chooseTags.bind(this)}>
|
||||
{tags.length
|
||||
? tagsLabel(tags)
|
||||
: <span className="TagLabel untagged">{app.translator.trans('flarum-tags.forum.composer_discussion.choose_tags_link')}</span>}
|
||||
</a>
|
||||
), 10);
|
||||
});
|
||||
|
||||
override(DiscussionComposer.prototype, 'onsubmit', function (original) {
|
||||
const chosenTags = this.composer.fields.tags || [];
|
||||
const chosenPrimaryTags = chosenTags.filter(tag => tag.position() !== null && !tag.isChild());
|
||||
const chosenSecondaryTags = chosenTags.filter(tag => tag.position() === null);
|
||||
const selectableTags = getSelectableTags();
|
||||
|
||||
if ((!chosenTags.length
|
||||
|| (chosenPrimaryTags.length < app.forum.attribute('minPrimaryTags'))
|
||||
|| (chosenSecondaryTags.length < app.forum.attribute('minSecondaryTags'))
|
||||
) && selectableTags.length) {
|
||||
app.modal.show(TagDiscussionModal, {
|
||||
selectedTags: chosenTags,
|
||||
onsubmit: tags => {
|
||||
this.composer.fields.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.composer.fields.tags;
|
||||
});
|
||||
}
|
16
extensions/tags/js/src/forum/addTagControl.js
Normal file
16
extensions/tags/js/src/forum/addTagControl.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { extend } from 'flarum/extend';
|
||||
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
||||
import Button from 'flarum/components/Button';
|
||||
|
||||
import TagDiscussionModal from './components/TagDiscussionModal';
|
||||
|
||||
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 icon="fas fa-tag" onclick={() => app.modal.show(TagDiscussionModal, { discussion })}>
|
||||
{app.translator.trans('flarum-tags.forum.discussion_controls.edit_tags_button')}
|
||||
</Button>);
|
||||
}
|
||||
});
|
||||
}
|
112
extensions/tags/js/src/forum/addTagFilter.js
Normal file
112
extensions/tags/js/src/forum/addTagFilter.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
import { extend, override } from 'flarum/extend';
|
||||
import IndexPage from 'flarum/components/IndexPage';
|
||||
import DiscussionListState from 'flarum/states/DiscussionListState';
|
||||
import GlobalSearchState from 'flarum/states/GlobalSearchState';
|
||||
import classList from 'flarum/utils/classList';
|
||||
|
||||
import TagHero from './components/TagHero';
|
||||
|
||||
const findTag = slug => app.store.all('tags').find(tag => tag.slug().localeCompare(slug, undefined, { sensitivity: 'base' }) === 0);
|
||||
|
||||
export default function() {
|
||||
IndexPage.prototype.currentTag = function() {
|
||||
if (this.currentActiveTag) {
|
||||
return this.currentActiveTag;
|
||||
}
|
||||
|
||||
const slug = app.search.params().tags;
|
||||
let tag = null;
|
||||
|
||||
if (slug) {
|
||||
tag = findTag(slug);
|
||||
}
|
||||
|
||||
if (slug && !tag || (tag && !tag.isChild() && !tag.children())) {
|
||||
if (this.currentTagLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentTagLoading = true;
|
||||
|
||||
// Unlike the backend, no need to fetch parent.children because if we're on
|
||||
// a child tag page, then either:
|
||||
// - We loaded in that child tag (and its siblings) in the API document
|
||||
// - We first navigated to the current tag's parent, which would have loaded in the current tag's siblings.
|
||||
app.store.find('tags', slug, { include: 'children,children.parent,parent,state'}).then(() => {
|
||||
this.currentActiveTag = findTag(slug);
|
||||
|
||||
m.redraw();
|
||||
}).finally(() => {
|
||||
this.currentTagLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
this.currentActiveTag = tag;
|
||||
return this.currentActiveTag;
|
||||
}
|
||||
};
|
||||
|
||||
// If currently viewing a tag, insert a tag hero at the top of the view.
|
||||
override(IndexPage.prototype, 'hero', function(original) {
|
||||
const tag = this.currentTag();
|
||||
|
||||
if (tag) return <TagHero model={tag} />;
|
||||
|
||||
return original();
|
||||
});
|
||||
|
||||
extend(IndexPage.prototype, 'view', function(vdom) {
|
||||
const tag = this.currentTag();
|
||||
|
||||
if (tag) vdom.attrs.className += ' IndexPage--tag'+tag.id();
|
||||
});
|
||||
|
||||
extend(IndexPage.prototype, 'setTitle', function() {
|
||||
const tag = this.currentTag();
|
||||
|
||||
if (tag) {
|
||||
app.setTitle(tag.name());
|
||||
}
|
||||
});
|
||||
|
||||
// If currently viewing a tag, restyle the 'new discussion' button to use
|
||||
// the tag's color, and disable if the user isn't allowed to edit.
|
||||
extend(IndexPage.prototype, 'sidebarItems', function(items) {
|
||||
const tag = this.currentTag();
|
||||
|
||||
if (tag) {
|
||||
const color = tag.color();
|
||||
const canStartDiscussion = tag.canStartDiscussion() || !app.session.user;
|
||||
const newDiscussion = items.get('newDiscussion');
|
||||
|
||||
if (color) {
|
||||
newDiscussion.attrs.className = classList([newDiscussion.attrs.className, 'Button--tagColored']);
|
||||
newDiscussion.attrs.style = { '--color': color };
|
||||
}
|
||||
|
||||
newDiscussion.attrs.disabled = !canStartDiscussion;
|
||||
newDiscussion.children = app.translator.trans(canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button');
|
||||
}
|
||||
});
|
||||
|
||||
// Add a parameter for the global search state to pass on to the
|
||||
// DiscussionListState that will let us filter discussions by tag.
|
||||
extend(GlobalSearchState.prototype, 'params', function(params) {
|
||||
params.tags = m.route.param('tags');
|
||||
});
|
||||
|
||||
// Translate that parameter into a gambit appended to the search query.
|
||||
extend(DiscussionListState.prototype, 'requestParams', function(params) {
|
||||
params.include.push('tags', 'tags.parent');
|
||||
|
||||
if (this.params.tags) {
|
||||
params.filter.tag = this.params.tags;
|
||||
// TODO: replace this with a more robust system.
|
||||
const q = params.filter.q;
|
||||
if (q) {
|
||||
params.filter.q = `${q} tag:${this.params.tags}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
40
extensions/tags/js/src/forum/addTagLabels.js
Normal file
40
extensions/tags/js/src/forum/addTagLabels.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { extend } from 'flarum/extend';
|
||||
import DiscussionListItem from 'flarum/components/DiscussionListItem';
|
||||
import DiscussionHero from 'flarum/components/DiscussionHero';
|
||||
|
||||
import tagsLabel from '../common/helpers/tagsLabel';
|
||||
import sortTags from '../common/utils/sortTags';
|
||||
|
||||
export default function() {
|
||||
// Add tag labels to each discussion in the discussion list.
|
||||
extend(DiscussionListItem.prototype, 'infoItems', function(items) {
|
||||
const tags = this.attrs.discussion.tags();
|
||||
|
||||
if (tags && tags.length) {
|
||||
items.add('tags', tagsLabel(tags), 10);
|
||||
}
|
||||
});
|
||||
|
||||
// Restyle a discussion's hero to use its first tag's color.
|
||||
extend(DiscussionHero.prototype, 'view', function(view) {
|
||||
const tags = sortTags(this.attrs.discussion.tags());
|
||||
|
||||
if (tags && tags.length) {
|
||||
const color = tags[0].color();
|
||||
if (color) {
|
||||
view.attrs.style = { '--hero-bg': 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) {
|
||||
const tags = this.attrs.discussion.tags();
|
||||
|
||||
if (tags && tags.length) {
|
||||
items.add('tags', tagsLabel(tags, {link: true}), 5);
|
||||
}
|
||||
});
|
||||
}
|
58
extensions/tags/js/src/forum/addTagList.js
Normal file
58
extensions/tags/js/src/forum/addTagList.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { extend } from 'flarum/extend';
|
||||
import IndexPage from 'flarum/components/IndexPage';
|
||||
import Separator from 'flarum/components/Separator';
|
||||
import LinkButton from 'flarum/components/LinkButton';
|
||||
|
||||
import TagLinkButton from './components/TagLinkButton';
|
||||
import TagsPage from './components/TagsPage';
|
||||
import sortTags from '../common/utils/sortTags';
|
||||
|
||||
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 icon="fas fa-th-large" href={app.route('tags')}>
|
||||
{app.translator.trans('flarum-tags.forum.index.tags_link')}
|
||||
</LinkButton>
|
||||
, -10);
|
||||
|
||||
if (app.current.matches(TagsPage)) return;
|
||||
|
||||
items.add('separator', Separator.component(), -12);
|
||||
|
||||
const params = app.search.stickyParams();
|
||||
const tags = app.store.all('tags');
|
||||
const currentTag = this.currentTag();
|
||||
|
||||
const addTag = tag => {
|
||||
let active = currentTag === tag;
|
||||
|
||||
if (!active && currentTag) {
|
||||
active = currentTag.parent() === tag;
|
||||
}
|
||||
|
||||
// tag.name() is passed here as children even though it isn't used directly
|
||||
// because when we need to get the active child in SelectDropdown, we need to
|
||||
// use its children to populate the dropdown. The problem here is that `view`
|
||||
// on TagLinkButton is only called AFTER SelectDropdown, so no children are available
|
||||
// for SelectDropdown to use at the time.
|
||||
items.add('tag' + tag.id(), TagLinkButton.component({model: tag, params, active}, tag?.name()), -14);
|
||||
};
|
||||
|
||||
sortTags(tags)
|
||||
.filter(tag => tag.position() !== null && (!tag.isChild() || (currentTag && (tag.parent() === currentTag || tag.parent() === currentTag.parent()))))
|
||||
.forEach(addTag);
|
||||
|
||||
const more = tags
|
||||
.filter(tag => tag.position() === null)
|
||||
.sort((a, b) => b.discussionCount() - a.discussionCount());
|
||||
|
||||
more.splice(0, 3).forEach(addTag);
|
||||
|
||||
if (more.length) {
|
||||
items.add('moreTags', <LinkButton href={app.route('tags')}>
|
||||
{app.translator.trans('flarum-tags.forum.index.more_link')}
|
||||
</LinkButton>, -16)
|
||||
}
|
||||
});
|
||||
}
|
27
extensions/tags/js/src/forum/compat.js
Normal file
27
extensions/tags/js/src/forum/compat.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import compat from '../common/compat';
|
||||
|
||||
import addTagFilter from './addTagFilter';
|
||||
import addTagControl from './addTagControl';
|
||||
import TagHero from './components/TagHero';
|
||||
import TagDiscussionModal from './components/TagDiscussionModal';
|
||||
import TagsPage from './components/TagsPage';
|
||||
import DiscussionTaggedPost from './components/DiscussionTaggedPost';
|
||||
import TagLinkButton from './components/TagLinkButton';
|
||||
import addTagList from './addTagList';
|
||||
import addTagLabels from './addTagLabels';
|
||||
import addTagComposer from './addTagComposer';
|
||||
import getSelectableTags from './utils/getSelectableTags';
|
||||
|
||||
export default Object.assign(compat, {
|
||||
'tags/addTagFilter': addTagFilter,
|
||||
'tags/addTagControl': addTagControl,
|
||||
'tags/components/TagHero': TagHero,
|
||||
'tags/components/TagDiscussionModal': TagDiscussionModal,
|
||||
'tags/components/TagsPage': TagsPage,
|
||||
'tags/components/DiscussionTaggedPost': DiscussionTaggedPost,
|
||||
'tags/components/TagLinkButton': TagLinkButton,
|
||||
'tags/addTagList': addTagList,
|
||||
'tags/addTagLabels': addTagLabels,
|
||||
'tags/addTagComposer': addTagComposer,
|
||||
'tags/utils/getSelectableTags': getSelectableTags,
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
import EventPost from 'flarum/components/EventPost';
|
||||
import tagsLabel from '../../common/helpers/tagsLabel';
|
||||
|
||||
export default class DiscussionTaggedPost extends EventPost {
|
||||
static initAttrs(attrs) {
|
||||
super.initAttrs(attrs);
|
||||
|
||||
const oldTags = attrs.post.content()[0];
|
||||
const newTags = attrs.post.content()[1];
|
||||
|
||||
function diffTags(tags1, tags2) {
|
||||
return tags1
|
||||
.filter(tag => tags2.indexOf(tag) === -1)
|
||||
.map(id => app.store.getById('tags', id));
|
||||
}
|
||||
|
||||
attrs.tagsAdded = diffTags(newTags, oldTags);
|
||||
attrs.tagsRemoved = diffTags(oldTags, newTags);
|
||||
}
|
||||
|
||||
icon() {
|
||||
return 'fas fa-tag';
|
||||
}
|
||||
|
||||
descriptionKey() {
|
||||
if (this.attrs.tagsAdded.length) {
|
||||
if (this.attrs.tagsRemoved.length) {
|
||||
return 'flarum-tags.forum.post_stream.added_and_removed_tags_text';
|
||||
}
|
||||
|
||||
return 'flarum-tags.forum.post_stream.added_tags_text';
|
||||
}
|
||||
|
||||
return 'flarum-tags.forum.post_stream.removed_tags_text';
|
||||
}
|
||||
|
||||
descriptionData() {
|
||||
const data = {};
|
||||
|
||||
if (this.attrs.tagsAdded.length) {
|
||||
data.tagsAdded = app.translator.trans('flarum-tags.forum.post_stream.tags_text', {
|
||||
tags: tagsLabel(this.attrs.tagsAdded, {link: true}),
|
||||
count: this.attrs.tagsAdded.length
|
||||
});
|
||||
}
|
||||
|
||||
if (this.attrs.tagsRemoved.length) {
|
||||
data.tagsRemoved = app.translator.trans('flarum-tags.forum.post_stream.tags_text', {
|
||||
tags: tagsLabel(this.attrs.tagsRemoved, {link: true}),
|
||||
count: this.attrs.tagsRemoved.length
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
348
extensions/tags/js/src/forum/components/TagDiscussionModal.js
Normal file
348
extensions/tags/js/src/forum/components/TagDiscussionModal.js
Normal file
|
@ -0,0 +1,348 @@
|
|||
import Modal from 'flarum/components/Modal';
|
||||
import DiscussionPage from 'flarum/components/DiscussionPage';
|
||||
import Button from 'flarum/components/Button';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
import highlight from 'flarum/helpers/highlight';
|
||||
import classList from 'flarum/utils/classList';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
import KeyboardNavigatable from 'flarum/utils/KeyboardNavigatable';
|
||||
import Stream from 'flarum/utils/Stream';
|
||||
|
||||
import tagLabel from '../../common/helpers/tagLabel';
|
||||
import tagIcon from '../../common/helpers/tagIcon';
|
||||
import sortTags from '../../common/utils/sortTags';
|
||||
import getSelectableTags from '../utils/getSelectableTags';
|
||||
import ToggleButton from './ToggleButton';
|
||||
|
||||
export default class TagDiscussionModal extends Modal {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.tagsLoading = true;
|
||||
|
||||
this.selected = [];
|
||||
this.filter = Stream('');
|
||||
this.focused = false;
|
||||
|
||||
this.minPrimary = app.forum.attribute('minPrimaryTags');
|
||||
this.maxPrimary = app.forum.attribute('maxPrimaryTags');
|
||||
this.minSecondary = app.forum.attribute('minSecondaryTags');
|
||||
this.maxSecondary = app.forum.attribute('maxSecondaryTags');
|
||||
|
||||
this.bypassReqs = false;
|
||||
|
||||
this.navigator = new KeyboardNavigatable();
|
||||
this.navigator
|
||||
.onUp(() => this.setIndex(this.getCurrentNumericIndex() - 1, true))
|
||||
.onDown(() => this.setIndex(this.getCurrentNumericIndex() + 1, true))
|
||||
.onSelect(this.select.bind(this))
|
||||
.onRemove(() => this.selected.splice(this.selected.length - 1, 1));
|
||||
|
||||
app.tagList.load(['parent']).then(() => {
|
||||
this.tagsLoading = false;
|
||||
|
||||
this.tags = sortTags(getSelectableTags(this.attrs.discussion));
|
||||
|
||||
if (this.attrs.selectedTags) {
|
||||
this.attrs.selectedTags.map(this.addTag.bind(this));
|
||||
} else if (this.attrs.discussion) {
|
||||
this.attrs.discussion.tags().map(this.addTag.bind(this));
|
||||
}
|
||||
|
||||
this.index = this.tags[0].id();
|
||||
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
primaryCount() {
|
||||
return this.selected.filter(tag => tag.isPrimary()).length;
|
||||
}
|
||||
|
||||
secondaryCount() {
|
||||
return this.selected.filter(tag => !tag.isPrimary()).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given tag to the list of selected tags.
|
||||
*
|
||||
* @param {Tag} tag
|
||||
*/
|
||||
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.
|
||||
const parent = tag.parent();
|
||||
if (parent && !this.selected.includes(parent)) {
|
||||
this.selected.push(parent);
|
||||
}
|
||||
|
||||
if (!this.selected.includes(tag)) {
|
||||
this.selected.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given tag from the list of selected tags.
|
||||
*
|
||||
* @param {Tag} tag
|
||||
*/
|
||||
removeTag(tag) {
|
||||
const 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(selected => selected.parent() === tag)
|
||||
.forEach(this.removeTag.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'TagDiscussionModal';
|
||||
}
|
||||
|
||||
title() {
|
||||
return this.attrs.discussion
|
||||
? app.translator.trans('flarum-tags.forum.choose_tags.edit_title', {title: <em>{this.attrs.discussion.title()}</em>})
|
||||
: app.translator.trans('flarum-tags.forum.choose_tags.title');
|
||||
}
|
||||
|
||||
getInstruction(primaryCount, secondaryCount) {
|
||||
if (this.bypassReqs) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (primaryCount < this.minPrimary) {
|
||||
const remaining = this.minPrimary - primaryCount;
|
||||
return app.translator.trans('flarum-tags.forum.choose_tags.choose_primary_placeholder', {count: remaining});
|
||||
} else if (secondaryCount < this.minSecondary) {
|
||||
const remaining = this.minSecondary - secondaryCount;
|
||||
return app.translator.trans('flarum-tags.forum.choose_tags.choose_secondary_placeholder', {count: remaining});
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
content() {
|
||||
if (this.tagsLoading) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
let tags = this.tags;
|
||||
const filter = this.filter().toLowerCase();
|
||||
const primaryCount = this.primaryCount();
|
||||
const 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(tag => {
|
||||
const parent = tag.parent();
|
||||
return parent === false || this.selected.includes(parent);
|
||||
});
|
||||
|
||||
// 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 >= this.maxPrimary && !this.bypassReqs) {
|
||||
tags = tags.filter(tag => !tag.isPrimary() || this.selected.includes(tag));
|
||||
}
|
||||
|
||||
if (secondaryCount >= this.maxSecondary && !this.bypassReqs) {
|
||||
tags = tags.filter(tag => tag.isPrimary() || this.selected.includes(tag));
|
||||
}
|
||||
|
||||
// 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(tag => tag.name().substr(0, filter.length).toLowerCase() === filter);
|
||||
}
|
||||
|
||||
if (!tags.includes(this.index)) this.index = tags[0];
|
||||
|
||||
const inputWidth = Math.max(extractText(this.getInstruction(primaryCount, secondaryCount)).length, this.filter().length);
|
||||
|
||||
return [
|
||||
<div className="Modal-body">
|
||||
<div className="TagDiscussionModal-form">
|
||||
<div className="TagDiscussionModal-form-input">
|
||||
<div className={'TagsInput FormControl ' + (this.focused ? 'focus' : '')}
|
||||
onclick={() => this.$('.TagsInput input').focus()}
|
||||
>
|
||||
<span className="TagsInput-selected">
|
||||
{this.selected.map(tag =>
|
||||
<span className="TagsInput-tag" onclick={() => {
|
||||
this.removeTag(tag);
|
||||
this.onready();
|
||||
}}>
|
||||
{tagLabel(tag)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<input className="FormControl"
|
||||
placeholder={extractText(this.getInstruction(primaryCount, secondaryCount))}
|
||||
bidi={this.filter}
|
||||
style={{ width: inputWidth + 'ch' }}
|
||||
onkeydown={this.navigator.navigate.bind(this.navigator)}
|
||||
onfocus={() => this.focused = true}
|
||||
onblur={() => this.focused = false}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="TagDiscussionModal-form-submit App-primaryControl">
|
||||
<Button type="submit" className="Button Button--primary" disabled={!this.meetsRequirements(primaryCount, secondaryCount)} icon="fas fa-check">
|
||||
{app.translator.trans('flarum-tags.forum.choose_tags.submit_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
|
||||
<div className="Modal-footer">
|
||||
<ul className="TagDiscussionModal-list SelectTagList">
|
||||
{tags
|
||||
.filter(tag => filter || !tag.parent() || this.selected.includes(tag.parent()))
|
||||
.map(tag => (
|
||||
<li data-index={tag.id()}
|
||||
className={classList({
|
||||
pinned: tag.position() !== null,
|
||||
child: !!tag.parent(),
|
||||
colored: !!tag.color(),
|
||||
selected: this.selected.includes(tag),
|
||||
active: this.index === tag
|
||||
})}
|
||||
style={{color: tag.color()}}
|
||||
onmouseover={() => this.index = tag}
|
||||
onclick={this.toggleTag.bind(this, tag)}
|
||||
>
|
||||
{tagIcon(tag)}
|
||||
<span className="SelectTagListItem-name">
|
||||
{highlight(tag.name(), filter)}
|
||||
</span>
|
||||
{tag.description()
|
||||
? (
|
||||
<span className="SelectTagListItem-description">
|
||||
{tag.description()}
|
||||
</span>
|
||||
) : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{!!app.forum.attribute('canBypassTagCounts') && (
|
||||
<div className="TagDiscussionModal-controls">
|
||||
<ToggleButton className="Button" onclick={() => this.bypassReqs = !this.bypassReqs} isToggled={this.bypassReqs}>
|
||||
{app.translator.trans('flarum-tags.forum.choose_tags.bypass_requirements')}
|
||||
</ToggleButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
];
|
||||
}
|
||||
|
||||
meetsRequirements(primaryCount, secondaryCount) {
|
||||
if (this.bypassReqs) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return primaryCount >= this.minPrimary && secondaryCount >= this.minSecondary;
|
||||
}
|
||||
|
||||
toggleTag(tag) {
|
||||
if (this.selected.includes(tag)) {
|
||||
this.removeTag(tag);
|
||||
} else {
|
||||
this.addTag(tag);
|
||||
}
|
||||
|
||||
if (this.filter()) {
|
||||
this.filter('');
|
||||
this.index = this.tags[0];
|
||||
}
|
||||
|
||||
this.onready();
|
||||
}
|
||||
|
||||
select(e) {
|
||||
// Ctrl + Enter submits the selection, just Enter completes the current entry
|
||||
if (e.metaKey || e.ctrlKey || this.selected.includes(this.index)) {
|
||||
if (this.selected.length) {
|
||||
// The DOM submit method doesn't emit a `submit event, so we
|
||||
// simulate a manual submission so our `onsubmit` logic is run.
|
||||
this.$('button[type="submit"]').click();
|
||||
}
|
||||
} else {
|
||||
this.getItem(this.index)[0].dispatchEvent(new Event('click'));
|
||||
}
|
||||
}
|
||||
|
||||
selectableItems() {
|
||||
return this.$('.TagDiscussionModal-list > li');
|
||||
}
|
||||
|
||||
getCurrentNumericIndex() {
|
||||
return this.selectableItems().index(
|
||||
this.getItem(this.index)
|
||||
);
|
||||
}
|
||||
|
||||
getItem(index) {
|
||||
return this.selectableItems().filter(`[data-index="${index.id()}"]`);
|
||||
}
|
||||
|
||||
setIndex(index, scrollToItem) {
|
||||
const $items = this.selectableItems();
|
||||
const $dropdown = $items.parent();
|
||||
|
||||
if (index < 0) {
|
||||
index = $items.length - 1;
|
||||
} else if (index >= $items.length) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
const $item = $items.eq(index);
|
||||
|
||||
this.index = app.store.getById('tags', $item.attr('data-index'));
|
||||
|
||||
m.redraw();
|
||||
|
||||
if (scrollToItem) {
|
||||
const dropdownScroll = $dropdown.scrollTop();
|
||||
const dropdownTop = $dropdown.offset().top;
|
||||
const dropdownBottom = dropdownTop + $dropdown.outerHeight();
|
||||
const itemTop = $item.offset().top;
|
||||
const itemBottom = itemTop + $item.outerHeight();
|
||||
|
||||
let scrollTop;
|
||||
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}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const discussion = this.attrs.discussion;
|
||||
const tags = this.selected;
|
||||
|
||||
if (discussion) {
|
||||
discussion.save({relationships: {tags}})
|
||||
.then(() => {
|
||||
if (app.current.matches(DiscussionPage)) {
|
||||
app.current.get('stream').update();
|
||||
}
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.attrs.onsubmit) this.attrs.onsubmit(tags);
|
||||
|
||||
this.hide();
|
||||
}
|
||||
}
|
21
extensions/tags/js/src/forum/components/TagHero.js
Normal file
21
extensions/tags/js/src/forum/components/TagHero.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Component from 'flarum/Component';
|
||||
import tagIcon from '../../common/helpers/tagIcon';
|
||||
|
||||
export default class TagHero extends Component {
|
||||
view() {
|
||||
const tag = this.attrs.model;
|
||||
const color = tag.color();
|
||||
|
||||
return (
|
||||
<header className={'Hero TagHero' + (color ? ' TagHero--colored' : '')}
|
||||
style={color ? { '--hero-bg': color } : ''}>
|
||||
<div className="container">
|
||||
<div className="containerNarrow">
|
||||
<h2 className="Hero-title">{tag.icon() && tagIcon(tag, {}, { useColor: false })} {tag.name()}</h2>
|
||||
<div className="Hero-subtitle">{tag.description()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
38
extensions/tags/js/src/forum/components/TagLinkButton.js
Normal file
38
extensions/tags/js/src/forum/components/TagLinkButton.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import Link from 'flarum/components/Link';
|
||||
import LinkButton from 'flarum/components/LinkButton';
|
||||
import classList from 'flarum/utils/classList';
|
||||
import tagIcon from '../../common/helpers/tagIcon';
|
||||
|
||||
export default class TagLinkButton extends LinkButton {
|
||||
view(vnode) {
|
||||
const tag = this.attrs.model;
|
||||
const active = this.constructor.isActive(this.attrs);
|
||||
const description = tag && tag.description();
|
||||
const className = classList([
|
||||
'TagLinkButton',
|
||||
'hasIcon',
|
||||
this.attrs.className,
|
||||
tag.isChild() && 'child',
|
||||
]);
|
||||
|
||||
return (
|
||||
<Link className={className} href={this.attrs.route}
|
||||
style={tag ? { '--color': tag.color() } : ''}
|
||||
title={description || ''}>
|
||||
{tagIcon(tag, { className: 'Button-icon' })}
|
||||
<span className="Button-label">
|
||||
{tag ? tag.name() : app.translator.trans('flarum-tags.forum.index.untagged_link')}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
static initAttrs(attrs) {
|
||||
super.initAttrs(attrs);
|
||||
|
||||
const tag = attrs.model;
|
||||
|
||||
attrs.params.tags = tag ? tag.slug() : 'untagged';
|
||||
attrs.route = app.route('tag', attrs.params);
|
||||
}
|
||||
}
|
115
extensions/tags/js/src/forum/components/TagsPage.js
Executable file
115
extensions/tags/js/src/forum/components/TagsPage.js
Executable file
|
@ -0,0 +1,115 @@
|
|||
import Page from 'flarum/components/Page';
|
||||
import IndexPage from 'flarum/components/IndexPage';
|
||||
import Link from 'flarum/components/Link';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
import humanTime from 'flarum/helpers/humanTime';
|
||||
|
||||
import tagIcon from '../../common/helpers/tagIcon';
|
||||
import tagLabel from '../../common/helpers/tagLabel';
|
||||
import sortTags from '../../common/utils/sortTags';
|
||||
|
||||
export default class TagsPage extends Page {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
app.history.push('tags', app.translator.trans('flarum-tags.forum.header.back_to_tags_tooltip'));
|
||||
|
||||
this.tags = [];
|
||||
|
||||
const preloaded = app.preloadedApiDocument();
|
||||
|
||||
if (preloaded) {
|
||||
this.tags = sortTags(preloaded.filter(tag => !tag.isChild()));
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
app.tagList.load(['children', 'lastPostedDiscussion', 'parent']).then(() => {
|
||||
this.tags = sortTags(app.store.all('tags').filter(tag => !tag.isChild()));
|
||||
|
||||
this.loading = false;
|
||||
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
view() {
|
||||
if (this.loading) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
const pinned = this.tags.filter(tag => tag.position() !== null);
|
||||
const cloud = this.tags.filter(tag => tag.position() === null);
|
||||
|
||||
return (
|
||||
<div className="TagsPage">
|
||||
{IndexPage.prototype.hero()}
|
||||
<div className="container">
|
||||
<nav className="TagsPage-nav IndexPage-nav sideNav">
|
||||
<ul>{listItems(IndexPage.prototype.sidebarItems().toArray())}</ul>
|
||||
</nav>
|
||||
|
||||
<div className="TagsPage-content sideNavOffset">
|
||||
<ul className="TagTiles">
|
||||
{pinned.map(tag => {
|
||||
const lastPostedDiscussion = tag.lastPostedDiscussion();
|
||||
const children = sortTags(tag.children() || []);
|
||||
|
||||
return (
|
||||
<li className={'TagTile ' + (tag.color() ? 'colored' : '')}
|
||||
style={{ '--tag-bg': tag.color() }}>
|
||||
<Link className="TagTile-info" href={app.route.tag(tag)}>
|
||||
{tag.icon() && tagIcon(tag, {}, { useColor: false })}
|
||||
<h3 className="TagTile-name">{tag.name()}</h3>
|
||||
<p className="TagTile-description">{tag.description()}</p>
|
||||
{children
|
||||
? (
|
||||
<div className="TagTile-children">
|
||||
{children.map(child => [
|
||||
<Link href={app.route.tag(child)}>
|
||||
{child.name()}
|
||||
</Link>,
|
||||
' '
|
||||
])}
|
||||
</div>
|
||||
) : ''}
|
||||
</Link>
|
||||
{lastPostedDiscussion
|
||||
? (
|
||||
<Link className="TagTile-lastPostedDiscussion"
|
||||
href={app.route.discussion(lastPostedDiscussion, lastPostedDiscussion.lastPostNumber())}
|
||||
>
|
||||
<span className="TagTile-lastPostedDiscussion-title">{lastPostedDiscussion.title()}</span>
|
||||
{humanTime(lastPostedDiscussion.lastPostedAt())}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="TagTile-lastPostedDiscussion"/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{cloud.length ? (
|
||||
<div className="TagCloud">
|
||||
{cloud.map(tag => [
|
||||
tagLabel(tag, {link: true}),
|
||||
' ',
|
||||
])}
|
||||
</div>
|
||||
) : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
app.setTitle(app.translator.trans('flarum-tags.forum.all_tags.meta_title_text'));
|
||||
app.setTitleCount(0);
|
||||
}
|
||||
}
|
19
extensions/tags/js/src/forum/components/ToggleButton.js
Normal file
19
extensions/tags/js/src/forum/components/ToggleButton.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import Component from 'flarum/common/Component';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import classList from 'flarum/common/utils/classList';
|
||||
|
||||
/**
|
||||
* @TODO move to core
|
||||
*/
|
||||
export default class ToggleButton extends Component {
|
||||
view(vnode) {
|
||||
const { className, isToggled, ...attrs } = this.attrs;
|
||||
const icon = isToggled ? 'far fa-check-circle' : 'far fa-circle';
|
||||
|
||||
return (
|
||||
<Button {...attrs} icon={icon} className={classList([className, isToggled && 'Button--toggled'])}>
|
||||
{vnode.children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
44
extensions/tags/js/src/forum/index.js
Normal file
44
extensions/tags/js/src/forum/index.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import Model from 'flarum/Model';
|
||||
import Discussion from 'flarum/models/Discussion';
|
||||
import IndexPage from 'flarum/components/IndexPage';
|
||||
|
||||
import Tag from '../common/models/Tag';
|
||||
import TagsPage from './components/TagsPage';
|
||||
import DiscussionTaggedPost from './components/DiscussionTaggedPost';
|
||||
|
||||
import TagListState from './states/TagListState';
|
||||
|
||||
import addTagList from './addTagList';
|
||||
import addTagFilter from './addTagFilter';
|
||||
import addTagLabels from './addTagLabels';
|
||||
import addTagControl from './addTagControl';
|
||||
import addTagComposer from './addTagComposer';
|
||||
|
||||
app.initializers.add('flarum-tags', function(app) {
|
||||
app.routes.tags = {path: '/tags', component: TagsPage };
|
||||
app.routes.tag = {path: '/t/:tags', component: IndexPage };
|
||||
|
||||
app.route.tag = tag => app.route('tag', {tags: tag.slug()});
|
||||
|
||||
app.postComponents.discussionTagged = DiscussionTaggedPost;
|
||||
|
||||
app.store.models.tags = Tag;
|
||||
|
||||
app.tagList = new TagListState();
|
||||
|
||||
Discussion.prototype.tags = Model.hasMany('tags');
|
||||
Discussion.prototype.canTag = Model.attribute('canTag');
|
||||
|
||||
addTagList();
|
||||
addTagFilter();
|
||||
addTagLabels();
|
||||
addTagControl();
|
||||
addTagComposer();
|
||||
});
|
||||
|
||||
|
||||
// Expose compat API
|
||||
import tagsCompat from './compat';
|
||||
import { compat } from '@flarum/core/forum';
|
||||
|
||||
Object.assign(compat, tagsCompat);
|
20
extensions/tags/js/src/forum/states/TagListState.js
Normal file
20
extensions/tags/js/src/forum/states/TagListState.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
export default class TagListState {
|
||||
constructor() {
|
||||
this.loadedIncludes = new Set();
|
||||
}
|
||||
|
||||
async load(includes = []) {
|
||||
const unloadedIncludes = includes.filter(include => !this.loadedIncludes.has(include));
|
||||
|
||||
if (unloadedIncludes.length === 0) {
|
||||
return Promise.resolve(app.store.all('tags'));
|
||||
}
|
||||
|
||||
return app.store
|
||||
.find('tags', { include: unloadedIncludes.join(',') })
|
||||
.then(val => {
|
||||
unloadedIncludes.forEach(include => this.loadedIncludes.add(include));
|
||||
return val;
|
||||
});
|
||||
}
|
||||
}
|
11
extensions/tags/js/src/forum/utils/getSelectableTags.js
Normal file
11
extensions/tags/js/src/forum/utils/getSelectableTags.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export default function getSelectableTags(discussion) {
|
||||
let tags = app.store.all('tags');
|
||||
|
||||
if (discussion) {
|
||||
tags = tags.filter(tag => tag.canAddToDiscussion() || discussion.tags().indexOf(tag) !== -1);
|
||||
} else {
|
||||
tags = tags.filter(tag => tag.canStartDiscussion());
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
1
extensions/tags/js/webpack.config.js
Executable file
1
extensions/tags/js/webpack.config.js
Executable file
|
@ -0,0 +1 @@
|
|||
module.exports = require('flarum-webpack-config')();
|
2279
extensions/tags/js/yarn.lock
Normal file
2279
extensions/tags/js/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
9
extensions/tags/less/admin.less
Normal file
9
extensions/tags/less/admin.less
Normal file
|
@ -0,0 +1,9 @@
|
|||
@import "common/common";
|
||||
|
||||
@import "admin/TagsPage";
|
||||
@import "admin/EditTagModal";
|
||||
|
||||
.Dropdown--restrictByTag .Dropdown-menu {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
8
extensions/tags/less/admin/EditTagModal.less
Normal file
8
extensions/tags/less/admin/EditTagModal.less
Normal file
|
@ -0,0 +1,8 @@
|
|||
.EditTagModal {
|
||||
.Form-group:not(:last-child) {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
.EditTagModal-delete {
|
||||
float: right;
|
||||
}
|
156
extensions/tags/less/admin/TagsPage.less
Normal file
156
extensions/tags/less/admin/TagsPage.less
Normal file
|
@ -0,0 +1,156 @@
|
|||
.flarum-tags-Page {
|
||||
padding-bottom: 140px;
|
||||
}
|
||||
|
||||
.TagsContent-footer {
|
||||
color: @control-color;
|
||||
padding: 20px 0;
|
||||
|
||||
p {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.TagsContent-list {
|
||||
padding: 20px 0 0;
|
||||
|
||||
}
|
||||
|
||||
.TagList,
|
||||
.TagList ol {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
color: @muted-color;
|
||||
font-size: 13px;
|
||||
|
||||
>li {
|
||||
display: inline-block;
|
||||
max-height: 40px;
|
||||
cursor: move;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.TagIcon,
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.TagListItem-info {
|
||||
border-radius: @border-radius;
|
||||
padding: 5px;
|
||||
|
||||
&:hover {
|
||||
background: @control-bg;
|
||||
}
|
||||
|
||||
.Button {
|
||||
float: right;
|
||||
visibility: hidden;
|
||||
margin: -8px -16px -8px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
li:not(.sortable-dragging)>.TagListItem-info:hover>.Button {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.TagList--primary {
|
||||
font-size: 16px;
|
||||
|
||||
>.sortable-placeholder {
|
||||
height: 38px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.TagList ol {
|
||||
margin-left: 27px;
|
||||
min-height: 10px;
|
||||
padding: 0;
|
||||
|
||||
&> :last-child {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.sortable-placeholder {
|
||||
border: 2px dashed @control-bg;
|
||||
border-radius: @border-radius;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.SettingsGroups {
|
||||
display: flex;
|
||||
column-count: 3;
|
||||
column-gap: 30px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (@tablet-up) {
|
||||
.TagGroup--secondary {
|
||||
max-width: 250px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.Form {
|
||||
min-width: 300px;
|
||||
max-height: 500px;
|
||||
|
||||
>label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.TagSettings-rangeInput {
|
||||
input {
|
||||
width: 80px;
|
||||
display: inline;
|
||||
margin: 0 5px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.TagGroup,
|
||||
.Form {
|
||||
display: inline-grid;
|
||||
padding: 10px 20px;
|
||||
min-height: 20vh;
|
||||
max-width: 400px;
|
||||
grid-template-rows: min-content;
|
||||
border: 1px solid @control-bg;
|
||||
border-radius: @border-radius;
|
||||
flex: 1 1 160px;
|
||||
|
||||
@media (max-width: 1209px) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
>ol {
|
||||
>li {
|
||||
margin-top: 8px;
|
||||
|
||||
.Button {
|
||||
float: right;
|
||||
visibility: hidden;
|
||||
margin: -8px -16px -8px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.TagList-button {
|
||||
background: none;
|
||||
border: 1px dashed @control-bg;
|
||||
height: 40px;
|
||||
margin: auto auto 0 0;
|
||||
}
|
||||
|
||||
>label {
|
||||
float: left;
|
||||
font-weight: bold;
|
||||
color: @muted-color;
|
||||
}
|
||||
}
|
||||
}
|
14
extensions/tags/less/common/TagIcon.less
Normal file
14
extensions/tags/less/common/TagIcon.less
Normal file
|
@ -0,0 +1,14 @@
|
|||
.TagIcon {
|
||||
border-radius: @border-radius;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
vertical-align: -3px;
|
||||
margin-left: 1px;
|
||||
background: var(--color, @control-bg);
|
||||
|
||||
&.untagged {
|
||||
border: 1px dotted @muted-color;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
61
extensions/tags/less/common/TagLabel.less
Normal file
61
extensions/tags/less/common/TagLabel.less
Normal file
|
@ -0,0 +1,61 @@
|
|||
.TagLabel {
|
||||
font-size: 85%;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
padding: 0.1em 0.5em;
|
||||
border-radius: @border-radius;
|
||||
background: var(--tag-bg);
|
||||
color: var(--tag-color);
|
||||
text-transform: none;
|
||||
vertical-align: bottom;
|
||||
|
||||
&.untagged {
|
||||
--tag-bg: transparent;
|
||||
--tag-color: @muted-color;
|
||||
border: 1px dotted;
|
||||
}
|
||||
|
||||
&.colored {
|
||||
--tag-color: @body-bg;
|
||||
|
||||
.TagLabel-text {
|
||||
color: var(--tag-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.DiscussionHero .TagsLabel & {
|
||||
background: transparent;
|
||||
border-radius: @border-radius !important;
|
||||
font-size: 14px;
|
||||
|
||||
&.colored {
|
||||
--tag-color: var(--tag-bg);
|
||||
margin-right: 5px;
|
||||
background: @body-bg !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.DiscussionHero--colored {
|
||||
&, a {
|
||||
color: @body-bg;
|
||||
}
|
||||
}
|
||||
.TagsLabel {
|
||||
.DiscussionTaggedPost & {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.TagLabel {
|
||||
border-radius: 0;
|
||||
|
||||
&:first-child {
|
||||
border-radius: @border-radius 0 0 @border-radius;
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0 @border-radius @border-radius 0;
|
||||
}
|
||||
&:first-child:last-child {
|
||||
border-radius: @border-radius;
|
||||
}
|
||||
}
|
||||
}
|
3
extensions/tags/less/common/common.less
Normal file
3
extensions/tags/less/common/common.less
Normal file
|
@ -0,0 +1,3 @@
|
|||
@import "root";
|
||||
@import "TagLabel";
|
||||
@import "TagIcon";
|
4
extensions/tags/less/common/root.less
Normal file
4
extensions/tags/less/common/root.less
Normal file
|
@ -0,0 +1,4 @@
|
|||
:root {
|
||||
--tag-bg: @control-bg;
|
||||
--tag-color: @control-color;
|
||||
}
|
94
extensions/tags/less/forum.less
Normal file
94
extensions/tags/less/forum.less
Normal file
|
@ -0,0 +1,94 @@
|
|||
@import "common/common";
|
||||
|
||||
@import "forum/TagCloud";
|
||||
@import "forum/TagDiscussionModal";
|
||||
@import "forum/TagHero";
|
||||
@import "forum/TagTiles";
|
||||
@import "forum/ToggleButton";
|
||||
|
||||
.Button--tagColored {
|
||||
--button-primary-bg: var(--color);
|
||||
--button-primary-bg-hover: var(--color);
|
||||
--button-primary-bg-active: var(--color);
|
||||
}
|
||||
.DiscussionHero {
|
||||
.item-title {
|
||||
display: block;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
.TagLinkButton {
|
||||
&.child {
|
||||
@media @tablet-up {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
margin-left: 10px;
|
||||
|
||||
.TagIcon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.active > & {
|
||||
--sidenav-color-active: var(--color);
|
||||
}
|
||||
}
|
||||
.DiscussionComposer-changeTags {
|
||||
margin-right: 15px;
|
||||
vertical-align: 2px;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
.DiscussionListItem-info > .item-tags {
|
||||
margin-right: 4px;
|
||||
opacity: 1;
|
||||
}
|
||||
@media @tablet-up {
|
||||
.IndexPage, .UserPage {
|
||||
.DiscussionListItem-title {
|
||||
margin-right: 155px;
|
||||
}
|
||||
.DiscussionListItem-info > .item-tags {
|
||||
margin-right: 0;
|
||||
position: absolute;
|
||||
right: 80px;
|
||||
top: 14px;
|
||||
max-width: 150px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
transition: max-width 0.2s ease-in-out, -webkit-mask-image 0.2s;
|
||||
-webkit-mask-image: linear-gradient(to right, rgba(0,0,0,1) 140px, rgba(0,0,0,0) 150px);
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
max-width: 400px;
|
||||
-webkit-mask-image: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media @desktop-up {
|
||||
.TagsPage {
|
||||
.sideNav {
|
||||
.sideNav--horizontal();
|
||||
float: none;
|
||||
width: auto;
|
||||
padding-top: 0;
|
||||
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> ul > li:first-child {
|
||||
width: 190px;
|
||||
}
|
||||
}
|
||||
.sideNavOffset {
|
||||
margin: 15px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
10
extensions/tags/less/forum/TagCloud.less
Normal file
10
extensions/tags/less/forum/TagCloud.less
Normal file
|
@ -0,0 +1,10 @@
|
|||
.TagCloud {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
|
||||
a {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
175
extensions/tags/less/forum/TagDiscussionModal.less
Normal file
175
extensions/tags/less/forum/TagDiscussionModal.less
Normal file
|
@ -0,0 +1,175 @@
|
|||
.TagDiscussionModal {
|
||||
@media @tablet-up {
|
||||
.Modal-header {
|
||||
background: @control-bg;
|
||||
padding: 20px 20px 0;
|
||||
|
||||
& h3 {
|
||||
text-align: left;
|
||||
color: @control-color;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.Modal-body {
|
||||
padding: 20px;
|
||||
|
||||
@media @phone {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
.Modal-footer {
|
||||
padding: 1px 0 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&-controls {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media @tablet, @desktop, @desktop-hd {
|
||||
.TagDiscussionModal-form {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
.TagDiscussionModal-form-input {
|
||||
display: table-cell;
|
||||
width: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
.TagDiscussionModal-form-submit {
|
||||
display: table-cell;
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
.TagsInput {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
overflow: hidden;
|
||||
height: auto;
|
||||
cursor: text;
|
||||
|
||||
input {
|
||||
display: inline;
|
||||
outline: none;
|
||||
margin-top: -2px;
|
||||
border: 0 !important;
|
||||
padding: 0;
|
||||
max-width: 100%;
|
||||
margin-right: -100%;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
.TagsInput-tag {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.TagsInput-selected {
|
||||
.TagsInput-tag {
|
||||
margin-right: 5px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.SelectTagList {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
overflow: auto;
|
||||
max-height: 50vh;
|
||||
|
||||
@media @phone {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
> li {
|
||||
padding: 7px 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
|
||||
&.pinned:not(.child) {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.SelectTagListItem-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
&.pinned + li:not(.pinned) {
|
||||
border-top: 2px solid @control-bg;
|
||||
}
|
||||
&.child {
|
||||
padding-left: 45px;
|
||||
|
||||
.SelectTagListItem-name {
|
||||
width: 125px;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
background: @control-bg;
|
||||
}
|
||||
|
||||
.icon::before {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.icon::before {
|
||||
.fa();
|
||||
content: @fa-var-check !important;
|
||||
color: @muted-color;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
vertical-align: 1px;
|
||||
}
|
||||
|
||||
&.colored .TagIcon:before {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.TagIcon {
|
||||
vertical-align: top;
|
||||
margin-top: 3px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.SelectTagListItem-name {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: top;
|
||||
|
||||
@media @phone {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
.SelectTagListItem-description {
|
||||
color: @muted-color;
|
||||
font-size: 12px;
|
||||
width: 370px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 2px;
|
||||
|
||||
@media @phone {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.SelectTagListItem mark {
|
||||
font-weight: bold;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
color: inherit;
|
||||
}
|
5
extensions/tags/less/forum/TagHero.less
Normal file
5
extensions/tags/less/forum/TagHero.less
Normal file
|
@ -0,0 +1,5 @@
|
|||
.TagHero {
|
||||
&--colored {
|
||||
--hero-color: #fff;
|
||||
}
|
||||
}
|
144
extensions/tags/less/forum/TagTiles.less
Executable file
144
extensions/tags/less/forum/TagTiles.less
Executable file
|
@ -0,0 +1,144 @@
|
|||
.TagTiles {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
|
||||
@media @phone {
|
||||
margin: -15px -15px 0;
|
||||
}
|
||||
|
||||
> li {
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
|
||||
@media @tablet {
|
||||
float: left;
|
||||
width: 50%;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: @border-radius;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
border-top-right-radius: @border-radius;
|
||||
}
|
||||
&:nth-last-child(2):nth-child(even), &:last-child {
|
||||
border-bottom-right-radius: @border-radius;
|
||||
}
|
||||
&:nth-last-child(2):nth-child(odd), &:last-child:nth-child(odd) {
|
||||
border-bottom-left-radius: @border-radius;
|
||||
}
|
||||
}
|
||||
@media @desktop-up {
|
||||
float: left;
|
||||
width: 33.333%;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: @border-radius;
|
||||
}
|
||||
&:nth-child(3),
|
||||
&:nth-child(2):last-child,
|
||||
&:first-child:last-child {
|
||||
border-top-right-radius: @border-radius;
|
||||
}
|
||||
&:nth-child(3n):nth-last-child(2),
|
||||
&:nth-child(3n):nth-last-child(3),
|
||||
&:last-child {
|
||||
border-bottom-right-radius: @border-radius;
|
||||
}
|
||||
&:nth-child(3n+1):last-child,
|
||||
&:nth-child(3n+1):nth-last-child(2),
|
||||
&:nth-child(3n+1):nth-last-child(3) {
|
||||
border-bottom-left-radius: @border-radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.TagTile {
|
||||
position: relative;
|
||||
background: var(--tag-bg);
|
||||
|
||||
&, a {
|
||||
color: @control-color;
|
||||
}
|
||||
&.colored {
|
||||
&, a {
|
||||
color: @body-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
.TagTile-info, .TagTile-lastPostedDiscussion {
|
||||
padding: 20px;
|
||||
text-decoration: none !important;
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.TagTile-info {
|
||||
top: 0;
|
||||
bottom: 42px;
|
||||
padding-right: 20px;
|
||||
transition: background 0.2s;
|
||||
overflow: auto;
|
||||
|
||||
&:hover {
|
||||
background: fade(#000, 5%);
|
||||
}
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.TagTile-name {
|
||||
font-size: 18px;
|
||||
margin: 0 0 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.TagTile-description {
|
||||
font-size: 12px;
|
||||
margin: 0 0 10px;
|
||||
|
||||
.TagTile.colored & {
|
||||
color: fade(@body-bg, 70%);
|
||||
}
|
||||
}
|
||||
.TagTile-children {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
|
||||
a {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.TagTile-lastPostedDiscussion {
|
||||
bottom: 0;
|
||||
height: 42px;
|
||||
padding: 7px 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 21px;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.15);
|
||||
margin: 0 20px;
|
||||
|
||||
.TagTile.colored & {
|
||||
color: fade(@body-bg, 70%);
|
||||
}
|
||||
|
||||
&:hover .TagTile-lastPostedDiscussion-title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
time {
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.TagTile-lastPostedDiscussion-title {
|
||||
margin-right: 10px;
|
||||
}
|
7
extensions/tags/less/forum/ToggleButton.less
Normal file
7
extensions/tags/less/forum/ToggleButton.less
Normal file
|
@ -0,0 +1,7 @@
|
|||
:root {
|
||||
.Button--color-vars(@control-bg, @control-color, 'button-toggled');
|
||||
}
|
||||
|
||||
.Button--toggled {
|
||||
.Button--color-auto('button-toggled');
|
||||
}
|
117
extensions/tags/locale/en.yml
Normal file
117
extensions/tags/locale/en.yml
Normal file
|
@ -0,0 +1,117 @@
|
|||
flarum-tags:
|
||||
|
||||
##
|
||||
# UNIQUE KEYS - The following keys are used in only one location each.
|
||||
##
|
||||
|
||||
# Translations in this namespace are used by the admin interface.
|
||||
admin:
|
||||
|
||||
# These translations are used in the Basics page.
|
||||
basics:
|
||||
tags_label: => flarum-tags.ref.tags
|
||||
|
||||
# These translations are used in the Edit Tag modal dialog.
|
||||
edit_tag:
|
||||
color_label: => core.ref.color
|
||||
delete_tag_button: Delete Tag
|
||||
delete_tag_confirmation: "Are you sure you want to delete this tag? The tag's discussions will NOT be deleted."
|
||||
description_label: Description
|
||||
hide_label: Hide from All Discussions
|
||||
icon_label: => core.ref.icon
|
||||
icon_text: => core.ref.icon_text
|
||||
name_label: => flarum-tags.ref.name
|
||||
name_placeholder: => flarum-tags.ref.name
|
||||
slug_label: Slug
|
||||
submit_button: => core.ref.save_changes
|
||||
title: Create Tag
|
||||
|
||||
# These translations are used in the navigation bar.
|
||||
nav:
|
||||
tags_button: => flarum-tags.ref.tags
|
||||
tags_text: Manage the list of tags available to organise discussions with.
|
||||
|
||||
# These translations are used in the Permissions page of the admin interface.
|
||||
permissions:
|
||||
allow_edit_tags_label: Allow tag editing
|
||||
bypass_tag_counts_label: Bypass tag requirements
|
||||
restrict_by_tag_heading: Restrict by Tag
|
||||
tag_discussions_label: Tag discussions
|
||||
|
||||
# These translations are used in the Tag Settings modal dialog.
|
||||
tag_settings:
|
||||
range_separator_text: " to "
|
||||
required_primary_heading: Required Number of Primary Tags
|
||||
required_primary_text: Enter the minimum and maximum number of primary tags that may be applied to a discussion.
|
||||
required_secondary_heading: Required Number of Secondary Tags
|
||||
required_secondary_text: Enter the minimum and maximum number of secondary tags that may be applied to a discussion.
|
||||
title: Tag Settings
|
||||
|
||||
# These translations are used in the Tags page.
|
||||
tags:
|
||||
about_tags_text: "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."
|
||||
create_primary_tag_button: Create Primary Tag
|
||||
create_secondary_tag_button: Create Secondary Tag
|
||||
primary_heading: Primary Tags
|
||||
secondary_heading: Secondary Tags
|
||||
settings_heading: Settings
|
||||
|
||||
# Translations in this namespace are used by the forum user interface.
|
||||
forum:
|
||||
|
||||
# These translations are displayed on the page that lists all tags.
|
||||
all_tags:
|
||||
meta_description_text: All Tags
|
||||
meta_title_text: => flarum-tags.ref.tags
|
||||
|
||||
# These translations are used by the Choose Tags modal dialog.
|
||||
choose_tags:
|
||||
bypass_requirements: Bypass tag requirements
|
||||
choose_primary_placeholder: "{count, plural, one {Choose a primary tag} other {Choose # primary tags}}"
|
||||
choose_secondary_placeholder: "{count, plural, one {Choose 1 more tag} other {Choose # more tags}}"
|
||||
edit_title: "Edit Tags for {title}"
|
||||
submit_button: => core.ref.okay
|
||||
title: Choose Tags for Your Discussion
|
||||
|
||||
# These translations are used by the composer when starting a discussion.
|
||||
composer_discussion:
|
||||
choose_tags_link: Choose Tags
|
||||
|
||||
# These translations are used by the discussion control buttons.
|
||||
discussion_controls:
|
||||
edit_tags_button: Edit Tags
|
||||
|
||||
header:
|
||||
back_to_tags_tooltip: Back to Tag List
|
||||
|
||||
# These translations are used on the index page, peripheral to the discussion list.
|
||||
index:
|
||||
more_link: More...
|
||||
tags_link: => flarum-tags.ref.tags
|
||||
untagged_link: Untagged
|
||||
|
||||
# These translations are displayed between posts in the post stream.
|
||||
post_stream:
|
||||
added_and_removed_tags_text: "{username} added the {tagsAdded} and removed the {tagsRemoved}."
|
||||
added_tags_text: "{username} added the {tagsAdded}."
|
||||
removed_tags_text: "{username} removed the {tagsRemoved}."
|
||||
tags_text: "{count, plural, one {{tags} tag} other {{tags} tags}}"
|
||||
|
||||
# These translations are used when visiting a single tag's discussion list.
|
||||
tag:
|
||||
meta_description_text: "All discussions with the {tag} tag"
|
||||
|
||||
# Translations in this namespace are used by the forum and admin interfaces.
|
||||
lib:
|
||||
|
||||
# This translation is displayed in place of the name of a tag that's been deleted.
|
||||
deleted_tag_text: Deleted
|
||||
|
||||
##
|
||||
# REUSED TRANSLATIONS - These keys should not be used directly in code!
|
||||
##
|
||||
|
||||
# Translations in this namespace are referenced by two or more unique keys.
|
||||
ref:
|
||||
name: Name
|
||||
tags: Tags
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
return Migration::createTable(
|
||||
'discussions_tags',
|
||||
function (Blueprint $table) {
|
||||
$table->integer('discussion_id')->unsigned();
|
||||
$table->integer('tag_id')->unsigned();
|
||||
$table->primary(['discussion_id', 'tag_id']);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$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('background_mode', 100)->nullable();
|
||||
|
||||
$table->integer('position')->nullable();
|
||||
$table->integer('parent_id')->unsigned()->nullable();
|
||||
$table->string('default_sort', 50)->nullable();
|
||||
$table->boolean('is_restricted')->default(0);
|
||||
$table->boolean('is_hidden')->default(0);
|
||||
|
||||
$table->integer('discussions_count')->unsigned()->default(0);
|
||||
$table->dateTime('last_time')->nullable();
|
||||
$table->integer('last_discussion_id')->unsigned()->nullable();
|
||||
});
|
||||
|
||||
$schema->getConnection()->table('tags')->insert([
|
||||
'name' => 'General',
|
||||
'slug' => 'general',
|
||||
'color' => '#888',
|
||||
'position' => '0'
|
||||
]);
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->drop('tags');
|
||||
}
|
||||
];
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
return Migration::createTable(
|
||||
'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']);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Database\Migration;
|
||||
|
||||
return Migration::addSettings([
|
||||
'flarum-tags.max_primary_tags' => '1',
|
||||
'flarum-tags.min_primary_tags' => '1',
|
||||
'flarum-tags.max_secondary_tags' => '3',
|
||||
'flarum-tags.min_secondary_tags' => '0',
|
||||
]);
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->string('slug', 100)->change();
|
||||
$table->unique('slug');
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->string('slug', 255)->change();
|
||||
$table->dropUnique(['slug']);
|
||||
});
|
||||
}
|
||||
];
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Database\Migration;
|
||||
use Flarum\Group\Group;
|
||||
|
||||
return Migration::addPermissions([
|
||||
'discussion.tag' => Group::MODERATOR_ID
|
||||
]);
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->renameColumn('discussions_count', 'discussion_count');
|
||||
$table->renameColumn('last_time', 'last_posted_at');
|
||||
$table->renameColumn('last_discussion_id', 'last_posted_discussion_id');
|
||||
|
||||
$table->integer('parent_id')->unsigned()->nullable()->change();
|
||||
|
||||
$table->integer('last_posted_user_id')->unsigned()->nullable();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->dropColumn('last_posted_user_id');
|
||||
|
||||
$table->integer('parent_id')->nullable()->change();
|
||||
|
||||
$table->renameColumn('discussion_count', 'discussions_count');
|
||||
$table->renameColumn('last_posted_at', 'last_time');
|
||||
$table->renameColumn('last_posted_discussion_id', 'last_discussion_id');
|
||||
});
|
||||
}
|
||||
];
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Query\Expression;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
// Set non-existent entity IDs to NULL so that we will be able to create
|
||||
// foreign keys without any issues.
|
||||
$connection = $schema->getConnection();
|
||||
|
||||
$select = function ($id, $table, $column) use ($connection) {
|
||||
return new Expression(
|
||||
'('.$connection->table($table)->whereColumn('id', $column)->select($id)->toSql().')'
|
||||
);
|
||||
};
|
||||
|
||||
$connection->table('tags')->update([
|
||||
'last_posted_user_id' => $select('last_posted_user_id', 'discussions', 'last_posted_discussion_id'),
|
||||
'last_posted_discussion_id' => $select('id', 'discussions', 'last_posted_discussion_id'),
|
||||
]);
|
||||
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->foreign('parent_id')->references('id')->on('tags')->onDelete('set null');
|
||||
$table->foreign('last_posted_user_id')->references('id')->on('users')->onDelete('set null');
|
||||
$table->foreign('last_posted_discussion_id')->references('id')->on('discussions')->onDelete('set null');
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->table('tags', function (Blueprint $table) {
|
||||
$table->dropForeign(['parent_id']);
|
||||
$table->dropForeign(['last_posted_discussion_id']);
|
||||
$table->dropForeign(['last_posted_user_id']);
|
||||
});
|
||||
}
|
||||
];
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Database\Migration;
|
||||
|
||||
return Migration::renameTable('users_tags', 'tag_user');
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Database\Migration;
|
||||
|
||||
return Migration::renameColumn('tag_user', 'read_time', 'marked_as_read_at');
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
// Delete rows with non-existent entities so that we will be able to create
|
||||
// foreign keys without any issues.
|
||||
$schema->getConnection()
|
||||
->table('tag_user')
|
||||
->whereNotExists(function ($query) {
|
||||
$query->selectRaw(1)->from('tags')->whereColumn('id', 'tag_id');
|
||||
})
|
||||
->orWhereNotExists(function ($query) {
|
||||
$query->selectRaw(1)->from('users')->whereColumn('id', 'user_id');
|
||||
})
|
||||
->delete();
|
||||
|
||||
$schema->table('tag_user', function (Blueprint $table) {
|
||||
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->table('tag_user', function (Blueprint $table) {
|
||||
$table->dropForeign(['tag_id']);
|
||||
$table->dropForeign(['user_id']);
|
||||
});
|
||||
}
|
||||
];
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Database\Migration;
|
||||
|
||||
return Migration::renameTable('discussions_tags', 'discussion_tag');
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
// Delete rows with non-existent entities so that we will be able to create
|
||||
// foreign keys without any issues.
|
||||
$schema->getConnection()
|
||||
->table('discussion_tag')
|
||||
->whereNotExists(function ($query) {
|
||||
$query->selectRaw(1)->from('discussions')->whereColumn('id', 'discussion_id');
|
||||
})
|
||||
->orWhereNotExists(function ($query) {
|
||||
$query->selectRaw(1)->from('tags')->whereColumn('id', 'tag_id');
|
||||
})
|
||||
->delete();
|
||||
|
||||
$schema->table('discussion_tag', function (Blueprint $table) {
|
||||
$table->foreign('discussion_id')->references('id')->on('discussions')->onDelete('cascade');
|
||||
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->table('discussion_tag', function (Blueprint $table) {
|
||||
$table->dropForeign(['discussion_id']);
|
||||
$table->dropForeign(['tag_id']);
|
||||
});
|
||||
}
|
||||
];
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Database\Migration;
|
||||
|
||||
return Migration::addColumns('tags', [
|
||||
'icon' => ['string', 'length' => 100, 'nullable' => true]
|
||||
]);
|
87
extensions/tags/src/Access/DiscussionPolicy.php
Executable file
87
extensions/tags/src/Access/DiscussionPolicy.php
Executable file
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Access;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\Access\AbstractPolicy;
|
||||
use Flarum\User\User;
|
||||
|
||||
class DiscussionPolicy extends AbstractPolicy
|
||||
{
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
*/
|
||||
public function __construct(SettingsRepositoryInterface $settings)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param string $ability
|
||||
* @param Discussion $discussion
|
||||
* @return bool
|
||||
*/
|
||||
public function can(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, the user must have the permission for that tag.
|
||||
$tags = $discussion->tags;
|
||||
|
||||
if (count($tags)) {
|
||||
$restrictedButHasAccess = false;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if ($tag->is_restricted) {
|
||||
if (! $actor->hasPermission('tag'.$tag->id.'.discussion.'.$ability)) {
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
$restrictedButHasAccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($restrictedButHasAccess) {
|
||||
return $this->allow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks, if the user is still allowed to edit the tags
|
||||
* based on the configuration item.
|
||||
*
|
||||
* @param User $actor
|
||||
* @param Discussion $discussion
|
||||
* @return bool
|
||||
*/
|
||||
public function tag(User $actor, Discussion $discussion)
|
||||
{
|
||||
if ($discussion->user_id == $actor->id && $actor->can('reply', $discussion)) {
|
||||
$allowEditTags = $this->settings->get('allow_tag_change');
|
||||
|
||||
if (
|
||||
$allowEditTags === '-1'
|
||||
|| ($allowEditTags === 'reply' && $discussion->participant_count <= 1)
|
||||
|| (is_numeric($allowEditTags) && $discussion->created_at->diffInMinutes(new Carbon) < $allowEditTags)
|
||||
) {
|
||||
return $this->allow();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
81
extensions/tags/src/Access/GlobalPolicy.php
Normal file
81
extensions/tags/src/Access/GlobalPolicy.php
Normal file
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Access;
|
||||
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\Access\AbstractPolicy;
|
||||
use Flarum\User\User;
|
||||
|
||||
class GlobalPolicy extends AbstractPolicy
|
||||
{
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
public function __construct(SettingsRepositoryInterface $settings)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Flarum\User\User $actor
|
||||
* @param string $ability
|
||||
* @return bool|void
|
||||
*/
|
||||
public function can(User $actor, string $ability)
|
||||
{
|
||||
static $enoughPrimary;
|
||||
static $enoughSecondary;
|
||||
|
||||
if ($ability === 'startDiscussion'
|
||||
&& $actor->hasPermission($ability)
|
||||
&& $actor->hasPermission('bypassTagCounts')) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
if (in_array($ability, ['viewForum', 'startDiscussion'])) {
|
||||
if (! isset($enoughPrimary[$actor->id][$ability])) {
|
||||
$primaryTagsWhereNeedsPermission = $this->settings->get('flarum-tags.min_primary_tags');
|
||||
$primaryTagsWhereHasPermission = Tag::whereHasPermission($actor, $ability)
|
||||
->where('tags.position', '!=', null)
|
||||
->count();
|
||||
|
||||
if ($ability === 'viewForum') {
|
||||
$primaryTagsCount = Tag::query()->where('position', '!=', null)->count();
|
||||
$enoughPrimary[$actor->id][$ability] = $primaryTagsWhereHasPermission >= min($primaryTagsCount, $primaryTagsWhereNeedsPermission);
|
||||
} else {
|
||||
$enoughPrimary[$actor->id][$ability] = $primaryTagsWhereHasPermission >= $primaryTagsWhereNeedsPermission;
|
||||
}
|
||||
}
|
||||
|
||||
if (! isset($enoughSecondary[$actor->id][$ability])) {
|
||||
$secondaryTagsWhereNeedsPermission = $this->settings->get('flarum-tags.min_secondary_tags');
|
||||
$secondaryTagsWhereHasPermission = Tag::whereHasPermission($actor, $ability)
|
||||
->where('tags.position', '=', null)
|
||||
->count();
|
||||
|
||||
if ($ability === 'viewForum') {
|
||||
$secondaryTagsCount = Tag::query()->where(['position' => null, 'parent_id' => null])->count();
|
||||
$enoughSecondary[$actor->id][$ability] = $secondaryTagsWhereHasPermission >= min($secondaryTagsCount, $secondaryTagsWhereNeedsPermission);
|
||||
} else {
|
||||
$enoughSecondary[$actor->id][$ability] = $secondaryTagsWhereHasPermission >= $secondaryTagsWhereNeedsPermission;
|
||||
}
|
||||
}
|
||||
|
||||
if ($enoughPrimary[$actor->id][$ability] && $enoughSecondary[$actor->id][$ability]) {
|
||||
return $this->allow();
|
||||
} else {
|
||||
return $this->deny();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Access;
|
||||
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ScopeDiscussionVisibilityForAbility
|
||||
{
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param Builder $query
|
||||
* @param string $ability
|
||||
*/
|
||||
public function __invoke(User $actor, Builder $query, $ability)
|
||||
{
|
||||
// Automatic scoping should be applied to the global `view` ability,
|
||||
// and to arbitrary abilities that aren't subqueries of `view`.
|
||||
// For example, if we want to scope discussions where the user can
|
||||
// edit posts, this should apply.
|
||||
// But if we are expanding a restriction of `view` (for example,
|
||||
// `viewPrivate`), we shouldn't apply this query again.
|
||||
if (substr($ability, 0, 4) === 'view' && $ability !== 'view') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid an infinite recursive loop.
|
||||
if (Str::endsWith($ability, 'InRestrictedTags')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// `view` is a special case where the permission string is represented by `viewForum`.
|
||||
$permission = $ability === 'view' ? 'viewForum' : $ability;
|
||||
|
||||
// Restrict discussions where users don't have necessary permissions in all tags.
|
||||
// We use a double notIn instead of a doubleIn because the permission must be present in ALL tags,
|
||||
// not just one.
|
||||
$query->where(function ($query) use ($actor, $permission) {
|
||||
$query
|
||||
->whereNotIn('discussions.id', function ($query) use ($actor, $permission) {
|
||||
return $query->select('discussion_id')
|
||||
->from('discussion_tag')
|
||||
->whereNotIn('tag_id', function ($query) use ($actor, $permission) {
|
||||
Tag::query()->setQuery($query->from('tags'))->whereHasPermission($actor, $permission)->select('tags.id');
|
||||
});
|
||||
})
|
||||
->orWhere(function ($query) use ($actor, $permission) {
|
||||
// Allow extensions a way to override scoping for any given permission.
|
||||
$query->whereVisibleTo($actor, "${permission}InRestrictedTags");
|
||||
});
|
||||
});
|
||||
|
||||
// Hide discussions with no tags if the user doesn't have that global
|
||||
// permission.
|
||||
if (! $actor->hasPermission($permission)) {
|
||||
$query->has('tags');
|
||||
}
|
||||
}
|
||||
}
|
28
extensions/tags/src/Access/ScopeTagVisibility.php
Normal file
28
extensions/tags/src/Access/ScopeTagVisibility.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Access;
|
||||
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ScopeTagVisibility
|
||||
{
|
||||
/**
|
||||
* @param User $actor
|
||||
* @param Builder $query
|
||||
*/
|
||||
public function __invoke(User $actor, Builder $query)
|
||||
{
|
||||
$query->whereIn('id', function ($query) use ($actor) {
|
||||
Tag::query()->setQuery($query->from('tags'))->whereHasPermission($actor, 'viewForum')->select('tags.id');
|
||||
});
|
||||
}
|
||||
}
|
35
extensions/tags/src/Access/TagPolicy.php
Executable file
35
extensions/tags/src/Access/TagPolicy.php
Executable file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Access;
|
||||
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\Access\AbstractPolicy;
|
||||
use Flarum\User\User;
|
||||
|
||||
class TagPolicy extends AbstractPolicy
|
||||
{
|
||||
public function can(User $actor, string $ability, Tag $tag)
|
||||
{
|
||||
if ($tag->parent_id !== null && ! $actor->can($ability, $tag->parent)) {
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
if ($tag->is_restricted) {
|
||||
$id = $tag->id;
|
||||
|
||||
return $actor->hasPermission("tag$id.$ability");
|
||||
}
|
||||
}
|
||||
|
||||
public function addToDiscussion(User $actor, Tag $tag)
|
||||
{
|
||||
return $actor->can('startDiscussion', $tag);
|
||||
}
|
||||
}
|
55
extensions/tags/src/Api/Controller/CreateTagController.php
Normal file
55
extensions/tags/src/Api/Controller/CreateTagController.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed 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\AbstractCreateController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Tags\Api\Serializer\TagSerializer;
|
||||
use Flarum\Tags\Command\CreateTag;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class CreateTagController extends AbstractCreateController
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $serializer = TagSerializer::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $include = ['parent'];
|
||||
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
protected $bus;
|
||||
|
||||
/**
|
||||
* @param Dispatcher $bus
|
||||
*/
|
||||
public function __construct(Dispatcher $bus)
|
||||
{
|
||||
$this->bus = $bus;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
return $this->bus->dispatch(
|
||||
new CreateTag(RequestUtil::getActor($request), Arr::get($request->getParsedBody(), 'data', []))
|
||||
);
|
||||
}
|
||||
}
|
43
extensions/tags/src/Api/Controller/DeleteTagController.php
Normal file
43
extensions/tags/src/Api/Controller/DeleteTagController.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed 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\AbstractDeleteController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Tags\Command\DeleteTag;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class DeleteTagController extends AbstractDeleteController
|
||||
{
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
protected $bus;
|
||||
|
||||
/**
|
||||
* @param Dispatcher $bus
|
||||
*/
|
||||
public function __construct(Dispatcher $bus)
|
||||
{
|
||||
$this->bus = $bus;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function delete(ServerRequestInterface $request)
|
||||
{
|
||||
$this->bus->dispatch(
|
||||
new DeleteTag(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request))
|
||||
);
|
||||
}
|
||||
}
|
70
extensions/tags/src/Api/Controller/ListTagsController.php
Normal file
70
extensions/tags/src/Api/Controller/ListTagsController.php
Normal file
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed 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\AbstractListController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Tags\Api\Serializer\TagSerializer;
|
||||
use Flarum\Tags\TagRepository;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ListTagsController extends AbstractListController
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $serializer = TagSerializer::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $include = [
|
||||
'parent'
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $optionalInclude = [
|
||||
'children',
|
||||
'lastPostedDiscussion',
|
||||
'state'
|
||||
];
|
||||
|
||||
/**
|
||||
* @var TagRepository
|
||||
*/
|
||||
protected $tags;
|
||||
|
||||
public function __construct(TagRepository $tags)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$include = $this->extractInclude($request);
|
||||
|
||||
if (in_array('lastPostedDiscussion', $include)) {
|
||||
$include = array_merge($include, ['lastPostedDiscussion.tags', 'lastPostedDiscussion.state']);
|
||||
}
|
||||
|
||||
return $this->tags
|
||||
->with($include, $actor)
|
||||
->whereVisibleTo($actor)
|
||||
->withStateFor($actor)
|
||||
->get();
|
||||
}
|
||||
}
|
57
extensions/tags/src/Api/Controller/OrderTagsController.php
Normal file
57
extensions/tags/src/Api/Controller/OrderTagsController.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Api\Controller;
|
||||
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Tags\Tag;
|
||||
use Illuminate\Support\Arr;
|
||||
use Laminas\Diactoros\Response\EmptyResponse;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
class OrderTagsController implements RequestHandlerInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
RequestUtil::getActor($request)->assertAdmin();
|
||||
|
||||
$order = Arr::get($request->getParsedBody(), 'order');
|
||||
|
||||
if ($order === null) {
|
||||
return new EmptyResponse(422);
|
||||
}
|
||||
|
||||
Tag::query()->update([
|
||||
'position' => null,
|
||||
'parent_id' => null
|
||||
]);
|
||||
|
||||
foreach ($order as $i => $parent) {
|
||||
$parentId = Arr::get($parent, 'id');
|
||||
|
||||
Tag::where('id', $parentId)->update(['position' => $i]);
|
||||
|
||||
if (isset($parent['children']) && is_array($parent['children'])) {
|
||||
foreach ($parent['children'] as $j => $childId) {
|
||||
Tag::where('id', $childId)->update([
|
||||
'position' => $j,
|
||||
'parent_id' => $parentId
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
}
|
59
extensions/tags/src/Api/Controller/ShowTagController.php
Normal file
59
extensions/tags/src/Api/Controller/ShowTagController.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed 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\AbstractShowController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Tags\Api\Serializer\TagSerializer;
|
||||
use Flarum\Tags\TagRepository;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ShowTagController extends AbstractShowController
|
||||
{
|
||||
public $serializer = TagSerializer::class;
|
||||
|
||||
public $optionalInclude = [
|
||||
'children',
|
||||
'children.parent',
|
||||
'lastPostedDiscussion',
|
||||
'parent',
|
||||
'parent.children',
|
||||
'parent.children.parent',
|
||||
'state'
|
||||
];
|
||||
|
||||
/**
|
||||
* @var TagRepository
|
||||
*/
|
||||
private $tags;
|
||||
|
||||
public function __construct(TagRepository $tags)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$slug = Arr::get($request->getQueryParams(), 'slug');
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$include = $this->extractInclude($request);
|
||||
|
||||
return $this->tags
|
||||
->with($include, $actor)
|
||||
->whereVisibleTo($actor)
|
||||
->where('slug', $slug)
|
||||
->firstOrFail();
|
||||
}
|
||||
}
|
54
extensions/tags/src/Api/Controller/UpdateTagController.php
Normal file
54
extensions/tags/src/Api/Controller/UpdateTagController.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed 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\AbstractShowController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Tags\Api\Serializer\TagSerializer;
|
||||
use Flarum\Tags\Command\EditTag;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class UpdateTagController extends AbstractShowController
|
||||
{
|
||||
/**
|
||||
* {@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 = Arr::get($request->getQueryParams(), 'id');
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$data = Arr::get($request->getParsedBody(), 'data', []);
|
||||
|
||||
return $this->bus->dispatch(
|
||||
new EditTag($id, $actor, $data)
|
||||
);
|
||||
}
|
||||
}
|
72
extensions/tags/src/Api/Serializer/TagSerializer.php
Normal file
72
extensions/tags/src/Api/Serializer/TagSerializer.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Api\Serializer;
|
||||
|
||||
use Flarum\Api\Serializer\AbstractSerializer;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
|
||||
class TagSerializer extends AbstractSerializer
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $type = 'tags';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getDefaultAttributes($tag)
|
||||
{
|
||||
$attributes = [
|
||||
'name' => $tag->name,
|
||||
'description' => $tag->description,
|
||||
'slug' => $tag->slug,
|
||||
'color' => $tag->color,
|
||||
'backgroundUrl' => $tag->background_path,
|
||||
'backgroundMode' => $tag->background_mode,
|
||||
'icon' => $tag->icon,
|
||||
'discussionCount' => (int) $tag->discussion_count,
|
||||
'position' => $tag->position === null ? null : (int) $tag->position,
|
||||
'defaultSort' => $tag->default_sort,
|
||||
'isChild' => (bool) $tag->parent_id,
|
||||
'isHidden' => (bool) $tag->is_hidden,
|
||||
'lastPostedAt' => $this->formatDate($tag->last_posted_at),
|
||||
'canStartDiscussion' => $this->actor->can('startDiscussion', $tag),
|
||||
'canAddToDiscussion' => $this->actor->can('addToDiscussion', $tag)
|
||||
];
|
||||
|
||||
if ($this->actor->isAdmin()) {
|
||||
$attributes['isRestricted'] = (bool) $tag->is_restricted;
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Tobscure\JsonApi\Relationship
|
||||
*/
|
||||
protected function parent($tag)
|
||||
{
|
||||
return $this->hasOne($tag, self::class);
|
||||
}
|
||||
|
||||
protected function children($tag)
|
||||
{
|
||||
return $this->hasMany($tag, self::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Tobscure\JsonApi\Relationship
|
||||
*/
|
||||
protected function lastPostedDiscussion($tag)
|
||||
{
|
||||
return $this->hasOne($tag, DiscussionSerializer::class);
|
||||
}
|
||||
}
|
39
extensions/tags/src/Command/CreateTag.php
Normal file
39
extensions/tags/src/Command/CreateTag.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Command;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
class CreateTag
|
||||
{
|
||||
/**
|
||||
* The user performing the action.
|
||||
*
|
||||
* @var User
|
||||
*/
|
||||
public $actor;
|
||||
|
||||
/**
|
||||
* The attributes of the new tag.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* @param User $actor The user performing the action.
|
||||
* @param array $data The attributes of the new tag.
|
||||
*/
|
||||
public function __construct(User $actor, array $data)
|
||||
{
|
||||
$this->actor = $actor;
|
||||
$this->data = $data;
|
||||
}
|
||||
}
|
76
extensions/tags/src/Command/CreateTagHandler.php
Normal file
76
extensions/tags/src/Command/CreateTagHandler.php
Normal file
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Command;
|
||||
|
||||
use Flarum\Tags\Event\Creating;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Tags\TagValidator;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class CreateTagHandler
|
||||
{
|
||||
/**
|
||||
* @var TagValidator
|
||||
*/
|
||||
protected $validator;
|
||||
|
||||
/**
|
||||
* @param TagValidator $validator
|
||||
*/
|
||||
public function __construct(TagValidator $validator)
|
||||
{
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CreateTag $command
|
||||
* @return Tag
|
||||
*/
|
||||
public function handle(CreateTag $command)
|
||||
{
|
||||
$actor = $command->actor;
|
||||
$data = $command->data;
|
||||
|
||||
$actor->assertCan('createTag');
|
||||
|
||||
$tag = Tag::build(
|
||||
Arr::get($data, 'attributes.name'),
|
||||
Arr::get($data, 'attributes.slug'),
|
||||
Arr::get($data, 'attributes.description'),
|
||||
Arr::get($data, 'attributes.color'),
|
||||
Arr::get($data, 'attributes.icon'),
|
||||
Arr::get($data, 'attributes.isHidden')
|
||||
);
|
||||
|
||||
$parentId = Arr::get($data, 'relationships.parent.data.id');
|
||||
$primary = Arr::get($data, 'attributes.primary');
|
||||
|
||||
if ($parentId !== null || $primary) {
|
||||
$rootTags = Tag::whereNull('parent_id')->whereNotNull('position');
|
||||
|
||||
if ($parentId === 0 || $primary) {
|
||||
$tag->position = $rootTags->max('position') + 1;
|
||||
} elseif ($rootTags->find($parentId)) {
|
||||
$position = Tag::where('parent_id', $parentId)->max('position');
|
||||
|
||||
$tag->parent()->associate($parentId);
|
||||
$tag->position = $position === null ? 0 : $position + 1;
|
||||
}
|
||||
}
|
||||
|
||||
event(new Creating($tag, $actor, $data));
|
||||
|
||||
$this->validator->assertValid($tag->getAttributes());
|
||||
|
||||
$tag->save();
|
||||
|
||||
return $tag;
|
||||
}
|
||||
}
|
50
extensions/tags/src/Command/DeleteTag.php
Normal file
50
extensions/tags/src/Command/DeleteTag.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Command;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
class DeleteTag
|
||||
{
|
||||
/**
|
||||
* The ID of the tag to delete.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $tagId;
|
||||
|
||||
/**
|
||||
* The user performing the action.
|
||||
*
|
||||
* @var User
|
||||
*/
|
||||
public $actor;
|
||||
|
||||
/**
|
||||
* Any other tag input associated with the action. This is unused by
|
||||
* default, but may be used by extensions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* @param int $tagId The ID of the tag to delete.
|
||||
* @param User $actor The user performing the action.
|
||||
* @param array $data Any other tag input associated with the action. This
|
||||
* is unused by default, but may be used by extensions.
|
||||
*/
|
||||
public function __construct($tagId, User $actor, array $data = [])
|
||||
{
|
||||
$this->tagId = $tagId;
|
||||
$this->actor = $actor;
|
||||
$this->data = $data;
|
||||
}
|
||||
}
|
49
extensions/tags/src/Command/DeleteTagHandler.php
Normal file
49
extensions/tags/src/Command/DeleteTagHandler.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Command;
|
||||
|
||||
use Flarum\Tags\Event\Deleting;
|
||||
use Flarum\Tags\TagRepository;
|
||||
|
||||
class DeleteTagHandler
|
||||
{
|
||||
/**
|
||||
* @var TagRepository
|
||||
*/
|
||||
protected $tags;
|
||||
|
||||
/**
|
||||
* @param TagRepository $tags
|
||||
*/
|
||||
public function __construct(TagRepository $tags)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DeleteTag $command
|
||||
* @return \Flarum\Tags\Tag
|
||||
* @throws \Flarum\User\Exception\PermissionDeniedException
|
||||
*/
|
||||
public function handle(DeleteTag $command)
|
||||
{
|
||||
$actor = $command->actor;
|
||||
|
||||
$tag = $this->tags->findOrFail($command->tagId, $actor);
|
||||
|
||||
$actor->assertCan('delete', $tag);
|
||||
|
||||
event(new Deleting($tag, $actor));
|
||||
|
||||
$tag->delete();
|
||||
|
||||
return $tag;
|
||||
}
|
||||
}
|
48
extensions/tags/src/Command/EditTag.php
Normal file
48
extensions/tags/src/Command/EditTag.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Command;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
class EditTag
|
||||
{
|
||||
/**
|
||||
* The ID of the tag to edit.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $tagId;
|
||||
|
||||
/**
|
||||
* The user performing the action.
|
||||
*
|
||||
* @var User
|
||||
*/
|
||||
public $actor;
|
||||
|
||||
/**
|
||||
* The attributes to update on the tag.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* @param int $tagId The ID of the tag to edit.
|
||||
* @param User $actor The user performing the action.
|
||||
* @param array $data The attributes to update on the tag.
|
||||
*/
|
||||
public function __construct($tagId, User $actor, array $data)
|
||||
{
|
||||
$this->tagId = $tagId;
|
||||
$this->actor = $actor;
|
||||
$this->data = $data;
|
||||
}
|
||||
}
|
91
extensions/tags/src/Command/EditTagHandler.php
Normal file
91
extensions/tags/src/Command/EditTagHandler.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Command;
|
||||
|
||||
use Flarum\Tags\Event\Saving;
|
||||
use Flarum\Tags\TagRepository;
|
||||
use Flarum\Tags\TagValidator;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class EditTagHandler
|
||||
{
|
||||
/**
|
||||
* @var TagRepository
|
||||
*/
|
||||
protected $tags;
|
||||
|
||||
/**
|
||||
* @var TagValidator
|
||||
*/
|
||||
protected $validator;
|
||||
|
||||
/**
|
||||
* @param TagRepository $tags
|
||||
* @param TagValidator $validator
|
||||
*/
|
||||
public function __construct(TagRepository $tags, TagValidator $validator)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EditTag $command
|
||||
* @return \Flarum\Tags\Tag
|
||||
* @throws \Flarum\User\Exception\PermissionDeniedException
|
||||
*/
|
||||
public function handle(EditTag $command)
|
||||
{
|
||||
$actor = $command->actor;
|
||||
$data = $command->data;
|
||||
|
||||
$tag = $this->tags->findOrFail($command->tagId, $actor);
|
||||
|
||||
$actor->assertCan('edit', $tag);
|
||||
|
||||
$attributes = Arr::get($data, 'attributes', []);
|
||||
|
||||
if (isset($attributes['name'])) {
|
||||
$tag->name = $attributes['name'];
|
||||
}
|
||||
|
||||
if (isset($attributes['slug'])) {
|
||||
$tag->slug = $attributes['slug'];
|
||||
}
|
||||
|
||||
if (isset($attributes['description'])) {
|
||||
$tag->description = $attributes['description'];
|
||||
}
|
||||
|
||||
if (isset($attributes['color'])) {
|
||||
$tag->color = $attributes['color'];
|
||||
}
|
||||
|
||||
if (isset($attributes['icon'])) {
|
||||
$tag->icon = $attributes['icon'];
|
||||
}
|
||||
|
||||
if (isset($attributes['isHidden'])) {
|
||||
$tag->is_hidden = (bool) $attributes['isHidden'];
|
||||
}
|
||||
|
||||
if (isset($attributes['isRestricted'])) {
|
||||
$tag->is_restricted = (bool) $attributes['isRestricted'];
|
||||
}
|
||||
|
||||
event(new Saving($tag, $actor, $data));
|
||||
|
||||
$this->validator->assertValid($tag->getDirty());
|
||||
|
||||
$tag->save();
|
||||
|
||||
return $tag;
|
||||
}
|
||||
}
|
134
extensions/tags/src/Content/Tag.php
Normal file
134
extensions/tags/src/Content/Tag.php
Normal file
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Content;
|
||||
|
||||
use Flarum\Api\Client;
|
||||
use Flarum\Frontend\Document;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Tags\TagRepository;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class Tag
|
||||
{
|
||||
/**
|
||||
* @var Client
|
||||
*/
|
||||
protected $api;
|
||||
|
||||
/**
|
||||
* @var Factory
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* @var TagRepository
|
||||
*/
|
||||
protected $tags;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* @param Client $api
|
||||
* @param Factory $view
|
||||
* @param TagRepository $tags
|
||||
* @param TranslatorInterface $translator
|
||||
*/
|
||||
public function __construct(Client $api, Factory $view, TagRepository $tags, TranslatorInterface $translator)
|
||||
{
|
||||
$this->api = $api;
|
||||
$this->view = $view;
|
||||
$this->tags = $tags;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
public function __invoke(Document $document, Request $request)
|
||||
{
|
||||
$queryParams = $request->getQueryParams();
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$slug = Arr::pull($queryParams, 'slug');
|
||||
$sort = Arr::pull($queryParams, 'sort');
|
||||
$q = Arr::pull($queryParams, 'q', '');
|
||||
$page = Arr::pull($queryParams, 'page', 1);
|
||||
$filters = Arr::pull($queryParams, 'filter', []);
|
||||
|
||||
$sortMap = $this->getSortMap();
|
||||
|
||||
$tagId = $this->tags->getIdForSlug($slug);
|
||||
$tag = $this->tags->findOrFail($tagId, $actor);
|
||||
|
||||
$params = [
|
||||
'sort' => $sort && isset($sortMap[$sort]) ? $sortMap[$sort] : '',
|
||||
'filter' => [
|
||||
'tag' => "$slug"
|
||||
],
|
||||
'page' => ['offset' => ($page - 1) * 20, 'limit' => 20]
|
||||
];
|
||||
|
||||
$params['filter'] = array_merge($filters, $params['filter']);
|
||||
|
||||
$apiDocument = $this->getApiDocument($request, $params);
|
||||
|
||||
$tagsDocument = $this->getTagsDocument($request, $slug);
|
||||
|
||||
$apiDocument->included[] = $tagsDocument->data;
|
||||
$includedTags = $tagsDocument->included ?? [];
|
||||
foreach ((array) $includedTags as $includedTag) {
|
||||
$apiDocument->included[] = $includedTag;
|
||||
}
|
||||
|
||||
$document->title = $tag->name;
|
||||
if ($tag->description) {
|
||||
$document->meta['description'] = $tag->description;
|
||||
} else {
|
||||
$document->meta['description'] = $this->translator->trans('flarum-tags.forum.tag.meta_description_text', ['{tag}' => $tag->name]);
|
||||
}
|
||||
$document->content = $this->view->make('tags::frontend.content.tag', compact('apiDocument', 'page', 'tag'));
|
||||
$document->payload['apiDocument'] = $apiDocument;
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a map of sort query param values and their API sort params.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getSortMap()
|
||||
{
|
||||
return [
|
||||
'latest' => '-lastPostedAt',
|
||||
'top' => '-commentCount',
|
||||
'newest' => '-createdAt',
|
||||
'oldest' => 'createdAt'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the result of an API request to list discussions.
|
||||
*/
|
||||
private function getApiDocument(Request $request, array $params)
|
||||
{
|
||||
return json_decode($this->api->withParentRequest($request)->withQueryParams($params)->get('/discussions')->getBody());
|
||||
}
|
||||
|
||||
private function getTagsDocument(Request $request, string $slug)
|
||||
{
|
||||
return json_decode($this->api->withParentRequest($request)->withQueryParams([
|
||||
'include' => 'children,children.parent,parent,parent.children.parent,state'
|
||||
])->get("/tags/$slug")->getBody());
|
||||
}
|
||||
}
|
109
extensions/tags/src/Content/Tags.php
Normal file
109
extensions/tags/src/Content/Tags.php
Normal file
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Content;
|
||||
|
||||
use Flarum\Api\Client;
|
||||
use Flarum\Frontend\Document;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\Tags\TagRepository;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class Tags
|
||||
{
|
||||
/**
|
||||
* @var Client
|
||||
*/
|
||||
protected $api;
|
||||
|
||||
/**
|
||||
* @var Factory
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* @var TagRepository
|
||||
*/
|
||||
protected $tags;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var UrlGenerator
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @param Client $api
|
||||
* @param Factory $view
|
||||
* @param TagRepository $tags
|
||||
* @param TranslatorInterface $translator
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param UrlGenerator $url
|
||||
*/
|
||||
public function __construct(
|
||||
Client $api,
|
||||
Factory $view,
|
||||
TagRepository $tags,
|
||||
TranslatorInterface $translator,
|
||||
SettingsRepositoryInterface $settings,
|
||||
UrlGenerator $url
|
||||
) {
|
||||
$this->api = $api;
|
||||
$this->view = $view;
|
||||
$this->tags = $tags;
|
||||
$this->settings = $settings;
|
||||
$this->translator = $translator;
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
public function __invoke(Document $document, Request $request)
|
||||
{
|
||||
$apiDocument = $this->getTagsDocument($request);
|
||||
$tags = collect(Arr::get($apiDocument, 'data', []));
|
||||
|
||||
$childTags = $tags->where('attributes.isChild', true);
|
||||
$primaryTags = $tags->where('attributes.isChild', false)->where('attributes.position', '!==', null)->sortBy('attributes.position');
|
||||
$secondaryTags = $tags->where('attributes.isChild', false)->where('attributes.position', '===', null)->sortBy('attributes.name');
|
||||
|
||||
$children = $primaryTags->mapWithKeys(function ($tag) use ($childTags) {
|
||||
$childIds = collect(Arr::get($tag, 'relationships.children.data'))->pluck('id');
|
||||
|
||||
return [$tag['id'] => $childTags->whereIn('id', $childIds)->sortBy('position')];
|
||||
});
|
||||
|
||||
$defaultRoute = $this->settings->get('default_route');
|
||||
$document->title = $this->translator->trans('flarum-tags.forum.all_tags.meta_title_text');
|
||||
$document->meta['description'] = $this->translator->trans('flarum-tags.forum.all_tags.meta_description_text');
|
||||
$document->content = $this->view->make('tags::frontend.content.tags', compact('primaryTags', 'secondaryTags', 'children'));
|
||||
$document->canonicalUrl = $this->url->to('forum')->base().($defaultRoute === '/tags' ? '' : $request->getUri()->getPath());
|
||||
$document->payload['apiDocument'] = $apiDocument;
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
private function getTagsDocument(Request $request)
|
||||
{
|
||||
return json_decode($this->api->withParentRequest($request)->withQueryParams([
|
||||
'include' => 'children,lastPostedDiscussion,parent'
|
||||
])->get('/tags')->getBody(), true);
|
||||
}
|
||||
}
|
43
extensions/tags/src/Event/Creating.php
Normal file
43
extensions/tags/src/Event/Creating.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Event;
|
||||
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\User;
|
||||
|
||||
class Creating
|
||||
{
|
||||
/**
|
||||
* @var Tag
|
||||
*/
|
||||
public $tag;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
public $actor;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* @param Tag $tag
|
||||
* @param User $actor
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(Tag $tag, User $actor, array $data)
|
||||
{
|
||||
$this->tag = $tag;
|
||||
$this->actor = $actor;
|
||||
$this->data = $data;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user