From e589071328c2595be48caa19288e2d3c9bf65340 Mon Sep 17 00:00:00 2001 From: arditdomi <32884230+arditdomi@users.noreply.github.com> Date: Thu, 8 Apr 2021 14:55:06 +0100 Subject: [PATCH] [AAE-4841] - Fix content node selector current selection is lost after uploading files (#6862) * [AAE-4841] - Preserve current selection when preselecting the newly uploaded nodes * add a method to handle unselection of a preselected row in document list and emit the change * Fix-refactor unselection * Fix document list reload * Partial revert share datatable adapter * try with overwriting datatable selection * Sync datatable selection after every page load * refactor selection/unselection in datatable by using a row id * Fix/Add some unit tests * Move preselection from adapter to doc list, fix single selection mode * Add some unit tests * Add document list unit tests --- ...tent-node-selector-panel.component.spec.ts | 68 ++++- .../content-node-selector-panel.component.ts | 49 ++-- .../document-list.component.spec.ts | 236 ++++++++++++++++-- .../components/document-list.component.ts | 102 ++++++-- .../data/share-data-row.model.ts | 6 + .../data/share-datatable-adapter.spec.ts | 51 ++-- .../data/share-datatable-adapter.ts | 32 +-- .../lib/mock/document-library.model.mock.ts | 4 +- .../lib/mock/document-list.component.mock.ts | 2 +- .../datatable/datatable.component.spec.ts | 40 +++ .../datatable/datatable.component.ts | 6 +- lib/core/datatable/data/data-row.model.ts | 1 + 12 files changed, 459 insertions(+), 138 deletions(-) diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts index 3c90d74ae9..aaada7dee0 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts @@ -19,7 +19,7 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Node, NodeEntry, NodePaging, RequestScope, ResultSetPaging, SiteEntry, SitePaging } from '@alfresco/js-api'; -import { AppConfigService, FileModel, FileUploadStatus, NodesApiService, setupTestBed, SitesService, UploadService } from '@alfresco/adf-core'; +import { AppConfigService, FileModel, FileUploadStatus, NodesApiService, setupTestBed, SitesService, UploadService, FileUploadCompleteEvent } from '@alfresco/adf-core'; import { of, throwError } from 'rxjs'; import { DropdownBreadcrumbComponent } from '../breadcrumb'; import { ContentNodeSelectorPanelComponent } from './content-node-selector-panel.component'; @@ -1214,18 +1214,74 @@ describe('ContentNodeSelectorPanelComponent', () => { spyOn(documentListService, 'getFolder'); }); - it('should remove the node from the chosenNodes when an upload gets deleted', () => { + it('should trigger preselection only after the upload batch contains all the uploads of the queue and no other uploads are in progress', fakeAsync(() => { fixture.detectChanges(); - const selectSpy = spyOn(component.select, 'next'); + component.selectionMode = 'multiple'; + + const isUploadingSpy = spyOn(uploadService, 'isUploading').and.returnValue(true); + const documentListReloadSpy = spyOn(component.documentList, 'reloadWithoutResettingSelection'); + + const fakeFileModels = [new FileModel( { name: 'fake-name', size: 100 }), new FileModel( { name: 'fake-name-2', size: 200 })]; + const fileUploadCompleteEvent = new FileUploadCompleteEvent(fakeFileModels[0], 1, fakeFileModels[0], 0); + uploadService.fileUploadComplete.next(fileUploadCompleteEvent); + + tick(500); + fixture.detectChanges(); + + expect(component.currentUploadBatch).toEqual([fileUploadCompleteEvent.data]); + expect(component.preselectedNodes).toEqual([]); + expect(documentListReloadSpy).not.toHaveBeenCalled(); + + isUploadingSpy.and.returnValue(false); + + const secondFileUploadCompleteEvent = new FileUploadCompleteEvent(fakeFileModels[1], 2, fakeFileModels[1], 0); + uploadService.fileUploadComplete.next(secondFileUploadCompleteEvent); + tick(500); + + expect(component.currentUploadBatch).toEqual([]); + expect(component.preselectedNodes).toEqual([fileUploadCompleteEvent.data, secondFileUploadCompleteEvent.data]); + expect(documentListReloadSpy).toHaveBeenCalled(); + })); + + it('should call document list to unselect the row of the deleted upload', () => { + fixture.detectChanges(); + + const documentListUnselectRowSpy = spyOn(component.documentList, 'unselectRowFromNodeId'); + const documentListReloadSpy = spyOn(component.documentList, 'reloadWithoutResettingSelection'); const fakeFileModel = new FileModel( { name: 'fake-name', size: 10000000 }); const fakeNodes = [ { id: 'fakeNodeId' }, { id: 'fakeNodeId2' }]; + fakeFileModel.data = { entry: fakeNodes[0] }; fakeFileModel.status = FileUploadStatus.Deleted; - component._chosenNode = [...fakeNodes]; uploadService.cancelUpload(fakeFileModel); - expect(selectSpy).toHaveBeenCalledWith([fakeNodes[1]]); - expect(component._chosenNode).toEqual([fakeNodes[1]]); + expect(documentListUnselectRowSpy).toHaveBeenCalledWith(fakeNodes[0].id); + expect(documentListReloadSpy).toHaveBeenCalled(); + }); + + it('should return only the last uploaded node to become preselected when the selection mode is single', () => { + fixture.detectChanges(); + const fakeNodes = [new NodeEntry({ id: 'fakeNode1' }), new NodeEntry({ id: 'fakeNode2' })]; + component.currentUploadBatch = fakeNodes; + component.selectionMode = 'single'; + + expect(component.getPreselectNodesBasedOnSelectionMode()).toEqual([fakeNodes[1]]); + }); + + it('should return all the uploaded nodes to become preselected when the selection mode is multiple', () => { + fixture.detectChanges(); + const fakeNodes = [new NodeEntry({ id: 'fakeNode1' }), new NodeEntry({ id: 'fakeNode2' })]; + component.currentUploadBatch = fakeNodes; + component.selectionMode = 'multiple'; + + expect(component.getPreselectNodesBasedOnSelectionMode()).toEqual(fakeNodes); + }); + + it('should return an empty array when no files are uploaded', () => { + fixture.detectChanges(); + component.currentUploadBatch = []; + + expect(component.getPreselectNodesBasedOnSelectionMode()).toEqual([]); }); }); diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts index f1b9e506cf..8cdb25bc54 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts @@ -36,7 +36,6 @@ import { UploadService, FileUploadCompleteEvent, FileUploadDeleteEvent, - FileModel, AppConfigService, DataSorting } from '@alfresco/adf-core'; @@ -45,7 +44,7 @@ import { Node, NodePaging, Pagination, SiteEntry, SitePaging, NodeEntry, QueryBo import { DocumentListComponent } from '../document-list/components/document-list.component'; import { RowFilter } from '../document-list/data/row-filter.model'; import { ImageResolver } from '../document-list/data/image-resolver.model'; -import { debounceTime, takeUntil, scan } from 'rxjs/operators'; +import { debounceTime, takeUntil } from 'rxjs/operators'; import { CustomResourcesService } from '../document-list/services/custom-resources.service'; import { NodeEntryEvent, ShareDataRow } from '../document-list'; import { Subject } from 'rxjs'; @@ -253,6 +252,8 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { target: PaginatedComponent; preselectedNodes: NodeEntry[] = []; + currentUploadBatch: NodeEntry[] = []; + sorting: string[] | DataSorting; searchPanelExpanded: boolean = false; @@ -359,36 +360,29 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { private onFileUploadEvent() { this.uploadService.fileUploadComplete - .pipe( - debounceTime(500), - scan((files, currentFile) => [...files, currentFile], []), - takeUntil(this.onDestroy$) - ) - .subscribe((uploadedFiles: FileUploadCompleteEvent[]) => { - this.preselectedNodes = this.getPreselectNodesBasedOnSelectionMode(uploadedFiles); - this.documentList.reload(); - }); + .pipe( + debounceTime(500), + takeUntil(this.onDestroy$) + ) + .subscribe((fileUploadEvent: FileUploadCompleteEvent) => { + this.currentUploadBatch.push(fileUploadEvent.data); + if (!this.uploadService.isUploading()) { + this.preselectedNodes = this.getPreselectNodesBasedOnSelectionMode(); + this.currentUploadBatch = []; + this.documentList.reloadWithoutResettingSelection(); + } + }); } private onFileUploadDeletedEvent() { this.uploadService.fileUploadDeleted .pipe(takeUntil(this.onDestroy$)) .subscribe((deletedFileEvent: FileUploadDeleteEvent) => { - this.removeFromChosenNodes(deletedFileEvent.file); - this.documentList.reload(); + this.documentList.unselectRowFromNodeId(deletedFileEvent.file.data.entry.id); + this.documentList.reloadWithoutResettingSelection(); }); } - private removeFromChosenNodes(file: FileModel) { - if (this.chosenNode) { - const fileIndex = this.chosenNode.findIndex((chosenNode: Node) => chosenNode.id === file.data.entry.id); - if (fileIndex !== -1) { - this._chosenNode.splice(fileIndex, 1); - this.select.next(this._chosenNode); - } - } - } - private getStartSite() { this.nodesApiService.getNode(this.currentFolderId).subscribe((startNodeEntry) => { this.startSiteGuid = this.sitesService.getSiteNameFromNodePath(startNodeEntry); @@ -462,6 +456,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { this.infinitePaginationComponent.reset(); } this.folderIdToShow = null; + this.preselectedNodes = []; this.loadingSearchResults = true; this.addCorrespondingNodeIdsQuery(); this.resetChosenNode(); @@ -630,14 +625,14 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { return this.selectionMode === 'single'; } - private getPreselectNodesBasedOnSelectionMode(uploadedFiles: FileUploadCompleteEvent[]): NodeEntry[] { + getPreselectNodesBasedOnSelectionMode(): NodeEntry[] { let selectedNodes: NodeEntry[] = []; - if (uploadedFiles && uploadedFiles.length > 0 ) { + if (this.currentUploadBatch?.length) { if (this.isSingleSelectionMode()) { - selectedNodes = [...[uploadedFiles[uploadedFiles.length - 1]].map((uploadedFile) => uploadedFile.data)]; + selectedNodes = [this.currentUploadBatch[this.currentUploadBatch.length - 1]]; } else { - selectedNodes = [...uploadedFiles.map((uploadedFile) => uploadedFile.data)]; + selectedNodes = this.currentUploadBatch; } } diff --git a/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts b/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts index 7874b9c29e..6f30a98bad 100644 --- a/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts +++ b/lib/content-services/src/lib/document-list/components/document-list.component.spec.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange, QueryList, Component, ViewChild } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange, QueryList, Component, ViewChild, SimpleChanges } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { setupTestBed, @@ -32,13 +32,17 @@ import { } from '@alfresco/adf-core'; import { Subject, of, throwError } from 'rxjs'; import { - FileNode, FolderNode, + FileNode, + FolderNode, fakeNodeAnswerWithNOEntries, fakeNodeWithNoPermission, fakeGetSitesAnswer, fakeGetSiteMembership, mockPreselectedNodes, - mockNodePagingWithPreselectedNodes + mockNodePagingWithPreselectedNodes, + mockNode1, + mockNode2, + mockNode3 } from '../../mock'; import { ContentActionModel } from '../models/content-action.model'; import { NodeMinimal, NodeMinimalEntry, NodePaging } from '../models/document-library.model'; @@ -236,6 +240,47 @@ describe('DocumentList', () => { }); }); + it('should not reset the selection when preselectNodes input changes', () => { + const resetSelectionSpy = spyOn(documentList, 'resetSelection').and.callThrough(); + documentList.selection = [{ entry: mockNode3 }]; + const changes: SimpleChanges = { + 'preselectNodes': { + previousValue: undefined, + currentValue: mockPreselectedNodes, + firstChange: true, + isFirstChange(): boolean { return this.firstChange; } + } + }; + documentList.ngOnChanges(changes); + + expect(resetSelectionSpy).not.toHaveBeenCalled(); + expect(documentList.selection).toEqual([{ entry: mockNode3 }]); + }); + + it('should reset the selection for every change other than preselectNodes', () => { + const resetSelectionSpy = spyOn(documentList, 'resetSelection').and.callThrough(); + documentList.selection = [{ entry: mockNode3 }]; + const changes: SimpleChanges = { + 'mockChange': { + previousValue: undefined, + currentValue: ['mockChangeValue'], + firstChange: true, + isFirstChange(): boolean { return this.firstChange; } + } + }; + documentList.ngOnChanges(changes); + + expect(resetSelectionSpy).toHaveBeenCalled(); + expect(documentList.selection).toEqual([]); + }); + + it('should reloadWithoutResettingSelection not reset the selection', () => { + documentList.selection = [{ entry: mockNode3 }]; + documentList.reloadWithoutResettingSelection(); + + expect(documentList.selection).toEqual([{ entry: mockNode3 }]); + }); + it('should update schema if columns change', fakeAsync(() => { documentList.columnList = new DataColumnListComponent(); documentList.columnList.columns = new QueryList(); @@ -1532,23 +1577,7 @@ describe('DocumentList', () => { spyOn(thumbnailService, 'getMimeTypeIcon').and.returnValue(`assets/images/ft_ic_created.svg`); }); - it('should able to emit nodeSelected event with preselectedNodes on the reload', async () => { - const nodeSelectedSpy = spyOn(documentList.nodeSelected, 'emit'); - - fixture.detectChanges(); - - documentList.node = mockNodePagingWithPreselectedNodes; - documentList.preselectNodes = mockPreselectedNodes; - documentList.reload(); - - fixture.detectChanges(); - await fixture.whenStable(); - - expect(documentList.preselectNodes.length).toBe(2); - expect(nodeSelectedSpy).toHaveBeenCalled(); - }); - - it('should be able to select first node from the preselectedNodes when selectionMode set to single', async () => { + it('should return only the first node of the preselectedNodes when selection mode is single', async () => { documentList.selectionMode = 'single'; fixture.detectChanges(); @@ -1563,7 +1592,7 @@ describe('DocumentList', () => { expect(documentList.getPreselectedNodesBasedOnSelectionMode().length).toBe(1); }); - it('should be able to select all preselectedNodes when selectionMode set to multiple', async () => { + it('should return all the preselectedNodes when selection mode is multiple', async () => { documentList.selectionMode = 'multiple'; fixture.detectChanges(); @@ -1581,11 +1610,11 @@ describe('DocumentList', () => { it('should call the datatable select row method for each preselected node', async () => { const datatableSelectRowSpy = spyOn(documentList.dataTable, 'selectRow'); const fakeDatatableRows = [new ShareDataRow(mockPreselectedNodes[0], contentService, null), new ShareDataRow(mockPreselectedNodes[1], contentService, null)]; - spyOn(documentList.data, 'hasPreselectedRows').and.returnValue(true); - spyOn(documentList.data, 'getPreselectedRows').and.returnValue(fakeDatatableRows); - documentList.selectionMode = 'multiple'; + spyOn(documentList, 'preselectRowsOfPreselectedNodes'); + documentList.preselectedRows = fakeDatatableRows; documentList.preselectNodes = mockPreselectedNodes; + documentList.selectionMode = 'multiple'; documentList.onPreselectNodes(); fixture.detectChanges(); @@ -1608,6 +1637,165 @@ describe('DocumentList', () => { expect(nodeSelectedSpy).not.toHaveBeenCalled(); }); + + it('should return only the first preselected row when selection mode is single', () => { + documentList.selectionMode = 'single'; + const fakeDatatableRows = [new ShareDataRow(mockPreselectedNodes[0], contentService, null), new ShareDataRow(mockPreselectedNodes[1], contentService, null)]; + documentList.preselectedRows = fakeDatatableRows; + const preselectedRows = documentList.getPreselectedRowsBasedOnSelectionMode(); + + expect(preselectedRows.length).toEqual(1); + expect(preselectedRows[0]).toEqual(fakeDatatableRows[0]); + }); + + it('should return all the preselected rows when selection mode is multiple', () => { + documentList.selectionMode = 'multiple'; + const fakeDatatableRows = [new ShareDataRow(mockPreselectedNodes[0], contentService, null), new ShareDataRow(mockPreselectedNodes[1], contentService, null)]; + documentList.preselectedRows = fakeDatatableRows; + const preselectedRows = documentList.getPreselectedRowsBasedOnSelectionMode(); + + expect(preselectedRows.length).toEqual(2); + expect(preselectedRows).toEqual(fakeDatatableRows); + }); + + it('should return an empty array when there are no preselected rows', () => { + documentList.preselectedRows = undefined; + const preselectedRows = documentList.getPreselectedRowsBasedOnSelectionMode(); + + expect(preselectedRows).toEqual([]); + }); + + it('should the combined selection be only the first preselected row when selection mode is single', () => { + const getSelectionFromAdapterSpy = spyOn(documentList.data, 'getSelectedRows'); + const fakeDatatableRows = [new ShareDataRow(mockPreselectedNodes[0], contentService, null), new ShareDataRow(mockPreselectedNodes[1], contentService, null)]; + documentList.preselectedRows = fakeDatatableRows; + documentList.selectionMode = 'single'; + const selection = documentList.getSelectionBasedOnSelectionMode(); + + expect(selection.length).toEqual(1); + expect(selection[0]).toEqual(fakeDatatableRows[0]); + expect(getSelectionFromAdapterSpy).not.toHaveBeenCalled(); + }); + + it('should get the selection from the adapter when selection mode is multiple', () => { + const fakeDatatableRows = [new ShareDataRow(mockPreselectedNodes[0], contentService, null), new ShareDataRow(mockPreselectedNodes[1], contentService, null)]; + fakeDatatableRows[0].isSelected = true; + fakeDatatableRows[1].isSelected = false; + documentList.data.setRows(fakeDatatableRows); + + const getSelectionFromAdapterSpy = spyOn(documentList.data, 'getSelectedRows').and.callThrough(); + documentList.selectionMode = 'multiple'; + documentList.preselectedRows = fakeDatatableRows; + const selection = documentList.getSelectionBasedOnSelectionMode(); + + expect(getSelectionFromAdapterSpy).toHaveBeenCalled(); + expect(selection.length).toEqual(1); + expect(selection[0]).toEqual(fakeDatatableRows[0]); + }); + + it('should preserve the existing selection when selection mode is multiple', () => { + fixture.detectChanges(); + documentList.node = mockNodePagingWithPreselectedNodes; + documentList.selection = [{ entry: mockNode1 }, { entry: mockNode2 }]; + documentList.selectionMode = 'multiple'; + documentList.reloadWithoutResettingSelection(); + const selectedRows = documentList.data.getSelectedRows(); + + expect(selectedRows.length).toEqual(2); + expect(selectedRows[0].id).toEqual(mockNode1.id); + expect(selectedRows[0].isSelected).toEqual(true); + expect(selectedRows[1].id).toEqual(mockNode2.id); + expect(selectedRows[1].isSelected).toEqual(true); + }); + + it('should not preserve the existing selection when selection mode is single', () => { + fixture.detectChanges(); + documentList.node = mockNodePagingWithPreselectedNodes; + documentList.selection = [{ entry: mockNode1 }, { entry: mockNode2 }]; + documentList.selectionMode = 'single'; + documentList.reloadWithoutResettingSelection(); + const selectedRows = documentList.data.getSelectedRows(); + + expect(selectedRows.length).toEqual(0); + }); + + it('should unselect the row from the node id', () => { + const datatableSelectRowSpy = spyOn(documentList.dataTable, 'selectRow'); + const getRowByNodeIdSpy = spyOn(documentList.data, 'getRowByNodeId').and.callThrough(); + const onNodeUnselectSpy = spyOn(documentList, 'onNodeUnselect'); + const getSelectionSpy = spyOn(documentList, 'getSelectionBasedOnSelectionMode').and.callThrough(); + + const fakeDatatableRows = [new ShareDataRow(mockPreselectedNodes[0], contentService, null), new ShareDataRow(mockPreselectedNodes[1], contentService, null)]; + fakeDatatableRows[0].isSelected = true; + documentList.data.setRows(fakeDatatableRows); + let selectedRows = documentList.data.getSelectedRows(); + + expect(selectedRows.length).toEqual(1); + + documentList.unselectRowFromNodeId(mockPreselectedNodes[0].entry.id); + selectedRows = documentList.data.getSelectedRows(); + + expect(selectedRows).toEqual([]); + expect(getSelectionSpy).toHaveBeenCalled(); + expect(getRowByNodeIdSpy).toHaveBeenCalledWith(mockPreselectedNodes[0].entry.id); + expect(datatableSelectRowSpy).toHaveBeenCalledWith(fakeDatatableRows[0], false); + expect(onNodeUnselectSpy).toHaveBeenCalledWith({ row: undefined, selection: selectedRows }); + }); + + it('should preselect the rows of the preselected nodes', () => { + const getRowByNodeIdSpy = spyOn(documentList.data, 'getRowByNodeId').and.callThrough(); + const getPreselectedNodesSpy = spyOn(documentList, 'getPreselectedNodesBasedOnSelectionMode').and.callThrough(); + + const fakeDatatableRows = [new ShareDataRow(mockPreselectedNodes[0], contentService, null), new ShareDataRow(mockPreselectedNodes[1], contentService, null)]; + documentList.data.setRows(fakeDatatableRows); + + documentList.selectionMode = 'multiple'; + documentList.preselectNodes = [...mockPreselectedNodes, { entry: mockNode3 }]; + + documentList.preselectRowsOfPreselectedNodes(); + const selectedRows = documentList.data.getSelectedRows(); + + expect(getPreselectedNodesSpy).toHaveBeenCalled(); + expect(selectedRows.length).toEqual(2); + expect(selectedRows[0].isSelected).toEqual(true); + expect(selectedRows[1].isSelected).toEqual(true); + + expect(documentList.preselectedRows.length).toEqual(2); + expect(documentList.preselectedRows).toEqual(fakeDatatableRows); + + expect(getRowByNodeIdSpy).toHaveBeenCalledWith(fakeDatatableRows[0].id); + expect(getRowByNodeIdSpy).toHaveBeenCalledWith(fakeDatatableRows[1].id); + }); + + it('should select the rows of the preselected nodes and emit the new combined selection', () => { + const hasPreselectedNodesSpy = spyOn(documentList, 'hasPreselectedNodes').and.callThrough(); + const preselectRowsOfPreselectedNodesSpy = spyOn(documentList, 'preselectRowsOfPreselectedNodes').and.callThrough(); + const getPreselectedRowsBasedOnSelectionModeSpy = spyOn(documentList, 'getPreselectedRowsBasedOnSelectionMode').and.callThrough(); + const onNodeSelectSpy = spyOn(documentList, 'onNodeSelect').and.callThrough(); + + const fakeDatatableRows = [ + new ShareDataRow(mockPreselectedNodes[0], contentService, null), + new ShareDataRow(mockPreselectedNodes[1], contentService, null), + new ShareDataRow({ entry: mockNode3 }, contentService, null) + ]; + fakeDatatableRows[2].isSelected = true; + documentList.data.setRows(fakeDatatableRows); + + documentList.selection = [{ entry: mockNode3 }]; + documentList.selectionMode = 'multiple'; + documentList.preselectNodes = mockPreselectedNodes; + documentList.onPreselectNodes(); + const selectedRows = documentList.data.getSelectedRows(); + + expect(hasPreselectedNodesSpy).toHaveBeenCalled(); + expect(preselectRowsOfPreselectedNodesSpy).toHaveBeenCalled(); + expect(getPreselectedRowsBasedOnSelectionModeSpy).toHaveBeenCalled(); + expect(selectedRows.length).toEqual(3); + expect(selectedRows[0].id).toEqual(mockNode1.id); + expect(selectedRows[1].id).toEqual(mockNode2.id); + expect(selectedRows[2].id).toEqual(mockNode3.id); + expect(onNodeSelectSpy).toHaveBeenCalledWith({ row: undefined, selection: selectedRows }); + }); }); }); diff --git a/lib/content-services/src/lib/document-list/components/document-list.component.ts b/lib/content-services/src/lib/document-list/components/document-list.component.ts index d7485d51e7..465450034d 100644 --- a/lib/content-services/src/lib/document-list/components/document-list.component.ts +++ b/lib/content-services/src/lib/document-list/components/document-list.component.ts @@ -44,7 +44,8 @@ import { RequestPaginationModel, AlfrescoApiService, UserPreferenceValues, - LockService + LockService, + DataRow } from '@alfresco/adf-core'; import { Node, NodeEntry, NodePaging, Pagination } from '@alfresco/js-api'; @@ -329,6 +330,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte $folderNode: Subject = new Subject(); allowFiltering: boolean = true; orderBy: string[] = null; + preselectedRows: DataRow[] = []; // @deprecated 3.0.0 folderNode: Node; @@ -455,7 +457,9 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte } ngOnChanges(changes: SimpleChanges) { - this.resetSelection(); + if (!changes['preselectNodes']) { + this.resetSelection(); + } if (Array.isArray(this.sorting)) { const [key, direction] = this.sorting; @@ -487,7 +491,8 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte if (this.data) { if (changes.node && changes.node.currentValue) { const merge = this._pagination ? this._pagination.merge : false; - this.data.loadPage(changes.node.currentValue, merge, null, this.getPreselectedNodesBasedOnSelectionMode()); + this.data.loadPage(changes.node.currentValue, merge, null); + this.preserveExistingSelection(); this.onPreselectNodes(); this.onDataReady(changes.node.currentValue); } else if (changes.imageResolver) { @@ -499,19 +504,24 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte reload() { this.ngZone.run(() => { this.resetSelection(); - if (this.node) { - if (this.data) { - this.data.loadPage(this.node, this._pagination.merge, null, this.getPreselectedNodesBasedOnSelectionMode()); - } - this.onPreselectNodes(); - this.syncPagination(); - this.onDataReady(this.node); - } else { - this.loadFolder(); - } + this.reloadWithoutResettingSelection(); }); } + reloadWithoutResettingSelection() { + if (this.node) { + if (this.data) { + this.data.loadPage(this.node, this._pagination.merge, null); + this.preserveExistingSelection(); + } + this.onPreselectNodes(); + this.syncPagination(); + this.onDataReady(this.node); + } else { + this.loadFolder(); + } + } + contextActionCallback(action) { if (action) { this.executeContentAction(action.node, action.model); @@ -694,7 +704,8 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte onPageLoaded(nodePaging: NodePaging) { if (nodePaging) { if (this.data) { - this.data.loadPage(nodePaging, this._pagination.merge, this.allowDropFiles, this.getPreselectedNodesBasedOnSelectionMode()); + this.data.loadPage(nodePaging, this._pagination.merge, this.allowDropFiles); + this.preserveExistingSelection(); } this.onPreselectNodes(); this.setLoadingState(false); @@ -800,7 +811,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte } onNodeSelect(event: { row: ShareDataRow, selection: Array }) { - this.selection = event.selection.filter(entry => entry.node).map((entry) => entry.node); + this.selection = event.selection.map((entry) => entry.node); const domEvent = new CustomEvent('node-select', { detail: { node: event.row ? event.row.node : null, @@ -923,23 +934,74 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte return this.hasPreselectedNodes() ? (this.isSingleSelectionMode() ? [this.preselectNodes[0]] : this.preselectNodes) : []; } - onPreselectNodes() { - if (this.data?.hasPreselectedRows()) { - const preselectedNodes = [...this.isSingleSelectionMode() ? [this.data.getPreselectedRows()[0]] : this.data.getPreselectedRows()]; - const selectedNodes = [...this.selection, ...preselectedNodes]; + getPreselectedRowsBasedOnSelectionMode(): DataRow[] { + return this.hasPreselectedRows() ? (this.isSingleSelectionMode() ? [this.preselectedRows[0]] : this.preselectedRows) : []; + } - for (const node of preselectedNodes) { + getSelectionBasedOnSelectionMode(): DataRow[] { + return this.hasPreselectedRows() ? (this.isSingleSelectionMode() ? [this.preselectedRows[0]] : this.data.getSelectedRows()) : this.data.getSelectedRows(); + } + + onPreselectNodes() { + if (this.hasPreselectedNodes()) { + this.preselectRowsOfPreselectedNodes(); + const preselectedRows = this.getPreselectedRowsBasedOnSelectionMode(); + const selectedNodes = this.data.getSelectedRows(); + + for (const node of preselectedRows) { this.dataTable.selectRow(node, true); } this.onNodeSelect({ row: undefined, selection: selectedNodes }); } } + preserveExistingSelection() { + if (this.isMultipleSelectionMode()) { + for (const selection of this.selection) { + const rowOfSelection = this.data.getRowByNodeId(selection.entry.id); + if (rowOfSelection) { + rowOfSelection.isSelected = true; + } + } + } + } + + preselectRowsOfPreselectedNodes() { + this.preselectedRows = []; + const preselectedNodes = this.getPreselectedNodesBasedOnSelectionMode(); + + preselectedNodes.forEach((preselectedNode: NodeEntry) => { + const rowOfPreselectedNode = this.data.getRowByNodeId(preselectedNode.entry.id); + if (rowOfPreselectedNode) { + rowOfPreselectedNode.isSelected = true; + this.preselectedRows.push(rowOfPreselectedNode); + } + }); + } + + unselectRowFromNodeId(nodeId: string) { + const rowToUnselect = this.data.getRowByNodeId(nodeId); + if (rowToUnselect?.isSelected) { + rowToUnselect.isSelected = false; + this.dataTable.selectRow(rowToUnselect, false); + const selection = this.getSelectionBasedOnSelectionMode(); + this.onNodeUnselect({ row: undefined, selection: selection }); + } + } + isSingleSelectionMode(): boolean { return this.selectionMode === 'single'; } + isMultipleSelectionMode(): boolean { + return this.selectionMode === 'multiple'; + } + hasPreselectedNodes(): boolean { return this.preselectNodes?.length > 0; } + + hasPreselectedRows(): boolean { + return this.preselectedRows?.length > 0; + } } diff --git a/lib/content-services/src/lib/document-list/data/share-data-row.model.ts b/lib/content-services/src/lib/document-list/data/share-data-row.model.ts index 053934d499..0d856ff723 100644 --- a/lib/content-services/src/lib/document-list/data/share-data-row.model.ts +++ b/lib/content-services/src/lib/document-list/data/share-data-row.model.ts @@ -27,6 +27,7 @@ export class ShareDataRow implements DataRow { isSelected: boolean = false; isDropTarget: boolean; cssClass: string = ''; + id: string; get node(): NodeEntry { return this.obj; @@ -50,6 +51,7 @@ export class ShareDataRow implements DataRow { if (permissionsStyle) { this.cssClass = this.getPermissionClass(obj); } + this.id = this.getId(); } checkNodeTypeAndPermissions(nodeEntry: NodeEntry) { @@ -118,4 +120,8 @@ export class ShareDataRow implements DataRow { hasValue(key: string): boolean { return this.getValue(key) !== undefined; } + + getId(): string { + return this.obj.entry.id || undefined; + } } diff --git a/lib/content-services/src/lib/document-list/data/share-datatable-adapter.spec.ts b/lib/content-services/src/lib/document-list/data/share-datatable-adapter.spec.ts index b4027eb037..7abd906cc6 100644 --- a/lib/content-services/src/lib/document-list/data/share-datatable-adapter.spec.ts +++ b/lib/content-services/src/lib/document-list/data/share-datatable-adapter.spec.ts @@ -16,7 +16,7 @@ */ import { DataColumn, DataRow, DataSorting, ContentService, ThumbnailService, setupTestBed } from '@alfresco/adf-core'; -import { FileNode, FolderNode, SmartFolderNode, RuleFolderNode, LinkFolderNode, mockPreselectedNodes, mockNodePagingWithPreselectedNodes, mockNode2, fakeNodePaging, mockNode1 } from './../../mock'; +import { FileNode, FolderNode, SmartFolderNode, RuleFolderNode, LinkFolderNode } from './../../mock'; import { ShareDataRow } from './share-data-row.model'; import { ShareDataTableAdapter } from './share-datatable-adapter'; import { ContentTestingModule } from '../../testing/content.testing.module'; @@ -480,42 +480,31 @@ describe('ShareDataTableAdapter', () => { expect(row.isDropTarget).toBeFalsy(); }); - }); - describe('Preselect rows', () => { + it('should return all the selected rows', () => { + const file = new FileNode(); + const adapter = new ShareDataTableAdapter(thumbnailService, contentService, null); + const row1 = new ShareDataRow(file, contentService, null); + const row2 = new ShareDataRow(file, contentService, null); + const row3 = new ShareDataRow(file, contentService, null); - it('should set isSelected to be true for each preselectRow if the preselectedNodes are defined', () => { - const adapter = new ShareDataTableAdapter(thumbnailService, contentService, []); - adapter.loadPage(mockNodePagingWithPreselectedNodes, null, null, mockPreselectedNodes); + row1.isSelected = true; + row2.isSelected = true; + adapter.setRows([row1, row2, row3]); + const selectedRows = adapter.getSelectedRows(); - expect(adapter.getPreselectedRows().length).toBe(1); - expect(adapter.getPreselectedRows()[0].isSelected).toBe(true); + expect(selectedRows.length).toEqual(2); + expect(selectedRows).toEqual([row1, row2]); }); - it('should set preselectedRows empty if preselectedNodes are undefined/empty', () => { - const adapter = new ShareDataTableAdapter(thumbnailService, contentService, []); - adapter.loadPage(mockNodePagingWithPreselectedNodes, null, null, []); + it('should return the row of the requested node id', () => { + const adapter = new ShareDataTableAdapter(thumbnailService, contentService, null); + const fakeFiles = [new FileNode('fake-file-1', 'text/plain', 'fake-node-id-1'), new FileNode('fake-file-2', 'text/plain', 'fake-node-id-2')]; + const fakeShareDataRows = [new ShareDataRow(fakeFiles[0], contentService, null), new ShareDataRow(fakeFiles[1], contentService, null)]; + adapter.setRows(fakeShareDataRows); - expect(adapter.getPreselectedRows().length).toBe(0); - }); - - it('should set preselectedRows empty if preselectedNodes are not found in the list', () => { - const adapter = new ShareDataTableAdapter(thumbnailService, contentService, []); - mockNode2.id = 'mock-file-id'; - const preselectedNode = [ { entry: mockNode2 }]; - adapter.loadPage(fakeNodePaging, null, null, preselectedNode); - - expect(adapter.getPreselectedRows().length).toBe(0); - }); - - it('should preselected rows contain only the valid rows that exist in the datatable', () => { - const adapter = new ShareDataTableAdapter(thumbnailService, contentService, []); - const nonExistingEntry = {...mockNode1}; - nonExistingEntry.id = 'non-existing-entry-id'; - const preselectedNodes = [{ entry: nonExistingEntry }, { entry: mockNode1 }, { entry: mockNode2 }]; - adapter.loadPage(mockNodePagingWithPreselectedNodes, null, null, preselectedNodes); - - expect(adapter.getPreselectedRows().length).toBe(2); + expect(adapter.getRowByNodeId('fake-node-id-1')).toEqual(fakeShareDataRows[0]); + expect(adapter.getRowByNodeId('fake-node-id-2')).toEqual(fakeShareDataRows[1]); }); }); }); diff --git a/lib/content-services/src/lib/document-list/data/share-datatable-adapter.ts b/lib/content-services/src/lib/document-list/data/share-datatable-adapter.ts index 953b3cfb47..9c6e880b9a 100644 --- a/lib/content-services/src/lib/document-list/data/share-datatable-adapter.ts +++ b/lib/content-services/src/lib/document-list/data/share-datatable-adapter.ts @@ -45,7 +45,6 @@ export class ShareDataTableAdapter implements DataTableAdapter { permissionsStyle: PermissionStyleModel[]; selectedRow: DataRow; allowDropFiles: boolean; - preselectedRows: DataRow[] = []; set sortingMode(value: string) { let newValue = (value || 'client').toLowerCase(); @@ -82,10 +81,6 @@ export class ShareDataTableAdapter implements DataTableAdapter { this.sort(); } - getPreselectedRows(): Array { - return this.preselectedRows; - } - getColumns(): Array { return this.columns; } @@ -250,7 +245,7 @@ export class ShareDataTableAdapter implements DataTableAdapter { } } - public loadPage(nodePaging: NodePaging, merge: boolean = false, allowDropFiles?: boolean, preselectNodes: NodeEntry[] = []) { + public loadPage(nodePaging: NodePaging, merge: boolean = false, allowDropFiles?: boolean) { let shareDataRows: ShareDataRow[] = []; if (allowDropFiles !== undefined) { this.allowDropFiles = allowDropFiles; @@ -258,8 +253,7 @@ export class ShareDataTableAdapter implements DataTableAdapter { if (nodePaging?.list) { const nodeEntries: NodeEntry[] = nodePaging.list.entries; if (nodeEntries?.length) { - shareDataRows = nodeEntries.map((item) => new ShareDataRow(item, this.contentService, this.permissionsStyle, - this.thumbnailService, this.allowDropFiles)); + shareDataRows = nodeEntries.map((item) => new ShareDataRow(item, this.contentService, this.permissionsStyle, this.thumbnailService, this.allowDropFiles)); if (this.filter) { shareDataRows = shareDataRows.filter(this.filter); @@ -297,27 +291,13 @@ export class ShareDataTableAdapter implements DataTableAdapter { } else { this.rows = shareDataRows; } - - this.selectRowsBasedOnGivenNodes(preselectNodes); } - selectRowsBasedOnGivenNodes(preselectNodes: NodeEntry[]) { - if (preselectNodes?.length) { - this.rows = this.rows.map((row) => { - preselectNodes.map((preselectedNode) => { - if (row.obj.entry.id === preselectedNode.entry.id) { - row.isSelected = true; - } - }); - return row; - }); - } - - this.preselectedRows = [...this.rows.filter((res) => res.isSelected)]; + getSelectedRows(): DataRow[] { + return this.rows.filter((row: DataRow) => row.isSelected); } - hasPreselectedRows(): boolean { - return this.preselectedRows?.length > 0; + getRowByNodeId(nodeId: string): DataRow { + return this.rows.find((row: DataRow) => row.node.entry.id === nodeId); } - } diff --git a/lib/content-services/src/lib/mock/document-library.model.mock.ts b/lib/content-services/src/lib/mock/document-library.model.mock.ts index 58d3442c62..2b489388d0 100644 --- a/lib/content-services/src/lib/mock/document-library.model.mock.ts +++ b/lib/content-services/src/lib/mock/document-library.model.mock.ts @@ -33,10 +33,10 @@ export class PageNode extends NodePaging { } export class FileNode extends NodeMinimalEntry { - constructor(name?: string, mimeType?: string) { + constructor(name?: string, mimeType?: string, id?: string) { super(); this.entry = new NodeMinimal(); - this.entry.id = 'file-id'; + this.entry.id = id || 'file-id'; this.entry.isFile = true; this.entry.isFolder = false; this.entry.name = name; diff --git a/lib/content-services/src/lib/mock/document-list.component.mock.ts b/lib/content-services/src/lib/mock/document-list.component.mock.ts index 2f41038841..5c79b52999 100644 --- a/lib/content-services/src/lib/mock/document-list.component.mock.ts +++ b/lib/content-services/src/lib/mock/document-list.component.mock.ts @@ -308,7 +308,7 @@ export const mockPreselectedNodes: NodeEntry[] = [ entry: mockNode1 }, { - entry: mockNode1 + entry: mockNode2 } ]; diff --git a/lib/core/datatable/components/datatable/datatable.component.spec.ts b/lib/core/datatable/components/datatable/datatable.component.spec.ts index 78bb1c4e59..0b3eea12ab 100644 --- a/lib/core/datatable/components/datatable/datatable.component.spec.ts +++ b/lib/core/datatable/components/datatable/datatable.component.spec.ts @@ -40,6 +40,7 @@ class CustomColumnTemplateComponent { class FakeDataRow implements DataRow { isDropTarget = false; isSelected = true; + id?: string; hasValue() { return true; @@ -593,6 +594,45 @@ describe('DataTable', () => { }); }); + it('should unselect the row searching it by row id, when row id is defined', () => { + const findSelectionByIdSpy = spyOn(dataTable, 'findSelectionById'); + dataTable.data = new ObjectDataTableAdapter([], + [new ObjectDataColumn({ key: 'name' })] + ); + + const fakeDataRows = [new FakeDataRow(), new FakeDataRow()]; + fakeDataRows[0].id = 'fakeRowId'; + fakeDataRows[1].id = 'fakeRowId2'; + + dataTable.data.setRows(fakeDataRows); + dataTable.selection = [...fakeDataRows]; + const indexOfSpy = spyOn(dataTable.selection, 'indexOf'); + + dataTable.selectRow(fakeDataRows[0], false); + + expect(indexOfSpy).not.toHaveBeenCalled(); + expect(findSelectionByIdSpy).toHaveBeenCalledWith(fakeDataRows[0].id); + }); + + it('should unselect the row by searching for the exact same reference of it (indexOf), when row id is not defined ', () => { + const findSelectionByIdSpy = spyOn(dataTable, 'findSelectionById'); + dataTable.data = new ObjectDataTableAdapter([], + [new ObjectDataColumn({ key: 'name' })] + ); + + const fakeDataRows = [new FakeDataRow(), new FakeDataRow()]; + dataTable.data.setRows(fakeDataRows); + dataTable.selection = [...fakeDataRows]; + const indexOfSpy = spyOn(dataTable.selection, 'indexOf').and.returnValue(0); + + dataTable.selectRow(fakeDataRows[0], false); + + expect(indexOfSpy).toHaveBeenCalled(); + expect(findSelectionByIdSpy).not.toHaveBeenCalled(); + expect(dataTable.selection.length).toEqual(1); + expect(dataTable.selection[0]).toEqual(fakeDataRows[1]); + }); + it('should select multiple rows with [multiple] selection mode and modifier key', (done) => { dataTable.selectionMode = 'multiple'; dataTable.data = new ObjectDataTableAdapter( diff --git a/lib/core/datatable/components/datatable/datatable.component.ts b/lib/core/datatable/components/datatable/datatable.component.ts index 2c187e9337..eed4c4c313 100644 --- a/lib/core/datatable/components/datatable/datatable.component.ts +++ b/lib/core/datatable/components/datatable/datatable.component.ts @@ -710,7 +710,7 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck, selectRow(row: DataRow, value: boolean) { if (row) { row.isSelected = value; - const idx = this.selection.indexOf(row); + const idx = row?.id ? this.findSelectionById(row.id) : this.selection.indexOf(row); if (value) { if (idx < 0) { this.selection.push(row); @@ -723,6 +723,10 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck, } } + findSelectionById(id: string): number { + return this.selection.findIndex(selection => selection?.id === id); + } + getCellTooltip(row: DataRow, col: DataColumn): string { if (row && col && col.formatTooltip) { const result: string = col.formatTooltip(row, col); diff --git a/lib/core/datatable/data/data-row.model.ts b/lib/core/datatable/data/data-row.model.ts index 59a64e76be..b25d73bbe3 100644 --- a/lib/core/datatable/data/data-row.model.ts +++ b/lib/core/datatable/data/data-row.model.ts @@ -21,6 +21,7 @@ export interface DataRow { isSelected: boolean; isDropTarget?: boolean; cssClass?: string; + id?: string; hasValue(key: string): boolean;