diff --git a/docs/core/services/nodes-api.service.md b/docs/core/services/nodes-api.service.md index bad26f0fbb..3f3a0a2e08 100644 --- a/docs/core/services/nodes-api.service.md +++ b/docs/core/services/nodes-api.service.md @@ -18,6 +18,7 @@ Accesses and manipulates ACS document nodes using their node IDs. - [Getting folder node contents](#getting-folder-node-contents) - [Creating and updating nodes](#creating-and-updating-nodes) - [Deleting and restoring nodes](#deleting-and-restoring-nodes) + - [Checking out nodes](#checking-out-nodes) - [See also](#see-also) ## Class members @@ -103,6 +104,16 @@ Accesses and manipulates ACS document nodes using their node IDs. - _nodeId:_ `string` - ID of the target node - _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. +- **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)`>`
+ 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)`>`
+ 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 @@ -160,6 +171,12 @@ and 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. +### 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 - [Deleted nodes api service](deleted-nodes-api.service.md) diff --git a/lib/content-services/src/lib/common/services/nodes-api.service.spec.ts b/lib/content-services/src/lib/common/services/nodes-api.service.spec.ts index 3be4e591ed..8da5a9e199 100644 --- a/lib/content-services/src/lib/common/services/nodes-api.service.spec.ts +++ b/lib/content-services/src/lib/common/services/nodes-api.service.spec.ts @@ -18,11 +18,19 @@ import { TestBed } from '@angular/core/testing'; import { RedirectAuthService } from '@alfresco/adf-core'; 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 { AlfrescoApiService } from '../../services/alfresco-api.service'; 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 = { entry: { jobId: 'fake-job-id' @@ -75,4 +83,20 @@ describe('NodesApiService', () => { 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); + }); }); diff --git a/lib/content-services/src/lib/common/services/nodes-api.service.ts b/lib/content-services/src/lib/common/services/nodes-api.service.ts index 475695f5bd..ba20a90f9f 100644 --- a/lib/content-services/src/lib/common/services/nodes-api.service.ts +++ b/lib/content-services/src/lib/common/services/nodes-api.service.ts @@ -293,6 +293,30 @@ export class NodesApiService { 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 { + 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 { + return from(this.nodesApi.cancelCheckoutNode(nodeId, opts)); + } + private randomNodeName(): string { return `node_${Date.now()}`; } diff --git a/lib/js-api/src/api/content-rest-api/api/nodes.api.ts b/lib/js-api/src/api/content-rest-api/api/nodes.api.ts index e9882cfa21..c9f740c6be 100644 --- a/lib/js-api/src/api/content-rest-api/api/nodes.api.ts +++ b/lib/js-api/src/api/content-rest-api/api/nodes.api.ts @@ -1033,4 +1033,63 @@ export class NodesApi extends BaseApi { 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 - the working copy node + */ + checkoutNode(nodeId: string, opts?: NodesIncludeQuery): Promise { + 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 - the original (unlocked) node + */ + cancelCheckoutNode(nodeId: string, opts?: NodesIncludeQuery): Promise { + 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 + }); + } } diff --git a/lib/js-api/src/api/content-rest-api/docs/NodesApi.md b/lib/js-api/src/api/content-rest-api/docs/NodesApi.md index a2be171308..11ca9b0111 100644 --- a/lib/js-api/src/api/content-rest-api/docs/NodesApi.md +++ b/lib/js-api/src/api/content-rest-api/docs/NodesApi.md @@ -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 | | [updateNode](#updateNode) | **PUT** /nodes/{nodeId} | Update a node | | [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 | +| [cancelCheckoutNode](#cancelCheckoutNode) | **POST** /nodes/{nodeId}/cancel-checkout | Cancel checkout of a node | +| [checkoutNode](#checkoutNode) | **POST** /nodes/{nodeId}/checkout | Checkout a node | ## copyNode @@ -1260,6 +1262,66 @@ nodesApi.getFolderSizeInfo(``, ``).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(``, 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(``, opts).then((data) => { + console.log('API called successfully. Returned data: ' + data); +}); +``` + # Models ## NodeBodyUpdate diff --git a/lib/js-api/test/content-services/nodeApi.spec.ts b/lib/js-api/test/content-services/nodeApi.spec.ts index 4b5f6acc65..ec9bde13cb 100644 --- a/lib/js-api/test/content-services/nodeApi.spec.ts +++ b/lib/js-api/test/content-services/nodeApi.spec.ts @@ -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', () => { it('should return jobId on initiateFolderSizeCalculation API call if everything is ok', async () => { nodeMock.post200ResponseInitiateFolderSizeCalculation(); diff --git a/lib/js-api/test/mockObjects/content-services/node.mock.ts b/lib/js-api/test/mockObjects/content-services/node.mock.ts index d4e87b691a..5fa5151132 100644 --- a/lib/js-api/test/mockObjects/content-services/node.mock.ts +++ b/lib/js-api/test/mockObjects/content-services/node.mock.ts @@ -16,6 +16,7 @@ */ import { BaseMock } from '../base.mock'; +import { NodeEntry } from '../../../src'; export class NodeMock extends BaseMock { 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); + } }