Refactor filter (search query) event handling

Refactor filter event handling to a unified event with visitor pattern
to simplify the code, avoid future bugs and provide better test
coverage.

This commit shifts from using separate `filtered` and `filterRemoved`
events to a singular, more expressive `filterChanged` event. The new
approach emits a detailed payload that explicitly indicates the filter
action and the associated filter data. The event object unifies the way
the presentation layer reacts to the events.

Benefits with this approach include:

- Simplifying event listeners by reducing the number of events to
  handle.
- Increasing code clarity and reduces potential for oversight by
  providing explicit action details in the event payload.
- Offering extensibility for future actions without introducing new
  events.
- Providing visitor pattern to handle different kind of events in easy
  and robust manner without code repetition.

Other changes:

- Refactor components handling of events to follow DRY and KISS
  principles better.
- Refactor `UserFilter.spec.ts` to:
  - Make it easier to add new tests.
  - Increase code coverage by running all event-based tests on the
    current property.
This commit is contained in:
undergroundwires
2023-08-14 15:28:15 +02:00
parent ae75059cc1
commit 6a20d804dc
17 changed files with 488 additions and 229 deletions

View File

@@ -20,7 +20,7 @@ import {
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import {
@@ -64,9 +64,7 @@ export default defineComponent({
nodes.value = parseAllCategories(state.collection);
}
events.unsubscribeAll();
modifyCurrentState((mutableState) => {
registerStateMutators(mutableState);
});
subscribeToState(state);
}, { immediate: true });
function toggleNodeSelection(event: INodeSelectedEvent) {
@@ -99,20 +97,26 @@ export default defineComponent({
.map((selected) => getScriptNodeId(selected.script));
}
function registerStateMutators(state: ICategoryCollectionState) {
function subscribeToState(state: IReadOnlyCategoryCollectionState) {
events.register(
state.selection.changed.on((scripts) => handleSelectionChanged(scripts)),
state.filter.filterRemoved.on(() => handleFilterRemoved()),
state.filter.filtered.on((filterResult) => handleFiltered(filterResult)),
state.filter.filterChanged.on((event) => {
event.visit({
onApply: (filter) => {
filterText.value = filter.query;
filtered = filter;
},
onClear: () => {
filterText.value = '';
},
});
}),
);
}
function setCurrentFilter(currentFilter: IFilterResult | undefined) {
if (!currentFilter) {
handleFilterRemoved();
} else {
handleFiltered(currentFilter);
}
filtered = currentFilter;
filterText.value = currentFilter?.query || '';
}
function handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
@@ -120,15 +124,6 @@ export default defineComponent({
.map((node) => node.id);
}
function handleFilterRemoved() {
filterText.value = '';
}
function handleFiltered(result: IFilterResult) {
filterText.value = result.query;
filtered = result;
}
return {
nodes,
selectedNodeIds,

View File

@@ -40,10 +40,8 @@ import { useApplicationKey, useCollectionStateKey } from '@/presentation/injecti
import ScriptsTree from '@/presentation/components/Scripts/View/ScriptsTree/ScriptsTree.vue';
import CardList from '@/presentation/components/Scripts/View/Cards/CardList.vue';
import { ViewType } from '@/presentation/components/Scripts/Menu/View/ViewType';
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IReadOnlyUserFilter } from '@/application/Context/State/Filter/IUserFilter';
/** Shows content of single category or many categories */
export default defineComponent({
components: {
ScriptsTree,
@@ -74,25 +72,29 @@ export default defineComponent({
onStateChange((newState) => {
events.unsubscribeAll();
subscribeState(newState);
subscribeToFilterChanges(newState.filter);
});
function clearSearchQuery() {
modifyCurrentState((state) => {
const { filter } = state;
filter.removeFilter();
filter.clearFilter();
});
}
function subscribeState(state: IReadOnlyCategoryCollectionState) {
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {
events.register(
state.filter.filterRemoved.on(() => {
isSearching.value = false;
}),
state.filter.filtered.on((result: IFilterResult) => {
searchQuery.value = result.query;
isSearching.value = true;
searchHasMatches.value = result.hasAnyMatches();
filter.filterChanged.on((event) => {
event.visit({
onApply: (newFilter) => {
searchQuery.value = newFilter.query;
isSearching.value = true;
searchHasMatches.value = newFilter.hasAnyMatches();
},
onClear: () => {
isSearching.value = false;
},
});
}),
);
}