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

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

* [ACS-12332] cr fix
This commit is contained in:
Mykyta Maliarchuk
2026-08-13 09:32:32 +02:00
committed by GitHub
parent 4931475950
commit 3aa17e49fe
7 changed files with 229 additions and 2 deletions
+17
View File
@@ -18,6 +18,7 @@ Accesses and manipulates ACS document nodes using their node IDs.
- [Getting folder node contents](#getting-folder-node-contents) - [Getting folder node contents](#getting-folder-node-contents)
- [Creating and updating nodes](#creating-and-updating-nodes) - [Creating and updating nodes](#creating-and-updating-nodes)
- [Deleting and restoring nodes](#deleting-and-restoring-nodes) - [Deleting and restoring nodes](#deleting-and-restoring-nodes)
- [Checking out nodes](#checking-out-nodes)
- [See also](#see-also) - [See also](#see-also)
## Class members ## Class members
@@ -103,6 +104,16 @@ Accesses and manipulates ACS document nodes using their node IDs.
- _nodeId:_ `string` - ID of the target node - _nodeId:_ `string` - ID of the target node
- _opts:_ `{ where?: string; includeSource?: boolean;} & NodesIncludeQuery & ContentPagingQuery` - additional options that API can take - _opts:_ `{ where?: string; includeSource?: boolean;} & NodesIncludeQuery & ContentPagingQuery` - additional options that API can take
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodeAssociationPaging`](../../../lib/js-api/src/api/content-rest-api/docs/NodesApi.md#NodeAssociationPaging)`>` - List of node's parents. - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodeAssociationPaging`](../../../lib/js-api/src/api/content-rest-api/docs/NodesApi.md#NodeAssociationPaging)`>` - List of node's parents.
- **checkoutNode**(nodeId: `string`, opts?: `NodesIncludeQuery`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodeEntry`](https://github.com/Alfresco/alfresco-ng2-components/blob/develop/lib/js-api/src/api/content-rest-api/docs/NodeEntry.md)`>`<br/>
Checks out a file node for offline editing. Creates a private working copy and locks the original.
- _nodeId:_ `string` - ID of the file node to check out
- _opts:_ `NodesIncludeQuery` - (Optional) Additional query parameters (`include`, `fields`)
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodeEntry`](https://github.com/Alfresco/alfresco-ng2-components/blob/develop/lib/js-api/src/api/content-rest-api/docs/NodeEntry.md)`>` - The working copy node
- **cancelCheckoutNode**(nodeId: `string`, opts?: `NodesIncludeQuery`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodeEntry`](https://github.com/Alfresco/alfresco-ng2-components/blob/develop/lib/js-api/src/api/content-rest-api/docs/NodeEntry.md)`>`<br/>
Cancels a checkout. Accepts either the working copy or the original checked-out node. Deletes the working copy and unlocks the original.
- _nodeId:_ `string` - ID of the working copy or original checked-out node
- _opts:_ `NodesIncludeQuery` - (Optional) Additional query parameters (`include`, `fields`)
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodeEntry`](https://github.com/Alfresco/alfresco-ng2-components/blob/develop/lib/js-api/src/api/content-rest-api/docs/NodeEntry.md)`>` - The original (unlocked) node
## Details ## Details
@@ -160,6 +171,12 @@ and
pages in the Alfresco JS API for further details and options. Note that you can also use the pages in the Alfresco JS API for further details and options. Note that you can also use the
[Deleted Nodes Api service](deleted-nodes-api.service.md) get a list of all items currently in the trashcan. [Deleted Nodes Api service](deleted-nodes-api.service.md) get a list of all items currently in the trashcan.
### Checking out nodes
Use `checkoutNode` to lock a file node for offline editing. This creates a private working copy and locks the original node so other users cannot modify it. On success, the Observable emits the working copy `NodeEntry`.
Use `cancelCheckoutNode` to discard the working copy and unlock the original. You can pass either the working copy node ID or the original node ID. On success, the Observable emits the original (unlocked) `NodeEntry`.
## See also ## See also
- [Deleted nodes api service](deleted-nodes-api.service.md) - [Deleted nodes api service](deleted-nodes-api.service.md)
@@ -18,11 +18,19 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { RedirectAuthService } from '@alfresco/adf-core'; import { RedirectAuthService } from '@alfresco/adf-core';
import { EMPTY, firstValueFrom, of } from 'rxjs'; import { EMPTY, firstValueFrom, of } from 'rxjs';
import { JobIdBodyEntry, SizeDetails, SizeDetailsEntry } from '@alfresco/js-api'; import { JobIdBodyEntry, NodeEntry, SizeDetails, SizeDetailsEntry } from '@alfresco/js-api';
import { NodesApiService } from './nodes-api.service'; import { NodesApiService } from './nodes-api.service';
import { AlfrescoApiService } from '../../services/alfresco-api.service'; import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { AlfrescoApiServiceMock } from '../../mock/alfresco-api.service.mock'; import { AlfrescoApiServiceMock } from '../../mock/alfresco-api.service.mock';
const fakeNodeEntry = {
entry: {
id: 'fake-node-id',
name: 'fake-file.txt',
nodeType: 'cm:content'
}
} as NodeEntry;
const fakeInitiateFolderSizeResponse: JobIdBodyEntry = { const fakeInitiateFolderSizeResponse: JobIdBodyEntry = {
entry: { entry: {
jobId: 'fake-job-id' jobId: 'fake-job-id'
@@ -75,4 +83,20 @@ describe('NodesApiService', () => {
expect(nodesApiService.nodesApi.listParents).toHaveBeenCalledWith('fake-node-id', { include: ['path'], where: 'isPrimary=true' }); expect(nodesApiService.nodesApi.listParents).toHaveBeenCalledWith('fake-node-id', { include: ['path'], where: 'isPrimary=true' });
}); });
it('should call nodesApi.checkoutNode with the node ID and optional query params', async () => {
const opts = { include: ['path', 'allowableOperations'], fields: ['id', 'name'] };
spyOn(nodesApiService.nodesApi, 'checkoutNode').and.returnValue(Promise.resolve(fakeNodeEntry));
await firstValueFrom(nodesApiService.checkoutNode(fakeNodeEntry.entry.id, opts));
expect(nodesApiService.nodesApi.checkoutNode).toHaveBeenCalledWith(fakeNodeEntry.entry.id, opts);
});
it('should call nodesApi.cancelCheckoutNode with the node ID and optional query params', async () => {
const opts = { include: ['path'], fields: ['id'] };
spyOn(nodesApiService.nodesApi, 'cancelCheckoutNode').and.returnValue(Promise.resolve(fakeNodeEntry));
await firstValueFrom(nodesApiService.cancelCheckoutNode(fakeNodeEntry.entry.id, opts));
expect(nodesApiService.nodesApi.cancelCheckoutNode).toHaveBeenCalledWith(fakeNodeEntry.entry.id, opts);
});
}); });
@@ -293,6 +293,30 @@ export class NodesApiService {
return from(this.nodesApi.listParents(nodeId, opts)); return from(this.nodesApi.listParents(nodeId, opts));
} }
/**
* Checks out a file node for offline editing.
* Creates a private working copy and locks the original.
*
* @param nodeId ID of the node to check out
* @param opts Optional query parameters (`include`, `fields`)
* @returns Observable emitting the working copy node
*/
checkoutNode(nodeId: string, opts?: NodesIncludeQuery): Observable<NodeEntry> {
return from(this.nodesApi.checkoutNode(nodeId, opts));
}
/**
* Cancels a checkout. Accepts either the working copy or the original node.
* Deletes the working copy and unlocks the original.
*
* @param nodeId ID of the working copy or the original checked-out node
* @param opts Optional query parameters (`include`, `fields`)
* @returns Observable emitting the original (unlocked) node
*/
cancelCheckoutNode(nodeId: string, opts?: NodesIncludeQuery): Observable<NodeEntry> {
return from(this.nodesApi.cancelCheckoutNode(nodeId, opts));
}
private randomNodeName(): string { private randomNodeName(): string {
return `node_${Date.now()}`; return `node_${Date.now()}`;
} }
@@ -1033,4 +1033,63 @@ export class NodesApi extends BaseApi {
returnType: SizeDetailsEntry returnType: SizeDetailsEntry
}); });
} }
/**
* Checkout a node
*
* Checks out a file node for offline editing. Creates a private working copy and locks the original.
*
* @param nodeId The identifier of a file node to check out.
* @param opts Optional parameters
* @returns Promise<NodeEntry> - the working copy node
*/
checkoutNode(nodeId: string, opts?: NodesIncludeQuery): Promise<NodeEntry> {
throwIfNotDefined(nodeId, 'nodeId');
const pathParams = {
nodeId
};
const queryParams = {
include: buildCollectionParam(opts?.include, 'csv'),
fields: buildCollectionParam(opts?.fields, 'csv')
};
return this.post({
path: '/nodes/{nodeId}/checkout',
pathParams,
queryParams,
returnType: NodeEntry
});
}
/**
* Cancel checkout of a node
*
* Cancels a checkout. Accepts either the working copy or the original checked-out node.
* Deletes the working copy, unlocks the original, and returns the original node.
*
* @param nodeId The identifier of the working copy or the original checked-out node.
* @param opts Optional parameters
* @returns Promise<NodeEntry> - the original (unlocked) node
*/
cancelCheckoutNode(nodeId: string, opts?: NodesIncludeQuery): Promise<NodeEntry> {
throwIfNotDefined(nodeId, 'nodeId');
const pathParams = {
nodeId
};
const queryParams = {
include: buildCollectionParam(opts?.include, 'csv'),
fields: buildCollectionParam(opts?.fields, 'csv')
};
return this.post({
path: '/nodes/{nodeId}/cancel-checkout',
pathParams,
queryParams,
returnType: NodeEntry
});
}
} }
@@ -25,8 +25,10 @@ All URIs are relative to *https://localhost/alfresco/api/-default-/public/alfres
| [unlockNode](#unlockNode) | **POST** /nodes/{nodeId}/unlock | Unlock a node | | [unlockNode](#unlockNode) | **POST** /nodes/{nodeId}/unlock | Unlock a node |
| [updateNode](#updateNode) | **PUT** /nodes/{nodeId} | Update a node | | [updateNode](#updateNode) | **PUT** /nodes/{nodeId} | Update a node |
| [updateNodeContent](#updateNodeContent) | **PUT** /nodes/{nodeId}/content | Update node content | | [updateNodeContent](#updateNodeContent) | **PUT** /nodes/{nodeId}/content | Update node content |
| [initiateFolderSizeCalculation](#initiateFolderSizeCalculation) | **POST** /nodes/{nodeId}/size-details | Initiate a new request to calculate folder size | | [initiateFolderSizeCalculation](#initiateFolderSizeCalculation) | **POST** /nodes/{nodeId}/size-details | Initiate a new request to calculate folder size |
| [getFolderSizeInfo](#getFolderSizeInfo) | **GET** /nodes/{nodeId}/size-details/{jobId} | Gets the details of a folder | | [getFolderSizeInfo](#getFolderSizeInfo) | **GET** /nodes/{nodeId}/size-details/{jobId} | Gets the details of a folder |
| [cancelCheckoutNode](#cancelCheckoutNode) | **POST** /nodes/{nodeId}/cancel-checkout | Cancel checkout of a node |
| [checkoutNode](#checkoutNode) | **POST** /nodes/{nodeId}/checkout | Checkout a node |
## copyNode ## copyNode
@@ -1260,6 +1262,66 @@ nodesApi.getFolderSizeInfo(`<nodeId>`, `<jobId>`).then((data) => {
}); });
``` ```
## checkoutNode
Checkout a node
Checks out a file node for offline editing. Creates a private working copy and locks the original.
**Parameters**
| Name | Type | Description |
|--------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **nodeId** | string | The identifier of a file node to check out. |
| opts.include | string[] | Returns additional information about the node. The following optional fields can be requested: `allowableOperations`, `association`, `isLink`, `isFavorite`, `isLocked`, `path`, `permissions`, `definition` |
| opts.fields | string[] | A list of field names. You can use this parameter to restrict the fields returned within a response if, for example, you want to save on overall bandwidth. The list applies to a returned individual entity or entries within a collection. If the API method also supports the **include** parameter, then the fields specified in the **include** parameter are returned in addition to those specified in the **fields** parameter. |
**Return type**: [NodeEntry](NodeEntry.md)
**Example**
```javascript
import { AlfrescoApi, NodesApi } from '@alfresco/js-api';
const alfrescoApi = new AlfrescoApi(/*..*/);
const nodesApi = new NodesApi(alfrescoApi);
const opts = {};
nodesApi.checkoutNode(`<nodeId>`, opts).then((data) => {
console.log('API called successfully. Returned data: ' + data);
});
```
## cancelCheckoutNode
Cancel checkout of a node
Cancels a checkout. Accepts either the working copy or the original checked-out node. Deletes the working copy, unlocks the original, and returns the original node.
**Parameters**
| Name | Type | Description |
|--------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **nodeId** | string | The identifier of the working copy or original checked-out node. |
| opts.include | string[] | Returns additional information about the node. The following optional fields can be requested: `allowableOperations`, `association`, `isLink`, `isFavorite`, `isLocked`, `path`, `permissions`, `definition` |
| opts.fields | string[] | A list of field names. You can use this parameter to restrict the fields returned within a response if, for example, you want to save on overall bandwidth. The list applies to a returned individual entity or entries within a collection. If the API method also supports the **include** parameter, then the fields specified in the **include** parameter are returned in addition to those specified in the **fields** parameter. |
**Return type**: [NodeEntry](NodeEntry.md)
**Example**
```javascript
import { AlfrescoApi, NodesApi } from '@alfresco/js-api';
const alfrescoApi = new AlfrescoApi(/*..*/);
const nodesApi = new NodesApi(alfrescoApi);
const opts = {};
nodesApi.cancelCheckoutNode(`<nodeId>`, opts).then((data) => {
console.log('API called successfully. Returned data: ' + data);
});
```
# Models # Models
## NodeBodyUpdate ## NodeBodyUpdate
@@ -141,6 +141,30 @@ describe('Node', () => {
}); });
}); });
describe('Checkout', () => {
it('should POST to the checkout endpoint', async () => {
nodeMock.post200CheckoutNode('fake-node-id');
const result = await nodesApi.checkoutNode('fake-node-id');
assert.ok(result.entry, 'cancelCheckoutNode should return a NodeEntry');
});
it('should throw if nodeId is not defined', () => {
assert.throws(() => nodesApi.checkoutNode(undefined));
});
});
describe('Cancel Checkout', () => {
it('should POST to the cancel-checkout endpoint', async () => {
nodeMock.post200CancelCheckoutNode('fake-node-id');
const result = await nodesApi.cancelCheckoutNode('fake-node-id');
assert.ok(result.entry, 'checkoutNode should return a NodeEntry');
});
it('should throw if nodeId is not defined', () => {
assert.throws(() => nodesApi.cancelCheckoutNode(undefined));
});
});
describe('FolderInformation', () => { describe('FolderInformation', () => {
it('should return jobId on initiateFolderSizeCalculation API call if everything is ok', async () => { it('should return jobId on initiateFolderSizeCalculation API call if everything is ok', async () => {
nodeMock.post200ResponseInitiateFolderSizeCalculation(); nodeMock.post200ResponseInitiateFolderSizeCalculation();
@@ -16,6 +16,7 @@
*/ */
import { BaseMock } from '../base.mock'; import { BaseMock } from '../base.mock';
import { NodeEntry } from '../../../src';
export class NodeMock extends BaseMock { export class NodeMock extends BaseMock {
get200ResponseChildren(): void { get200ResponseChildren(): void {
@@ -284,4 +285,20 @@ export class NodeMock extends BaseMock {
} }
}); });
} }
post200CheckoutNode(nodeId: string): void {
this.mock()
.post(`/alfresco/api/-default-/public/alfresco/versions/1/nodes/${nodeId}/checkout`)
.reply(200, {
entry: { id: 'test-node-id', name: 'Test Node' }
} as NodeEntry);
}
post200CancelCheckoutNode(nodeId: string): void {
this.mock()
.post(`/alfresco/api/-default-/public/alfresco/versions/1/nodes/${nodeId}/cancel-checkout`)
.reply(200, {
entry: { id: 'test-node-id', name: 'Test Node' }
} as NodeEntry);
}
} }