From 84bc8a47e8af65388ac534ec32b4a9f3b0102a15 Mon Sep 17 00:00:00 2001 From: Lukas Hagen <43916057+Greenscreen23@users.noreply.github.com> Date: Fri, 12 May 2023 13:58:15 +0200 Subject: [PATCH] Vehicles can now be the leader of a simulated region --- CHANGELOG.md | 1 + ...view-behavior-assign-leader.component.html | 61 +++++- ...erview-behavior-assign-leader.component.ts | 103 ++++++++-- ...egion-overview-behavior-tab.component.html | 3 +- .../vehicle-occupation-editor.component.html | 3 + .../utils/occupations/exercise-occupation.ts | 3 + shared/src/models/utils/occupations/index.ts | 1 + .../utils/occupations/is-leader-occupation.ts | 22 +++ .../src/simulation/behaviors/assign-leader.ts | 184 ++++++++++-------- ...dd-leadership-vehicles-to-assign-leader.ts | 90 +++++++++ .../state-migrations/migration-functions.ts | 2 + shared/src/state.ts | 2 +- .../src/store/action-reducers/simulation.ts | 74 +++++++ shared/src/utils/validators/is-string-set.ts | 23 +++ test-scenarios | 2 +- 15 files changed, 480 insertions(+), 94 deletions(-) create mode 100644 shared/src/models/utils/occupations/is-leader-occupation.ts create mode 100644 shared/src/state-migrations/35-add-leadership-vehicles-to-assign-leader.ts create mode 100644 shared/src/utils/validators/is-string-set.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f782d00a..9751b8faf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org - Add behaviors button now opens towards the top. - Simulated regions can now send patients to any hospital. The hospitals tab was removed. +- The assign leader behavior now prioritized vehicles instead of gfs. Custom command vehicles can be set in the frontend. ### Fixed diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/assign-leader/simulated-region-overview-behavior-assign-leader.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/assign-leader/simulated-region-overview-behavior-assign-leader.component.html index 5aa889b44..ac9cad531 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/assign-leader/simulated-region-overview-behavior-assign-leader.component.html +++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/assign-leader/simulated-region-overview-behavior-assign-leader.component.html @@ -1,9 +1,64 @@ -
Aktuell zugewiesene Führungskraft
+
Aktuell zugewiesene Führungskraft
-

- {{ (currentLeader | async)?.personnelType | personnelName }} +

+ {{ currentLeader.personnelType | personnelName }} + aus Fahrzeug {{ vehicleOfCurrentLeader.name }}

Keine Führungskraft zugewiesen.

+ +
Mögliche Kommandofahrzeuge
+ + + +
+ +
+ +
+
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/assign-leader/simulated-region-overview-behavior-assign-leader.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/assign-leader/simulated-region-overview-behavior-assign-leader.component.ts index 7840418cf..4d0f1a5cf 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/assign-leader/simulated-region-overview-behavior-assign-leader.component.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/assign-leader/simulated-region-overview-behavior-assign-leader.component.ts @@ -1,11 +1,22 @@ import type { OnChanges } from '@angular/core'; import { Component, Input } from '@angular/core'; import { Store } from '@ngrx/store'; -import type { Personnel } from 'digital-fuesim-manv-shared'; -import { AssignLeaderBehaviorState } from 'digital-fuesim-manv-shared'; +import { UUID } from 'digital-fuesim-manv-shared'; +import type { + AssignLeaderBehaviorState, + Personnel, + Vehicle, +} from 'digital-fuesim-manv-shared'; import type { Observable } from 'rxjs'; +import { combineLatest, map } from 'rxjs'; +import { ExerciseService } from 'src/app/core/exercise.service'; import type { AppState } from 'src/app/state/app.state'; -import { createSelectPersonnel } from 'src/app/state/application/selectors/exercise.selectors'; +import { + createSelectBehaviorState, + selectPersonnel, + selectVehicleTemplates, + selectVehicles, +} from 'src/app/state/application/selectors/exercise.selectors'; @Component({ selector: 'app-simulated-region-overview-behavior-assign-leader', @@ -18,18 +29,86 @@ import { createSelectPersonnel } from 'src/app/state/application/selectors/exerc export class SimulatedRegionOverviewBehaviorAssignLeaderComponent implements OnChanges { - @Input() - assignLeaderBehaviorState!: AssignLeaderBehaviorState; + @Input() assignLeaderBehaviorId!: UUID; + @Input() simulatedRegionId!: UUID; - currentLeader?: Observable; + behaviorState$!: Observable; - constructor(private readonly store: Store) {} + currentLeader$!: Observable; + + vehicleOfCurrentLeader$!: Observable; + + vehicleTypesToAdd$!: Observable; + + constructor( + private readonly store: Store, + private readonly exerciseService: ExerciseService + ) {} ngOnChanges(): void { - if (this.assignLeaderBehaviorState.leaderId) { - this.currentLeader = this.store.select( - createSelectPersonnel(this.assignLeaderBehaviorState.leaderId) - ); - } + this.behaviorState$ = this.store.select( + createSelectBehaviorState( + this.simulatedRegionId, + this.assignLeaderBehaviorId + ) + ); + + const personnel$ = this.store.select(selectPersonnel); + + this.currentLeader$ = combineLatest([ + this.behaviorState$, + personnel$, + ]).pipe( + map(([behaviorState, personnel]) => + behaviorState.leaderId + ? personnel[behaviorState.leaderId] + : undefined + ) + ); + + const vehicles$ = this.store.select(selectVehicles); + + this.vehicleOfCurrentLeader$ = combineLatest([ + this.currentLeader$, + vehicles$, + ]).pipe( + map(([currentLeader, vehicles]) => + currentLeader ? vehicles[currentLeader.vehicleId] : undefined + ) + ); + + const vehicleTemplates$ = this.store.select(selectVehicleTemplates); + + this.vehicleTypesToAdd$ = combineLatest([ + this.behaviorState$, + vehicleTemplates$, + ]).pipe( + map(([behaviorState, vehicleTemplates]) => + vehicleTemplates + .map((vehicleTemplate) => vehicleTemplate.vehicleType) + .filter( + (vehicleType) => + !behaviorState.leadershipVehicleTypes[vehicleType] + ) + ) + ); + } + + addVehicleType(vehicleType: string) { + this.exerciseService.proposeAction({ + type: '[AssignLeaderBehavior] Add Leadership Vehicle Type', + simulatedRegionId: this.simulatedRegionId, + behaviorId: this.assignLeaderBehaviorId, + vehicleType, + }); + } + + removeVehicleType(vehicleType: string) { + this.exerciseService.proposeAction({ + type: '[AssignLeaderBehavior] Remove Leadership Vehicle Type', + simulatedRegionId: this.simulatedRegionId, + behaviorId: this.assignLeaderBehaviorId, + vehicleType, + }); } } diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.html index 4f35f466d..9a45399e3 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.html +++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.html @@ -30,7 +30,8 @@
+ + Das Fahrzeug führt diese Region + Die Tätigkeit ist unbekannt. diff --git a/shared/src/models/utils/occupations/exercise-occupation.ts b/shared/src/models/utils/occupations/exercise-occupation.ts index 68a57dd36..287e57028 100644 --- a/shared/src/models/utils/occupations/exercise-occupation.ts +++ b/shared/src/models/utils/occupations/exercise-occupation.ts @@ -7,6 +7,7 @@ import { LoadOccupation } from './load-occupation'; import { WaitForTransferOccupation } from './wait-for-transfer-occupation'; import { UnloadingOccupation } from './unloading-occupation'; import { PatientTransferOccupation } from './patient-transfer-occupation'; +import { IsLeaderOccupation } from './is-leader-occupation'; export const occupations = { IntermediateOccupation, @@ -15,6 +16,7 @@ export const occupations = { WaitForTransferOccupation, UnloadingOccupation, PatientTransferOccupation, + IsLeaderOccupation, }; export type ExerciseOccupation = InstanceType< @@ -34,6 +36,7 @@ export const occupationDictionary: ExerciseOccupationDictionary = { waitForTransferOccupation: WaitForTransferOccupation, unloadingOccupation: UnloadingOccupation, patientTransferOccupation: PatientTransferOccupation, + isLeaderOccupation: IsLeaderOccupation, }; export const occupationTypeOptions: Parameters = [ diff --git a/shared/src/models/utils/occupations/index.ts b/shared/src/models/utils/occupations/index.ts index 40f2e8f16..f941bd0f7 100644 --- a/shared/src/models/utils/occupations/index.ts +++ b/shared/src/models/utils/occupations/index.ts @@ -7,3 +7,4 @@ export * from './occupation-helpers'; export * from './patient-transfer-occupation'; export * from './unloading-occupation'; export * from './wait-for-transfer-occupation'; +export * from './is-leader-occupation'; diff --git a/shared/src/models/utils/occupations/is-leader-occupation.ts b/shared/src/models/utils/occupations/is-leader-occupation.ts new file mode 100644 index 000000000..719cfbec1 --- /dev/null +++ b/shared/src/models/utils/occupations/is-leader-occupation.ts @@ -0,0 +1,22 @@ +import { IsUUID } from 'class-validator'; +import { UUID, uuidValidationOptions } from '../../../utils'; +import { IsValue } from '../../../utils/validators'; +import { getCreate } from '../get-create'; +import type { Occupation } from './occupation'; + +export class IsLeaderOccupation implements Occupation { + @IsValue('isLeaderOccupation') + readonly type = 'isLeaderOccupation'; + + @IsUUID(4, uuidValidationOptions) + readonly simulatedRegionId: UUID; + + /** + * @deprecated Use static `create` method instead. + */ + constructor(simulatedRegionId: UUID) { + this.simulatedRegionId = simulatedRegionId; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/simulation/behaviors/assign-leader.ts b/shared/src/simulation/behaviors/assign-leader.ts index 02438c812..e50d43f7d 100644 --- a/shared/src/simulation/behaviors/assign-leader.ts +++ b/shared/src/simulation/behaviors/assign-leader.ts @@ -1,22 +1,35 @@ import { IsOptional, IsUUID } from 'class-validator'; import { groupBy } from 'lodash-es'; -import type { SimulatedRegion } from '../../models'; +import type { SimulatedRegion, Vehicle } from '../../models'; import type { MaterialCountRadiogram, PersonnelCountRadiogram, VehicleCountRadiogram, } from '../../models/radiogram'; import type { PersonnelType } from '../../models/utils'; -import { getCreate, isInSpecificSimulatedRegion } from '../../models/utils'; -import { getActivityById, getElement } from '../../store/action-reducers/utils'; +import { + IsLeaderOccupation, + NoOccupation, + isUnoccupied, + getCreate, + isInSpecificSimulatedRegion, +} from '../../models/utils'; +import { getActivityById } from '../../store/action-reducers/utils'; import type { Mutable } from '../../utils'; -import { StrictObject, UUID, uuid, uuidValidationOptions } from '../../utils'; +import { + StrictObject, + cloneDeepMutable, + UUID, + uuid, + uuidValidationOptions, +} from '../../utils'; import { IsValue } from '../../utils/validators'; import { LeaderChangedEvent } from '../events/leader-changed'; import type { ExerciseState } from '../../state'; import { addActivity } from '../activities/utils'; import { DelayEventActivityState } from '../activities'; import { nextUUID } from '../utils/randomness'; +import { IsStringSet } from '../../utils/validators/is-string-set'; import type { SimulationBehavior, SimulationBehaviorState, @@ -33,6 +46,11 @@ export class AssignLeaderBehaviorState implements SimulationBehaviorState { @IsUUID(4, uuidValidationOptions) public readonly leaderId: UUID | undefined; + @IsStringSet() + public readonly leadershipVehicleTypes: { [key: string]: true } = { + 'GW-San': true, + }; + static readonly create = getCreate(this); } @@ -49,63 +67,84 @@ export const assignLeaderBehavior: SimulationBehavior behaviorState: AssignLeaderBehaviorState, handleEvent(draftState, simulatedRegion, behaviorState, event) { switch (event.type) { - case 'personnelAvailableEvent': + case 'tickEvent': { - // If a gf (group leader of GW San) enters the region, we want to assign them as leader, since a gf can't treat patients - // A gf has the highest priority, so they would be chosen by the logic for the tick event anyways - // Therefore, this branch only serves the purpose to switch the leader - if (!behaviorState.leaderId) { - return; - } - - const currentLeader = getElement( - draftState, - 'personnel', - behaviorState.leaderId - ); - - if (currentLeader.personnelType === 'gf') { - return; - } + const leader = behaviorState.leaderId + ? draftState.personnel[behaviorState.leaderId] + : undefined; - const newPersonnel = getElement( - draftState, - 'personnel', - event.personnelId - ); + const vehicleOfLeader = leader + ? draftState.vehicles[leader.vehicleId] + : undefined; - if (newPersonnel.personnelType === 'gf') { + if ( + behaviorState.leaderId && + (!leader || + !vehicleOfLeader || + !isInSpecificSimulatedRegion( + vehicleOfLeader, + simulatedRegion.id + ) || + vehicleOfLeader.occupation.type !== + 'isLeaderOccupation' || + vehicleOfLeader.occupation.simulatedRegionId !== + simulatedRegion.id) + ) { changeLeader( draftState, simulatedRegion, behaviorState, - event.personnelId + vehicleOfLeader, + undefined ); } - } - break; - case 'tickEvent': - { - if (!behaviorState.leaderId) { - selectNewLeader( - draftState, - simulatedRegion, - behaviorState, - false + + const vehicles = Object.values( + draftState.vehicles + ).filter( + (vehicle) => + isInSpecificSimulatedRegion( + vehicle, + simulatedRegion.id + ) && + isUnoccupied(vehicle, draftState.currentTime) + ); + + if ( + !behaviorState.leaderId || + !vehicleOfLeader || + !behaviorState.leadershipVehicleTypes[ + vehicleOfLeader.vehicleType + ] + ) { + const leadershipVehicles = vehicles.filter( + (vehicle) => + behaviorState.leadershipVehicleTypes[ + vehicle.vehicleType + ] ); + + if (leadershipVehicles.length > 0) { + changeLeader( + draftState, + simulatedRegion, + behaviorState, + vehicleOfLeader, + leadershipVehicles[0]! + ); + } } - } - break; - case 'personnelRemovedEvent': - { - // if the leader is removed from the region a new leader is selected - if (event.personnelId === behaviorState.leaderId) { - selectNewLeader( - draftState, - simulatedRegion, - behaviorState, - true - ); + + if (!behaviorState.leaderId) { + if (vehicles.length > 0) { + changeLeader( + draftState, + simulatedRegion, + behaviorState, + vehicleOfLeader, + vehicles[0]! + ); + } } } break; @@ -242,39 +281,32 @@ export const assignLeaderBehavior: SimulationBehavior }, }; -function selectNewLeader( +function changeLeader( draftState: Mutable, simulatedRegion: Mutable, behaviorState: Mutable, - changeLeaderIfNoLeaderSelected: boolean + currentVehicle: Mutable | undefined, + newVehicle: Mutable | undefined ) { - const personnel = Object.values(draftState.personnel).filter( - (pers) => - isInSpecificSimulatedRegion(pers, simulatedRegion.id) && - pers.personnelType !== 'notarzt' - ); - - if (personnel.length === 0) { - if (changeLeaderIfNoLeaderSelected) { - changeLeader(draftState, simulatedRegion, behaviorState, undefined); - } - return; + if (currentVehicle) { + currentVehicle.occupation = cloneDeepMutable(NoOccupation.create()); + } + if (newVehicle) { + newVehicle.occupation = cloneDeepMutable( + IsLeaderOccupation.create(simulatedRegion.id) + ); } - personnel.sort( - (a, b) => - personnelPriorities[b.personnelType] - - personnelPriorities[a.personnelType] - ); - changeLeader(draftState, simulatedRegion, behaviorState, personnel[0]?.id); -} + const newLeaderId = newVehicle + ? Object.values(draftState.personnel) + .filter((personnel) => newVehicle.personnelIds[personnel.id]) + .sort( + (a, b) => + personnelPriorities[b.personnelType] - + personnelPriorities[a.personnelType] + )[0]?.id + : undefined; -function changeLeader( - draftState: Mutable, - simulatedRegion: Mutable, - behaviorState: Mutable, - newLeaderId: UUID | undefined -) { addActivity( simulatedRegion, DelayEventActivityState.create( diff --git a/shared/src/state-migrations/35-add-leadership-vehicles-to-assign-leader.ts b/shared/src/state-migrations/35-add-leadership-vehicles-to-assign-leader.ts new file mode 100644 index 000000000..b8516bf7f --- /dev/null +++ b/shared/src/state-migrations/35-add-leadership-vehicles-to-assign-leader.ts @@ -0,0 +1,90 @@ +import type { Migration } from './migration-functions'; + +interface BehaviorState { + type: `${string}Behavior`; +} + +interface AssignLeaderBehaviorState extends BehaviorState { + type: 'assignLeaderBehavior'; + leadershipVehicleTypes?: { [key: string]: true }; +} + +interface SimulatedRegion { + behaviors: BehaviorState[]; +} + +interface Action { + type: string; +} + +interface AddBehaviorAction extends Action { + type: '[SimulatedRegion] Add Behavior'; + behaviorState: BehaviorState; +} + +interface AddSimulatedRegionAction extends Action { + type: '[SimulatedRegion] Add simulated region'; + simulatedRegion: SimulatedRegion; +} + +interface State { + simulatedRegions: { + [key: string]: SimulatedRegion; + }; +} + +export const addLeadershipVehiclesToAssignLeader35: Migration = { + action: (_intermediaryState, action) => { + const typedAction = action as Action; + + if (typedAction.type === '[SimulatedRegion] Add Behavior') { + const specificlyTypedAction = action as AddBehaviorAction; + + if ( + specificlyTypedAction.behaviorState.type === + 'assignLeaderBehavior' + ) { + migrateBehavior( + specificlyTypedAction.behaviorState as AssignLeaderBehaviorState + ); + } + } + + if (typedAction.type === '[SimulatedRegion] Add simulated region') { + const specificlyTypedAction = action as AddSimulatedRegionAction; + specificlyTypedAction.simulatedRegion.behaviors.forEach( + (behavior) => { + if (behavior.type === 'assignLeaderBehavior') { + migrateBehavior(behavior as AssignLeaderBehaviorState); + } + } + ); + } + + return true; + }, + state: (state) => { + const typedState = state as State; + + Object.values(typedState.simulatedRegions).forEach( + (simulatedRegion) => { + simulatedRegion.behaviors.forEach((behavior) => { + if (behavior.type === 'assignLeaderBehavior') { + migrateBehavior(behavior as AssignLeaderBehaviorState); + } + }); + } + ); + }, +}; + +function migrateBehavior(behavior: AssignLeaderBehaviorState) { + const typedBehavior = behavior as { + type: 'assignLeaderBehavior'; + leadershipVehicleTypes?: { [key: string]: true }; + }; + + typedBehavior.leadershipVehicleTypes = { + 'GW-San': true, + }; +} diff --git a/shared/src/state-migrations/migration-functions.ts b/shared/src/state-migrations/migration-functions.ts index fec12d232..c30f44592 100644 --- a/shared/src/state-migrations/migration-functions.ts +++ b/shared/src/state-migrations/migration-functions.ts @@ -24,6 +24,7 @@ import { improveLoadVehicleActivity31 } from './31-improve-load-vehicle-activity import { removeIdFromEvents32 } from './32-remove-id-from-events'; import { reportTransferCategoryCompleted33 } from './33-report-transfer-category-completed'; import { addCatchAllHospital34 } from './34-add-catch-all-hospital'; +import { addLeadershipVehiclesToAssignLeader35 } from './35-add-leadership-vehicles-to-assign-leader'; import { removeSetParticipantIdAction4 } from './4-remove-set-participant-id-action'; import { removeStatistics5 } from './5-remove-statistics'; import { removeStateHistory6 } from './6-remove-state-history'; @@ -91,4 +92,5 @@ export const migrations: { 32: removeIdFromEvents32, 33: reportTransferCategoryCompleted33, 34: addCatchAllHospital34, + 35: addLeadershipVehiclesToAssignLeader35, }; diff --git a/shared/src/state.ts b/shared/src/state.ts index 21600aa2f..59275d4f5 100644 --- a/shared/src/state.ts +++ b/shared/src/state.ts @@ -166,5 +166,5 @@ export class ExerciseState { * * This number MUST be increased every time a change to any object (that is part of the state or the state itself) is made in a way that there may be states valid before that are no longer valid. */ - static readonly currentStateVersion = 34; + static readonly currentStateVersion = 35; } diff --git a/shared/src/store/action-reducers/simulation.ts b/shared/src/store/action-reducers/simulation.ts index 149e0fbe2..1c9d0234e 100644 --- a/shared/src/store/action-reducers/simulation.ts +++ b/shared/src/store/action-reducers/simulation.ts @@ -392,6 +392,35 @@ export class SendTransferRequestEventAction implements Action { public readonly patients!: UUIDSet; } +export class AddLeadershipVehicleTypeAction implements Action { + @IsValue('[AssignLeaderBehavior] Add Leadership Vehicle Type') + public readonly type = '[AssignLeaderBehavior] Add Leadership Vehicle Type'; + + @IsUUID(4, uuidValidationOptions) + public readonly simulatedRegionId!: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly behaviorId!: UUID; + + @IsString() + public readonly vehicleType!: string; +} + +export class RemoveLeadershipVehicleTypeAction implements Action { + @IsValue('[AssignLeaderBehavior] Remove Leadership Vehicle Type') + public readonly type = + '[AssignLeaderBehavior] Remove Leadership Vehicle Type'; + + @IsUUID(4, uuidValidationOptions) + public readonly simulatedRegionId!: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly behaviorId!: UUID; + + @IsString() + public readonly vehicleType!: string; +} + export namespace SimulationActionReducers { export const updateTreatPatientsIntervals: ActionReducer = { @@ -990,4 +1019,49 @@ export namespace SimulationActionReducers { }, rights: 'trainer', }; + + export const addLeadershipVehicleType: ActionReducer = + { + action: AddLeadershipVehicleTypeAction, + reducer( + draftState, + { simulatedRegionId, behaviorId, vehicleType } + ) { + const behaviorState = getBehaviorById( + draftState, + simulatedRegionId, + behaviorId, + 'assignLeaderBehavior' + ); + behaviorState.leadershipVehicleTypes[vehicleType] = true; + + return draftState; + }, + rights: 'trainer', + }; + + export const removeLeadershipVehicleType: ActionReducer = + { + action: RemoveLeadershipVehicleTypeAction, + reducer( + draftState, + { simulatedRegionId, behaviorId, vehicleType } + ) { + const behaviorState = getBehaviorById( + draftState, + simulatedRegionId, + behaviorId, + 'assignLeaderBehavior' + ); + if (!behaviorState.leadershipVehicleTypes[vehicleType]) { + throw new ReducerError( + `Tried to remove ${vehicleType} from leadership vehicle types, but it was not present` + ); + } + delete behaviorState.leadershipVehicleTypes[vehicleType]; + + return draftState; + }, + rights: 'trainer', + }; } diff --git a/shared/src/utils/validators/is-string-set.ts b/shared/src/utils/validators/is-string-set.ts new file mode 100644 index 000000000..c5cea862b --- /dev/null +++ b/shared/src/utils/validators/is-string-set.ts @@ -0,0 +1,23 @@ +import type { ValidationArguments, ValidationOptions } from 'class-validator'; +import { isString } from 'class-validator'; +import { createMapValidator } from './create-map-validator'; +import type { GenericPropertyDecorator } from './generic-property-decorator'; +import { makeValidator } from './make-validator'; + +export const isStringSet = createMapValidator({ + keyValidator: ((key) => isString(key)) as (key: unknown) => key is string, + valueValidator: ((value) => value === true) as ( + value: unknown + ) => value is true, +}); + +// eslint-disable-next-line @typescript-eslint/naming-convention +export function IsStringSet( + validationOptions?: ValidationOptions & { each?: Each } +): GenericPropertyDecorator<{ [key: string]: true }, Each> { + return makeValidator<{ [key: string]: true }, Each>( + 'isStringSet', + (value: unknown, args?: ValidationArguments) => isStringSet(value), + validationOptions + ); +} diff --git a/test-scenarios b/test-scenarios index 08f1f971d..c289b4a06 160000 --- a/test-scenarios +++ b/test-scenarios @@ -1 +1 @@ -Subproject commit 08f1f971dfec9fa998bc44267631424ec1220347 +Subproject commit c289b4a06c08f0bf4779063c9190a973d5ab755d