[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
This commit is contained in:
arditdomi
2021-04-08 14:55:06 +01:00
committed by GitHub
parent b08e2731bf
commit e589071328
12 changed files with 459 additions and 138 deletions

View File

@@ -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(<File> { name: 'fake-name', size: 100 }), new FileModel(<File> { 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(<File> { name: 'fake-name', size: 10000000 });
const fakeNodes = [<Node> { id: 'fakeNodeId' }, <Node> { 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([]);
});
});

View File

@@ -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;
}
}

View File

@@ -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<DataColumnComponent>();
@@ -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: <ShareDataRow[]> 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: <ShareDataRow[]> selectedRows });
});
});
});

View File

@@ -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<Node> = new Subject<Node>();
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<ShareDataRow> }) {
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: <ShareDataRow[]> 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: <ShareDataRow[]> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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]);
});
});
});

View File

@@ -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<DataRow> {
return this.preselectedRows;
}
getColumns(): Array<DataColumn> {
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);
}
}

View File

@@ -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;

View File

@@ -308,7 +308,7 @@ export const mockPreselectedNodes: NodeEntry[] = [
entry: mockNode1
},
{
entry: mockNode1
entry: mockNode2
}
];

View File

@@ -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(

View File

@@ -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);

View File

@@ -21,6 +21,7 @@ export interface DataRow {
isSelected: boolean;
isDropTarget?: boolean;
cssClass?: string;
id?: string;
hasValue(key: string): boolean;