Fix searching/filtering bugs #235

- Fix a bug (introduced in 1b9be8fe) preventing the tree view from being
  visible during a search.
- Fix a minor bug where the scripts view does not render based on the
  initial filter.
- Add Vue component tests for `TheScriptView` to prevent regressions.
- Refactor `isSearching` in `TheScriptView` to simplify its logic.
This commit is contained in:
undergroundwires
2023-08-25 00:32:01 +02:00
parent 75c9b51bf2
commit 62f8bfac2f
11 changed files with 613 additions and 31 deletions

View File

@@ -1,6 +1,7 @@
import { IEventSubscription } from './IEventSource';
import { IEventSubscriptionCollection } from './IEventSubscriptionCollection';
export class EventSubscriptionCollection {
export class EventSubscriptionCollection implements IEventSubscriptionCollection {
private readonly subscriptions = new Array<IEventSubscription>();
public register(...subscriptions: IEventSubscription[]) {

View File

@@ -0,0 +1,7 @@
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
export interface IEventSubscriptionCollection {
register(...subscriptions: IEventSubscription[]);
unsubscribeAll();
}

View File

@@ -1,19 +1,24 @@
<template>
<div class="scripts">
<div v-if="!isSearching">
<CardList v-if="currentView === ViewType.Cards" />
<div class="tree" v-else-if="currentView === ViewType.Tree">
<ScriptsTree />
</div>
<template v-if="currentView === ViewType.Cards">
<CardList />
</template>
<template v-else-if="currentView === ViewType.Tree">
<div class="tree">
<ScriptsTree />
</div>
</template>
</div>
<div v-else> <!-- Searching -->
<div class="search">
<div class="search__query">
<div>Searching for "{{ trimmedSearchQuery }}"</div>
<div class="search__query__close-button">
<font-awesome-icon
:icon="['fas', 'times']"
v-on:click="clearSearchQuery()" />
<div
class="search__query__close-button"
v-on:click="clearSearchQuery()"
>
<font-awesome-icon :icon="['fas', 'times']" />
</div>
</div>
<div v-if="!searchHasMatches" class="search-no-matches">
@@ -41,6 +46,7 @@ import ScriptsTree from '@/presentation/components/Scripts/View/ScriptsTree/Scri
import CardList from '@/presentation/components/Scripts/View/Cards/CardList.vue';
import { ViewType } from '@/presentation/components/Scripts/Menu/View/ViewType';
import { IReadOnlyUserFilter } from '@/application/Context/State/Filter/IUserFilter';
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
export default defineComponent({
components: {
@@ -58,8 +64,8 @@ export default defineComponent({
const { info } = inject(useApplicationKey);
const repositoryUrl = computed<string>(() => info.repositoryWebUrl);
const searchQuery = ref<string>();
const isSearching = ref(false);
const searchQuery = ref<string | undefined>();
const isSearching = computed(() => Boolean(searchQuery.value));
const searchHasMatches = ref(false);
const trimmedSearchQuery = computed(() => {
const query = searchQuery.value;
@@ -72,8 +78,9 @@ export default defineComponent({
onStateChange((newState) => {
events.unsubscribeAll();
updateFromInitialFilter(newState.filter.currentFilter);
subscribeToFilterChanges(newState.filter);
});
}, { immediate: true });
function clearSearchQuery() {
modifyCurrentState((state) => {
@@ -82,17 +89,21 @@ export default defineComponent({
});
}
function updateFromInitialFilter(filter?: IFilterResult) {
searchQuery.value = filter?.query;
searchHasMatches.value = filter?.hasAnyMatches();
}
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {
events.register(
filter.filterChanged.on((event) => {
event.visit({
onApply: (newFilter) => {
searchQuery.value = newFilter.query;
isSearching.value = true;
searchHasMatches.value = newFilter.hasAnyMatches();
},
onClear: () => {
isSearching.value = false;
searchQuery.value = undefined;
},
});
}),

View File

@@ -2,6 +2,7 @@ import { ref, computed, readonly } from 'vue';
import { IApplicationContext, IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
export function useCollectionState(context: IApplicationContext) {
if (!context) {
@@ -18,13 +19,6 @@ export function useCollectionState(context: IApplicationContext) {
}),
);
type NewStateEventHandler = (
newState: IReadOnlyCategoryCollectionState,
oldState: IReadOnlyCategoryCollectionState | undefined,
) => void;
interface IStateCallbackSettings {
readonly immediate: boolean;
}
const defaultSettings: IStateCallbackSettings = {
immediate: false,
};
@@ -49,9 +43,6 @@ export function useCollectionState(context: IApplicationContext) {
}
}
type StateModifier = (
state: ICategoryCollectionState,
) => void;
function modifyCurrentState(mutator: StateModifier) {
if (!mutator) {
throw new Error('missing state mutator');
@@ -59,9 +50,6 @@ export function useCollectionState(context: IApplicationContext) {
mutator(context.state);
}
type ContextModifier = (
state: IApplicationContext,
) => void;
function modifyCurrentContext(mutator: ContextModifier) {
if (!mutator) {
throw new Error('missing context mutator');
@@ -75,6 +63,23 @@ export function useCollectionState(context: IApplicationContext) {
onStateChange,
currentContext: context as IReadOnlyApplicationContext,
currentState: readonly(computed<IReadOnlyCategoryCollectionState>(() => currentState.value)),
events,
events: events as IEventSubscriptionCollection,
};
}
export type NewStateEventHandler = (
newState: IReadOnlyCategoryCollectionState,
oldState: IReadOnlyCategoryCollectionState | undefined,
) => void;
export interface IStateCallbackSettings {
readonly immediate: boolean;
}
export type StateModifier = (
state: ICategoryCollectionState,
) => void;
export type ContextModifier = (
state: IApplicationContext,
) => void;