From 336ce00f7cf34b9e0bc4f1213710e9ff81905376 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 22 Sep 2023 14:16:58 +0100 Subject: [PATCH] refactor: move gambits to frontend (#3885) * refactor: move gambits to frontend * Apply fixes from StyleCI * test: GambitManager --- extensions/lock/js/src/admin/extend.ts | 1 + extensions/lock/js/src/admin/index.js | 2 + extensions/lock/js/src/common/extend.ts | 7 +++ .../common/query/discussions/LockedGambit.ts | 15 +++++ extensions/lock/js/src/forum/extend.ts | 4 ++ extensions/sticky/js/src/admin/extend.ts | 1 + extensions/sticky/js/src/admin/index.js | 2 + extensions/sticky/js/src/common/extend.ts | 7 +++ .../common/query/discussions/StickyGambit.ts | 15 +++++ extensions/sticky/js/src/forum/extend.ts | 4 ++ extensions/subscriptions/extend.php | 3 + extensions/subscriptions/js/admin.ts | 1 + .../subscriptions/js/src/admin/extend.ts | 1 + .../subscriptions/js/src/admin/index.ts | 1 + .../subscriptions/js/src/common/extend.ts | 7 +++ .../query/discussions/SubscriptionGambit.ts | 15 +++++ .../subscriptions/js/src/forum/extend.ts | 4 ++ extensions/suspend/js/src/admin/extend.ts | 1 + extensions/suspend/js/src/admin/index.js | 2 + extensions/suspend/js/src/common/extend.ts | 7 +++ .../src/common/query/users/SuspendedGambit.ts | 15 +++++ extensions/suspend/js/src/forum/extend.ts | 6 +- extensions/tags/js/src/common/extend.ts | 4 ++ .../src/common/query/discussions/TagGambit.ts | 15 +++++ framework/core/js/src/common/GambitManager.ts | 51 ++++++++++++++++ framework/core/js/src/common/Store.ts | 13 +++- .../core/js/src/common/extenders/Search.ts | 24 ++++++++ .../core/js/src/common/extenders/index.ts | 2 + framework/core/js/src/common/query/IGambit.ts | 4 ++ .../common/query/discussions/AuthorGambit.ts | 15 +++++ .../common/query/discussions/CreatedGambit.ts | 20 +++++++ .../common/query/discussions/HiddenGambit.ts | 15 +++++ .../common/query/discussions/UnreadGambit.ts | 15 +++++ .../js/src/common/query/users/EmailGambit.ts | 15 +++++ .../js/src/common/query/users/GroupGambit.ts | 15 +++++ .../tests/unit/common/GambitManager.test.ts | 34 +++++++++++ .../AuthorFilter.php} | 16 +---- .../src/Discussion/Filter/CreatedFilter.php | 56 +++++++++++++++++ .../HiddenFilter.php} | 16 +---- .../UnreadFilter.php} | 30 ++-------- .../Discussion/Query/CreatedFilterGambit.php | 60 ------------------- .../core/src/Filter/FilterServiceProvider.php | 16 ++--- .../core/src/Search/SearchServiceProvider.php | 15 +---- .../EmailFilter.php} | 25 +------- .../GroupFilter.php} | 16 +---- 45 files changed, 441 insertions(+), 172 deletions(-) create mode 100644 extensions/lock/js/src/admin/extend.ts create mode 100644 extensions/lock/js/src/common/extend.ts create mode 100644 extensions/lock/js/src/common/query/discussions/LockedGambit.ts create mode 100644 extensions/sticky/js/src/admin/extend.ts create mode 100644 extensions/sticky/js/src/common/extend.ts create mode 100644 extensions/sticky/js/src/common/query/discussions/StickyGambit.ts create mode 100644 extensions/subscriptions/js/admin.ts create mode 100644 extensions/subscriptions/js/src/admin/extend.ts create mode 100644 extensions/subscriptions/js/src/admin/index.ts create mode 100644 extensions/subscriptions/js/src/common/extend.ts create mode 100644 extensions/subscriptions/js/src/common/query/discussions/SubscriptionGambit.ts create mode 100644 extensions/suspend/js/src/admin/extend.ts create mode 100644 extensions/suspend/js/src/common/extend.ts create mode 100644 extensions/suspend/js/src/common/query/users/SuspendedGambit.ts create mode 100644 extensions/tags/js/src/common/query/discussions/TagGambit.ts create mode 100644 framework/core/js/src/common/GambitManager.ts create mode 100644 framework/core/js/src/common/extenders/Search.ts create mode 100644 framework/core/js/src/common/query/IGambit.ts create mode 100644 framework/core/js/src/common/query/discussions/AuthorGambit.ts create mode 100644 framework/core/js/src/common/query/discussions/CreatedGambit.ts create mode 100644 framework/core/js/src/common/query/discussions/HiddenGambit.ts create mode 100644 framework/core/js/src/common/query/discussions/UnreadGambit.ts create mode 100644 framework/core/js/src/common/query/users/EmailGambit.ts create mode 100644 framework/core/js/src/common/query/users/GroupGambit.ts create mode 100644 framework/core/js/tests/unit/common/GambitManager.test.ts rename framework/core/src/Discussion/{Query/AuthorFilterGambit.php => Filter/AuthorFilter.php} (70%) create mode 100644 framework/core/src/Discussion/Filter/CreatedFilter.php rename framework/core/src/Discussion/{Query/HiddenFilterGambit.php => Filter/HiddenFilter.php} (67%) rename framework/core/src/Discussion/{Query/UnreadFilterGambit.php => Filter/UnreadFilter.php} (62%) delete mode 100644 framework/core/src/Discussion/Query/CreatedFilterGambit.php rename framework/core/src/User/{Query/EmailFilterGambit.php => Filter/EmailFilter.php} (58%) rename framework/core/src/User/{Query/GroupFilterGambit.php => Filter/GroupFilter.php} (77%) diff --git a/extensions/lock/js/src/admin/extend.ts b/extensions/lock/js/src/admin/extend.ts new file mode 100644 index 0000000000..9ce8b5f124 --- /dev/null +++ b/extensions/lock/js/src/admin/extend.ts @@ -0,0 +1 @@ +export { default as default } from '../common/extend'; diff --git a/extensions/lock/js/src/admin/index.js b/extensions/lock/js/src/admin/index.js index 55c5a208ac..f32f9daf78 100644 --- a/extensions/lock/js/src/admin/index.js +++ b/extensions/lock/js/src/admin/index.js @@ -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( { diff --git a/extensions/lock/js/src/common/extend.ts b/extensions/lock/js/src/common/extend.ts new file mode 100644 index 0000000000..8f6a32cf5b --- /dev/null +++ b/extensions/lock/js/src/common/extend.ts @@ -0,0 +1,7 @@ +import Extend from 'flarum/common/extenders'; +import LockedGambit from './query/discussions/LockedGambit'; + +export default [ + new Extend.Search() // + .gambit('discussions', LockedGambit), +]; diff --git a/extensions/lock/js/src/common/query/discussions/LockedGambit.ts b/extensions/lock/js/src/common/query/discussions/LockedGambit.ts new file mode 100644 index 0000000000..aa2f8d08ce --- /dev/null +++ b/extensions/lock/js/src/common/query/discussions/LockedGambit.ts @@ -0,0 +1,15 @@ +import IGambit from 'flarum/common/query/IGambit'; + +export default class LockedGambit implements IGambit { + pattern(): string { + return 'is:locked'; + } + + toFilter(_matches: string[], negate: boolean): Record { + const key = (negate ? '-' : '') + 'locked'; + + return { + [key]: true, + }; + } +} diff --git a/extensions/lock/js/src/forum/extend.ts b/extensions/lock/js/src/forum/extend.ts index 2e510e1054..2a65da5633 100644 --- a/extensions/lock/js/src/forum/extend.ts +++ b/extensions/lock/js/src/forum/extend.ts @@ -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), diff --git a/extensions/sticky/js/src/admin/extend.ts b/extensions/sticky/js/src/admin/extend.ts new file mode 100644 index 0000000000..9ce8b5f124 --- /dev/null +++ b/extensions/sticky/js/src/admin/extend.ts @@ -0,0 +1 @@ +export { default as default } from '../common/extend'; diff --git a/extensions/sticky/js/src/admin/index.js b/extensions/sticky/js/src/admin/index.js index 29b4448a83..057c5ea345 100644 --- a/extensions/sticky/js/src/admin/index.js +++ b/extensions/sticky/js/src/admin/index.js @@ -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( { diff --git a/extensions/sticky/js/src/common/extend.ts b/extensions/sticky/js/src/common/extend.ts new file mode 100644 index 0000000000..7a677a5c35 --- /dev/null +++ b/extensions/sticky/js/src/common/extend.ts @@ -0,0 +1,7 @@ +import Extend from 'flarum/common/extenders'; +import StickyGambit from './query/discussions/StickyGambit'; + +export default [ + new Extend.Search() // + .gambit('discussions', StickyGambit), +]; diff --git a/extensions/sticky/js/src/common/query/discussions/StickyGambit.ts b/extensions/sticky/js/src/common/query/discussions/StickyGambit.ts new file mode 100644 index 0000000000..1c9ed860ad --- /dev/null +++ b/extensions/sticky/js/src/common/query/discussions/StickyGambit.ts @@ -0,0 +1,15 @@ +import IGambit from 'flarum/common/query/IGambit'; + +export default class StickyGambit implements IGambit { + pattern(): string { + return 'is:sticky'; + } + + toFilter(_matches: string[], negate: boolean): Record { + const key = (negate ? '-' : '') + 'sticky'; + + return { + [key]: true, + }; + } +} diff --git a/extensions/sticky/js/src/forum/extend.ts b/extensions/sticky/js/src/forum/extend.ts index c3a73c859a..9dc89ed7a2 100644 --- a/extensions/sticky/js/src/forum/extend.ts +++ b/extensions/sticky/js/src/forum/extend.ts @@ -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), diff --git a/extensions/subscriptions/extend.php b/extensions/subscriptions/extend.php index 468265cc1e..5a8974e588 100644 --- a/extensions/subscriptions/extend.php +++ b/extensions/subscriptions/extend.php @@ -28,6 +28,9 @@ 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') diff --git a/extensions/subscriptions/js/admin.ts b/extensions/subscriptions/js/admin.ts new file mode 100644 index 0000000000..3e69ff3b97 --- /dev/null +++ b/extensions/subscriptions/js/admin.ts @@ -0,0 +1 @@ +export * from './src/admin'; diff --git a/extensions/subscriptions/js/src/admin/extend.ts b/extensions/subscriptions/js/src/admin/extend.ts new file mode 100644 index 0000000000..9ce8b5f124 --- /dev/null +++ b/extensions/subscriptions/js/src/admin/extend.ts @@ -0,0 +1 @@ +export { default as default } from '../common/extend'; diff --git a/extensions/subscriptions/js/src/admin/index.ts b/extensions/subscriptions/js/src/admin/index.ts new file mode 100644 index 0000000000..6d2293da0b --- /dev/null +++ b/extensions/subscriptions/js/src/admin/index.ts @@ -0,0 +1 @@ +export { default as extend } from './extend'; diff --git a/extensions/subscriptions/js/src/common/extend.ts b/extensions/subscriptions/js/src/common/extend.ts new file mode 100644 index 0000000000..081bda241a --- /dev/null +++ b/extensions/subscriptions/js/src/common/extend.ts @@ -0,0 +1,7 @@ +import Extend from 'flarum/common/extenders'; +import SubscriptionGambit from './query/discussions/SubscriptionGambit'; + +export default [ + new Extend.Search() // + .gambit('discussions', SubscriptionGambit), +]; diff --git a/extensions/subscriptions/js/src/common/query/discussions/SubscriptionGambit.ts b/extensions/subscriptions/js/src/common/query/discussions/SubscriptionGambit.ts new file mode 100644 index 0000000000..a702a43eaf --- /dev/null +++ b/extensions/subscriptions/js/src/common/query/discussions/SubscriptionGambit.ts @@ -0,0 +1,15 @@ +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 { + const type = matches[1] === 'follow' ? 'following' : 'ignoring'; + + return { + subscription: type, + }; + } +} diff --git a/extensions/subscriptions/js/src/forum/extend.ts b/extensions/subscriptions/js/src/forum/extend.ts index 18a948b212..065976b40e 100644 --- a/extensions/subscriptions/js/src/forum/extend.ts +++ b/extensions/subscriptions/js/src/forum/extend.ts @@ -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), diff --git a/extensions/suspend/js/src/admin/extend.ts b/extensions/suspend/js/src/admin/extend.ts new file mode 100644 index 0000000000..9ce8b5f124 --- /dev/null +++ b/extensions/suspend/js/src/admin/extend.ts @@ -0,0 +1 @@ +export { default as default } from '../common/extend'; diff --git a/extensions/suspend/js/src/admin/index.js b/extensions/suspend/js/src/admin/index.js index 5b1d905f98..b7ff301320 100644 --- a/extensions/suspend/js/src/admin/index.js +++ b/extensions/suspend/js/src/admin/index.js @@ -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( { diff --git a/extensions/suspend/js/src/common/extend.ts b/extensions/suspend/js/src/common/extend.ts new file mode 100644 index 0000000000..c576d539fb --- /dev/null +++ b/extensions/suspend/js/src/common/extend.ts @@ -0,0 +1,7 @@ +import Extend from 'flarum/common/extenders'; +import SuspendedGambit from './query/users/SuspendedGambit'; + +export default [ + new Extend.Search() // + .gambit('users', SuspendedGambit), +]; diff --git a/extensions/suspend/js/src/common/query/users/SuspendedGambit.ts b/extensions/suspend/js/src/common/query/users/SuspendedGambit.ts new file mode 100644 index 0000000000..2ef338f590 --- /dev/null +++ b/extensions/suspend/js/src/common/query/users/SuspendedGambit.ts @@ -0,0 +1,15 @@ +import IGambit from 'flarum/common/query/IGambit'; + +export default class SuspendedGambit implements IGambit { + pattern(): string { + return 'is:suspended'; + } + + toFilter(_matches: string[], negate: boolean): Record { + const key = (negate ? '-' : '') + 'suspended'; + + return { + [key]: true, + }; + } +} diff --git a/extensions/suspend/js/src/forum/extend.ts b/extensions/suspend/js/src/forum/extend.ts index 6fd80fc861..d4de0dec11 100644 --- a/extensions/suspend/js/src/forum/extend.ts +++ b/extensions/suspend/js/src/forum/extend.ts @@ -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('canSuspend') - .attribute('suspendedUntil', Model.transformDate) + .attribute('suspendedUntil', Model.transformDate) .attribute('suspendReason') .attribute('suspendMessage'), ]; diff --git a/extensions/tags/js/src/common/extend.ts b/extensions/tags/js/src/common/extend.ts index ca8be1f80a..b464dd7d54 100644 --- a/extensions/tags/js/src/common/extend.ts +++ b/extensions/tags/js/src/common/extend.ts @@ -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), ]; diff --git a/extensions/tags/js/src/common/query/discussions/TagGambit.ts b/extensions/tags/js/src/common/query/discussions/TagGambit.ts new file mode 100644 index 0000000000..fae0333354 --- /dev/null +++ b/extensions/tags/js/src/common/query/discussions/TagGambit.ts @@ -0,0 +1,15 @@ +import IGambit from 'flarum/common/query/IGambit'; + +export default class TagGambit implements IGambit { + pattern(): string { + return 'tag:(.+)'; + } + + toFilter(matches: string[], negate: boolean): Record { + const key = (negate ? '-' : '') + 'tag'; + + return { + [key]: matches[1].split(','), + }; + } +} diff --git a/framework/core/js/src/common/GambitManager.ts b/framework/core/js/src/common/GambitManager.ts new file mode 100644 index 0000000000..11cc0569ec --- /dev/null +++ b/framework/core/js/src/common/GambitManager.ts @@ -0,0 +1,51 @@ +import IGambit from './query/IGambit'; +import AuthorGambit from './query/discussions/AuthorGambit'; +import CreatedGambit from './query/discussions/CreatedGambit'; +import HiddenGambit from './query/discussions/HiddenGambit'; +import UnreadGambit from './query/discussions/UnreadGambit'; +import EmailGambit from './query/users/EmailGambit'; +import GroupGambit from './query/users/GroupGambit'; + +/** + * The gambit registry. A map of resource types to gambit classes that + * should be used to filter resources of that type. Gambits are automatically + * converted to API filters when requesting resources. Gambits must be applied + * on a filter object that has a `q` property containing the search query. + */ +export default class GambitManager { + gambits: Record IGambit>> = { + discussions: [AuthorGambit, CreatedGambit, HiddenGambit, UnreadGambit], + users: [EmailGambit, GroupGambit], + }; + + public apply(type: string, filter: Record): Record { + const gambits = this.gambits[type] || []; + + if (gambits.length === 0) return filter; + + const bits: string[] = filter.q.split(' '); + + for (const gambitClass of gambits) { + const gambit = new gambitClass(); + + for (const bit of bits) { + const pattern = `^(-?)${gambit.pattern()}$`; + let matches = bit.match(pattern); + + if (matches) { + const negate = matches[1] === '-'; + + matches.splice(1, 1); + + Object.assign(filter, gambit.toFilter(matches, negate)); + + filter.q = filter.q.replace(bit, ''); + } + } + } + + filter.q = filter.q.trim().replace(/\s+/g, ' '); + + return filter; + } +} diff --git a/framework/core/js/src/common/Store.ts b/framework/core/js/src/common/Store.ts index 0293a2f795..de085c7499 100644 --- a/framework/core/js/src/common/Store.ts +++ b/framework/core/js/src/common/Store.ts @@ -2,6 +2,7 @@ import app from '../common/app'; import { FlarumRequestOptions } from './Application'; import { fireDeprecationWarning } from './helpers/fireDebugWarning'; import Model, { ModelData, SavedModelData } from './Model'; +import GambitManager from './GambitManager'; export interface MetaInformation { [key: string]: any; @@ -21,7 +22,7 @@ export interface ApiQueryParamsPlural { | { q: string; } - | Record; + | Record; page?: { near?: number; offset?: number; @@ -85,6 +86,12 @@ export default class Store { */ models: Record; + /** + * The gambit manager that will convert search query gambits + * into API filters. + */ + gambits = new GambitManager(); + constructor(models: Record) { this.models = models; } @@ -178,6 +185,10 @@ export default class Store { url += '/' + idOrParams; } + if ('filter' in params && params?.filter?.q) { + params.filter = this.gambits.apply(type, params.filter); + } + return app .request ? ApiPayloadPlural : ApiPayloadSingle>({ method: 'GET', diff --git a/framework/core/js/src/common/extenders/Search.ts b/framework/core/js/src/common/extenders/Search.ts new file mode 100644 index 0000000000..c2c54cc0b8 --- /dev/null +++ b/framework/core/js/src/common/extenders/Search.ts @@ -0,0 +1,24 @@ +import type IExtender from './IExtender'; +import type { IExtensionModule } from './IExtender'; +import type Application from '../Application'; +import IGambit from '../query/IGambit'; + +export default class Search implements IExtender { + protected gambits: Record IGambit>> = {}; + + public gambit(modelType: string, gambit: new () => IGambit): this { + this.gambits[modelType] = this.gambits[modelType] || []; + this.gambits[modelType].push(gambit); + + return this; + } + + extend(app: Application, extension: IExtensionModule): void { + for (const [modelType, gambits] of Object.entries(this.gambits)) { + for (const gambit of gambits) { + app.store.gambits.gambits[modelType] = app.store.gambits.gambits[modelType] || []; + app.store.gambits.gambits[modelType].push(gambit); + } + } + } +} diff --git a/framework/core/js/src/common/extenders/index.ts b/framework/core/js/src/common/extenders/index.ts index e2d5dda532..86efd55d2a 100644 --- a/framework/core/js/src/common/extenders/index.ts +++ b/framework/core/js/src/common/extenders/index.ts @@ -2,12 +2,14 @@ import Model from './Model'; import PostTypes from './PostTypes'; import Routes from './Routes'; import Store from './Store'; +import Search from './Search'; const extenders = { Model, PostTypes, Routes, Store, + Search, }; export default extenders; diff --git a/framework/core/js/src/common/query/IGambit.ts b/framework/core/js/src/common/query/IGambit.ts new file mode 100644 index 0000000000..719c319689 --- /dev/null +++ b/framework/core/js/src/common/query/IGambit.ts @@ -0,0 +1,4 @@ +export default interface IGambit { + pattern(): string; + toFilter(matches: string[], negate: boolean): Record; +} diff --git a/framework/core/js/src/common/query/discussions/AuthorGambit.ts b/framework/core/js/src/common/query/discussions/AuthorGambit.ts new file mode 100644 index 0000000000..367686dff2 --- /dev/null +++ b/framework/core/js/src/common/query/discussions/AuthorGambit.ts @@ -0,0 +1,15 @@ +import IGambit from '../IGambit'; + +export default class AuthorGambit implements IGambit { + public pattern(): string { + return 'author:(.+)'; + } + + public toFilter(matches: string[], negate: boolean): Record { + const key = (negate ? '-' : '') + 'author'; + + return { + [key]: matches[1].split(','), + }; + } +} diff --git a/framework/core/js/src/common/query/discussions/CreatedGambit.ts b/framework/core/js/src/common/query/discussions/CreatedGambit.ts new file mode 100644 index 0000000000..c1eed2809d --- /dev/null +++ b/framework/core/js/src/common/query/discussions/CreatedGambit.ts @@ -0,0 +1,20 @@ +import IGambit from '../IGambit'; + +export default class CreatedGambit implements IGambit { + pattern(): string { + return 'created:(\\d{4}\\-\\d\\d\\-\\d\\d)(?:\\.\\.(\\d{4}\\-\\d\\d\\-\\d\\d))?'; + } + + toFilter(matches: string[], negate: boolean): Record { + const key = (negate ? '-' : '') + 'created'; + + return { + [key]: matches[2] + ? { + from: matches[1], + to: matches[2], + } + : matches[1], + }; + } +} diff --git a/framework/core/js/src/common/query/discussions/HiddenGambit.ts b/framework/core/js/src/common/query/discussions/HiddenGambit.ts new file mode 100644 index 0000000000..2e2157307b --- /dev/null +++ b/framework/core/js/src/common/query/discussions/HiddenGambit.ts @@ -0,0 +1,15 @@ +import IGambit from '../IGambit'; + +export default class HiddenGambit implements IGambit { + public pattern(): string { + return 'is:hidden'; + } + + public toFilter(_matches: string[], negate: boolean): Record { + const key = (negate ? '-' : '') + 'hidden'; + + return { + [key]: true, + }; + } +} diff --git a/framework/core/js/src/common/query/discussions/UnreadGambit.ts b/framework/core/js/src/common/query/discussions/UnreadGambit.ts new file mode 100644 index 0000000000..ac3fc920e8 --- /dev/null +++ b/framework/core/js/src/common/query/discussions/UnreadGambit.ts @@ -0,0 +1,15 @@ +import IGambit from '../IGambit'; + +export default class UnreadGambit implements IGambit { + pattern(): string { + return 'is:unread'; + } + + toFilter(_matches: string[], negate: boolean): Record { + const key = (negate ? '-' : '') + 'unread'; + + return { + [key]: true, + }; + } +} diff --git a/framework/core/js/src/common/query/users/EmailGambit.ts b/framework/core/js/src/common/query/users/EmailGambit.ts new file mode 100644 index 0000000000..ad851290f5 --- /dev/null +++ b/framework/core/js/src/common/query/users/EmailGambit.ts @@ -0,0 +1,15 @@ +import IGambit from '../IGambit'; + +export default class EmailGambit implements IGambit { + pattern(): string { + return 'email:(.+)'; + } + + toFilter(matches: string[], negate: boolean): Record { + const key = (negate ? '-' : '') + 'email'; + + return { + [key]: matches[1], + }; + } +} diff --git a/framework/core/js/src/common/query/users/GroupGambit.ts b/framework/core/js/src/common/query/users/GroupGambit.ts new file mode 100644 index 0000000000..279b895bc2 --- /dev/null +++ b/framework/core/js/src/common/query/users/GroupGambit.ts @@ -0,0 +1,15 @@ +import IGambit from '../IGambit'; + +export default class GroupGambit implements IGambit { + pattern(): string { + return 'group:(.+)'; + } + + toFilter(matches: string[], negate: boolean): Record { + const key = (negate ? '-' : '') + 'group'; + + return { + [key]: matches[1].split(','), + }; + } +} diff --git a/framework/core/js/tests/unit/common/GambitManager.test.ts b/framework/core/js/tests/unit/common/GambitManager.test.ts new file mode 100644 index 0000000000..50e40ed104 --- /dev/null +++ b/framework/core/js/tests/unit/common/GambitManager.test.ts @@ -0,0 +1,34 @@ +import GambitManager from '../../../src/common/GambitManager'; + +const gambits = new GambitManager(); + +test('gambits are converted to filters', function () { + expect(gambits.apply('discussions', { q: 'lorem created:2023-07-07 is:hidden author:behz' })).toStrictEqual({ + q: 'lorem', + created: '2023-07-07', + hidden: true, + author: ['behz'], + }); +}); + +test('gambits are negated when prefixed with a dash', function () { + expect(gambits.apply('discussions', { q: 'lorem -created:2023-07-07 -is:hidden -author:behz' })).toStrictEqual({ + q: 'lorem', + '-created': '2023-07-07', + '-hidden': true, + '-author': ['behz'], + }); +}); + +test('gambits are only applied for the correct resource type', function () { + expect(gambits.apply('users', { q: 'lorem created:2023-07-07 is:hidden author:behz email:behz@machine.local' })).toStrictEqual({ + q: 'lorem created:2023-07-07 is:hidden author:behz', + email: 'behz@machine.local', + }); + expect(gambits.apply('discussions', { q: 'lorem created:2023-07-07 is:hidden -author:behz email:behz@machine.local' })).toStrictEqual({ + q: 'lorem email:behz@machine.local', + created: '2023-07-07', + hidden: true, + '-author': ['behz'], + }); +}); diff --git a/framework/core/src/Discussion/Query/AuthorFilterGambit.php b/framework/core/src/Discussion/Filter/AuthorFilter.php similarity index 70% rename from framework/core/src/Discussion/Query/AuthorFilterGambit.php rename to framework/core/src/Discussion/Filter/AuthorFilter.php index 16919ff2a6..27a35ad2cb 100644 --- a/framework/core/src/Discussion/Query/AuthorFilterGambit.php +++ b/framework/core/src/Discussion/Filter/AuthorFilter.php @@ -7,17 +7,15 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\Discussion\Query; +namespace Flarum\Discussion\Filter; use Flarum\Filter\FilterInterface; use Flarum\Filter\FilterState; use Flarum\Filter\ValidateFilterTrait; -use Flarum\Search\AbstractRegexGambit; -use Flarum\Search\SearchState; use Flarum\User\UserRepository; use Illuminate\Database\Query\Builder; -class AuthorFilterGambit extends AbstractRegexGambit implements FilterInterface +class AuthorFilter implements FilterInterface { use ValidateFilterTrait; @@ -26,16 +24,6 @@ public function __construct( ) { } - public function getGambitPattern(): string - { - return 'author:(.+)'; - } - - protected function conditions(SearchState $search, array $matches, bool $negate): void - { - $this->constrain($search->getQuery(), $matches[1], $negate); - } - public function getFilterKey(): string { return 'author'; diff --git a/framework/core/src/Discussion/Filter/CreatedFilter.php b/framework/core/src/Discussion/Filter/CreatedFilter.php new file mode 100644 index 0000000000..1dd2b25c15 --- /dev/null +++ b/framework/core/src/Discussion/Filter/CreatedFilter.php @@ -0,0 +1,56 @@ +asString($filterValue) + : $this->asStringArray($filterValue); + + if (is_array($filterValue)) { + $from = Arr::get($filterValue, 'from'); + $to = Arr::get($filterValue, 'to'); + } else { + $from = $filterValue; + $to = null; + } + + $this->constrain($filterState->getQuery(), $from, $to, $negate); + } + + public function constrain(Builder $query, ?string $from, ?string $to, bool $negate): void + { + // If we've just been provided with a single YYYY-MM-DD date, then find + // discussions that were started on that exact date. But if we've been + // provided with a YYYY-MM-DD..YYYY-MM-DD range, then find discussions + // that were started during that period. + if (empty($to)) { + $query->whereDate('created_at', $negate ? '!=' : '=', $from); + } else { + $query->whereBetween('created_at', [$from, $to], 'and', $negate); + } + } +} diff --git a/framework/core/src/Discussion/Query/HiddenFilterGambit.php b/framework/core/src/Discussion/Filter/HiddenFilter.php similarity index 67% rename from framework/core/src/Discussion/Query/HiddenFilterGambit.php rename to framework/core/src/Discussion/Filter/HiddenFilter.php index d0bebdf95b..831b3f1270 100644 --- a/framework/core/src/Discussion/Query/HiddenFilterGambit.php +++ b/framework/core/src/Discussion/Filter/HiddenFilter.php @@ -7,26 +7,14 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\Discussion\Query; +namespace Flarum\Discussion\Filter; use Flarum\Filter\FilterInterface; use Flarum\Filter\FilterState; -use Flarum\Search\AbstractRegexGambit; -use Flarum\Search\SearchState; use Illuminate\Database\Query\Builder; -class HiddenFilterGambit extends AbstractRegexGambit implements FilterInterface +class HiddenFilter implements FilterInterface { - public function getGambitPattern(): string - { - return 'is:hidden'; - } - - protected function conditions(SearchState $search, array $matches, bool $negate): void - { - $this->constrain($search->getQuery(), $negate); - } - public function getFilterKey(): string { return 'hidden'; diff --git a/framework/core/src/Discussion/Query/UnreadFilterGambit.php b/framework/core/src/Discussion/Filter/UnreadFilter.php similarity index 62% rename from framework/core/src/Discussion/Query/UnreadFilterGambit.php rename to framework/core/src/Discussion/Filter/UnreadFilter.php index 5709681e3f..b4b84468a8 100644 --- a/framework/core/src/Discussion/Query/UnreadFilterGambit.php +++ b/framework/core/src/Discussion/Filter/UnreadFilter.php @@ -7,39 +7,19 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\Discussion\Query; +namespace Flarum\Discussion\Filter; use Flarum\Discussion\DiscussionRepository; use Flarum\Filter\FilterInterface; use Flarum\Filter\FilterState; -use Flarum\Search\AbstractRegexGambit; -use Flarum\Search\SearchState; use Flarum\User\User; use Illuminate\Database\Query\Builder; -class UnreadFilterGambit extends AbstractRegexGambit implements FilterInterface +class UnreadFilter implements FilterInterface { - /** - * @var \Flarum\Discussion\DiscussionRepository - */ - protected $discussions; - - /** - * @param \Flarum\Discussion\DiscussionRepository $discussions - */ - public function __construct(DiscussionRepository $discussions) - { - $this->discussions = $discussions; - } - - public function getGambitPattern(): string - { - return 'is:unread'; - } - - protected function conditions(SearchState $search, array $matches, bool $negate): void - { - $this->constrain($search->getQuery(), $search->getActor(), $negate); + public function __construct( + protected DiscussionRepository $discussions + ) { } public function getFilterKey(): string diff --git a/framework/core/src/Discussion/Query/CreatedFilterGambit.php b/framework/core/src/Discussion/Query/CreatedFilterGambit.php deleted file mode 100644 index 7d5a98a6bc..0000000000 --- a/framework/core/src/Discussion/Query/CreatedFilterGambit.php +++ /dev/null @@ -1,60 +0,0 @@ -constrain($search->getQuery(), Arr::get($matches, 1), Arr::get($matches, 3), $negate); - } - - public function getFilterKey(): string - { - return 'created'; - } - - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void - { - $filterValue = $this->asString($filterValue); - - preg_match('/^'.$this->getGambitPattern().'$/i', 'created:'.$filterValue, $matches); - - $this->constrain($filterState->getQuery(), Arr::get($matches, 1), Arr::get($matches, 3), $negate); - } - - public function constrain(Builder $query, ?string $firstDate, ?string $secondDate, bool $negate): void - { - // If we've just been provided with a single YYYY-MM-DD date, then find - // discussions that were started on that exact date. But if we've been - // provided with a YYYY-MM-DD..YYYY-MM-DD range, then find discussions - // that were started during that period. - if (empty($secondDate)) { - $query->whereDate('created_at', $negate ? '!=' : '=', $firstDate); - } else { - $query->whereBetween('created_at', [$firstDate, $secondDate], 'and', $negate); - } - } -} diff --git a/framework/core/src/Filter/FilterServiceProvider.php b/framework/core/src/Filter/FilterServiceProvider.php index 0698d5ad76..df6e947e4d 100644 --- a/framework/core/src/Filter/FilterServiceProvider.php +++ b/framework/core/src/Filter/FilterServiceProvider.php @@ -9,8 +9,8 @@ namespace Flarum\Filter; +use Flarum\Discussion\Filter as DiscussionFilter; use Flarum\Discussion\Filter\DiscussionFilterer; -use Flarum\Discussion\Query as DiscussionQuery; use Flarum\Foundation\AbstractServiceProvider; use Flarum\Foundation\ContainerUtil; use Flarum\Group\Filter as GroupFilter; @@ -19,8 +19,8 @@ use Flarum\Http\Filter as HttpFilter; use Flarum\Post\Filter as PostFilter; use Flarum\Post\Filter\PostFilterer; +use Flarum\User\Filter as UserFilter; use Flarum\User\Filter\UserFilterer; -use Flarum\User\Query as UserQuery; use Illuminate\Contracts\Container\Container; use Illuminate\Support\Arr; @@ -34,14 +34,14 @@ public function register(): void HttpFilter\UserFilter::class, ], DiscussionFilterer::class => [ - DiscussionQuery\AuthorFilterGambit::class, - DiscussionQuery\CreatedFilterGambit::class, - DiscussionQuery\HiddenFilterGambit::class, - DiscussionQuery\UnreadFilterGambit::class, + DiscussionFilter\AuthorFilter::class, + DiscussionFilter\CreatedFilter::class, + DiscussionFilter\HiddenFilter::class, + DiscussionFilter\UnreadFilter::class, ], UserFilterer::class => [ - UserQuery\EmailFilterGambit::class, - UserQuery\GroupFilterGambit::class, + UserFilter\EmailFilter::class, + UserFilter\GroupFilter::class, ], GroupFilterer::class => [ GroupFilter\HiddenFilter::class, diff --git a/framework/core/src/Search/SearchServiceProvider.php b/framework/core/src/Search/SearchServiceProvider.php index 65d1eb2757..e71e60fcbf 100644 --- a/framework/core/src/Search/SearchServiceProvider.php +++ b/framework/core/src/Search/SearchServiceProvider.php @@ -9,12 +9,10 @@ namespace Flarum\Search; -use Flarum\Discussion\Query as DiscussionQuery; use Flarum\Discussion\Search\DiscussionSearcher; use Flarum\Discussion\Search\Gambit\FulltextGambit as DiscussionFulltextGambit; use Flarum\Foundation\AbstractServiceProvider; use Flarum\Foundation\ContainerUtil; -use Flarum\User\Query as UserQuery; use Flarum\User\Search\Gambit\FulltextGambit as UserFulltextGambit; use Flarum\User\Search\UserSearcher; use Illuminate\Contracts\Container\Container; @@ -33,16 +31,9 @@ public function register(): void $this->container->singleton('flarum.simple_search.gambits', function () { return [ - DiscussionSearcher::class => [ - DiscussionQuery\AuthorFilterGambit::class, - DiscussionQuery\CreatedFilterGambit::class, - DiscussionQuery\HiddenFilterGambit::class, - DiscussionQuery\UnreadFilterGambit::class, - ], - UserSearcher::class => [ - UserQuery\EmailFilterGambit::class, - UserQuery\GroupFilterGambit::class, - ] + // @TODO searcher filters + DiscussionSearcher::class => [], + UserSearcher::class => [] ]; }); diff --git a/framework/core/src/User/Query/EmailFilterGambit.php b/framework/core/src/User/Filter/EmailFilter.php similarity index 58% rename from framework/core/src/User/Query/EmailFilterGambit.php rename to framework/core/src/User/Filter/EmailFilter.php index c0028fe843..b5b93b0746 100644 --- a/framework/core/src/User/Query/EmailFilterGambit.php +++ b/framework/core/src/User/Filter/EmailFilter.php @@ -7,38 +7,17 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\User\Query; +namespace Flarum\User\Filter; use Flarum\Filter\FilterInterface; use Flarum\Filter\FilterState; use Flarum\Filter\ValidateFilterTrait; -use Flarum\Search\AbstractRegexGambit; -use Flarum\Search\SearchState; use Illuminate\Database\Query\Builder; -class EmailFilterGambit extends AbstractRegexGambit implements FilterInterface +class EmailFilter implements FilterInterface { use ValidateFilterTrait; - public function apply(SearchState $search, string $bit): bool - { - if (! $search->getActor()->hasPermission('user.edit')) { - return false; - } - - return parent::apply($search, $bit); - } - - public function getGambitPattern(): string - { - return 'email:(.+)'; - } - - protected function conditions(SearchState $search, array $matches, bool $negate): void - { - $this->constrain($search->getQuery(), $matches[1], $negate); - } - public function getFilterKey(): string { return 'email'; diff --git a/framework/core/src/User/Query/GroupFilterGambit.php b/framework/core/src/User/Filter/GroupFilter.php similarity index 77% rename from framework/core/src/User/Query/GroupFilterGambit.php rename to framework/core/src/User/Filter/GroupFilter.php index ebb118b42d..bec0d10bc3 100644 --- a/framework/core/src/User/Query/GroupFilterGambit.php +++ b/framework/core/src/User/Filter/GroupFilter.php @@ -7,31 +7,19 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\User\Query; +namespace Flarum\User\Filter; use Flarum\Filter\FilterInterface; use Flarum\Filter\FilterState; use Flarum\Filter\ValidateFilterTrait; use Flarum\Group\Group; -use Flarum\Search\AbstractRegexGambit; -use Flarum\Search\SearchState; use Flarum\User\User; use Illuminate\Database\Query\Builder; -class GroupFilterGambit extends AbstractRegexGambit implements FilterInterface +class GroupFilter implements FilterInterface { use ValidateFilterTrait; - public function getGambitPattern(): string - { - return 'group:(.+)'; - } - - protected function conditions(SearchState $search, array $matches, bool $negate): void - { - $this->constrain($search->getQuery(), $search->getActor(), $matches[1], $negate); - } - public function getFilterKey(): string { return 'group';