[ACS-12332] Add checkout and cancel-checkout node support (#5334)

* [ACS-12332] Add checkout and cancel-checkout node support

* [ACS-12332] cr fix
This commit is contained in:
Mykyta Maliarchuk
2026-08-19 10:36:47 +02:00
committed by GitHub
parent ca364f23d4
commit df92e11d9a
3 changed files with 133 additions and 1 deletions
+16
View File
@@ -743,5 +743,21 @@
"TITLE": "AI Agents",
"DISCOVERY": "Discovery"
}
},
"CHECKOUT": {
"ERRORS": {
"CHECKOUT": {
"400": "This node cannot be checked out. It must be a content (cm:content) node.",
"403": "You do not have permission to check out this node.",
"409": "This node is already checked out or locked by another user."
},
"CANCEL_CHECKOUT": {
"400": "Checkout cannot be cancelled. The node is not a content node or is not currently checked out.",
"403": "You do not have permission to cancel the checkout for this node.",
"409": "Failed to cancel checkout. Please try again or contact your IT team."
},
"404": "Node not found.",
"UNKNOWN": "An unexpected error occurred. Please try again or contact your IT team."
}
}
}
@@ -2410,4 +2410,91 @@ describe('ContentManagementService', () => {
expect(showErrorSpy).toHaveBeenCalledWith('APP.MESSAGES.ERRORS.GENERIC');
});
});
describe('Checkout operations', () => {
const nodeId = 'fake-node-id';
const makeApiError = (statusCode: number): Error => new Error(JSON.stringify({ error: { statusCode } }));
describe('checkout()', () => {
it('should return the working copy NodeEntry on success', () => {
const fakeEntry = { entry: { id: nodeId } } as NodeEntry;
spyOn(nodesApiService, 'checkoutNode').and.returnValue(of(fakeEntry));
contentManagementService.checkout(nodeId).subscribe((result) => {
expect(nodesApiService.checkoutNode).toHaveBeenCalledWith(nodeId);
expect(result).toEqual(fakeEntry);
});
});
it('should show 400 error notification', () => {
spyOn(nodesApiService, 'checkoutNode').and.returnValue(throwError(() => makeApiError(400)));
contentManagementService.checkout(nodeId).subscribe({ next: () => {} });
expect(showErrorSpy).toHaveBeenCalledWith('CHECKOUT.ERRORS.CHECKOUT.400');
});
it('should show 403 error notification', () => {
spyOn(nodesApiService, 'checkoutNode').and.returnValue(throwError(() => makeApiError(403)));
contentManagementService.checkout(nodeId).subscribe({ next: () => {} });
expect(showErrorSpy).toHaveBeenCalledWith('CHECKOUT.ERRORS.CHECKOUT.403');
});
it('should show 404 error notification', () => {
spyOn(nodesApiService, 'checkoutNode').and.returnValue(throwError(() => makeApiError(404)));
contentManagementService.checkout(nodeId).subscribe({ next: () => {} });
expect(showErrorSpy).toHaveBeenCalledWith('CHECKOUT.ERRORS.404');
});
it('should show 409 error notification', () => {
spyOn(nodesApiService, 'checkoutNode').and.returnValue(throwError(() => makeApiError(409)));
contentManagementService.checkout(nodeId).subscribe({ next: () => {} });
expect(showErrorSpy).toHaveBeenCalledWith('CHECKOUT.ERRORS.CHECKOUT.409');
});
it('should show UNKNOWN notification for unrecognised status code (500)', () => {
spyOn(nodesApiService, 'checkoutNode').and.returnValue(throwError(() => makeApiError(500)));
contentManagementService.checkout(nodeId).subscribe({ next: () => {} });
expect(showErrorSpy).toHaveBeenCalledWith('CHECKOUT.ERRORS.UNKNOWN');
});
});
describe('cancelCheckout()', () => {
it('should return the original NodeEntry on success', () => {
const fakeEntry = { entry: { id: nodeId } } as NodeEntry;
spyOn(nodesApiService, 'cancelCheckoutNode').and.returnValue(of(fakeEntry));
contentManagementService.cancelCheckout(nodeId).subscribe((result) => {
expect(result).toEqual(fakeEntry);
expect(nodesApiService.cancelCheckoutNode).toHaveBeenCalledWith(nodeId);
});
});
it('should show 400 error notification', () => {
spyOn(nodesApiService, 'cancelCheckoutNode').and.returnValue(throwError(() => makeApiError(400)));
contentManagementService.cancelCheckout(nodeId).subscribe({ next: () => {} });
expect(showErrorSpy).toHaveBeenCalledWith('CHECKOUT.ERRORS.CANCEL_CHECKOUT.400');
});
it('should show 403 error notification', () => {
spyOn(nodesApiService, 'cancelCheckoutNode').and.returnValue(throwError(() => makeApiError(403)));
contentManagementService.cancelCheckout(nodeId).subscribe({ next: () => {} });
expect(showErrorSpy).toHaveBeenCalledWith('CHECKOUT.ERRORS.CANCEL_CHECKOUT.403');
});
it('should show 404 error notification', () => {
spyOn(nodesApiService, 'cancelCheckoutNode').and.returnValue(throwError(() => makeApiError(404)));
contentManagementService.cancelCheckout(nodeId).subscribe({ next: () => {} });
expect(showErrorSpy).toHaveBeenCalledWith('CHECKOUT.ERRORS.404');
});
it('should show 409 error notification', () => {
spyOn(nodesApiService, 'cancelCheckoutNode').and.returnValue(throwError(() => makeApiError(409)));
contentManagementService.cancelCheckout(nodeId).subscribe({ next: () => {} });
expect(showErrorSpy).toHaveBeenCalledWith('CHECKOUT.ERRORS.CANCEL_CHECKOUT.409');
});
it('should show UNKNOWN notification for unrecognised status code', () => {
spyOn(nodesApiService, 'cancelCheckoutNode').and.returnValue(throwError(() => makeApiError(503)));
contentManagementService.cancelCheckout(nodeId).subscribe({ next: () => {} });
expect(showErrorSpy).toHaveBeenCalledWith('CHECKOUT.ERRORS.UNKNOWN');
});
});
});
});
@@ -53,7 +53,7 @@ import { DeletedNodesPaging, Node, NodeEntry, PathInfo, SiteBodyCreate, SiteEntr
import { inject, Injectable } from '@angular/core';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { forkJoin, Observable, of, zip } from 'rxjs';
import { forkJoin, Observable, of, zip, EMPTY } from 'rxjs';
import { catchError, map, mergeMap, take, tap } from 'rxjs/operators';
import { LinkOperationResult, NodeActionsService } from './node-actions.service';
import { ActivatedRoute, Router } from '@angular/router';
@@ -543,6 +543,35 @@ export class ContentManagementService {
});
}
checkout(nodeId: string): Observable<NodeEntry> {
return this.nodesApiService.checkoutNode(nodeId).pipe(
catchError((err) => {
this.notificationService.showError(this.resolveCheckoutErrorKey(err, 'CHECKOUT'));
return EMPTY;
})
);
}
cancelCheckout(nodeId: string): Observable<NodeEntry> {
return this.nodesApiService.cancelCheckoutNode(nodeId).pipe(
catchError((err) => {
this.notificationService.showError(this.resolveCheckoutErrorKey(err, 'CANCEL_CHECKOUT'));
return EMPTY;
})
);
}
private resolveCheckoutErrorKey(error: Error, operation: 'CHECKOUT' | 'CANCEL_CHECKOUT'): string {
let statusCode = -1;
try {
statusCode = JSON.parse(error.message).error.statusCode;
} catch {}
if (statusCode === 404) {
return 'CHECKOUT.ERRORS.404';
}
return [400, 403, 409].includes(statusCode) ? `CHECKOUT.ERRORS.${operation}.${statusCode}` : 'CHECKOUT.ERRORS.UNKNOWN';
}
private showCopyMessage(info: any, nodes: Array<NodeEntry>, newItems?: Array<NodeEntry>) {
const succeeded = newItems?.length ?? 0;
const failed = nodes.length - succeeded;