mirror of
https://github.com/flarum/core.git
synced 2025-08-06 08:27:42 +02:00
feat: revamp search (#3893)
* refactor: move gambits to frontend (#3885) * refactor: move gambits to frontend * test: GambitManager * refactor: merge filterer and searcher concepts (#3892) * chore: drop remaining backend regex gambits * refactor: merge filterer & searcher concept * refactor: adapt extenders * refactor: no longer need to push gambits to `q` * refactor: filters to gambits * refactor: drop shred `Query` namespace * chore: cleanup * chore: leftover gambit references on the backend (#3894) * chore: leftover gambit references on the backend * chore: namespace * feat: search driver backend extension API (#3902) * feat: first iteration of search drivers * feat: indexer API & tweaks * feat: changes after POC driver * fix: properly fire custom observables * chore: remove debugging code * fix: phpstan * fix: custom eloquent events * chore: drop POC usage * test: indexer extender API * fix: extension searcher fails without filters * fix: phpstan * fix: frontend created gambit * feat: advanced page and localized driver settings (#3905) * feat: allow getting total search results and replacing filters (#3906) * feat: allow accessing total search results * feat: allow replacing filters * chore: phpstan
This commit is contained in:
@@ -19,9 +19,10 @@ use Flarum\Likes\Event\PostWasUnliked;
|
||||
use Flarum\Likes\Notification\PostLikedBlueprint;
|
||||
use Flarum\Likes\Query\LikedByFilter;
|
||||
use Flarum\Likes\Query\LikedFilter;
|
||||
use Flarum\Post\Filter\PostFilterer;
|
||||
use Flarum\Post\Filter\PostSearcher;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\Filter\UserFilterer;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\User\Search\UserSearcher;
|
||||
use Flarum\User\User;
|
||||
|
||||
return [
|
||||
@@ -76,11 +77,9 @@ return [
|
||||
->listen(PostWasUnliked::class, Listener\SendNotificationWhenPostIsUnliked::class)
|
||||
->subscribe(Listener\SaveLikesToDatabase::class),
|
||||
|
||||
(new Extend\Filter(PostFilterer::class))
|
||||
->addFilter(LikedByFilter::class),
|
||||
|
||||
(new Extend\Filter(UserFilterer::class))
|
||||
->addFilter(LikedFilter::class),
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(PostSearcher::class, LikedByFilter::class)
|
||||
->addFilter(UserSearcher::class, LikedFilter::class),
|
||||
|
||||
(new Extend\Settings())
|
||||
->default('flarum-likes.like_own_post', true),
|
||||
|
@@ -9,10 +9,14 @@
|
||||
|
||||
namespace Flarum\Likes\Query;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Filter\ValidateFilterTrait;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class LikedByFilter implements FilterInterface
|
||||
{
|
||||
use ValidateFilterTrait;
|
||||
@@ -22,11 +26,11 @@ class LikedByFilter implements FilterInterface
|
||||
return 'likedBy';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$likedId = $this->asInt($filterValue);
|
||||
$likedId = $this->asInt($value);
|
||||
|
||||
$filterState
|
||||
$state
|
||||
->getQuery()
|
||||
->whereIn('id', function ($query) use ($likedId, $negate) {
|
||||
$query->select('post_id')
|
||||
|
@@ -9,10 +9,14 @@
|
||||
|
||||
namespace Flarum\Likes\Query;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Filter\ValidateFilterTrait;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class LikedFilter implements FilterInterface
|
||||
{
|
||||
use ValidateFilterTrait;
|
||||
@@ -22,11 +26,11 @@ class LikedFilter implements FilterInterface
|
||||
return 'liked';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$likedId = $this->asString($filterValue);
|
||||
$likedId = $this->asString($value);
|
||||
|
||||
$filterState
|
||||
$state
|
||||
->getQuery()
|
||||
->whereIn('id', function ($query) use ($likedId) {
|
||||
$query->select('user_id')
|
||||
|
@@ -11,16 +11,16 @@ use Flarum\Api\Serializer\BasicDiscussionSerializer;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Discussion\Filter\DiscussionFilterer;
|
||||
use Flarum\Discussion\Search\DiscussionSearcher;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Lock\Access;
|
||||
use Flarum\Lock\Event\DiscussionWasLocked;
|
||||
use Flarum\Lock\Event\DiscussionWasUnlocked;
|
||||
use Flarum\Lock\Filter\LockedFilter;
|
||||
use Flarum\Lock\Listener;
|
||||
use Flarum\Lock\Notification\DiscussionLockedBlueprint;
|
||||
use Flarum\Lock\Post\DiscussionLockedPost;
|
||||
use Flarum\Lock\Query\LockedFilterGambit;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
|
||||
return [
|
||||
(new Extend\Frontend('forum'))
|
||||
@@ -57,9 +57,6 @@ return [
|
||||
(new Extend\Policy())
|
||||
->modelPolicy(Discussion::class, Access\DiscussionPolicy::class),
|
||||
|
||||
(new Extend\Filter(DiscussionFilterer::class))
|
||||
->addFilter(LockedFilterGambit::class),
|
||||
|
||||
(new Extend\SimpleFlarumSearch(DiscussionSearcher::class))
|
||||
->addGambit(LockedFilterGambit::class),
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(DiscussionSearcher::class, LockedFilter::class),
|
||||
];
|
||||
|
1
extensions/lock/js/src/admin/extend.ts
Normal file
1
extensions/lock/js/src/admin/extend.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as default } from '../common/extend';
|
@@ -1,5 +1,7 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('lock', () => {
|
||||
app.extensionData.for('flarum-lock').registerPermission(
|
||||
{
|
||||
|
7
extensions/lock/js/src/common/extend.ts
Normal file
7
extensions/lock/js/src/common/extend.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import LockedGambit from './query/discussions/LockedGambit';
|
||||
|
||||
export default [
|
||||
new Extend.Search() //
|
||||
.gambit('discussions', LockedGambit),
|
||||
];
|
@@ -0,0 +1,23 @@
|
||||
import IGambit from 'flarum/common/query/IGambit';
|
||||
|
||||
export default class LockedGambit implements IGambit {
|
||||
pattern(): string {
|
||||
return 'is:locked';
|
||||
}
|
||||
|
||||
toFilter(_matches: string[], negate: boolean): Record<string, any> {
|
||||
const key = (negate ? '-' : '') + 'locked';
|
||||
|
||||
return {
|
||||
[key]: true,
|
||||
};
|
||||
}
|
||||
|
||||
filterKey(): string {
|
||||
return 'locked';
|
||||
}
|
||||
|
||||
fromFilter(value: string, negate: boolean): string {
|
||||
return `${negate ? '-' : ''}is:locked`;
|
||||
}
|
||||
}
|
@@ -2,7 +2,11 @@ import Extend from 'flarum/common/extenders';
|
||||
import Discussion from 'flarum/common/models/Discussion';
|
||||
import DiscussionLockedPost from './components/DiscussionLockedPost';
|
||||
|
||||
import commonExtend from '../common/extend';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
new Extend.PostTypes() //
|
||||
.add('discussionLocked', DiscussionLockedPost),
|
||||
|
||||
|
36
extensions/lock/src/Filter/LockedFilter.php
Normal file
36
extensions/lock/src/Filter/LockedFilter.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\Lock\Filter;
|
||||
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class LockedFilter implements FilterInterface
|
||||
{
|
||||
public function getFilterKey(): string
|
||||
{
|
||||
return 'locked';
|
||||
}
|
||||
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$this->constrain($state->getQuery(), $negate);
|
||||
}
|
||||
|
||||
protected function constrain(Builder $query, bool $negate): void
|
||||
{
|
||||
$query->where('is_locked', ! $negate);
|
||||
}
|
||||
}
|
@@ -1,44 +0,0 @@
|
||||
<?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\Lock\Query;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Search\AbstractRegexGambit;
|
||||
use Flarum\Search\SearchState;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
class LockedFilterGambit extends AbstractRegexGambit implements FilterInterface
|
||||
{
|
||||
protected function getGambitPattern(): string
|
||||
{
|
||||
return 'is:locked';
|
||||
}
|
||||
|
||||
protected function conditions(SearchState $search, array $matches, bool $negate): void
|
||||
{
|
||||
$this->constrain($search->getQuery(), $negate);
|
||||
}
|
||||
|
||||
public function getFilterKey(): string
|
||||
{
|
||||
return 'locked';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
{
|
||||
$this->constrain($filterState->getQuery(), $negate);
|
||||
}
|
||||
|
||||
protected function constrain(Builder $query, bool $negate): void
|
||||
{
|
||||
$query->where('is_locked', ! $negate);
|
||||
}
|
||||
}
|
@@ -24,8 +24,9 @@ use Flarum\Post\Event\Hidden;
|
||||
use Flarum\Post\Event\Posted;
|
||||
use Flarum\Post\Event\Restored;
|
||||
use Flarum\Post\Event\Revised;
|
||||
use Flarum\Post\Filter\PostFilterer;
|
||||
use Flarum\Post\Filter\PostSearcher;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\Tags\Api\Serializer\TagSerializer;
|
||||
use Flarum\User\User;
|
||||
|
||||
@@ -114,9 +115,9 @@ return [
|
||||
->listen(Hidden::class, Listener\UpdateMentionsMetadataWhenInvisible::class)
|
||||
->listen(Deleted::class, Listener\UpdateMentionsMetadataWhenInvisible::class),
|
||||
|
||||
(new Extend\Filter(PostFilterer::class))
|
||||
->addFilter(Filter\MentionedFilter::class)
|
||||
->addFilter(Filter\MentionedPostFilter::class),
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(PostSearcher::class, Filter\MentionedFilter::class)
|
||||
->addFilter(PostSearcher::class, Filter\MentionedPostFilter::class),
|
||||
|
||||
(new Extend\ApiSerializer(CurrentUserSerializer::class))
|
||||
->attribute('canMentionGroups', function (CurrentUserSerializer $serializer, User $user): bool {
|
||||
|
@@ -9,10 +9,14 @@
|
||||
|
||||
namespace Flarum\Mentions\Filter;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Filter\ValidateFilterTrait;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class MentionedFilter implements FilterInterface
|
||||
{
|
||||
use ValidateFilterTrait;
|
||||
@@ -22,11 +26,11 @@ class MentionedFilter implements FilterInterface
|
||||
return 'mentioned';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$mentionedId = $this->asInt($filterValue);
|
||||
$mentionedId = $this->asInt($value);
|
||||
|
||||
$filterState
|
||||
$state
|
||||
->getQuery()
|
||||
->join('post_mentions_user', 'posts.id', '=', 'post_mentions_user.post_id')
|
||||
->where('post_mentions_user.mentions_user_id', $negate ? '!=' : '=', $mentionedId);
|
||||
|
@@ -9,9 +9,13 @@
|
||||
|
||||
namespace Flarum\Mentions\Filter;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class MentionedPostFilter implements FilterInterface
|
||||
{
|
||||
public function getFilterKey(): string
|
||||
@@ -19,11 +23,11 @@ class MentionedPostFilter implements FilterInterface
|
||||
return 'mentionedPost';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$mentionedId = trim($filterValue, '"');
|
||||
$mentionedId = trim($value, '"');
|
||||
|
||||
$filterState
|
||||
$state
|
||||
->getQuery()
|
||||
->join('post_mentions_post', 'posts.id', '=', 'post_mentions_post.post_id')
|
||||
->where('post_mentions_post.mentions_post_id', $negate ? '!=' : '=', $mentionedId);
|
||||
|
@@ -12,6 +12,7 @@ namespace Flarum\Nicknames;
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Nicknames\Access\UserPolicy;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\User\Event\Saving;
|
||||
use Flarum\User\Search\UserSearcher;
|
||||
use Flarum\User\User;
|
||||
@@ -52,8 +53,8 @@ return [
|
||||
(new Extend\Validator(UserValidator::class))
|
||||
->configure(AddNicknameValidation::class),
|
||||
|
||||
(new Extend\SimpleFlarumSearch(UserSearcher::class))
|
||||
->setFullTextGambit(NicknameFullTextGambit::class),
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->setFulltext(UserSearcher::class, NicknameFullTextFilter::class),
|
||||
|
||||
(new Extend\Policy())
|
||||
->modelPolicy(User::class, UserPolicy::class),
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import app from 'flarum/admin/app';
|
||||
import Alert from 'flarum/common/components/Alert';
|
||||
import Link from 'flarum/common/components/Link';
|
||||
import BasicsPage from 'flarum/admin/components/BasicsPage';
|
||||
import extractText from 'flarum/common/utils/extractText';
|
||||
import { extend } from 'flarum/common/extend';
|
||||
|
||||
app.initializers.add('flarum/nicknames', () => {
|
||||
app.extensionData
|
||||
@@ -55,4 +58,8 @@ app.initializers.add('flarum/nicknames', () => {
|
||||
},
|
||||
'start'
|
||||
);
|
||||
|
||||
extend(BasicsPage.prototype, 'driverLocale', function (locale) {
|
||||
locale.display_name['nickname'] = extractText(app.translator.trans('flarum-nicknames.admin.basics.display_name_driver_options.nickname'));
|
||||
});
|
||||
});
|
||||
|
@@ -1,5 +1,8 @@
|
||||
flarum-nicknames:
|
||||
admin:
|
||||
basics:
|
||||
display_name_driver_options:
|
||||
nickname: Nickname
|
||||
permissions:
|
||||
edit_own_nickname_label: Edit own nickname
|
||||
settings:
|
||||
|
@@ -9,19 +9,16 @@
|
||||
|
||||
namespace Flarum\Nicknames;
|
||||
|
||||
/*
|
||||
* 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\Search\GambitInterface;
|
||||
use Flarum\Search\AbstractFulltextFilter;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\User\UserRepository;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class NicknameFullTextGambit implements GambitInterface
|
||||
/**
|
||||
* @extends AbstractFulltextFilter<DatabaseSearchState>
|
||||
*/
|
||||
class NicknameFullTextFilter extends AbstractFulltextFilter
|
||||
{
|
||||
public function __construct(
|
||||
protected UserRepository $users
|
||||
@@ -37,14 +34,12 @@ class NicknameFullTextGambit implements GambitInterface
|
||||
->orWhere('nickname', 'like', "{$searchValue}%");
|
||||
}
|
||||
|
||||
public function apply(SearchState $search, string $bit): bool
|
||||
public function search(SearchState $state, string $value): void
|
||||
{
|
||||
$search->getQuery()
|
||||
$state->getQuery()
|
||||
->whereIn(
|
||||
'id',
|
||||
$this->getUserSearchSubQuery($bit)
|
||||
$this->getUserSearchSubQuery($value)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -11,16 +11,16 @@ use Flarum\Api\Controller\ListDiscussionsController;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Discussion\Filter\DiscussionFilterer;
|
||||
use Flarum\Discussion\Search\DiscussionSearcher;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\Sticky\Event\DiscussionWasStickied;
|
||||
use Flarum\Sticky\Event\DiscussionWasUnstickied;
|
||||
use Flarum\Sticky\Listener;
|
||||
use Flarum\Sticky\Listener\SaveStickyToDatabase;
|
||||
use Flarum\Sticky\PinStickiedDiscussionsToTop;
|
||||
use Flarum\Sticky\Post\DiscussionStickiedPost;
|
||||
use Flarum\Sticky\Query\StickyFilterGambit;
|
||||
use Flarum\Sticky\Query\StickyFilter;
|
||||
|
||||
return [
|
||||
(new Extend\Frontend('forum'))
|
||||
@@ -54,10 +54,7 @@ return [
|
||||
->listen(DiscussionWasStickied::class, [Listener\CreatePostWhenDiscussionIsStickied::class, 'whenDiscussionWasStickied'])
|
||||
->listen(DiscussionWasUnstickied::class, [Listener\CreatePostWhenDiscussionIsStickied::class, 'whenDiscussionWasUnstickied']),
|
||||
|
||||
(new Extend\Filter(DiscussionFilterer::class))
|
||||
->addFilter(StickyFilterGambit::class)
|
||||
->addFilterMutator(PinStickiedDiscussionsToTop::class),
|
||||
|
||||
(new Extend\SimpleFlarumSearch(DiscussionSearcher::class))
|
||||
->addGambit(StickyFilterGambit::class),
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(DiscussionSearcher::class, StickyFilter::class)
|
||||
->addMutator(DiscussionSearcher::class, PinStickiedDiscussionsToTop::class),
|
||||
];
|
||||
|
1
extensions/sticky/js/src/admin/extend.ts
Normal file
1
extensions/sticky/js/src/admin/extend.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as default } from '../common/extend';
|
@@ -1,5 +1,7 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-sticky', () => {
|
||||
app.extensionData.for('flarum-sticky').registerPermission(
|
||||
{
|
||||
|
7
extensions/sticky/js/src/common/extend.ts
Normal file
7
extensions/sticky/js/src/common/extend.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import StickyGambit from './query/discussions/StickyGambit';
|
||||
|
||||
export default [
|
||||
new Extend.Search() //
|
||||
.gambit('discussions', StickyGambit),
|
||||
];
|
@@ -0,0 +1,23 @@
|
||||
import IGambit from 'flarum/common/query/IGambit';
|
||||
|
||||
export default class StickyGambit implements IGambit {
|
||||
pattern(): string {
|
||||
return 'is:sticky';
|
||||
}
|
||||
|
||||
toFilter(_matches: string[], negate: boolean): Record<string, any> {
|
||||
const key = (negate ? '-' : '') + 'sticky';
|
||||
|
||||
return {
|
||||
[key]: true,
|
||||
};
|
||||
}
|
||||
|
||||
filterKey(): string {
|
||||
return 'sticky';
|
||||
}
|
||||
|
||||
fromFilter(value: string, negate: boolean): string {
|
||||
return `${negate ? '-' : ''}is:sticky`;
|
||||
}
|
||||
}
|
@@ -2,7 +2,11 @@ import Extend from 'flarum/common/extenders';
|
||||
import Discussion from 'flarum/common/models/Discussion';
|
||||
import DiscussionStickiedPost from './components/DiscussionStickiedPost';
|
||||
|
||||
import commonExtend from '../common/extend';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
new Extend.PostTypes() //
|
||||
.add('discussionStickied', DiscussionStickiedPost),
|
||||
|
||||
|
@@ -9,23 +9,23 @@
|
||||
|
||||
namespace Flarum\Sticky;
|
||||
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Query\QueryCriteria;
|
||||
use Flarum\Tags\Query\TagFilterGambit;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Tags\Search\Filter\TagFilter;
|
||||
|
||||
class PinStickiedDiscussionsToTop
|
||||
{
|
||||
public function __invoke(FilterState $filterState, QueryCriteria $criteria): void
|
||||
public function __invoke(DatabaseSearchState $state, SearchCriteria $criteria): void
|
||||
{
|
||||
if ($criteria->sortIsDefault) {
|
||||
$query = $filterState->getQuery();
|
||||
if ($criteria->sortIsDefault && ! $state->isFulltextSearch()) {
|
||||
$query = $state->getQuery();
|
||||
|
||||
// If we are viewing a specific tag, then pin all stickied
|
||||
// discussions to the top no matter what.
|
||||
$filters = $filterState->getActiveFilters();
|
||||
$filters = $state->getActiveFilters();
|
||||
|
||||
if ($count = count($filters)) {
|
||||
if ($count === 1 && $filters[0] instanceof TagFilterGambit) {
|
||||
if ($count === 1 && $filters[0] instanceof TagFilter) {
|
||||
if (! is_array($query->orders)) {
|
||||
$query->orders = [];
|
||||
}
|
||||
@@ -51,14 +51,14 @@ class PinStickiedDiscussionsToTop
|
||||
->selectRaw('1')
|
||||
->from('discussion_user as sticky')
|
||||
->whereColumn('sticky.discussion_id', 'id')
|
||||
->where('sticky.user_id', '=', $filterState->getActor()->id)
|
||||
->where('sticky.user_id', '=', $state->getActor()->id)
|
||||
->whereColumn('sticky.last_read_post_number', '>=', 'last_post_number');
|
||||
|
||||
// Add the bindings manually (rather than as the second
|
||||
// argument in orderByRaw) for now due to a bug in Laravel which
|
||||
// would add the bindings in the wrong order.
|
||||
$query->orderByRaw('is_sticky and not exists ('.$read->toSql().') and last_posted_at > ? desc')
|
||||
->addBinding(array_merge($read->getBindings(), [$filterState->getActor()->marked_all_as_read_at ?: 0]), 'union');
|
||||
->addBinding(array_merge($read->getBindings(), [$state->getActor()->marked_all_as_read_at ?: 0]), 'union');
|
||||
|
||||
$query->unionOrders = array_merge($query->unionOrders, $query->orders);
|
||||
$query->unionLimit = $query->limit;
|
||||
|
36
extensions/sticky/src/Query/StickyFilter.php
Normal file
36
extensions/sticky/src/Query/StickyFilter.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\Sticky\Query;
|
||||
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class StickyFilter implements FilterInterface
|
||||
{
|
||||
public function getFilterKey(): string
|
||||
{
|
||||
return 'sticky';
|
||||
}
|
||||
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$this->constrain($state->getQuery(), $negate);
|
||||
}
|
||||
|
||||
protected function constrain(Builder $query, bool $negate): void
|
||||
{
|
||||
$query->where('is_sticky', ! $negate);
|
||||
}
|
||||
}
|
@@ -1,44 +0,0 @@
|
||||
<?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\Sticky\Query;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Search\AbstractRegexGambit;
|
||||
use Flarum\Search\SearchState;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
class StickyFilterGambit extends AbstractRegexGambit implements FilterInterface
|
||||
{
|
||||
protected function getGambitPattern(): string
|
||||
{
|
||||
return 'is:sticky';
|
||||
}
|
||||
|
||||
protected function conditions(SearchState $search, array $matches, bool $negate): void
|
||||
{
|
||||
$this->constrain($search->getQuery(), $negate);
|
||||
}
|
||||
|
||||
public function getFilterKey(): string
|
||||
{
|
||||
return 'sticky';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
{
|
||||
$this->constrain($filterState->getQuery(), $negate);
|
||||
}
|
||||
|
||||
protected function constrain(Builder $query, bool $negate): void
|
||||
{
|
||||
$query->where('is_sticky', ! $negate);
|
||||
}
|
||||
}
|
@@ -12,7 +12,6 @@ use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Approval\Event\PostWasApproved;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Discussion\Filter\DiscussionFilterer;
|
||||
use Flarum\Discussion\Search\DiscussionSearcher;
|
||||
use Flarum\Discussion\UserState;
|
||||
use Flarum\Extend;
|
||||
@@ -20,14 +19,18 @@ use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Event\Hidden;
|
||||
use Flarum\Post\Event\Posted;
|
||||
use Flarum\Post\Event\Restored;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\Subscriptions\Filter\SubscriptionFilter;
|
||||
use Flarum\Subscriptions\HideIgnoredFromAllDiscussionsPage;
|
||||
use Flarum\Subscriptions\Listener;
|
||||
use Flarum\Subscriptions\Notification\FilterVisiblePostsBeforeSending;
|
||||
use Flarum\Subscriptions\Notification\NewPostBlueprint;
|
||||
use Flarum\Subscriptions\Query\SubscriptionFilterGambit;
|
||||
use Flarum\User\User;
|
||||
|
||||
return [
|
||||
(new Extend\Frontend('admin'))
|
||||
->js(__DIR__.'/js/dist/admin.js'),
|
||||
|
||||
(new Extend\Frontend('forum'))
|
||||
->js(__DIR__.'/js/dist/forum.js')
|
||||
->css(__DIR__.'/less/forum.less')
|
||||
@@ -67,12 +70,9 @@ return [
|
||||
->listen(Deleted::class, Listener\DeleteNotificationWhenPostIsHiddenOrDeleted::class)
|
||||
->listen(Posted::class, Listener\FollowAfterReply::class),
|
||||
|
||||
(new Extend\Filter(DiscussionFilterer::class))
|
||||
->addFilter(SubscriptionFilterGambit::class)
|
||||
->addFilterMutator(HideIgnoredFromAllDiscussionsPage::class),
|
||||
|
||||
(new Extend\SimpleFlarumSearch(DiscussionSearcher::class))
|
||||
->addGambit(SubscriptionFilterGambit::class),
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(DiscussionSearcher::class, SubscriptionFilter::class)
|
||||
->addMutator(DiscussionSearcher::class, HideIgnoredFromAllDiscussionsPage::class),
|
||||
|
||||
(new Extend\User())
|
||||
->registerPreference('flarum-subscriptions.notify_for_all_posts', 'boolval', false),
|
||||
|
1
extensions/subscriptions/js/admin.ts
Normal file
1
extensions/subscriptions/js/admin.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './src/admin';
|
1
extensions/subscriptions/js/src/admin/extend.ts
Normal file
1
extensions/subscriptions/js/src/admin/extend.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as default } from '../common/extend';
|
1
extensions/subscriptions/js/src/admin/index.ts
Normal file
1
extensions/subscriptions/js/src/admin/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as extend } from './extend';
|
7
extensions/subscriptions/js/src/common/extend.ts
Normal file
7
extensions/subscriptions/js/src/common/extend.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import SubscriptionGambit from './query/discussions/SubscriptionGambit';
|
||||
|
||||
export default [
|
||||
new Extend.Search() //
|
||||
.gambit('discussions', SubscriptionGambit),
|
||||
];
|
@@ -0,0 +1,23 @@
|
||||
import IGambit from 'flarum/common/query/IGambit';
|
||||
|
||||
export default class SubscriptionGambit implements IGambit {
|
||||
pattern(): string {
|
||||
return 'is:(follow|ignor)(?:ing|ed)';
|
||||
}
|
||||
|
||||
toFilter(matches: string[], negate: boolean): Record<string, any> {
|
||||
const type = matches[1] === 'follow' ? 'following' : 'ignoring';
|
||||
|
||||
return {
|
||||
subscription: type,
|
||||
};
|
||||
}
|
||||
|
||||
filterKey(): string {
|
||||
return 'subscription';
|
||||
}
|
||||
|
||||
fromFilter(value: string, negate: boolean): string {
|
||||
return `${negate ? '-' : ''}is:${value}`;
|
||||
}
|
||||
}
|
@@ -36,12 +36,7 @@ export default function addSubscriptionFilter() {
|
||||
extend(DiscussionListState.prototype, 'requestParams', function (params) {
|
||||
if (this.params.onFollowing) {
|
||||
params.filter ||= {};
|
||||
|
||||
if (params.filter.q) {
|
||||
params.filter.q += ' is:following';
|
||||
} else {
|
||||
params.filter.subscription = 'following';
|
||||
}
|
||||
params.filter.subscription = 'following';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -2,7 +2,11 @@ import Extend from 'flarum/common/extenders';
|
||||
import IndexPage from 'flarum/forum/components/IndexPage';
|
||||
import Discussion from 'flarum/common/models/Discussion';
|
||||
|
||||
import commonExtend from '../common/extend';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
new Extend.Routes() //
|
||||
.add('following', '/following', IndexPage),
|
||||
|
||||
|
@@ -7,42 +7,34 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Subscriptions\Query;
|
||||
namespace Flarum\Subscriptions\Filter;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Filter\ValidateFilterTrait;
|
||||
use Flarum\Search\AbstractRegexGambit;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
class SubscriptionFilterGambit extends AbstractRegexGambit implements FilterInterface
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class SubscriptionFilter implements FilterInterface
|
||||
{
|
||||
use ValidateFilterTrait;
|
||||
|
||||
protected function getGambitPattern(): string
|
||||
{
|
||||
return 'is:(follow|ignor)(?:ing|ed)';
|
||||
}
|
||||
|
||||
protected function conditions(SearchState $search, array $matches, bool $negate): void
|
||||
{
|
||||
$this->constrain($search->getQuery(), $search->getActor(), $matches[1], $negate);
|
||||
}
|
||||
|
||||
public function getFilterKey(): string
|
||||
{
|
||||
return 'subscription';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$filterValue = $this->asString($filterValue);
|
||||
$value = $this->asString($value);
|
||||
|
||||
preg_match('/^'.$this->getGambitPattern().'$/i', 'is:'.$filterValue, $matches);
|
||||
preg_match('/^(follow|ignor)(?:ing|ed)$/i', $value, $matches);
|
||||
|
||||
$this->constrain($filterState->getQuery(), $filterState->getActor(), $matches[1], $negate);
|
||||
$this->constrain($state->getQuery(), $state->getActor(), $matches[1], $negate);
|
||||
}
|
||||
|
||||
protected function constrain(Builder $query, User $actor, string $subscriptionType, bool $negate): void
|
@@ -9,18 +9,18 @@
|
||||
|
||||
namespace Flarum\Subscriptions;
|
||||
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Query\QueryCriteria;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
|
||||
class HideIgnoredFromAllDiscussionsPage
|
||||
{
|
||||
public function __invoke(FilterState $filterState, QueryCriteria $criteria): void
|
||||
public function __invoke(DatabaseSearchState $state, SearchCriteria $criteria): void
|
||||
{
|
||||
// We only want to hide on the "all discussions" page.
|
||||
if (count($filterState->getActiveFilters()) === 0) {
|
||||
if (count($state->getActiveFilters()) === 0 && ! $state->isFulltextSearch()) {
|
||||
// TODO: might be better as `id IN (subquery)`?
|
||||
$actor = $filterState->getActor();
|
||||
$filterState->getQuery()->whereNotExists(function ($query) use ($actor) {
|
||||
$actor = $state->getActor();
|
||||
$state->getQuery()->whereNotExists(function ($query) use ($actor) {
|
||||
$query->selectRaw(1)
|
||||
->from('discussion_user')
|
||||
->whereColumn('discussions.id', 'discussion_id')
|
||||
|
@@ -10,6 +10,7 @@
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\Suspend\Access\UserPolicy;
|
||||
use Flarum\Suspend\AddUserSuspendAttributes;
|
||||
use Flarum\Suspend\Event\Suspended;
|
||||
@@ -17,10 +18,9 @@ use Flarum\Suspend\Event\Unsuspended;
|
||||
use Flarum\Suspend\Listener;
|
||||
use Flarum\Suspend\Notification\UserSuspendedBlueprint;
|
||||
use Flarum\Suspend\Notification\UserUnsuspendedBlueprint;
|
||||
use Flarum\Suspend\Query\SuspendedFilterGambit;
|
||||
use Flarum\Suspend\Query\SuspendedFilter;
|
||||
use Flarum\Suspend\RevokeAccessFromSuspendedUsers;
|
||||
use Flarum\User\Event\Saving;
|
||||
use Flarum\User\Filter\UserFilterer;
|
||||
use Flarum\User\Search\UserSearcher;
|
||||
use Flarum\User\User;
|
||||
|
||||
@@ -58,11 +58,8 @@ return [
|
||||
(new Extend\User())
|
||||
->permissionGroups(RevokeAccessFromSuspendedUsers::class),
|
||||
|
||||
(new Extend\Filter(UserFilterer::class))
|
||||
->addFilter(SuspendedFilterGambit::class),
|
||||
|
||||
(new Extend\SimpleFlarumSearch(UserSearcher::class))
|
||||
->addGambit(SuspendedFilterGambit::class),
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(UserSearcher::class, SuspendedFilter::class),
|
||||
|
||||
(new Extend\View())
|
||||
->namespace('flarum-suspend', __DIR__.'/views'),
|
||||
|
1
extensions/suspend/js/src/admin/extend.ts
Normal file
1
extensions/suspend/js/src/admin/extend.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as default } from '../common/extend';
|
@@ -1,5 +1,7 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-suspend', () => {
|
||||
app.extensionData.for('flarum-suspend').registerPermission(
|
||||
{
|
||||
|
7
extensions/suspend/js/src/common/extend.ts
Normal file
7
extensions/suspend/js/src/common/extend.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import SuspendedGambit from './query/users/SuspendedGambit';
|
||||
|
||||
export default [
|
||||
new Extend.Search() //
|
||||
.gambit('users', SuspendedGambit),
|
||||
];
|
@@ -0,0 +1,23 @@
|
||||
import IGambit from 'flarum/common/query/IGambit';
|
||||
|
||||
export default class SuspendedGambit implements IGambit {
|
||||
pattern(): string {
|
||||
return 'is:suspended';
|
||||
}
|
||||
|
||||
toFilter(_matches: string[], negate: boolean): Record<string, any> {
|
||||
const key = (negate ? '-' : '') + 'suspended';
|
||||
|
||||
return {
|
||||
[key]: true,
|
||||
};
|
||||
}
|
||||
|
||||
filterKey(): string {
|
||||
return 'suspended';
|
||||
}
|
||||
|
||||
fromFilter(value: string, negate: boolean): string {
|
||||
return `${negate ? '-' : ''}is:suspended`;
|
||||
}
|
||||
}
|
@@ -2,10 +2,14 @@ import Extend from 'flarum/common/extenders';
|
||||
import User from 'flarum/common/models/User';
|
||||
import Model from 'flarum/common/Model';
|
||||
|
||||
import commonExtend from '../common/extend';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
new Extend.Model(User)
|
||||
.attribute<boolean>('canSuspend')
|
||||
.attribute<Date, string | null | undefined>('suspendedUntil', Model.transformDate)
|
||||
.attribute<Date | null | undefined, string | null | undefined>('suspendedUntil', Model.transformDate)
|
||||
.attribute<string | null | undefined>('suspendReason')
|
||||
.attribute<string | null | undefined>('suspendMessage'),
|
||||
];
|
||||
|
@@ -10,52 +10,35 @@
|
||||
namespace Flarum\Suspend\Query;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Search\AbstractRegexGambit;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\User\Guest;
|
||||
use Flarum\User\UserRepository;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
class SuspendedFilterGambit extends AbstractRegexGambit implements FilterInterface
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class SuspendedFilter implements FilterInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected UserRepository $users
|
||||
) {
|
||||
}
|
||||
|
||||
protected function getGambitPattern(): string
|
||||
{
|
||||
return 'is:suspended';
|
||||
}
|
||||
|
||||
public function apply(SearchState $search, string $bit): bool
|
||||
{
|
||||
if (! $search->getActor()->can('suspend', new Guest())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::apply($search, $bit);
|
||||
}
|
||||
|
||||
protected function conditions(SearchState $search, array $matches, bool $negate): void
|
||||
{
|
||||
$this->constrain($search->getQuery(), $negate);
|
||||
}
|
||||
|
||||
public function getFilterKey(): string
|
||||
{
|
||||
return 'suspended';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
if (! $filterState->getActor()->can('suspend', new Guest())) {
|
||||
if (! $state->getActor()->can('suspend', new Guest())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->constrain($filterState->getQuery(), $negate);
|
||||
$this->constrain($state->getQuery(), $negate);
|
||||
}
|
||||
|
||||
protected function constrain(Builder $query, bool $negate): void
|
@@ -13,25 +13,25 @@ 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\Post\Filter\PostSearcher;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
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\Search\Gambit\FulltextGambit;
|
||||
use Flarum\Tags\Search\Filter\PostTagFilter;
|
||||
use Flarum\Tags\Search\Filter\TagFilter;
|
||||
use Flarum\Tags\Search\FulltextFilter;
|
||||
use Flarum\Tags\Search\HideHiddenTagsFromAllDiscussionsPage;
|
||||
use Flarum\Tags\Search\TagSearcher;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Tags\Utf8SlugDriver;
|
||||
@@ -135,18 +135,12 @@ return [
|
||||
->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),
|
||||
|
||||
(new Extend\SimpleFlarumSearch(TagSearcher::class))
|
||||
->setFullTextGambit(FullTextGambit::class),
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(PostSearcher::class, PostTagFilter::class)
|
||||
->addFilter(DiscussionSearcher::class, TagFilter::class)
|
||||
->addMutator(DiscussionSearcher::class, HideHiddenTagsFromAllDiscussionsPage::class)
|
||||
->addSearcher(Tag::class, TagSearcher::class)
|
||||
->setFulltext(TagSearcher::class, FulltextFilter::class),
|
||||
|
||||
(new Extend\ModelUrl(Tag::class))
|
||||
->addSlugDriver('default', Utf8SlugDriver::class),
|
||||
|
@@ -10,6 +10,7 @@ import Form from 'flarum/common/components/Form';
|
||||
import EditTagModal from './EditTagModal';
|
||||
import tagIcon from '../../common/helpers/tagIcon';
|
||||
import sortTags from '../../common/utils/sortTags';
|
||||
import FormSectionGroup, { FormSection } from '@flarum/core/src/admin/components/FormSectionGroup';
|
||||
|
||||
function tagItem(tag) {
|
||||
return (
|
||||
@@ -66,17 +67,15 @@ export default class TagsPage extends ExtensionPage {
|
||||
<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>
|
||||
<FormSectionGroup>
|
||||
<FormSection className="TagGroup" label={app.translator.trans('flarum-tags.admin.tags.primary_heading')}>
|
||||
<ol className="TagList TagList--primary">{tags.filter((tag) => tag.position() !== null && !tag.isChild()).map(tagItem)}</ol>
|
||||
<Button 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')}
|
||||
</Button>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<div className="TagGroup TagGroup--secondary">
|
||||
<label>{app.translator.trans('flarum-tags.admin.tags.secondary_heading')}</label>
|
||||
<FormSection className="TagGroup TagGroup--secondary" label={app.translator.trans('flarum-tags.admin.tags.secondary_heading')}>
|
||||
<ul className="TagList">
|
||||
{tags
|
||||
.filter((tag) => tag.position() === null)
|
||||
@@ -86,41 +85,44 @@ export default class TagsPage extends ExtensionPage {
|
||||
<Button 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')}
|
||||
</Button>
|
||||
</div>
|
||||
<Form label={app.translator.trans('flarum-tags.admin.tags.settings_heading')}>
|
||||
<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} />
|
||||
</FormSection>
|
||||
|
||||
<FormSection label={app.translator.trans('flarum-tags.admin.tags.settings_heading')}>
|
||||
<Form>
|
||||
<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>
|
||||
<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 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>
|
||||
<div className="Form-group Form-controls">{this.submitButton()}</div>
|
||||
</Form>
|
||||
</div>
|
||||
<div className="Form-group Form-controls">{this.submitButton()}</div>
|
||||
</Form>
|
||||
</FormSection>
|
||||
</FormSectionGroup>
|
||||
<div className="TagsContent-footer">
|
||||
<p>{app.translator.trans('flarum-tags.admin.tags.about_tags_text')}</p>
|
||||
</div>
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import Tag from './models/Tag';
|
||||
import TagGambit from './query/discussions/TagGambit';
|
||||
|
||||
export default [
|
||||
new Extend.Store() //
|
||||
.add('tags', Tag),
|
||||
|
||||
new Extend.Search() //
|
||||
.gambit('discussions', TagGambit),
|
||||
];
|
||||
|
23
extensions/tags/js/src/common/query/discussions/TagGambit.ts
Normal file
23
extensions/tags/js/src/common/query/discussions/TagGambit.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import IGambit from 'flarum/common/query/IGambit';
|
||||
|
||||
export default class TagGambit implements IGambit {
|
||||
pattern(): string {
|
||||
return 'tag:(.+)';
|
||||
}
|
||||
|
||||
toFilter(matches: string[], negate: boolean): Record<string, any> {
|
||||
const key = (negate ? '-' : '') + 'tag';
|
||||
|
||||
return {
|
||||
[key]: matches[1].split(','),
|
||||
};
|
||||
}
|
||||
|
||||
filterKey(): string {
|
||||
return 'tag';
|
||||
}
|
||||
|
||||
fromFilter(value: string, negate: boolean): string {
|
||||
return `${negate ? '-' : ''}tag:${value}`;
|
||||
}
|
||||
}
|
@@ -124,14 +124,8 @@ export default function addTagFilter() {
|
||||
}
|
||||
|
||||
if (this.params.tags) {
|
||||
const filter = params.filter ?? {};
|
||||
filter.tag = this.params.tags;
|
||||
// TODO: replace this with a more robust system.
|
||||
const q = filter.q;
|
||||
if (q) {
|
||||
filter.q = `${q} tag:${this.params.tags}`;
|
||||
}
|
||||
params.filter = filter;
|
||||
params.filter ||= {};
|
||||
params.filter.tag = this.params.tags;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -13,7 +13,6 @@
|
||||
|
||||
.TagsContent-list {
|
||||
padding: 20px 0 0;
|
||||
|
||||
}
|
||||
|
||||
.TagList,
|
||||
@@ -22,6 +21,7 @@
|
||||
padding: 0;
|
||||
color: var(--muted-color);
|
||||
font-size: 13px;
|
||||
margin-top: 0;
|
||||
|
||||
>li {
|
||||
display: inline-block;
|
||||
@@ -80,77 +80,35 @@ li:not(.sortable-dragging)>.TagListItem-info:hover>.Button {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.SettingsGroups {
|
||||
display: flex;
|
||||
column-count: 3;
|
||||
column-gap: 30px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (@tablet-up) {
|
||||
.TagGroup--secondary {
|
||||
max-width: 250px !important;
|
||||
}
|
||||
@media (@tablet-up) {
|
||||
.TagGroup--secondary {
|
||||
max-width: 250px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.Form {
|
||||
min-width: 300px;
|
||||
max-height: 500px;
|
||||
.TagList-button {
|
||||
background: none;
|
||||
border: 1px dashed var(--control-bg);
|
||||
height: 40px;
|
||||
margin: auto auto 0 0;
|
||||
}
|
||||
|
||||
>label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.TagSettings-rangeInput {
|
||||
input {
|
||||
width: 80px;
|
||||
display: inline;
|
||||
margin: 0 5px;
|
||||
|
||||
.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 var(--control-bg);
|
||||
border-radius: var(--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 var(--control-bg);
|
||||
height: 40px;
|
||||
margin: auto auto 0 0;
|
||||
}
|
||||
|
||||
>label {
|
||||
float: left;
|
||||
font-weight: bold;
|
||||
color: var(--muted-color);
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.TagGroup {
|
||||
ol {
|
||||
> li:not(:first-child) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -12,9 +12,10 @@ namespace Flarum\Tags\Api\Controller;
|
||||
use Flarum\Api\Controller\AbstractListController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Query\QueryCriteria;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Search\SearchManager;
|
||||
use Flarum\Tags\Api\Serializer\TagSerializer;
|
||||
use Flarum\Tags\Search\TagSearcher;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Tags\TagRepository;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
@@ -35,7 +36,7 @@ class ListTagsController extends AbstractListController
|
||||
|
||||
public function __construct(
|
||||
protected TagRepository $tags,
|
||||
protected TagSearcher $searcher,
|
||||
protected SearchManager $search,
|
||||
protected UrlGenerator $url
|
||||
) {
|
||||
}
|
||||
@@ -53,7 +54,8 @@ class ListTagsController extends AbstractListController
|
||||
}
|
||||
|
||||
if (array_key_exists('q', $filters)) {
|
||||
$results = $this->searcher->search(new QueryCriteria($actor, $filters), $limit, $offset);
|
||||
$results = $this->search->query(Tag::class, new SearchCriteria($actor, $filters, $limit, $offset));
|
||||
|
||||
$tags = $results->getResults();
|
||||
|
||||
$document->addPaginationLinks(
|
||||
|
@@ -7,12 +7,16 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Filter;
|
||||
namespace Flarum\Tags\Search\Filter;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Filter\ValidateFilterTrait;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class PostTagFilter implements FilterInterface
|
||||
{
|
||||
use ValidateFilterTrait;
|
||||
@@ -22,11 +26,11 @@ class PostTagFilter implements FilterInterface
|
||||
return 'tag';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$ids = $this->asIntArray($filterValue);
|
||||
$ids = $this->asIntArray($value);
|
||||
|
||||
$filterState->getQuery()
|
||||
$state->getQuery()
|
||||
->join('discussion_tag', 'discussion_tag.discussion_id', '=', 'posts.discussion_id')
|
||||
->whereIn('discussion_tag.tag_id', $ids, 'and', $negate);
|
||||
}
|
@@ -7,20 +7,22 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Query;
|
||||
namespace Flarum\Tags\Search\Filter;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Filter\ValidateFilterTrait;
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\Search\AbstractRegexGambit;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
class TagFilterGambit extends AbstractRegexGambit implements FilterInterface
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class TagFilter implements FilterInterface
|
||||
{
|
||||
use ValidateFilterTrait;
|
||||
|
||||
@@ -29,24 +31,14 @@ class TagFilterGambit extends AbstractRegexGambit implements FilterInterface
|
||||
) {
|
||||
}
|
||||
|
||||
protected function getGambitPattern(): string
|
||||
{
|
||||
return 'tag:(.+)';
|
||||
}
|
||||
|
||||
protected function conditions(SearchState $search, array $matches, bool $negate): void
|
||||
{
|
||||
$this->constrain($search->getQuery(), $matches[1], $negate, $search->getActor());
|
||||
}
|
||||
|
||||
public function getFilterKey(): string
|
||||
{
|
||||
return 'tag';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$this->constrain($filterState->getQuery(), $filterValue, $negate, $filterState->getActor());
|
||||
$this->constrain($state->getQuery(), $value, $negate, $state->getActor());
|
||||
}
|
||||
|
||||
protected function constrain(Builder $query, string|array $rawSlugs, bool $negate, User $actor): void
|
@@ -7,14 +7,18 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Search\Gambit;
|
||||
namespace Flarum\Tags\Search;
|
||||
|
||||
use Flarum\Search\GambitInterface;
|
||||
use Flarum\Search\AbstractFulltextFilter;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Tags\TagRepository;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class FulltextGambit implements GambitInterface
|
||||
/**
|
||||
* @extends AbstractFulltextFilter<DatabaseSearchState>
|
||||
*/
|
||||
class FulltextFilter extends AbstractFulltextFilter
|
||||
{
|
||||
public function __construct(
|
||||
protected TagRepository $tags
|
||||
@@ -30,14 +34,12 @@ class FulltextGambit implements GambitInterface
|
||||
->orWhere('slug', 'like', "$searchValue%");
|
||||
}
|
||||
|
||||
public function apply(SearchState $search, string $bit): bool
|
||||
public function search(SearchState $state, string $value): void
|
||||
{
|
||||
$search->getQuery()
|
||||
$state->getQuery()
|
||||
->whereIn(
|
||||
'id',
|
||||
$this->getTagSearchSubQuery($bit)
|
||||
$this->getTagSearchSubQuery($value)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -7,21 +7,21 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tags\Filter;
|
||||
namespace Flarum\Tags\Search;
|
||||
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Query\QueryCriteria;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Tags\Tag;
|
||||
|
||||
class HideHiddenTagsFromAllDiscussionsPage
|
||||
{
|
||||
public function __invoke(FilterState $filter, QueryCriteria $queryCriteria): void
|
||||
public function __invoke(DatabaseSearchState $state, SearchCriteria $queryCriteria): void
|
||||
{
|
||||
if (count($filter->getActiveFilters()) > 0) {
|
||||
if (count($state->getActiveFilters()) > 0 || $state->isFulltextSearch()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filter->getQuery()->whereNotIn('discussions.id', function ($query) {
|
||||
$state->getQuery()->whereNotIn('discussions.id', function ($query) {
|
||||
return $query->select('discussion_id')
|
||||
->from('discussion_tag')
|
||||
->whereIn('tag_id', Tag::where('is_hidden', 1)->pluck('id'));
|
@@ -9,24 +9,15 @@
|
||||
|
||||
namespace Flarum\Tags\Search;
|
||||
|
||||
use Flarum\Search\AbstractSearcher;
|
||||
use Flarum\Search\GambitManager;
|
||||
use Flarum\Tags\TagRepository;
|
||||
use Flarum\Search\Database\AbstractSearcher;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class TagSearcher extends AbstractSearcher
|
||||
{
|
||||
public function __construct(
|
||||
protected TagRepository $tags,
|
||||
GambitManager $gambits,
|
||||
array $searchMutators
|
||||
) {
|
||||
parent::__construct($gambits, $searchMutators);
|
||||
}
|
||||
|
||||
protected function getQuery(User $actor): Builder
|
||||
public function getQuery(User $actor): Builder
|
||||
{
|
||||
return $this->tags->query()->whereVisibleTo($actor);
|
||||
return Tag::whereVisibleTo($actor)->select('tags.*');
|
||||
}
|
||||
}
|
||||
|
@@ -49,9 +49,9 @@ class ListWithFulltextSearchTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$data = json_decode($contents = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $contents);
|
||||
$this->assertEquals($expected, Arr::pluck($data, 'id'));
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user