From d554be862c689c8b8c7e2df95ea9b8cc4818293d Mon Sep 17 00:00:00 2001 From: Max Burri Date: Fri, 6 Feb 2026 15:04:04 +0100 Subject: [PATCH 01/14] fix: define contexts-list.component as standalone --- .../src/app/resources/contexts-list/contexts-list.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/AMW_angular/io/src/app/resources/contexts-list/contexts-list.component.ts b/AMW_angular/io/src/app/resources/contexts-list/contexts-list.component.ts index 39e08b4f3..f322c650c 100644 --- a/AMW_angular/io/src/app/resources/contexts-list/contexts-list.component.ts +++ b/AMW_angular/io/src/app/resources/contexts-list/contexts-list.component.ts @@ -9,6 +9,7 @@ import { map } from 'rxjs/operators'; @Component({ selector: 'app-contexts-list', + standalone: true, imports: [NgClass, UpperCasePipe], templateUrl: './contexts-list.component.html', styleUrl: './contexts-list.component.scss', From 69f2bdcb2c02326d02cec109b80dfd87170e0948 Mon Sep 17 00:00:00 2001 From: Max Burri Date: Fri, 6 Feb 2026 15:05:14 +0100 Subject: [PATCH 02/14] feat(AWM_angular): show releases in releases tile, highlight the current selected release and add links to switch between them --- .../resource-edit.component.html | 6 +++ .../resource-edit/resource-edit.component.ts | 2 + .../resource-properties.component.ts | 2 +- .../resource-releases.component.html | 29 ++++++++++++++ .../resource-releases.component.ts | 38 +++++++++++++++++++ 5 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html create mode 100644 AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html b/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html index 6504d9b59..df8fc5911 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html @@ -37,6 +37,12 @@
+ diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.ts index f943e6a04..5b41692de 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.ts +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.ts @@ -14,6 +14,7 @@ import { ButtonComponent } from '../../shared/button/button.component'; import { NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle } from '@ng-bootstrap/ng-bootstrap'; import { ContextsListComponent } from '../contexts-list/contexts-list.component'; import { ResourcePropertiesComponent } from './resource-properties/resource-properties.component'; +import { ResourceReleasesComponent } from './resource-releases/resource-releases.component'; @Component({ selector: 'app-resource-edit', @@ -30,6 +31,7 @@ import { ResourcePropertiesComponent } from './resource-properties/resource-prop NgbDropdownItem, ContextsListComponent, ResourcePropertiesComponent, + ResourceReleasesComponent, ], templateUrl: './resource-edit.component.html', styleUrl: './resource-edit.component.scss', diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-properties/resource-properties.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-properties/resource-properties.component.ts index 46981a3ee..4f4cbc97d 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-properties/resource-properties.component.ts +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-properties/resource-properties.component.ts @@ -69,7 +69,7 @@ export class ResourcePropertiesComponent { permissions = computed(() => { if (this.authService.restrictions().length > 0) { return { - canAddProperty: this.authService.hasPermission('RESOURCE', 'UPDATE', null, null, this.context().name), + canAddProperty: this.authService.hasPermission('RESOURCE', 'UPDATE', null, null, this.context()?.name), }; } else { return { canAddProperty: false }; diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html new file mode 100644 index 000000000..fe4fd5edc --- /dev/null +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html @@ -0,0 +1,29 @@ + + + @if (!releases() || releases().length < 1) { +
+ No Release for this resource +
+ } @else { + + } +
diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts new file mode 100644 index 000000000..6eaefb658 --- /dev/null +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts @@ -0,0 +1,38 @@ +import { Component, computed, inject, input, signal } from '@angular/core'; +import { LoadingIndicatorComponent } from '../../../shared/elements/loading-indicator.component'; +import { AuthService } from '../../../auth/auth.service'; +import { TileComponent } from '../../../shared/tile/tile.component'; +import { Release } from '../../models/release'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-resource-releases', + standalone: true, + imports: [LoadingIndicatorComponent, TileComponent, RouterLink], + templateUrl: './resource-releases.component.html', +}) +export class ResourceReleasesComponent { + private authService = inject(AuthService); + + isLoading = signal(false); + + id = input.required(); + releases = input.required(); + contextId = input.required(); + resourceTypeId = input.required(); + + // same permissions for crud + permissions = computed(() => { + if (this.authService.restrictions().length > 0) { + return { + //canAddRelease: this.authService.hasPermission('RESOURCE', 'UPDATE', null, null, this.context().name), + canAddRelease: true, // TODO: replace with actual permission check + }; + } + return { canAddRelease: false }; + }); + + addRelease() { + console.log('add release'); + } +} From 5d4cd3d8fe390ee0aade16e8cd5da8e8982ddcee Mon Sep 17 00:00:00 2001 From: Max Burri Date: Fri, 6 Feb 2026 15:13:32 +0100 Subject: [PATCH 03/14] feat(AWM_angular): use a table to show releases, add delete icon --- .../resource-releases.component.html | 32 +++++++++++++------ .../resource-releases.component.ts | 8 ++++- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html index fe4fd5edc..ef94c17af 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html @@ -13,17 +13,31 @@ No Release for this resource
} @else { - + + {{ release.name }} + + + + + + + + } + + } diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts index 6eaefb658..b57aa54cb 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts @@ -4,11 +4,13 @@ import { AuthService } from '../../../auth/auth.service'; import { TileComponent } from '../../../shared/tile/tile.component'; import { Release } from '../../models/release'; import { RouterLink } from '@angular/router'; +import { IconComponent } from '../../../shared/icon/icon.component'; +import { ButtonComponent } from '../../../shared/button/button.component'; @Component({ selector: 'app-resource-releases', standalone: true, - imports: [LoadingIndicatorComponent, TileComponent, RouterLink], + imports: [LoadingIndicatorComponent, TileComponent, RouterLink, IconComponent, ButtonComponent], templateUrl: './resource-releases.component.html', }) export class ResourceReleasesComponent { @@ -35,4 +37,8 @@ export class ResourceReleasesComponent { addRelease() { console.log('add release'); } + + deleteRelease(releaseId: number) { + console.log('delete release', releaseId); + } } From c7ae8079c256a30734590727adbfebda663db50f Mon Sep 17 00:00:00 2001 From: Max Burri Date: Fri, 6 Feb 2026 15:20:45 +0100 Subject: [PATCH 04/14] feat(AWM_angular): add delete confirmation dialog --- .../resource-releases.component.html | 20 ++++++++++++++++++- .../resource-releases.component.ts | 19 +++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html index ef94c17af..fe82d3a22 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html @@ -30,7 +30,11 @@ [size]="'sm'" [variant]="'link'" [additionalClasses]="'p-0 link-danger'" - (click)="deleteRelease(release.id); $event.stopPropagation(); $event.preventDefault()" + (click)=" + showDeleteConfirmation(deleteReleaseConfirmation, release); + $event.stopPropagation(); + $event.preventDefault() + " > @@ -41,3 +45,17 @@ } + + + + + + diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts index b57aa54cb..3fb9bb538 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts @@ -6,17 +6,21 @@ import { Release } from '../../models/release'; import { RouterLink } from '@angular/router'; import { IconComponent } from '../../../shared/icon/icon.component'; import { ButtonComponent } from '../../../shared/button/button.component'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ModalHeaderComponent } from '../../../shared/modal-header/modal-header.component'; @Component({ selector: 'app-resource-releases', standalone: true, - imports: [LoadingIndicatorComponent, TileComponent, RouterLink, IconComponent, ButtonComponent], + imports: [LoadingIndicatorComponent, TileComponent, RouterLink, IconComponent, ButtonComponent, ModalHeaderComponent], templateUrl: './resource-releases.component.html', }) export class ResourceReleasesComponent { private authService = inject(AuthService); + private modalService = inject(NgbModal); isLoading = signal(false); + selectedRelease = signal(null); id = input.required(); releases = input.required(); @@ -38,7 +42,20 @@ export class ResourceReleasesComponent { console.log('add release'); } + showDeleteConfirmation(content: unknown, release: Release) { + this.selectedRelease.set(release); + this.modalService.open(content).result.then( + () => { + this.deleteRelease(release.id); + }, + () => { + this.selectedRelease.set(null); + }, + ); + } + deleteRelease(releaseId: number) { console.log('delete release', releaseId); + this.selectedRelease.set(null); } } From a34cdb90555b5925b6e5fcd43045db0c5576eb49 Mon Sep 17 00:00:00 2001 From: Max Burri Date: Fri, 6 Feb 2026 16:28:32 +0100 Subject: [PATCH 05/14] feat(AWM_angular): implement resource release delete --- .../resource-edit.component.html | 1 + .../resource-releases.component.ts | 43 +++++++++++++++++-- .../resources/services/resource.service.ts | 8 ++++ .../rest/resources/ResourceGroupsRest.java | 13 ++++++ 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html b/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html index df8fc5911..01dd04b48 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html @@ -42,6 +42,7 @@ [releases]="releases()" [contextId]="contextId()" [resourceTypeId]="resource()?.resourceTypeId || 0" + [resourceGroupId]="resource()?.resourceGroupId || 0" > diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts index 3fb9bb538..9fe44612f 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts @@ -3,11 +3,12 @@ import { LoadingIndicatorComponent } from '../../../shared/elements/loading-indi import { AuthService } from '../../../auth/auth.service'; import { TileComponent } from '../../../shared/tile/tile.component'; import { Release } from '../../models/release'; -import { RouterLink } from '@angular/router'; +import { RouterLink, Router, ActivatedRoute } from '@angular/router'; import { IconComponent } from '../../../shared/icon/icon.component'; import { ButtonComponent } from '../../../shared/button/button.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ModalHeaderComponent } from '../../../shared/modal-header/modal-header.component'; +import { ResourceService } from '../../services/resource.service'; @Component({ selector: 'app-resource-releases', @@ -18,6 +19,9 @@ import { ModalHeaderComponent } from '../../../shared/modal-header/modal-header. export class ResourceReleasesComponent { private authService = inject(AuthService); private modalService = inject(NgbModal); + private resourceService = inject(ResourceService); + private router = inject(Router); + private route = inject(ActivatedRoute); isLoading = signal(false); selectedRelease = signal(null); @@ -26,6 +30,7 @@ export class ResourceReleasesComponent { releases = input.required(); contextId = input.required(); resourceTypeId = input.required(); + resourceGroupId = input.required(); // same permissions for crud permissions = computed(() => { @@ -54,8 +59,38 @@ export class ResourceReleasesComponent { ); } - deleteRelease(releaseId: number) { - console.log('delete release', releaseId); - this.selectedRelease.set(null); + deleteRelease(resourceIdToDelete: number) { + // Note: release.id is actually the Resource ID, not the Release ID + // This is how the backend constructs the ReleaseDTO + this.isLoading.set(true); + this.resourceService.deleteResourceByResourceId(resourceIdToDelete).subscribe({ + next: () => { + this.isLoading.set(false); + this.selectedRelease.set(null); + // Only navigate if we deleted the currently selected release + if (resourceIdToDelete === this.id()) { + const remainingReleases = this.releases().filter((r) => r.id !== resourceIdToDelete); + if (remainingReleases.length > 0) { + // Navigate to the first remaining release + void this.router.navigate([], { + relativeTo: this.route, + queryParams: { id: remainingReleases[0].id }, + queryParamsHandling: 'merge', + }); + } else { + // No releases left, navigate to resources list + void this.router.navigate(['/resources']); + } + } else { + // Reload the releases list to remove the deleted release from the UI + this.resourceService.setIdForResource(this.id()); + } + }, + error: (error) => { + console.error('Failed to delete release:', error); + this.isLoading.set(false); + this.selectedRelease.set(null); + }, + }); } } diff --git a/AMW_angular/io/src/app/resources/services/resource.service.ts b/AMW_angular/io/src/app/resources/services/resource.service.ts index 88c70c62d..145607902 100644 --- a/AMW_angular/io/src/app/resources/services/resource.service.ts +++ b/AMW_angular/io/src/app/resources/services/resource.service.ts @@ -202,6 +202,14 @@ export class ResourceService extends BaseService { .get(`${this.getBaseUrl()}/resources/resourceGroups/releases/${resourceId}`) .pipe(catchError(this.handleError)); } + + deleteResourceByResourceId(resourceId: number): Observable { + return this.http + .delete(`${this.getBaseUrl()}/resources/${resourceId}`, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } } function toAppWithVersion(r: any): AppWithVersion { diff --git a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java index 3820f597c..a7f04e82d 100644 --- a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java +++ b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java @@ -140,6 +140,19 @@ public Response getById(@Parameter(description = "Resource ID") @PathParam("id") return Response.ok(new ResourceDTO(resourceBoundary.getResource(id))).build(); } + @DELETE + @Path("/{id : \\d+}") + @Operation(summary = "Delete a resource by id") + @Produces(APPLICATION_JSON) + public Response deleteResourceById(@Parameter(description = "Resource ID") @PathParam("id") Integer id) throws NotFoundException, ElementAlreadyExistsException { + ResourceEntity resource = resourceLocator.getResourceById(id); + if (resource == null) { + return Response.status(NOT_FOUND).entity(new ExceptionDto("Resource not found")).build(); + } + resourceBoundary.removeResource(id); + return Response.ok().build(); + } + @GET @Produces(APPLICATION_JSON) @Operation(summary = "Get resource groups", description = "Returns the available resource groups") From c654711fe9fc83026caa5ebd73ba5de013e92050 Mon Sep 17 00:00:00 2001 From: Max Burri Date: Fri, 6 Feb 2026 16:32:17 +0100 Subject: [PATCH 06/14] ref(AWM_angular): just pass in the resource() --- .../resources/resource-edit/resource-edit.component.html | 3 +-- .../resource-releases/resource-releases.component.html | 4 ++-- .../resource-releases/resource-releases.component.ts | 7 +++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html b/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html index 01dd04b48..98bab997e 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html @@ -41,8 +41,7 @@ [id]="id()" [releases]="releases()" [contextId]="contextId()" - [resourceTypeId]="resource()?.resourceTypeId || 0" - [resourceGroupId]="resource()?.resourceGroupId || 0" + [resource]="resource()" > diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html index fe82d3a22..776b98495 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html @@ -19,7 +19,7 @@ {{ release.name }} @@ -47,7 +47,7 @@ - diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts index 9fe44612f..bef1494d2 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts @@ -7,13 +7,13 @@ import { RouterLink, Router, ActivatedRoute } from '@angular/router'; import { IconComponent } from '../../../shared/icon/icon.component'; import { ButtonComponent } from '../../../shared/button/button.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ModalHeaderComponent } from '../../../shared/modal-header/modal-header.component'; import { ResourceService } from '../../services/resource.service'; +import { Resource } from '../../models/resource'; @Component({ selector: 'app-resource-releases', standalone: true, - imports: [LoadingIndicatorComponent, TileComponent, RouterLink, IconComponent, ButtonComponent, ModalHeaderComponent], + imports: [LoadingIndicatorComponent, TileComponent, RouterLink, IconComponent, ButtonComponent], templateUrl: './resource-releases.component.html', }) export class ResourceReleasesComponent { @@ -29,8 +29,7 @@ export class ResourceReleasesComponent { id = input.required(); releases = input.required(); contextId = input.required(); - resourceTypeId = input.required(); - resourceGroupId = input.required(); + resource = input.required(); // same permissions for crud permissions = computed(() => { From 405593808d8feffde1670eb9c16b26a57a251360 Mon Sep 17 00:00:00 2001 From: Max Burri Date: Fri, 6 Feb 2026 16:42:21 +0100 Subject: [PATCH 07/14] feat(AWM_angular, rest): add new release --- .../resource-releases.component.html | 39 ++++++++- .../resource-releases.component.ts | 83 ++++++++++++++++++- .../resources/services/resource.service.ts | 18 ++++ .../modal-header/modal-header.component.ts | 1 + .../rest/resources/ResourceGroupsRest.java | 19 +++++ 5 files changed, 156 insertions(+), 4 deletions(-) diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html index 776b98495..710a64f99 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html @@ -47,7 +47,7 @@ - < app-modal-header + @@ -59,3 +59,40 @@ Delete + + + + + + diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts index bef1494d2..8feb96699 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts @@ -1,4 +1,4 @@ -import { Component, computed, inject, input, signal } from '@angular/core'; +import { Component, computed, inject, input, signal, ViewChild, TemplateRef } from '@angular/core'; import { LoadingIndicatorComponent } from '../../../shared/elements/loading-indicator.component'; import { AuthService } from '../../../auth/auth.service'; import { TileComponent } from '../../../shared/tile/tile.component'; @@ -9,11 +9,21 @@ import { ButtonComponent } from '../../../shared/button/button.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ResourceService } from '../../services/resource.service'; import { Resource } from '../../models/resource'; +import { FormsModule } from '@angular/forms'; +import { ModalHeaderComponent } from '../../../shared/modal-header/modal-header.component'; @Component({ selector: 'app-resource-releases', standalone: true, - imports: [LoadingIndicatorComponent, TileComponent, RouterLink, IconComponent, ButtonComponent], + imports: [ + LoadingIndicatorComponent, + TileComponent, + RouterLink, + IconComponent, + ButtonComponent, + FormsModule, + ModalHeaderComponent, + ], templateUrl: './resource-releases.component.html', }) export class ResourceReleasesComponent { @@ -25,6 +35,11 @@ export class ResourceReleasesComponent { isLoading = signal(false); selectedRelease = signal(null); + availableReleases = signal([]); + selectedReleaseId: number | null = null; + isCreatingRelease = signal(false); + + @ViewChild('createReleaseModal') createReleaseModal!: TemplateRef; id = input.required(); releases = input.required(); @@ -43,7 +58,69 @@ export class ResourceReleasesComponent { }); addRelease() { - console.log('add release'); + this.isLoading.set(true); + this.resourceService.getAvailableReleasesForResource(this.id()).subscribe({ + next: (releases) => { + this.availableReleases.set(releases); + this.selectedReleaseId = null; + this.isLoading.set(false); + this.showCreateReleaseModal(); + }, + error: (error) => { + console.error('Failed to load available releases:', error); + this.isLoading.set(false); + }, + }); + } + + showCreateReleaseModal() { + const modalRef = this.modalService.open(this.createReleaseModal); + modalRef.result.then( + () => { + if (this.selectedReleaseId) { + this.createRelease(this.selectedReleaseId); + } + }, + () => { + this.selectedReleaseId = null; + }, + ); + } + + createRelease(releaseId: number) { + const selectedRelease = this.availableReleases().find((r) => r.id === releaseId); + if (!selectedRelease || !selectedRelease.name) { + console.error('Selected release not found'); + return; + } + + const currentResource = this.resource(); + if (!currentResource || !currentResource.name) { + console.error('Current resource not found'); + return; + } + + const currentReleaseName = this.releases().find((r) => r.id === this.id())?.name; + if (!currentReleaseName) { + console.error('Current release name not found'); + return; + } + + this.isCreatingRelease.set(true); + this.resourceService + .createResourceRelease(currentResource.name, selectedRelease.name, currentReleaseName) + .subscribe({ + next: () => { + this.isCreatingRelease.set(false); + this.selectedReleaseId = null; + this.resourceService.setIdForResource(this.id()); + }, + error: (error) => { + console.error('Failed to create release:', error); + this.isCreatingRelease.set(false); + this.selectedReleaseId = null; + }, + }); } showDeleteConfirmation(content: unknown, release: Release) { diff --git a/AMW_angular/io/src/app/resources/services/resource.service.ts b/AMW_angular/io/src/app/resources/services/resource.service.ts index 145607902..b9298be1e 100644 --- a/AMW_angular/io/src/app/resources/services/resource.service.ts +++ b/AMW_angular/io/src/app/resources/services/resource.service.ts @@ -210,6 +210,24 @@ export class ResourceService extends BaseService { }) .pipe(catchError(this.handleError)); } + + getAvailableReleasesForResource(resourceId: number): Observable { + return this.http + .get(`${this.getBaseUrl()}/resources/${resourceId}/availableReleases`, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + createResourceRelease(resourceGroupName: string, releaseName: string, sourceReleaseName: string): Observable { + return this.http + .post( + `${this.getBaseUrl()}/resources/${resourceGroupName}`, + { releaseName, sourceReleaseName }, + { headers: this.getHeaders() }, + ) + .pipe(catchError(this.handleError)); + } } function toAppWithVersion(r: any): AppWithVersion { diff --git a/AMW_angular/io/src/app/shared/modal-header/modal-header.component.ts b/AMW_angular/io/src/app/shared/modal-header/modal-header.component.ts index 0da2c7f9c..9d2d1dd4e 100644 --- a/AMW_angular/io/src/app/shared/modal-header/modal-header.component.ts +++ b/AMW_angular/io/src/app/shared/modal-header/modal-header.component.ts @@ -2,6 +2,7 @@ import { Component, input, output } from '@angular/core'; @Component({ selector: 'app-modal-header', + standalone: true, imports: [], template: ` + + + + + + diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts index 8feb96699..81288584f 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts @@ -38,8 +38,11 @@ export class ResourceReleasesComponent { availableReleases = signal([]); selectedReleaseId: number | null = null; isCreatingRelease = signal(false); + releaseToChange = signal(null); + isChangingRelease = signal(false); @ViewChild('createReleaseModal') createReleaseModal!: TemplateRef; + @ViewChild('changeReleaseModal') changeReleaseModal!: TemplateRef; id = input.required(); releases = input.required(); @@ -123,6 +126,64 @@ export class ResourceReleasesComponent { }); } + showChangeReleaseModal(release: Release) { + this.releaseToChange.set(release); + this.isLoading.set(true); + this.resourceService.getAvailableReleasesForResource(release.id).subscribe({ + next: (releases) => { + this.availableReleases.set(releases); + this.selectedReleaseId = null; + this.isLoading.set(false); + this.openChangeReleaseModal(); + }, + error: (error) => { + console.error('Failed to load available releases:', error); + this.isLoading.set(false); + this.releaseToChange.set(null); + }, + }); + } + + openChangeReleaseModal() { + const modalRef = this.modalService.open(this.changeReleaseModal); + modalRef.result.then( + () => { + if (this.selectedReleaseId && this.releaseToChange()) { + this.changeRelease(this.releaseToChange()!.id, this.selectedReleaseId); + } + }, + () => { + this.selectedReleaseId = null; + this.releaseToChange.set(null); + }, + ); + } + + changeRelease(resourceId: number, releaseId: number) { + this.isChangingRelease.set(true); + this.resourceService.changeResourceRelease(resourceId, releaseId).subscribe({ + next: () => { + this.isChangingRelease.set(false); + this.selectedReleaseId = null; + this.releaseToChange.set(null); + // Reload the resource and releases data to reflect the change + this.resourceService.setIdForResource(resourceId); + // Navigate to the changed resource (stays on the same resource, now in new release) + void this.router.navigate([], { + relativeTo: this.route, + queryParams: { id: resourceId }, + queryParamsHandling: 'merge', + }); + }, + error: (error) => { + console.error('Failed to change release:', error); + this.isChangingRelease.set(false); + this.selectedReleaseId = null; + this.releaseToChange.set(null); + }, + }); + } + showDeleteConfirmation(content: unknown, release: Release) { this.selectedRelease.set(release); this.modalService.open(content).result.then( diff --git a/AMW_angular/io/src/app/resources/services/resource.service.ts b/AMW_angular/io/src/app/resources/services/resource.service.ts index b9298be1e..081077c46 100644 --- a/AMW_angular/io/src/app/resources/services/resource.service.ts +++ b/AMW_angular/io/src/app/resources/services/resource.service.ts @@ -228,6 +228,14 @@ export class ResourceService extends BaseService { ) .pipe(catchError(this.handleError)); } + + changeResourceRelease(resourceId: number, releaseId: number): Observable { + return this.http + .put(`${this.getBaseUrl()}/resources/${resourceId}/release/${releaseId}`, null, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } } function toAppWithVersion(r: any): AppWithVersion { diff --git a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java index a3693272a..c22e9dd65 100644 --- a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java +++ b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java @@ -512,4 +512,23 @@ public Response getAvailableReleasesForResource(@PathParam("resourceId") Integer .collect(Collectors.toList()); return Response.ok(releaseDTOs).build(); } + + @Path("/{resourceId}/release/{releaseId}") + @PUT + @Produces(APPLICATION_JSON) + @Operation(summary = "Change the release of a resource - used by Angular") + public Response changeResourceRelease(@PathParam("resourceId") Integer resourceId, + @PathParam("releaseId") Integer releaseId) { + try { + releaseMgmtService.changeReleaseOfResource( + resourceLocator.getResourceById(resourceId), + releaseLocator.getReleaseById(releaseId) + ); + return Response.ok().build(); + } catch (ResourceNotFoundException e) { + return Response.status(NOT_FOUND).entity(new ExceptionDto(e.getMessage())).build(); + } catch (NotFoundException e) { + return Response.status(NOT_FOUND).entity(new ExceptionDto(e.getMessage())).build(); + } + } } From 1b460ac1a865e218f98a57c78900a0a7cf595fba Mon Sep 17 00:00:00 2001 From: Max Burri Date: Fri, 13 Feb 2026 10:44:37 +0100 Subject: [PATCH 09/14] ref(rest): use exception mappers for error responses instead of manually handling them --- .../rest/resources/ResourceGroupsRest.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java index c22e9dd65..67b9d085d 100644 --- a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java +++ b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java @@ -518,17 +518,11 @@ public Response getAvailableReleasesForResource(@PathParam("resourceId") Integer @Produces(APPLICATION_JSON) @Operation(summary = "Change the release of a resource - used by Angular") public Response changeResourceRelease(@PathParam("resourceId") Integer resourceId, - @PathParam("releaseId") Integer releaseId) { - try { - releaseMgmtService.changeReleaseOfResource( - resourceLocator.getResourceById(resourceId), - releaseLocator.getReleaseById(releaseId) - ); - return Response.ok().build(); - } catch (ResourceNotFoundException e) { - return Response.status(NOT_FOUND).entity(new ExceptionDto(e.getMessage())).build(); - } catch (NotFoundException e) { - return Response.status(NOT_FOUND).entity(new ExceptionDto(e.getMessage())).build(); - } + @PathParam("releaseId") Integer releaseId) throws NotFoundException { + releaseMgmtService.changeReleaseOfResource( + resourceLocator.getResourceById(resourceId), + releaseLocator.getReleaseById(releaseId) + ); + return Response.ok().build(); } } From 411669eabb150111f6d374a39a505cd904fbbb47 Mon Sep 17 00:00:00 2001 From: Max Burri Date: Fri, 13 Feb 2026 10:48:49 +0100 Subject: [PATCH 10/14] ref(angular): remove unused import --- AMW_angular/io/src/app/resources/services/resource.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/AMW_angular/io/src/app/resources/services/resource.service.ts b/AMW_angular/io/src/app/resources/services/resource.service.ts index 081077c46..353265eff 100644 --- a/AMW_angular/io/src/app/resources/services/resource.service.ts +++ b/AMW_angular/io/src/app/resources/services/resource.service.ts @@ -5,7 +5,6 @@ import { map, catchError, switchMap, shareReplay } from 'rxjs/operators'; import { Resource } from '../models/resource'; import { Release } from '../models/release'; import { Relation } from '../models/relation'; -import { Property } from '../models/property'; import { AppWithVersion } from '../../deployment/app-with-version'; import { BaseService } from '../../base/base.service'; import { ResourceType } from '../models/resource-type'; From 2f13f0112cc2ec13a557516235c5d9e99b22c9da Mon Sep 17 00:00:00 2001 From: Max Burri Date: Fri, 13 Feb 2026 10:49:07 +0100 Subject: [PATCH 11/14] ref(angular): use correct type for templateRef --- .../resource-releases/resource-releases.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts index 81288584f..9ac4b3df0 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts @@ -41,8 +41,8 @@ export class ResourceReleasesComponent { releaseToChange = signal(null); isChangingRelease = signal(false); - @ViewChild('createReleaseModal') createReleaseModal!: TemplateRef; - @ViewChild('changeReleaseModal') changeReleaseModal!: TemplateRef; + @ViewChild('createReleaseModal') createReleaseModal!: TemplateRef; + @ViewChild('changeReleaseModal') changeReleaseModal!: TemplateRef; id = input.required(); releases = input.required(); From b25b3d7ea8c992078e488d856d5008e2f19bdbc3 Mon Sep 17 00:00:00 2001 From: Max Burri Date: Fri, 13 Feb 2026 10:52:20 +0100 Subject: [PATCH 12/14] ref(angular): use the toast service to show nice error messages to the user --- .../resource-releases/resource-releases.component.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts index 9ac4b3df0..6a6cce385 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts @@ -11,6 +11,7 @@ import { ResourceService } from '../../services/resource.service'; import { Resource } from '../../models/resource'; import { FormsModule } from '@angular/forms'; import { ModalHeaderComponent } from '../../../shared/modal-header/modal-header.component'; +import { ToastService } from '../../../shared/elements/toast/toast.service'; @Component({ selector: 'app-resource-releases', @@ -32,6 +33,7 @@ export class ResourceReleasesComponent { private resourceService = inject(ResourceService); private router = inject(Router); private route = inject(ActivatedRoute); + private toastService = inject(ToastService); isLoading = signal(false); selectedRelease = signal(null); @@ -71,6 +73,7 @@ export class ResourceReleasesComponent { }, error: (error) => { console.error('Failed to load available releases:', error); + this.toastService.error('Failed to load available releases.'); this.isLoading.set(false); }, }); @@ -120,6 +123,7 @@ export class ResourceReleasesComponent { }, error: (error) => { console.error('Failed to create release:', error); + this.toastService.error('Failed to create release.'); this.isCreatingRelease.set(false); this.selectedReleaseId = null; }, @@ -138,6 +142,7 @@ export class ResourceReleasesComponent { }, error: (error) => { console.error('Failed to load available releases:', error); + this.toastService.error('Failed to load available releases.'); this.isLoading.set(false); this.releaseToChange.set(null); }, @@ -177,6 +182,7 @@ export class ResourceReleasesComponent { }, error: (error) => { console.error('Failed to change release:', error); + this.toastService.error('Failed to change release.'); this.isChangingRelease.set(false); this.selectedReleaseId = null; this.releaseToChange.set(null); @@ -225,6 +231,7 @@ export class ResourceReleasesComponent { }, error: (error) => { console.error('Failed to delete release:', error); + this.toastService.error('Failed to delete release.'); this.isLoading.set(false); this.selectedRelease.set(null); }, From 2c0119259938ba3aed7793db976aaf0c2381909c Mon Sep 17 00:00:00 2001 From: Max Burri Date: Fri, 13 Feb 2026 11:23:01 +0100 Subject: [PATCH 13/14] ref(angular): use more descriptive names for properties to show their usage --- .../resource-releases.component.html | 12 ++++--- .../resource-releases.component.ts | 36 +++++++++---------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html index c150fd47d..9be22e1be 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html @@ -63,7 +63,7 @@