diff --git a/src/application/Context/State/CategoryCollectionState.ts b/src/application/Context/State/CategoryCollectionState.ts index 09efaf3a..c8a9936d 100644 --- a/src/application/Context/State/CategoryCollectionState.ts +++ b/src/application/Context/State/CategoryCollectionState.ts @@ -1,7 +1,7 @@ import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import { UserFilter } from './Filter/UserFilter'; -import { IUserFilter } from './Filter/IUserFilter'; +import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext'; +import { FilterContext } from './Filter/FilterContext'; import { ApplicationCode } from './Code/ApplicationCode'; import { UserSelection } from './Selection/UserSelection'; import { ICategoryCollectionState } from './ICategoryCollectionState'; @@ -15,7 +15,7 @@ export class CategoryCollectionState implements ICategoryCollectionState { public readonly selection: UserSelection; - public readonly filter: IUserFilter; + public readonly filter: FilterContext; public constructor( public readonly collection: ICategoryCollection, @@ -45,7 +45,7 @@ const DefaultSelectionFactory: SelectionFactory = ( ) => new UserSelectionFacade(...params); export type FilterFactory = ( - ...params: ConstructorParameters -) => IUserFilter; + ...params: ConstructorParameters +) => FilterContext; -const DefaultFilterFactory: FilterFactory = (...params) => new UserFilter(...params); +const DefaultFilterFactory: FilterFactory = (...params) => new AdaptiveFilterContext(...params); diff --git a/src/application/Context/State/Filter/AdaptiveFilterContext.ts b/src/application/Context/State/Filter/AdaptiveFilterContext.ts new file mode 100644 index 00000000..1a498f61 --- /dev/null +++ b/src/application/Context/State/Filter/AdaptiveFilterContext.ts @@ -0,0 +1,35 @@ +import { EventSource } from '@/infrastructure/Events/EventSource'; +import { ICategoryCollection } from '@/domain/ICategoryCollection'; +import { FilterResult } from './Result/FilterResult'; +import { FilterContext } from './FilterContext'; +import { FilterChangeDetails } from './Event/FilterChangeDetails'; +import { FilterChange } from './Event/FilterChange'; +import { FilterStrategy } from './Strategy/FilterStrategy'; +import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy'; + +export class AdaptiveFilterContext implements FilterContext { + public readonly filterChanged = new EventSource(); + + public currentFilter: FilterResult | undefined; + + constructor( + private readonly collection: ICategoryCollection, + private readonly filterStrategy: FilterStrategy = new LinearFilterStrategy(), + ) { + + } + + public applyFilter(filter: string): void { + if (!filter) { + throw new Error('Filter must be defined and not empty. Use clearFilter() to remove the filter'); + } + const result = this.filterStrategy.applyFilter(filter, this.collection); + this.currentFilter = result; + this.filterChanged.notify(FilterChange.forApply(this.currentFilter)); + } + + public clearFilter(): void { + this.currentFilter = undefined; + this.filterChanged.notify(FilterChange.forClear()); + } +} diff --git a/src/application/Context/State/Filter/Event/FilterChange.ts b/src/application/Context/State/Filter/Event/FilterChange.ts index bed54dc8..c4c0ffbd 100644 --- a/src/application/Context/State/Filter/Event/FilterChange.ts +++ b/src/application/Context/State/Filter/Event/FilterChange.ts @@ -1,24 +1,24 @@ -import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; +import { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult'; import { FilterActionType } from './FilterActionType'; import { - IFilterChangeDetails, IFilterChangeDetailsVisitor, + FilterChangeDetails, FilterChangeDetailsVisitor, ApplyFilterAction, ClearFilterAction, -} from './IFilterChangeDetails'; +} from './FilterChangeDetails'; -export class FilterChange implements IFilterChangeDetails { +export class FilterChange implements FilterChangeDetails { public static forApply( - filter: IFilterResult, - ): IFilterChangeDetails { + filter: FilterResult, + ): FilterChangeDetails { return new FilterChange({ type: FilterActionType.Apply, filter }); } - public static forClear(): IFilterChangeDetails { + public static forClear(): FilterChangeDetails { return new FilterChange({ type: FilterActionType.Clear }); } private constructor(public readonly action: ApplyFilterAction | ClearFilterAction) { } - public visit(visitor: IFilterChangeDetailsVisitor): void { + public visit(visitor: FilterChangeDetailsVisitor): void { switch (this.action.type) { case FilterActionType.Apply: if (visitor.onApply) { diff --git a/src/application/Context/State/Filter/Event/FilterChangeDetails.ts b/src/application/Context/State/Filter/Event/FilterChangeDetails.ts new file mode 100644 index 00000000..58462505 --- /dev/null +++ b/src/application/Context/State/Filter/Event/FilterChangeDetails.ts @@ -0,0 +1,23 @@ +import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult'; +import type { FilterActionType } from './FilterActionType'; + +export interface FilterChangeDetails { + readonly action: FilterAction; + visit(visitor: FilterChangeDetailsVisitor): void; +} + +export interface FilterChangeDetailsVisitor { + readonly onClear?: () => void; + readonly onApply?: (filter: FilterResult) => void; +} + +export type ApplyFilterAction = { + readonly type: FilterActionType.Apply, + readonly filter: FilterResult; +}; + +export type ClearFilterAction = { + readonly type: FilterActionType.Clear, +}; + +export type FilterAction = ApplyFilterAction | ClearFilterAction; diff --git a/src/application/Context/State/Filter/Event/IFilterChangeDetails.ts b/src/application/Context/State/Filter/Event/IFilterChangeDetails.ts deleted file mode 100644 index 1cf343e1..00000000 --- a/src/application/Context/State/Filter/Event/IFilterChangeDetails.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; -import { FilterActionType } from './FilterActionType'; - -export interface IFilterChangeDetails { - readonly action: FilterAction; - visit(visitor: IFilterChangeDetailsVisitor): void; -} - -export interface IFilterChangeDetailsVisitor { - readonly onClear?: () => void; - readonly onApply?: (filter: IFilterResult) => void; -} - -export type ApplyFilterAction = { - readonly type: FilterActionType.Apply, - readonly filter: IFilterResult; -}; - -export type ClearFilterAction = { - readonly type: FilterActionType.Clear, -}; - -export type FilterAction = ApplyFilterAction | ClearFilterAction; diff --git a/src/application/Context/State/Filter/FilterContext.ts b/src/application/Context/State/Filter/FilterContext.ts new file mode 100644 index 00000000..2c5f592f --- /dev/null +++ b/src/application/Context/State/Filter/FilterContext.ts @@ -0,0 +1,13 @@ +import { IEventSource } from '@/infrastructure/Events/IEventSource'; +import { FilterResult } from './Result/FilterResult'; +import { FilterChangeDetails } from './Event/FilterChangeDetails'; + +export interface ReadonlyFilterContext { + readonly currentFilter: FilterResult | undefined; + readonly filterChanged: IEventSource; +} + +export interface FilterContext extends ReadonlyFilterContext { + applyFilter(filter: string): void; + clearFilter(): void; +} diff --git a/src/application/Context/State/Filter/IUserFilter.ts b/src/application/Context/State/Filter/IUserFilter.ts deleted file mode 100644 index 60403187..00000000 --- a/src/application/Context/State/Filter/IUserFilter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IEventSource } from '@/infrastructure/Events/IEventSource'; -import { IFilterResult } from './IFilterResult'; -import { IFilterChangeDetails } from './Event/IFilterChangeDetails'; - -export interface IReadOnlyUserFilter { - readonly currentFilter: IFilterResult | undefined; - readonly filterChanged: IEventSource; -} - -export interface IUserFilter extends IReadOnlyUserFilter { - applyFilter(filter: string): void; - clearFilter(): void; -} diff --git a/src/application/Context/State/Filter/FilterResult.ts b/src/application/Context/State/Filter/Result/AppliedFilterResult.ts similarity index 81% rename from src/application/Context/State/Filter/FilterResult.ts rename to src/application/Context/State/Filter/Result/AppliedFilterResult.ts index 5c949ebe..a3aa2cb2 100644 --- a/src/application/Context/State/Filter/FilterResult.ts +++ b/src/application/Context/State/Filter/Result/AppliedFilterResult.ts @@ -1,8 +1,8 @@ import { IScript } from '@/domain/IScript'; import { ICategory } from '@/domain/ICategory'; -import { IFilterResult } from './IFilterResult'; +import { FilterResult } from './FilterResult'; -export class FilterResult implements IFilterResult { +export class AppliedFilterResult implements FilterResult { constructor( public readonly scriptMatches: ReadonlyArray, public readonly categoryMatches: ReadonlyArray, diff --git a/src/application/Context/State/Filter/IFilterResult.ts b/src/application/Context/State/Filter/Result/FilterResult.ts similarity index 86% rename from src/application/Context/State/Filter/IFilterResult.ts rename to src/application/Context/State/Filter/Result/FilterResult.ts index 8391dc82..d9613df1 100644 --- a/src/application/Context/State/Filter/IFilterResult.ts +++ b/src/application/Context/State/Filter/Result/FilterResult.ts @@ -1,6 +1,6 @@ import { IScript, ICategory } from '@/domain/ICategory'; -export interface IFilterResult { +export interface FilterResult { readonly categoryMatches: ReadonlyArray; readonly scriptMatches: ReadonlyArray; readonly query: string; diff --git a/src/application/Context/State/Filter/Strategy/FilterStrategy.ts b/src/application/Context/State/Filter/Strategy/FilterStrategy.ts new file mode 100644 index 00000000..2bda8de2 --- /dev/null +++ b/src/application/Context/State/Filter/Strategy/FilterStrategy.ts @@ -0,0 +1,9 @@ +import type { ICategoryCollection } from '@/domain/ICategoryCollection'; +import type { FilterResult } from '../Result/FilterResult'; + +export interface FilterStrategy { + applyFilter( + filter: string, + collection: ICategoryCollection, + ): FilterResult; +} diff --git a/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts b/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts new file mode 100644 index 00000000..c9c978ee --- /dev/null +++ b/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts @@ -0,0 +1,80 @@ +import type { ICategory, IScript } from '@/domain/ICategory'; +import type { IScriptCode } from '@/domain/IScriptCode'; +import type { IDocumentable } from '@/domain/IDocumentable'; +import type { ICategoryCollection } from '@/domain/ICategoryCollection'; +import { AppliedFilterResult } from '../Result/AppliedFilterResult'; +import type { FilterStrategy } from './FilterStrategy'; +import type { FilterResult } from '../Result/FilterResult'; + +export class LinearFilterStrategy implements FilterStrategy { + applyFilter(filter: string, collection: ICategoryCollection): FilterResult { + const filterLowercase = filter.toLocaleLowerCase(); + const filteredScripts = collection.getAllScripts().filter( + (script) => matchesScript(script, filterLowercase), + ); + const filteredCategories = collection.getAllCategories().filter( + (category) => matchesCategory(category, filterLowercase), + ); + return new AppliedFilterResult( + filteredScripts, + filteredCategories, + filter, + ); + } +} + +function matchesCategory( + category: ICategory, + filterLowercase: string, +): boolean { + return matchesAny( + () => matchName(category.name, filterLowercase), + () => matchDocumentation(category, filterLowercase), + ); +} + +function matchesScript( + script: IScript, + filterLowercase: string, +): boolean { + return matchesAny( + () => matchName(script.name, filterLowercase), + () => matchCode(script.code, filterLowercase), + () => matchDocumentation(script, filterLowercase), + ); +} + +function matchesAny( + ...predicates: ReadonlyArray<() => boolean> +): boolean { + return predicates.some((predicate) => predicate()); +} + +function matchName( + name: string, + filterLowercase: string, +): boolean { + return name.toLowerCase().includes(filterLowercase); +} + +function matchCode( + code: IScriptCode, + filterLowercase: string, +): boolean { + if (code.execute.toLowerCase().includes(filterLowercase)) { + return true; + } + if (code.revert?.toLowerCase().includes(filterLowercase)) { + return true; + } + return false; +} + +function matchDocumentation( + documentable: IDocumentable, + filterLowercase: string, +): boolean { + return documentable.docs.some( + (doc) => doc.toLocaleLowerCase().includes(filterLowercase), + ); +} diff --git a/src/application/Context/State/Filter/UserFilter.ts b/src/application/Context/State/Filter/UserFilter.ts deleted file mode 100644 index 5e1e026d..00000000 --- a/src/application/Context/State/Filter/UserFilter.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { IScript } from '@/domain/IScript'; -import { EventSource } from '@/infrastructure/Events/EventSource'; -import { ICategoryCollection } from '@/domain/ICategoryCollection'; -import { FilterResult } from './FilterResult'; -import { IFilterResult } from './IFilterResult'; -import { IUserFilter } from './IUserFilter'; -import { IFilterChangeDetails } from './Event/IFilterChangeDetails'; -import { FilterChange } from './Event/FilterChange'; - -export class UserFilter implements IUserFilter { - public readonly filterChanged = new EventSource(); - - public currentFilter: IFilterResult | undefined; - - constructor(private collection: ICategoryCollection) { - - } - - public applyFilter(filter: string): void { - if (!filter) { - throw new Error('Filter must be defined and not empty. Use clearFilter() to remove the filter'); - } - const filterLowercase = filter.toLocaleLowerCase(); - const filteredScripts = this.collection.getAllScripts().filter( - (script) => isScriptAMatch(script, filterLowercase), - ); - const filteredCategories = this.collection.getAllCategories().filter( - (category) => category.name.toLowerCase().includes(filterLowercase), - ); - const matches = new FilterResult( - filteredScripts, - filteredCategories, - filter, - ); - this.currentFilter = matches; - this.filterChanged.notify(FilterChange.forApply(this.currentFilter)); - } - - public clearFilter(): void { - this.currentFilter = undefined; - this.filterChanged.notify(FilterChange.forClear()); - } -} - -function isScriptAMatch(script: IScript, filterLowercase: string) { - if (script.name.toLowerCase().includes(filterLowercase)) { - return true; - } - if (script.code.execute.toLowerCase().includes(filterLowercase)) { - return true; - } - if (script.code.revert) { - return script.code.revert.toLowerCase().includes(filterLowercase); - } - return false; -} diff --git a/src/application/Context/State/ICategoryCollectionState.ts b/src/application/Context/State/ICategoryCollectionState.ts index 815aa413..3ae0e197 100644 --- a/src/application/Context/State/ICategoryCollectionState.ts +++ b/src/application/Context/State/ICategoryCollectionState.ts @@ -1,18 +1,18 @@ import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter'; +import { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext'; import { ReadonlyUserSelection, UserSelection } from './Selection/UserSelection'; import { IApplicationCode } from './Code/IApplicationCode'; export interface IReadOnlyCategoryCollectionState { readonly code: IApplicationCode; readonly os: OperatingSystem; - readonly filter: IReadOnlyUserFilter; + readonly filter: ReadonlyFilterContext; readonly selection: ReadonlyUserSelection; readonly collection: ICategoryCollection; } export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState { - readonly filter: IUserFilter; + readonly filter: FilterContext; readonly selection: UserSelection; } diff --git a/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue b/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue index f44c5062..8f3c24c1 100644 --- a/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue +++ b/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue @@ -16,7 +16,7 @@