From fc134510f1702d4a3948c5897aa6aacbdae47d21 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 1 Jan 2025 17:06:59 +0100 Subject: [PATCH] chore(spa): create core ui library (#1151) --- libs/spa/core/ui/.eslintrc.json | 39 ++++++ libs/spa/core/ui/README.md | 4 + libs/spa/core/ui/jest.config.ts | 21 +++ libs/spa/core/ui/project.json | 20 +++ libs/spa/core/ui/src/index.ts | 3 + .../core/ui/src/lib/entity-search/README.md | 7 + ...alert-group-autocomplete.component.spec.ts | 0 .../alert-group-autocomplete.component.ts | 0 .../alert-group-selection.component.spec.ts | 0 .../alert-group-selection.component.ts | 0 .../alert-group-selections.component.spec.ts | 0 .../alert-group-selections.component.ts | 0 .../unit-selection-option.component.spec.ts | 0 .../unit/unit-selection-option.component.ts | 2 +- .../unit/units-select.component.spec.ts | 0 .../component/unit/units-select.component.ts | 0 .../core/ui/src/lib/entity-search/index.ts | 9 ++ .../service/alert-group-selection.service.ts | 49 +++++++ .../service/entity-search.service.spec.ts | 47 +++++++ .../service/entity-search.service.ts | 30 +++++ .../entity-selection-search.service.spec.ts | 6 +- .../entity-selection-search.service.ts | 10 +- .../service/match-strategies.spec.ts | 99 ++++++++++++++ .../entity-search/service/match-strategies.ts | 43 ++++++ .../service/unit-selection.service.ts | 41 ++++++ .../src/lib}/status-badge.component.spec.ts | 0 .../ui/src/lib}/status-badge.component.ts | 2 +- .../ui}/src/lib/status-explanations.ts | 0 libs/spa/core/ui/src/test-setup.ts | 6 + libs/spa/core/ui/tsconfig.json | 27 ++++ libs/spa/core/ui/tsconfig.lib.json | 17 +++ libs/spa/core/ui/tsconfig.spec.json | 16 +++ .../alert-group-edit-modal.component.ts | 4 +- .../deployment-search-wrapper.component.ts | 51 +++---- .../unit/deployment-unit.component.ts | 2 +- .../alert-group-assignment.model.ts | 0 .../rescue-station-edit-modal.component.ts | 10 +- .../service/alert-group-selection.service.ts | 127 ------------------ .../service/rescue-station-edit.service.ts | 2 +- .../service/unit-selection.service.ts | 119 ---------------- .../services/entity-search.service.spec.ts | 70 ---------- .../src/lib/services/entity-search.service.ts | 60 --------- package-lock.json | 7 - package.json | 1 - tsconfig.base.json | 1 + 45 files changed, 522 insertions(+), 430 deletions(-) create mode 100644 libs/spa/core/ui/.eslintrc.json create mode 100644 libs/spa/core/ui/README.md create mode 100644 libs/spa/core/ui/jest.config.ts create mode 100644 libs/spa/core/ui/project.json create mode 100644 libs/spa/core/ui/src/index.ts create mode 100644 libs/spa/core/ui/src/lib/entity-search/README.md rename libs/spa/{feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal => core/ui/src/lib/entity-search}/component/alert-group/alert-group-autocomplete.component.spec.ts (100%) rename libs/spa/{feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal => core/ui/src/lib/entity-search}/component/alert-group/alert-group-autocomplete.component.ts (100%) rename libs/spa/{feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal => core/ui/src/lib/entity-search}/component/alert-group/alert-group-selection.component.spec.ts (100%) rename libs/spa/{feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal => core/ui/src/lib/entity-search}/component/alert-group/alert-group-selection.component.ts (100%) rename libs/spa/{feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal => core/ui/src/lib/entity-search}/component/alert-group/alert-group-selections.component.spec.ts (100%) rename libs/spa/{feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal => core/ui/src/lib/entity-search}/component/alert-group/alert-group-selections.component.ts (100%) rename libs/spa/{feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal => core/ui/src/lib/entity-search}/component/unit/unit-selection-option.component.spec.ts (100%) rename libs/spa/{feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal => core/ui/src/lib/entity-search}/component/unit/unit-selection-option.component.ts (93%) rename libs/spa/{feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal => core/ui/src/lib/entity-search}/component/unit/units-select.component.spec.ts (100%) rename libs/spa/{feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal => core/ui/src/lib/entity-search}/component/unit/units-select.component.ts (100%) create mode 100644 libs/spa/core/ui/src/lib/entity-search/index.ts create mode 100644 libs/spa/core/ui/src/lib/entity-search/service/alert-group-selection.service.ts create mode 100644 libs/spa/core/ui/src/lib/entity-search/service/entity-search.service.spec.ts create mode 100644 libs/spa/core/ui/src/lib/entity-search/service/entity-search.service.ts rename libs/spa/{feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal => core/ui/src/lib/entity-search}/service/entity-selection-search.service.spec.ts (91%) rename libs/spa/{feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal => core/ui/src/lib/entity-search}/service/entity-selection-search.service.ts (85%) create mode 100644 libs/spa/core/ui/src/lib/entity-search/service/match-strategies.spec.ts create mode 100644 libs/spa/core/ui/src/lib/entity-search/service/match-strategies.ts create mode 100644 libs/spa/core/ui/src/lib/entity-search/service/unit-selection.service.ts rename libs/spa/{feature/deployment/src/lib/components/deployment/unit => core/ui/src/lib}/status-badge.component.spec.ts (100%) rename libs/spa/{feature/deployment/src/lib/components/deployment/unit => core/ui/src/lib}/status-badge.component.ts (95%) rename libs/spa/{feature/deployment => core/ui}/src/lib/status-explanations.ts (100%) create mode 100644 libs/spa/core/ui/src/test-setup.ts create mode 100644 libs/spa/core/ui/tsconfig.json create mode 100644 libs/spa/core/ui/tsconfig.lib.json create mode 100644 libs/spa/core/ui/tsconfig.spec.json rename libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/{alert-group => }/alert-group-assignment.model.ts (100%) delete mode 100644 libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/alert-group-selection.service.ts delete mode 100644 libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/unit-selection.service.ts delete mode 100644 libs/spa/feature/deployment/src/lib/services/entity-search.service.spec.ts delete mode 100644 libs/spa/feature/deployment/src/lib/services/entity-search.service.ts diff --git a/libs/spa/core/ui/.eslintrc.json b/libs/spa/core/ui/.eslintrc.json new file mode 100644 index 000000000..7ec2dbe34 --- /dev/null +++ b/libs/spa/core/ui/.eslintrc.json @@ -0,0 +1,39 @@ +{ + "extends": [ + "../../../../.eslintrc.json", + "../../../../.eslintrc.angular.json" + ], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "krd", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "krd", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/libs/spa/core/ui/README.md b/libs/spa/core/ui/README.md new file mode 100644 index 000000000..57dc8c55e --- /dev/null +++ b/libs/spa/core/ui/README.md @@ -0,0 +1,4 @@ +# Common UI + +This library contains common UI components and some helper services that are +used across multiple domains. diff --git a/libs/spa/core/ui/jest.config.ts b/libs/spa/core/ui/jest.config.ts new file mode 100644 index 000000000..4f903f507 --- /dev/null +++ b/libs/spa/core/ui/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'spa-ui', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../../coverage/spa/core/ui', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/spa/core/ui/project.json b/libs/spa/core/ui/project.json new file mode 100644 index 000000000..0142521be --- /dev/null +++ b/libs/spa/core/ui/project.json @@ -0,0 +1,20 @@ +{ + "name": "spa-ui", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "spa/core/ui/src", + "prefix": "krd", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/spa/core/ui/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/spa/core/ui/src/index.ts b/libs/spa/core/ui/src/index.ts new file mode 100644 index 000000000..d011edbbc --- /dev/null +++ b/libs/spa/core/ui/src/index.ts @@ -0,0 +1,3 @@ +export * from './lib/entity-search/index'; +export * from './lib/status-badge.component'; +export * from './lib/status-explanations'; diff --git a/libs/spa/core/ui/src/lib/entity-search/README.md b/libs/spa/core/ui/src/lib/entity-search/README.md new file mode 100644 index 000000000..47e1b2a2d --- /dev/null +++ b/libs/spa/core/ui/src/lib/entity-search/README.md @@ -0,0 +1,7 @@ +# Entity Search + +This library contains components and services to search entities such as units +and alert-groups. Mainly, there are two types of components, selection +components where a selection service manages a state of entities and each entity +can only be selected once and autocomplete components, which is a simple search +component. diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-autocomplete.component.spec.ts b/libs/spa/core/ui/src/lib/entity-search/component/alert-group/alert-group-autocomplete.component.spec.ts similarity index 100% rename from libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-autocomplete.component.spec.ts rename to libs/spa/core/ui/src/lib/entity-search/component/alert-group/alert-group-autocomplete.component.spec.ts diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-autocomplete.component.ts b/libs/spa/core/ui/src/lib/entity-search/component/alert-group/alert-group-autocomplete.component.ts similarity index 100% rename from libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-autocomplete.component.ts rename to libs/spa/core/ui/src/lib/entity-search/component/alert-group/alert-group-autocomplete.component.ts diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-selection.component.spec.ts b/libs/spa/core/ui/src/lib/entity-search/component/alert-group/alert-group-selection.component.spec.ts similarity index 100% rename from libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-selection.component.spec.ts rename to libs/spa/core/ui/src/lib/entity-search/component/alert-group/alert-group-selection.component.spec.ts diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-selection.component.ts b/libs/spa/core/ui/src/lib/entity-search/component/alert-group/alert-group-selection.component.ts similarity index 100% rename from libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-selection.component.ts rename to libs/spa/core/ui/src/lib/entity-search/component/alert-group/alert-group-selection.component.ts diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-selections.component.spec.ts b/libs/spa/core/ui/src/lib/entity-search/component/alert-group/alert-group-selections.component.spec.ts similarity index 100% rename from libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-selections.component.spec.ts rename to libs/spa/core/ui/src/lib/entity-search/component/alert-group/alert-group-selections.component.spec.ts diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-selections.component.ts b/libs/spa/core/ui/src/lib/entity-search/component/alert-group/alert-group-selections.component.ts similarity index 100% rename from libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-selections.component.ts rename to libs/spa/core/ui/src/lib/entity-search/component/alert-group/alert-group-selections.component.ts diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/unit/unit-selection-option.component.spec.ts b/libs/spa/core/ui/src/lib/entity-search/component/unit/unit-selection-option.component.spec.ts similarity index 100% rename from libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/unit/unit-selection-option.component.spec.ts rename to libs/spa/core/ui/src/lib/entity-search/component/unit/unit-selection-option.component.spec.ts diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/unit/unit-selection-option.component.ts b/libs/spa/core/ui/src/lib/entity-search/component/unit/unit-selection-option.component.ts similarity index 93% rename from libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/unit/unit-selection-option.component.ts rename to libs/spa/core/ui/src/lib/entity-search/component/unit/unit-selection-option.component.ts index 65fa4b4b4..7071f4efb 100644 --- a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/unit/unit-selection-option.component.ts +++ b/libs/spa/core/ui/src/lib/entity-search/component/unit/unit-selection-option.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core'; import { Unit } from '@kordis/shared/model'; -import { StatusBadgeComponent } from '../../../../deployment/unit/status-badge.component'; +import { StatusBadgeComponent } from '../../../status-badge.component'; @Component({ selector: 'krd-unit-selection-option', diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/unit/units-select.component.spec.ts b/libs/spa/core/ui/src/lib/entity-search/component/unit/units-select.component.spec.ts similarity index 100% rename from libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/unit/units-select.component.spec.ts rename to libs/spa/core/ui/src/lib/entity-search/component/unit/units-select.component.spec.ts diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/unit/units-select.component.ts b/libs/spa/core/ui/src/lib/entity-search/component/unit/units-select.component.ts similarity index 100% rename from libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/unit/units-select.component.ts rename to libs/spa/core/ui/src/lib/entity-search/component/unit/units-select.component.ts diff --git a/libs/spa/core/ui/src/lib/entity-search/index.ts b/libs/spa/core/ui/src/lib/entity-search/index.ts new file mode 100644 index 000000000..110bcae5c --- /dev/null +++ b/libs/spa/core/ui/src/lib/entity-search/index.ts @@ -0,0 +1,9 @@ +export * from './service/match-strategies'; +export * from './service/entity-search.service'; +export * from './service/unit-selection.service'; +export * from './service/alert-group-selection.service'; +export * from './component/unit/unit-selection-option.component'; +export * from './component/unit/units-select.component'; +export * from './component/alert-group/alert-group-selection.component'; +export * from './component/alert-group/alert-group-autocomplete.component'; +export * from './component/alert-group/alert-group-selections.component'; diff --git a/libs/spa/core/ui/src/lib/entity-search/service/alert-group-selection.service.ts b/libs/spa/core/ui/src/lib/entity-search/service/alert-group-selection.service.ts new file mode 100644 index 000000000..64b44eddb --- /dev/null +++ b/libs/spa/core/ui/src/lib/entity-search/service/alert-group-selection.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@angular/core'; +import { TypedDocumentNode } from 'apollo-angular'; + +import { AlertGroup, Query } from '@kordis/shared/model'; +import { gql } from '@kordis/spa/core/graphql'; + +import { EntitySearchEngine } from './entity-search.service'; +import { EntitySelectionSearchService } from './entity-selection-search.service'; +import { alertGroupMatchesByName } from './match-strategies'; + +/* + This service handles the selection of alert groups in a context where a unit can only be selected once. + */ +@Injectable() +export class PossibleAlertGroupSelectionsService extends EntitySelectionSearchService< + AlertGroup, + { alertGroups: Query['alertGroups'] } +> { + protected override query: TypedDocumentNode<{ + alertGroups: Query['alertGroups']; + }> = gql` + query { + alertGroups { + id + name + currentUnits { + id + name + callSign + status { + status + } + assignment { + __typename + id + name + } + } + assignment { + __typename + id + name + } + } + } + `; + protected queryName = 'alertGroups' as const; + protected searchService = new EntitySearchEngine(alertGroupMatchesByName); +} diff --git a/libs/spa/core/ui/src/lib/entity-search/service/entity-search.service.spec.ts b/libs/spa/core/ui/src/lib/entity-search/service/entity-search.service.spec.ts new file mode 100644 index 000000000..6a02b2c87 --- /dev/null +++ b/libs/spa/core/ui/src/lib/entity-search/service/entity-search.service.spec.ts @@ -0,0 +1,47 @@ +import { EntitySearchEngine } from './entity-search.service'; + +describe('EntitySearchEngine', () => { + let searchEngine: EntitySearchEngine<{ id: string; name: string }>; + const mockMatchStrategy = jest.fn((entity, searchTerm) => + entity.name.includes(searchTerm), + ); + + beforeEach(() => { + searchEngine = new EntitySearchEngine(mockMatchStrategy); + }); + + it('should set searchable entities', () => { + const entities = [ + { id: '1', name: 'Entity One' }, + { id: '2', name: 'Entity Two' }, + ]; + searchEngine.setSearchableEntities(entities); + expect(searchEngine['searchableEntities']).toEqual(entities); + }); + + it('should search entities based on the match strategy', () => { + const entities = [ + { id: '1', name: 'Entity One' }, + { id: '2', name: 'Entity Two' }, + { id: '3', name: 'Another' }, + ]; + searchEngine.setSearchableEntities(entities); + + const result = searchEngine.search('Entity'); + expect(result).toEqual([ + { id: '1', name: 'Entity One' }, + { id: '2', name: 'Entity Two' }, + ]); + }); + + it('should return an empty array if no entities match the search term', () => { + const entities = [ + { id: '1', name: 'Entity One' }, + { id: '2', name: 'Entity Two' }, + ]; + searchEngine.setSearchableEntities(entities); + + const result = searchEngine.search('Nonexistent'); + expect(result).toEqual([]); + }); +}); diff --git a/libs/spa/core/ui/src/lib/entity-search/service/entity-search.service.ts b/libs/spa/core/ui/src/lib/entity-search/service/entity-search.service.ts new file mode 100644 index 000000000..701bf2893 --- /dev/null +++ b/libs/spa/core/ui/src/lib/entity-search/service/entity-search.service.ts @@ -0,0 +1,30 @@ +export interface IEntitySearchEngine { + search(searchTerm: string): Entity[]; + setSearchableEntities(entities: Entity[]): void; +} + +/* + * A generic search engine for entities which is able to search through a list of entities that can change over time. + */ +export class EntitySearchEngine + implements IEntitySearchEngine +{ + private searchableEntities: Entity[] = []; + + constructor( + private readonly entityMatchStrategy: ( + entity: Entity, + searchTerm: string, + ) => boolean, + ) {} + + setSearchableEntities(entities: Entity[]): void { + this.searchableEntities = entities; + } + + search(searchTerm: string): Entity[] { + return this.searchableEntities.filter((entity) => + this.entityMatchStrategy(entity, searchTerm), + ); + } +} diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/entity-selection-search.service.spec.ts b/libs/spa/core/ui/src/lib/entity-search/service/entity-selection-search.service.spec.ts similarity index 91% rename from libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/entity-selection-search.service.spec.ts rename to libs/spa/core/ui/src/lib/entity-search/service/entity-selection-search.service.spec.ts index bbbda4c6e..41280c52d 100644 --- a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/entity-selection-search.service.spec.ts +++ b/libs/spa/core/ui/src/lib/entity-search/service/entity-selection-search.service.spec.ts @@ -4,7 +4,7 @@ import { of } from 'rxjs'; import { GraphqlService } from '@kordis/spa/core/graphql'; -import { EntitySearchService } from '../../../../services/entity-search.service'; +import { IEntitySearchEngine } from './entity-search.service'; import { EntitySelectionSearchService } from './entity-selection-search.service'; class TestEntitySelectionService extends EntitySelectionSearchService< @@ -16,7 +16,7 @@ class TestEntitySelectionService extends EntitySelectionSearchService< protected query = {} as any; // Mock GraphQL query protected queryName = 'testEntities' as const; protected searchService = - createMock>(); // Mock search service + createMock>(); // Mock search service } describe('EntitySelectionSearchService', () => { @@ -69,7 +69,7 @@ describe('EntitySelectionSearchService', () => { it('should filter search', async () => { service.markAsSelected({ id: '1', name: 'Entity 1' }); - (service as any).searchService.searchByTerm.mockResolvedValue([ + (service as any).searchService.search.mockReturnValue([ { id: '1', name: 'Entity 1' }, { id: '2', name: 'Entity 2' }, ]); diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/entity-selection-search.service.ts b/libs/spa/core/ui/src/lib/entity-search/service/entity-selection-search.service.ts similarity index 85% rename from libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/entity-selection-search.service.ts rename to libs/spa/core/ui/src/lib/entity-search/service/entity-selection-search.service.ts index 94bdafd01..203d1df2a 100644 --- a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/entity-selection-search.service.ts +++ b/libs/spa/core/ui/src/lib/entity-search/service/entity-selection-search.service.ts @@ -1,10 +1,10 @@ import { inject } from '@angular/core'; import { TypedDocumentNode } from 'apollo-angular'; -import { Subject, map, shareReplay, startWith, switchMap } from 'rxjs'; +import { Subject, map, shareReplay, startWith, switchMap, tap } from 'rxjs'; import { GraphqlService } from '@kordis/spa/core/graphql'; -import { EntitySearchService } from '../../../../services/entity-search.service'; +import { IEntitySearchEngine } from './entity-search.service'; /* * This service handles the selection of entities in a context where an entity can only be selected once. @@ -16,11 +16,12 @@ export abstract class EntitySelectionSearchService< > { protected abstract query: TypedDocumentNode; protected abstract queryName: keyof TQuery; - protected abstract searchService: EntitySearchService; + protected abstract searchService: IEntitySearchEngine; private readonly entityIdsSelected = new Set(); private readonly gqlService = inject(GraphqlService); private readonly selectionChangedSubject$ = new Subject(); + readonly allPossibleEntitiesToSelect$ = this.selectionChangedSubject$.pipe( startWith(null), // trigger initial query switchMap(() => @@ -34,6 +35,7 @@ export abstract class EntitySelectionSearchService< ), ), ), + tap((entities) => this.searchService.setSearchableEntities(entities)), shareReplay({ bufferSize: 1, refCount: true }), ); @@ -54,7 +56,7 @@ export abstract class EntitySelectionSearchService< } async searchAllPossibilities(query: string): Promise { - const entities = await this.searchService.searchByTerm(query); + const entities = this.searchService.search(query); return entities.filter(({ id }) => !this.entityIdsSelected.has(id)); } } diff --git a/libs/spa/core/ui/src/lib/entity-search/service/match-strategies.spec.ts b/libs/spa/core/ui/src/lib/entity-search/service/match-strategies.spec.ts new file mode 100644 index 000000000..f6b815671 --- /dev/null +++ b/libs/spa/core/ui/src/lib/entity-search/service/match-strategies.spec.ts @@ -0,0 +1,99 @@ +import { AlertGroup, DeploymentAssignment, Unit } from '@kordis/shared/model'; + +import { + alertGroupMatchesByName, + unitMatchesByNameOrCallSign, + unitOrAlertGroupOfAssignmentMatches, +} from './match-strategies'; + +describe('unitMatchesByNameOrCallSign', () => { + const unit = { + name: 'Unit One', + callSign: 'Alpha', + callSignAbbreviation: 'A', + } as Unit; + + it('should return true if the unit name matches the search term', () => { + expect(unitMatchesByNameOrCallSign(unit, 'unit one')).toBe(true); + }); + + it('should return true if the call sign matches the search term', () => { + expect(unitMatchesByNameOrCallSign(unit, 'alpha')).toBe(true); + }); + + it('should return true if the call sign abbreviation matches the search term', () => { + expect(unitMatchesByNameOrCallSign(unit, 'a')).toBe(true); + }); + + it('should return false if no fields match the search term', () => { + expect(unitMatchesByNameOrCallSign(unit, 'bravo')).toBe(false); + }); +}); + +describe('alertGroupMatchesByName', () => { + const alertGroup = { + name: 'Alert Group One', + } as AlertGroup; + + it('should return true if the alert group name matches the search term', () => { + expect(alertGroupMatchesByName(alertGroup, 'alert group one')).toBe(true); + }); + + it('should return false if the alert group name does not match the search term', () => { + expect(alertGroupMatchesByName(alertGroup, 'alert group two')).toBe(false); + }); +}); + +describe('unitOrAlertGroupOfAssignmentMatches', () => { + const unitAssignment = { + __typename: 'DeploymentUnit', + unit: { + name: 'Unit One', + callSign: 'Alpha', + callSignAbbreviation: 'A', + }, + } as DeploymentAssignment; + + const alertGroupAssignment = { + __typename: 'DeploymentAlertGroup', + alertGroup: { + name: 'Alert Group One', + }, + assignedUnits: [ + { + unit: { + name: 'Unit Two', + callSign: 'Bravo', + callSignAbbreviation: 'B', + }, + }, + ], + } as DeploymentAssignment; + + it('should return true if the unit matches the search term', () => { + expect( + unitOrAlertGroupOfAssignmentMatches(unitAssignment, 'unit one'), + ).toBe(true); + }); + + it('should return true if the alert group matches the search term', () => { + expect( + unitOrAlertGroupOfAssignmentMatches( + alertGroupAssignment, + 'alert group one', + ), + ).toBe(true); + }); + + it('should return true if any assigned unit matches the search term', () => { + expect( + unitOrAlertGroupOfAssignmentMatches(alertGroupAssignment, 'unit two'), + ).toBe(true); + }); + + it('should return false if no fields match the search term', () => { + expect(unitOrAlertGroupOfAssignmentMatches(unitAssignment, 'bravo')).toBe( + false, + ); + }); +}); diff --git a/libs/spa/core/ui/src/lib/entity-search/service/match-strategies.ts b/libs/spa/core/ui/src/lib/entity-search/service/match-strategies.ts new file mode 100644 index 000000000..f20d37717 --- /dev/null +++ b/libs/spa/core/ui/src/lib/entity-search/service/match-strategies.ts @@ -0,0 +1,43 @@ +import { AlertGroup, DeploymentAssignment, Unit } from '@kordis/shared/model'; + +export function unitMatchesByNameOrCallSign( + unit: Unit, + searchTerm: string, +): boolean { + const transformedSearchTerm = searchTerm.toLowerCase(); + return ( + unit.name.toLowerCase().includes(transformedSearchTerm) || + unit.callSign.toLowerCase().includes(transformedSearchTerm) || + unit.callSignAbbreviation.toLowerCase().includes(transformedSearchTerm) + ); +} + +export function alertGroupMatchesByName( + alertGroup: AlertGroup, + searchTerm: string, +): boolean { + return alertGroup.name.toLowerCase().includes(searchTerm.toLowerCase()); +} + +export function unitOrAlertGroupOfAssignmentMatches( + assignment: DeploymentAssignment, + searchTerm: string, +): boolean { + const transformedSearchTerm = searchTerm.toLowerCase(); + switch (assignment.__typename) { + case 'DeploymentUnit': + return unitMatchesByNameOrCallSign( + assignment.unit, + transformedSearchTerm, + ); + case 'DeploymentAlertGroup': + return ( + alertGroupMatchesByName(assignment.alertGroup, transformedSearchTerm) || + assignment.assignedUnits.some(({ unit }) => + unitMatchesByNameOrCallSign(unit, transformedSearchTerm), + ) + ); + default: + return false; + } +} diff --git a/libs/spa/core/ui/src/lib/entity-search/service/unit-selection.service.ts b/libs/spa/core/ui/src/lib/entity-search/service/unit-selection.service.ts new file mode 100644 index 000000000..35a4fd5bb --- /dev/null +++ b/libs/spa/core/ui/src/lib/entity-search/service/unit-selection.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { TypedDocumentNode } from 'apollo-angular'; + +import { Query, Unit } from '@kordis/shared/model'; +import { gql } from '@kordis/spa/core/graphql'; + +import { EntitySearchEngine } from './entity-search.service'; +import { EntitySelectionSearchService } from './entity-selection-search.service'; +import { unitMatchesByNameOrCallSign } from './match-strategies'; + +/* + This service handles the selection of units in a context where a unit can only be selected once. + */ +@Injectable() +export class PossibleUnitSelectionsService extends EntitySelectionSearchService< + Unit, + { units: Query['units'] } +> { + protected query: TypedDocumentNode<{ units: Query['units'] }> = gql` + query { + units { + id + callSign + callSignAbbreviation + name + status { + status + receivedAt + source + } + assignment { + __typename + id + name + } + } + } + `; + protected queryName = 'units' as const; + protected searchService = new EntitySearchEngine(unitMatchesByNameOrCallSign); +} diff --git a/libs/spa/feature/deployment/src/lib/components/deployment/unit/status-badge.component.spec.ts b/libs/spa/core/ui/src/lib/status-badge.component.spec.ts similarity index 100% rename from libs/spa/feature/deployment/src/lib/components/deployment/unit/status-badge.component.spec.ts rename to libs/spa/core/ui/src/lib/status-badge.component.spec.ts diff --git a/libs/spa/feature/deployment/src/lib/components/deployment/unit/status-badge.component.ts b/libs/spa/core/ui/src/lib/status-badge.component.ts similarity index 95% rename from libs/spa/feature/deployment/src/lib/components/deployment/unit/status-badge.component.ts rename to libs/spa/core/ui/src/lib/status-badge.component.ts index f94d1e437..cd329cf4f 100644 --- a/libs/spa/feature/deployment/src/lib/components/deployment/unit/status-badge.component.ts +++ b/libs/spa/core/ui/src/lib/status-badge.component.ts @@ -7,7 +7,7 @@ import { } from '@angular/core'; import { NzTooltipDirective } from 'ng-zorro-antd/tooltip'; -import { STATUS_EXPLANATIONS } from '../../../status-explanations'; +import { STATUS_EXPLANATIONS } from './status-explanations'; @Component({ selector: 'krd-status-badge', diff --git a/libs/spa/feature/deployment/src/lib/status-explanations.ts b/libs/spa/core/ui/src/lib/status-explanations.ts similarity index 100% rename from libs/spa/feature/deployment/src/lib/status-explanations.ts rename to libs/spa/core/ui/src/lib/status-explanations.ts diff --git a/libs/spa/core/ui/src/test-setup.ts b/libs/spa/core/ui/src/test-setup.ts new file mode 100644 index 000000000..bc3333bb0 --- /dev/null +++ b/libs/spa/core/ui/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/spa/core/ui/tsconfig.json b/libs/spa/core/ui/tsconfig.json new file mode 100644 index 000000000..0522c8244 --- /dev/null +++ b/libs/spa/core/ui/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/spa/core/ui/tsconfig.lib.json b/libs/spa/core/ui/tsconfig.lib.json new file mode 100644 index 000000000..257edb93b --- /dev/null +++ b/libs/spa/core/ui/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts", "../../../../reset.d.ts"] +} diff --git a/libs/spa/core/ui/tsconfig.spec.json b/libs/spa/core/ui/tsconfig.spec.json new file mode 100644 index 000000000..fb725351d --- /dev/null +++ b/libs/spa/core/ui/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/spa/feature/deployment/src/lib/components/alert-group-edit-modal/alert-group-edit-modal.component.ts b/libs/spa/feature/deployment/src/lib/components/alert-group-edit-modal/alert-group-edit-modal.component.ts index 1efe03548..92e2d9a73 100644 --- a/libs/spa/feature/deployment/src/lib/components/alert-group-edit-modal/alert-group-edit-modal.component.ts +++ b/libs/spa/feature/deployment/src/lib/components/alert-group-edit-modal/alert-group-edit-modal.component.ts @@ -14,10 +14,10 @@ import { forkJoin } from 'rxjs'; import { Query, Unit } from '@kordis/shared/model'; import { GraphqlService, gql } from '@kordis/spa/core/graphql'; +import { PossibleUnitSelectionsService } from '@kordis/spa/core/ui'; +import { UnitsSelectComponent } from '@kordis/spa/core/ui'; -import { UnitsSelectComponent } from '../rescue-station/rescue-station-edit-modal/component/unit/units-select.component'; import { AlertGroupAssignmentFormGroup } from '../rescue-station/rescue-station-edit-modal/rescue-station-edit-modal.component'; -import { PossibleUnitSelectionsService } from '../rescue-station/rescue-station-edit-modal/service/unit-selection.service'; import { alertGroupMinUnitsValidator } from '../rescue-station/rescue-station-edit-modal/validator/alert-group-min-units.validator'; @Component({ diff --git a/libs/spa/feature/deployment/src/lib/components/deployment/deployment-search-wrapper.component.ts b/libs/spa/feature/deployment/src/lib/components/deployment/deployment-search-wrapper.component.ts index c5bd2d6ed..635244649 100644 --- a/libs/spa/feature/deployment/src/lib/components/deployment/deployment-search-wrapper.component.ts +++ b/libs/spa/feature/deployment/src/lib/components/deployment/deployment-search-wrapper.component.ts @@ -6,11 +6,16 @@ import { booleanAttribute, computed, contentChild, + effect, inject, input, } from '@angular/core'; -import { DeploymentAssignment, Unit } from '@kordis/shared/model'; +import { DeploymentAssignment } from '@kordis/shared/model'; +import { + EntitySearchEngine, + unitOrAlertGroupOfAssignmentMatches, +} from '@kordis/spa/core/ui'; import { DeploymentsSearchStateService } from '../../services/deployments-search-state.service'; @@ -22,7 +27,7 @@ import { DeploymentsSearchStateService } from '../../services/deployments-search } @@ -43,38 +48,20 @@ export class DeploymentSearchWrapperComponent { .toLowerCase() .includes(this.searchStateService.searchValue().toLowerCase()), ); - readonly computedAssignments: Signal = computed( + private readonly assignmentsSearchEngine = new EntitySearchEngine( + unitOrAlertGroupOfAssignmentMatches, + ); + readonly filteredAssignments: Signal = computed( () => { - const searchTerm = this.searchStateService.searchValue().toLowerCase(); // show if we have no search term or the name matches - if (!searchTerm || this.hasNameMatch()) { + if (!this.searchStateService.searchValue() || this.hasNameMatch()) { return this.assignments(); } - // otherwise we have a search term, return filtered assignments - return this.assignments().filter((assignment) => { - const hasUnitMatch = (unit: Unit): boolean => { - return ( - unit.name.toLowerCase().includes(searchTerm) || - unit.callSign.toLowerCase().includes(searchTerm) || - unit.callSignAbbreviation.toLowerCase().includes(searchTerm) - ); - }; - - switch (assignment.__typename) { - case 'DeploymentUnit': - return hasUnitMatch(assignment.unit); - case 'DeploymentAlertGroup': - return ( - assignment.alertGroup.name.toLowerCase().includes(searchTerm) || - assignment.assignedUnits.some(({ unit }) => hasUnitMatch(unit)) - ); - default: - return false; - } - }); + return this.assignmentsSearchEngine.search( + this.searchStateService.searchValue(), + ); }, ); - readonly isVisible = computed( () => this.alwaysShow() || @@ -83,6 +70,12 @@ export class DeploymentSearchWrapperComponent { // the user searches the rescue station this.hasNameMatch() || // or the user searches the assignments - this.computedAssignments().length, + this.filteredAssignments().length, ); + + constructor() { + effect(() => + this.assignmentsSearchEngine.setSearchableEntities(this.assignments()), + ); + } } diff --git a/libs/spa/feature/deployment/src/lib/components/deployment/unit/deployment-unit.component.ts b/libs/spa/feature/deployment/src/lib/components/deployment/unit/deployment-unit.component.ts index c431b2f1d..d7e92cf7a 100644 --- a/libs/spa/feature/deployment/src/lib/components/deployment/unit/deployment-unit.component.ts +++ b/libs/spa/feature/deployment/src/lib/components/deployment/unit/deployment-unit.component.ts @@ -6,10 +6,10 @@ import { Subject, debounceTime, delay, switchMap, tap } from 'rxjs'; import { Unit } from '@kordis/shared/model'; import { GraphqlService, gql } from '@kordis/spa/core/graphql'; +import { StatusBadgeComponent } from '@kordis/spa/core/ui'; import { DeploymentNotePopupComponent } from '../deployment-note-popup.component'; import { NoteIndicatorComponent } from '../note-indicator.component'; -import { StatusBadgeComponent } from './status-badge.component'; @Component({ selector: 'krd-deployment-unit', diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-assignment.model.ts b/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group-assignment.model.ts similarity index 100% rename from libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group/alert-group-assignment.model.ts rename to libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/component/alert-group-assignment.model.ts diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/rescue-station-edit-modal.component.ts b/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/rescue-station-edit-modal.component.ts index fa8ea4018..871a56b6d 100644 --- a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/rescue-station-edit-modal.component.ts +++ b/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/rescue-station-edit-modal.component.ts @@ -24,18 +24,20 @@ import { RescueStationDeployment, Unit, } from '@kordis/shared/model'; +import { + AlertGroupSelectionsComponent, + PossibleAlertGroupSelectionsService, + PossibleUnitSelectionsService, + UnitsSelectComponent, +} from '@kordis/spa/core/ui'; -import { AlertGroupSelectionsComponent } from './component/alert-group/alert-group-selections.component'; import { ProtocolDataComponent } from './component/protocol-data.component'; import { StrengthComponent } from './component/strength.component'; -import { UnitsSelectComponent } from './component/unit/units-select.component'; -import { PossibleAlertGroupSelectionsService } from './service/alert-group-selection.service'; import { ProtocolMessageData, RescueStationData, RescueStationEditService, } from './service/rescue-station-edit.service'; -import { PossibleUnitSelectionsService } from './service/unit-selection.service'; import { alertGroupMinUnitsValidator } from './validator/alert-group-min-units.validator'; import { minStrengthValidator } from './validator/min-strength.validator'; diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/alert-group-selection.service.ts b/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/alert-group-selection.service.ts deleted file mode 100644 index 2e1d067a5..000000000 --- a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/alert-group-selection.service.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { TypedDocumentNode } from 'apollo-angular'; -import { firstValueFrom, map } from 'rxjs'; - -import { AlertGroup, Query } from '@kordis/shared/model'; -import { GraphqlService, gql } from '@kordis/spa/core/graphql'; - -import { - AbstractEntitySearchService, - EntitySearchService, - SearchEntityProvider, -} from '../../../../services/entity-search.service'; -import { EntitySelectionSearchService } from './entity-selection-search.service'; - -@Injectable({ - providedIn: 'root', -}) -class SelectableAlertGroupsProvider - implements SearchEntityProvider -{ - private readonly gqlService = inject(GraphqlService); - - provideByIds(ids: string[]): Promise { - return firstValueFrom( - this.gqlService - .queryOnce$<{ alertGroups: Query['alertGroups'] }>( - gql` - query GetAlertGroupsByIds($ids: [String!]!) { - alertGroups(filter: { ids: $ids }) { - id - name - currentUnits { - id - name - callSign - note - status { - status - } - assignment { - __typename - id - name - } - } - assignment { - __typename - id - name - } - } - } - `, - { - ids, - }, - ) - .pipe(map(({ alertGroups }) => alertGroups)), - ); - } - - provideInitial(): Promise { - return firstValueFrom( - this.gqlService - .queryOnce$<{ alertGroups: Query['alertGroups'] }>(gql` - query GetAlertGroups { - alertGroups { - id - name - } - } - `) - .pipe(map(({ alertGroups }) => alertGroups)), - ); - } -} - -@Injectable({ - providedIn: 'root', -}) -class SelectableAlertGroupSearchService extends AbstractEntitySearchService { - protected constructor(alertGroupsProvider: SelectableAlertGroupsProvider) { - super(alertGroupsProvider, ['name']); - } -} - -/* - This service handles the selection of alert groups in a context where a unit can only be selected once. - */ -@Injectable() -export class PossibleAlertGroupSelectionsService extends EntitySelectionSearchService< - AlertGroup, - { alertGroups: Query['alertGroups'] } -> { - protected override query: TypedDocumentNode<{ - alertGroups: Query['alertGroups']; - }> = gql` - query { - alertGroups { - id - name - currentUnits { - id - name - callSign - status { - status - } - assignment { - __typename - id - name - } - } - assignment { - __typename - id - name - } - } - } - `; - protected queryName = 'alertGroups' as const; - protected searchService: EntitySearchService = inject( - SelectableAlertGroupSearchService, - ); -} diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/rescue-station-edit.service.ts b/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/rescue-station-edit.service.ts index db9003b13..9e3dd4276 100644 --- a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/rescue-station-edit.service.ts +++ b/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/rescue-station-edit.service.ts @@ -9,7 +9,7 @@ import { } from '@kordis/shared/model'; import { GraphqlService, gql } from '@kordis/spa/core/graphql'; -import { AlertGroupAssignment } from '../component/alert-group/alert-group-assignment.model'; +import { AlertGroupAssignment } from '../component/alert-group-assignment.model'; export interface ProtocolMessageData { sender: Unit | string; diff --git a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/unit-selection.service.ts b/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/unit-selection.service.ts deleted file mode 100644 index 30e1ab72e..000000000 --- a/libs/spa/feature/deployment/src/lib/components/rescue-station/rescue-station-edit-modal/service/unit-selection.service.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { TypedDocumentNode } from 'apollo-angular'; -import { firstValueFrom, map } from 'rxjs'; - -import { Query, Unit } from '@kordis/shared/model'; -import { GraphqlService, gql } from '@kordis/spa/core/graphql'; - -import { - AbstractEntitySearchService, - EntitySearchService, - SearchEntityProvider, -} from '../../../../services/entity-search.service'; -import { EntitySelectionSearchService } from './entity-selection-search.service'; - -@Injectable({ - providedIn: 'root', -}) -class SelectableUnitsProvider implements SearchEntityProvider { - private readonly gqlService = inject(GraphqlService); - - provideByIds(ids: string[]): Promise { - return firstValueFrom( - this.gqlService - .queryOnce$<{ units: Query['units'] }>( - gql` - query GetUnitsByIds($ids: [ID!]!) { - units(filter: { ids: $ids }) { - id - callSign - callSignAbbreviation - name - status { - status - } - assignment { - __typename - name - } - } - } - `, - { - ids, - }, - ) - .pipe(map(({ units }) => units)), - ); - } - - provideInitial(): Promise { - return firstValueFrom( - this.gqlService - .queryOnce$<{ units: Query['units'] }>(gql` - query GetUnits { - units { - id - callSign - callSignAbbreviation - name - status { - status - } - assignment { - __typename - name - } - } - } - `) - .pipe(map(({ units }) => units)), - ); - } -} - -@Injectable({ - providedIn: 'root', -}) -class UnitSearchService extends AbstractEntitySearchService { - constructor(unitSearchEntityProvider: SelectableUnitsProvider) { - super(unitSearchEntityProvider, [ - 'callSign', - 'callSignAbbreviation', - 'name', - ]); - } -} - -/* - This service handles the selection of units in a context where a unit can only be selected once. - */ -@Injectable() -export class PossibleUnitSelectionsService extends EntitySelectionSearchService< - Unit, - { units: Query['units'] } -> { - protected query: TypedDocumentNode<{ units: Query['units'] }> = gql` - query { - units { - id - callSign - callSignAbbreviation - name - status { - status - receivedAt - source - } - assignment { - __typename - id - name - } - } - } - `; - protected queryName = 'units' as const; - protected searchService: EntitySearchService = - inject(UnitSearchService); -} diff --git a/libs/spa/feature/deployment/src/lib/services/entity-search.service.spec.ts b/libs/spa/feature/deployment/src/lib/services/entity-search.service.spec.ts deleted file mode 100644 index 39d1504fb..000000000 --- a/libs/spa/feature/deployment/src/lib/services/entity-search.service.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { AbstractEntitySearchService } from './entity-search.service'; - -interface TestEntity { - id: string; - name: string; - someMoreData: string; -} - -const TEST_ENTITY_PROVIDER = { - provideByIds: jest.fn(), - provideInitial: jest.fn().mockResolvedValue([ - { id: '1', name: 'Entity One' }, - { id: '2', name: 'Entity Two' }, - ]), -}; - -class TestEntitySearchService extends AbstractEntitySearchService { - constructor() { - super(TEST_ENTITY_PROVIDER, ['name']); - } -} - -const searchEngineAddAllMock = jest.fn(); -const searchEngineSearchMock = jest.fn(); -jest.mock('minisearch', () => - jest.fn().mockImplementation(() => ({ - addAll: searchEngineAddAllMock, - search: searchEngineSearchMock, - })), -); - -describe('TestEntitySearchService', () => { - let service: TestEntitySearchService; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [TestEntitySearchService], - }); - - service = TestBed.inject(TestEntitySearchService); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should fetch all entities initially', () => { - expect(TEST_ENTITY_PROVIDER.provideInitial).toHaveBeenCalled(); - expect(searchEngineAddAllMock).toHaveBeenCalledWith([ - { id: '1', name: 'Entity One' }, - { id: '2', name: 'Entity Two' }, - ]); - }); - - it('should populate entities correctly on search by term', async () => { - searchEngineSearchMock.mockReturnValueOnce([ - { id: '1', name: 'Entity One' }, - ]); - TEST_ENTITY_PROVIDER.provideByIds.mockResolvedValueOnce([ - { id: '1', name: 'Entity One', someMoreData: 'Some more data' }, - ]); - const entities = await service.searchByTerm('One'); - expect(TEST_ENTITY_PROVIDER.provideByIds).toHaveBeenCalledWith(['1']); - expect(entities).toEqual([ - { id: '1', name: 'Entity One', someMoreData: 'Some more data' }, - ]); - }); -}); diff --git a/libs/spa/feature/deployment/src/lib/services/entity-search.service.ts b/libs/spa/feature/deployment/src/lib/services/entity-search.service.ts deleted file mode 100644 index 666565012..000000000 --- a/libs/spa/feature/deployment/src/lib/services/entity-search.service.ts +++ /dev/null @@ -1,60 +0,0 @@ -import MiniSearch from 'minisearch'; - -/* - * This provider is used to provide the entities that should be searchable. - * provideInitial is used to provide the initial entities that should be indexed. It returns the necessary properties to search. - * provideByIds is used to provide the entities by their ids. It returns the complete entity. - */ -export interface SearchEntityProvider< - TEntity extends { - id: string; - }, -> { - provideByIds(ids: string[]): Promise; - - provideInitial(): Promise[]>; -} - -export interface EntitySearchService { - searchByTerm(query: string): Promise; -} - -/* - * This service is used to search for entities by a search term. - * It indexes the entities and provides a search method to search for entities by a search term. - */ -export abstract class AbstractEntitySearchService< - TEntity extends { - id: string; - }, -> implements EntitySearchService -{ - protected readonly searchEngine: MiniSearch; - - protected constructor( - private readonly entityProvider: SearchEntityProvider, - searchFields: (keyof TEntity)[], - ) { - this.searchEngine = new MiniSearch({ - fields: searchFields as string[], - }); - // first index all entities with the necessary data to search - this.indexInitialEntitiesAsync(); - } - - searchByTerm(query: string): Promise { - const res = this.searchEngine.search(query, { - prefix: true, - combineWith: 'AND', - }); - - // populate the entities to get the whole entity - return this.entityProvider.provideByIds(res.map((r) => r.id)); - } - - private indexInitialEntitiesAsync(): void { - this.entityProvider - .provideInitial() - .then((entities) => this.searchEngine.addAll(entities)); - } -} diff --git a/package-lock.json b/package-lock.json index 4a3b3781c..ce1d18576 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,6 @@ "graphql-sse": "^2.5.3", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", - "minisearch": "^7.1.1", "mongodb-client-encryption": "^6.1.1", "mongoose": "^8.9.2", "nestjs-graphql-connection": "^1.0.3", @@ -27580,12 +27579,6 @@ "node": ">=8" } }, - "node_modules/minisearch": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.1.tgz", - "integrity": "sha512-b3YZEYCEH4EdCAtYP7OlDyx7FdPwNzuNwLQ34SfJpM9dlbBZzeXndGavTrC+VCiRWomL21SWfMc6SCKO/U2ZNw==", - "license": "MIT" - }, "node_modules/minizlib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", diff --git a/package.json b/package.json index fc70cd858..8d8b38280 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,6 @@ "graphql-sse": "^2.5.3", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", - "minisearch": "^7.1.1", "mongodb-client-encryption": "^6.1.1", "mongoose": "^8.9.2", "nestjs-graphql-connection": "^1.0.3", diff --git a/tsconfig.base.json b/tsconfig.base.json index f8ad3c6a8..0c4ad25d0 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -34,6 +34,7 @@ "@kordis/spa/core/observability": [ "libs/spa/core/observability/src/index.ts" ], + "@kordis/spa/core/ui": ["libs/spa/core/ui/src/index.ts"], "@kordis/spa/feature/deployment": [ "libs/spa/feature/deployment/src/index.ts" ],