diff --git a/apps/client-asset-sg/src/app/i18n/de.ts b/apps/client-asset-sg/src/app/i18n/de.ts index ab467b6a..1c45b2ac 100644 --- a/apps/client-asset-sg/src/app/i18n/de.ts +++ b/apps/client-asset-sg/src/app/i18n/de.ts @@ -124,6 +124,7 @@ export const deAppTranslations = { alternativeId: 'Alternativ-ID', alternativeIdDescription: 'Beschreibung Alternativ-ID', addNewAlternativeId: 'Neue Alternativ-ID hinzufügen', + referencesWarning: 'Um die Arbeitsgruppe zu ändern, müssen Sie erst alle Verweise entfernen.', }, files: { tabName: 'Dateien', diff --git a/apps/client-asset-sg/src/app/i18n/en.ts b/apps/client-asset-sg/src/app/i18n/en.ts index 25516501..1b9194af 100644 --- a/apps/client-asset-sg/src/app/i18n/en.ts +++ b/apps/client-asset-sg/src/app/i18n/en.ts @@ -125,6 +125,7 @@ export const enAppTranslations: AppTranslations = { alternativeId: 'Alternative ID', alternativeIdDescription: 'Alternative ID Description', addNewAlternativeId: 'Add new alternative ID', + referencesWarning: 'In order to change the workgroup, you must first remove all references.', }, files: { tabName: 'Files', diff --git a/apps/client-asset-sg/src/app/i18n/fr.ts b/apps/client-asset-sg/src/app/i18n/fr.ts index f758d055..4bee145e 100644 --- a/apps/client-asset-sg/src/app/i18n/fr.ts +++ b/apps/client-asset-sg/src/app/i18n/fr.ts @@ -126,6 +126,7 @@ export const frAppTranslations: AppTranslations = { alternativeId: 'ID alternative', alternativeIdDescription: "Description d'ID alternative", addNewAlternativeId: 'Ajouter une nouvelle ID alternative', + referencesWarning: 'Pour changer le groupe de travail, vous devez d’abord supprimer toutes les références.', }, files: { tabName: 'Fichiers', diff --git a/apps/client-asset-sg/src/app/i18n/it.ts b/apps/client-asset-sg/src/app/i18n/it.ts index cd12fb43..5520d37b 100644 --- a/apps/client-asset-sg/src/app/i18n/it.ts +++ b/apps/client-asset-sg/src/app/i18n/it.ts @@ -125,6 +125,7 @@ export const itAppTranslations: AppTranslations = { alternativeId: 'IT Alternativ-ID', alternativeIdDescription: 'IT Beschreibung Alternativ-ID', addNewAlternativeId: 'IT Neue Alternativ-ID hinzufügen', + referencesWarning: 'IT Um die Arbeitsgruppe zu ändern, müssen Sie erst alle Verweise entfernen.', }, files: { tabName: 'IT Dateien', diff --git a/apps/client-asset-sg/src/app/i18n/rm.ts b/apps/client-asset-sg/src/app/i18n/rm.ts index 45bfb3f9..7a678650 100644 --- a/apps/client-asset-sg/src/app/i18n/rm.ts +++ b/apps/client-asset-sg/src/app/i18n/rm.ts @@ -125,6 +125,7 @@ export const rmAppTranslations: AppTranslations = { alternativeId: 'RM Alternativ-ID', alternativeIdDescription: 'RM Beschreibung Alternativ-ID', addNewAlternativeId: 'RM Neue Alternativ-ID hinzufügen', + referencesWarning: 'RM Um die Arbeitsgruppe zu ändern, müssen Sie erst alle Verweise entfernen.', }, files: { tabName: 'RM Dateien', diff --git a/apps/server-asset-sg/src/app.module.ts b/apps/server-asset-sg/src/app.module.ts index 2512ceb7..11aa94fb 100644 --- a/apps/server-asset-sg/src/app.module.ts +++ b/apps/server-asset-sg/src/app.module.ts @@ -11,6 +11,7 @@ import { JwtMiddleware } from '@/core/middleware/jwt.middleware'; import { PrismaService } from '@/core/prisma.service'; import { AssetEditController } from '@/features/asset-edit/asset-edit.controller'; import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; +import { AssetEditService } from '@/features/asset-edit/asset-edit.service'; import { AssetInfoRepo } from '@/features/assets/asset-info.repo'; import { AssetRepo } from '@/features/assets/asset.repo'; import { AssetsController } from '@/features/assets/assets.controller'; @@ -51,6 +52,7 @@ import { WorkgroupsController } from '@/features/workgroups/workgroups.controlle provideElasticsearch, AssetEditRepo, AssetInfoRepo, + AssetEditService, AssetRepo, AssetSearchService, AssetSyncService, diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts index f505b30d..b283a68c 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts @@ -17,11 +17,16 @@ import { authorize } from '@/core/authorize'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; import { ParseBody } from '@/core/decorators/parse.decorator'; import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; +import { AssetEditService } from '@/features/asset-edit/asset-edit.service'; import { AssetSearchService } from '@/features/assets/search/asset-search.service'; @Controller('/asset-edit') export class AssetEditController { - constructor(private readonly assetEditRepo: AssetEditRepo, private readonly assetSearchService: AssetSearchService) {} + constructor( + private readonly assetEditRepo: AssetEditRepo, + private readonly assetEditService: AssetEditService, + private readonly assetSearchService: AssetSearchService + ) {} @Get('/:id') async show(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User): Promise { @@ -38,6 +43,8 @@ export class AssetEditController { authorize(AssetEditPolicy, user).canCreate(); validatePatch(user, patch); + await this.assetEditService.validateReferencesOrThrow({ user, patch }); + const asset = await this.assetEditRepo.create({ user, patch }); await this.assetSearchService.register(asset); return AssetEditDetail.encode(asset); @@ -56,6 +63,7 @@ export class AssetEditController { authorize(AssetEditPolicy, user).canUpdate(record); validatePatch(user, patch, record); + await this.assetEditService.validateReferencesOrThrow({ user, patch }, id); const asset = await this.assetEditRepo.update(record.assetId, { user, patch }); if (asset === null) { diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.http b/apps/server-asset-sg/src/features/asset-edit/asset-edit.http index c421a664..d6a9ac66 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.http +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.http @@ -35,7 +35,8 @@ Content-Type: application/json "titleOriginal": "My Cool Asset", "titlePublic": "Our Cool Asset", "typeNatRels": [], - "workgroupId": 1 + "workgroupId": 1, + "assetFiles": [] } ### Update asset-edit diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts index 8aa98832..bf14a32d 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts @@ -111,7 +111,6 @@ export class AssetEditRepo implements Repo { @@ -127,7 +126,7 @@ export class AssetEditRepo implements Repo { + // check if any of the siblings are in another workgroup + for (const assetYId of data.patch.siblingAssetIds) { + const siblingCandidate = await this.prismaService.asset.findUnique({ + where: { assetId: assetYId }, + select: { workgroupId: true }, + }); + if (siblingCandidate?.workgroupId !== data.patch.workgroupId) { + throw new HttpException( + 'Sibling assets must be in the same workgroup as the edited asset', + HttpStatus.UNPROCESSABLE_ENTITY + ); + } + } + + // check if the parent asset is in another workgroup + const assetMainId = O.toUndefined(data.patch.assetMainId); + if (assetMainId) { + const assetMain = await this.prismaService.asset.findUnique({ + where: { assetId: assetMainId }, + select: { workgroupId: true }, + }); + if (assetMain?.workgroupId !== data.patch.workgroupId) { + throw new HttpException('Cannot assign parent asset from different workgroup', HttpStatus.UNPROCESSABLE_ENTITY); + } + } + + // check if any of the subordinate assets are in another workgroup for exisiting assets + if (id) { + const childAssets = await this.prismaService.asset.findMany({ + where: { assetMainId: id }, + select: { workgroupId: true }, + }); + + for (const child of childAssets) { + if (child.workgroupId !== data.patch.workgroupId) { + throw new HttpException( + 'Child assets must be in the same workgroup as the parent asset', + HttpStatus.UNPROCESSABLE_ENTITY + ); + } + } + } + } +} diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.html b/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.html index 8fe4db56..ef8e72f9 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.html +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.html @@ -3,6 +3,10 @@
workgroup.title
+
+ + {{ "edit.tabs.general.referencesWarning" | translate }} +
workgroup.title diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.ts b/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.ts index ee48b446..e62bc314 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.ts @@ -56,6 +56,8 @@ export class AssetEditorTabGeneralComponent implements OnInit { description: new FormControl('', { nonNullable: true }), }); + public showWarningForReferences = false; + public readonly state: RxState = inject(RxState); public readonly _referenceDataVM$ = this.state.select('referenceDataVM'); @@ -174,6 +176,10 @@ export class AssetEditorTabGeneralComponent implements OnInit { this.idForm.patchValue({ idId, id, description }); } }); + this.setDisabledStatusOfWorkgroup(); + this.rootFormGroup.controls.references.valueChanges.pipe(untilDestroyed(this)).subscribe(() => { + this.setDisabledStatusOfWorkgroup(); + }); this.ngOnInit$.next(); } @@ -222,5 +228,19 @@ export class AssetEditorTabGeneralComponent implements OnInit { this.idForm.patchValue({ id, description }); } + private setDisabledStatusOfWorkgroup() { + if ( + this.rootFormGroup.getRawValue().references.siblingAssets.length > 0 || + this.rootFormGroup.getRawValue().references.childAssets.length > 0 || + this.rootFormGroup.getRawValue().references.assetMain + ) { + this.form.controls.workgroupId.disable({ emitEvent: false }); + this.showWarningForReferences = true; + } else { + this.form.controls.workgroupId.enable({ emitEvent: false }); + this.showWarningForReferences = false; + } + } + public eqAssetLanguageEdit = eqAssetLanguageEdit; } diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-references/asset-editor-tab-references.component.html b/libs/asset-editor/src/lib/components/asset-editor-tab-references/asset-editor-tab-references.component.html index e28879d1..f8c75875 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-references/asset-editor-tab-references.component.html +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-references/asset-editor-tab-references.component.html @@ -41,9 +41,9 @@ Typ - {{ - "edit.tabs.references.referenceType.parent" | translate - }} + {{ "edit.tabs.references.referenceType.parent" | translate }} + {{ "edit.tabs.references.referenceType.sibling" | translate }} diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-references/asset-editor-tab-references.component.ts b/libs/asset-editor/src/lib/components/asset-editor-tab-references/asset-editor-tab-references.component.ts index e0218657..aa928482 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-references/asset-editor-tab-references.component.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-references/asset-editor-tab-references.component.ts @@ -1,25 +1,25 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http'; -import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'; import { FormBuilder, FormControl, FormGroupDirective, Validators } from '@angular/forms'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { httpErrorResponseError } from '@asset-sg/client-shared'; import { unknownToUnknownError } from '@asset-sg/core'; -import { AssetByTitle, LinkedAsset } from '@asset-sg/shared'; +import { AssetByTitle, AssetEditDetail, LinkedAsset } from '@asset-sg/shared'; import { UntilDestroy } from '@ngneat/until-destroy'; import * as A from 'fp-ts/Array'; import * as E from 'fp-ts/Either'; import { flow, pipe } from 'fp-ts/function'; import * as D from 'io-ts/Decoder'; import { - Observable, - Subject, catchError, combineLatest, debounceTime, distinctUntilChanged, map, + Observable, of, startWith, + Subject, switchMap, } from 'rxjs'; @@ -79,19 +79,25 @@ export class AssetEditorTabReferencesComponent implements OnInit { this._authorSearchInput$.pipe( debounceTime(300), switchMap( - (value): Observable => + (value): Observable => value.length >= 3 - ? this._httpClient.get(`/api/asset-edit/search?title=${value}`).pipe( - catchError((err: HttpErrorResponse | unknown) => - of(err instanceof HttpErrorResponse ? httpErrorResponseError(err) : unknownToUnknownError(err)) - ), - map( - flow( - D.array(AssetByTitle).decode, - E.getOrElseW(() => []) + ? this._httpClient + .post(`/api/assets/search?limit=10`, { + text: value, + workgroupIds: [this.rootFormGroup.getRawValue().general.workgroupId], + }) + .pipe( + map((res) => (res as { data: AssetEditDetail[] }).data), + catchError((err: HttpErrorResponse | unknown) => + of(err instanceof HttpErrorResponse ? httpErrorResponseError(err) : unknownToUnknownError(err)) + ), + map( + flow( + D.array(AssetEditDetail).decode, + E.getOrElseW(() => []) + ) ) ) - ) : of([]) ) ),