[ACS-5566] - Add configurable columns to document list (#8968)

* [ACS-5566] - add configurable columns to document list

* [ACS-5566] - add max visible columns

* [ACS-5566] - add unit tests

* [ACS-5566] - add docs

* [ACS-5566] - style changes after CR

* [ACS-5566] - changes for pipeline

* [ACS-5566] - changes for pipeline

* [ACS-5566] - changes for pipeline
This commit is contained in:
DominikIwanek
2023-10-24 15:52:11 +02:00
committed by GitHub
parent 3912a652d5
commit 36c6e6d8ea
9 changed files with 113 additions and 108 deletions

View File

@@ -8,6 +8,7 @@
[contextMenu]="contextMenuActions"
[rowStyle]="rowStyle"
[rowStyleClass]="rowStyleClass"
[showMainDatatableActions]="true"
[loading]="loading"
[display]="display"
[noPermission]="noPermission"
@@ -72,4 +73,16 @@
</ng-template>
</adf-loading-content-template>
<adf-main-menu-datatable-template>
<ng-template let-mainMenuTrigger>
<adf-datatable-column-selector
[columns]="data.getColumns()"
[mainMenuTrigger]="mainMenuTrigger"
[columnsSorting]="false"
[maxColumnsVisible]="maxColumnsVisible"
(submitColumnsVisibility)="onColumnsVisibilityChange($event)">
</adf-datatable-column-selector>
</ng-template>
</adf-main-menu-datatable-template>
</adf-datatable>

View File

@@ -132,57 +132,39 @@ describe('DocumentList', () => {
});
};
it('should load -trashcan- preset', async () => {
documentList.currentFolderId = '-trashcan-';
fixture.detectChanges();
await fixture.whenStable();
it('should load -trashcan- preset', () => {
documentList.presetColumn = '-trashcan-';
documentList.ngAfterContentInit();
validatePreset(['$thumbnail', 'name', 'path', 'content.sizeInBytes', 'archivedAt', 'archivedByUser.displayName']);
});
it('should load -sites- preset', async () => {
documentList.currentFolderId = '-sites-';
fixture.detectChanges();
await fixture.whenStable();
documentList.presetColumn = '-sites-';
documentList.ngAfterContentInit();
validatePreset(['$thumbnail', 'title', 'visibility']);
});
it('shuld load -mysites- preset', async () => {
documentList.currentFolderId = '-mysites-';
fixture.detectChanges();
await fixture.whenStable();
documentList.presetColumn = '-mysites-';
documentList.ngAfterContentInit();
validatePreset(['$thumbnail', 'title', 'visibility']);
});
it('should load -favorites- preset', async () => {
documentList.currentFolderId = '-favorites-';
fixture.detectChanges();
await fixture.whenStable();
documentList.presetColumn = '-favorites-';
documentList.ngAfterContentInit();
validatePreset(['$thumbnail', 'name', 'path', 'content.sizeInBytes', 'modifiedAt', 'modifiedByUser.displayName']);
});
it('should load -recent- preset', async () => {
documentList.currentFolderId = '-recent-';
fixture.detectChanges();
await fixture.whenStable();
documentList.presetColumn = '-recent-';
documentList.ngAfterContentInit();
validatePreset(['$thumbnail', 'name', 'path', 'content.sizeInBytes', 'modifiedAt']);
});
it('should load -sharedlinks- preset', async () => {
documentList.currentFolderId = '-sharedlinks-';
fixture.detectChanges();
await fixture.whenStable();
documentList.presetColumn = '-sharedlinks-';
documentList.ngAfterContentInit();
validatePreset([
'$thumbnail',
'name',
@@ -195,11 +177,8 @@ describe('DocumentList', () => {
});
it('should load default preset', async () => {
documentList.currentFolderId = 'f5dacdb9-6d07-4fe9-9f2a-dedc21bae603';
fixture.detectChanges();
await fixture.whenStable();
documentList.presetColumn = 'f5dacdb9-6d07-4fe9-9f2a-dedc21bae603';
documentList.ngAfterContentInit();
validatePreset(['$thumbnail', 'name', 'content.sizeInBytes', 'modifiedAt', 'modifiedByUser.displayName']);
});
});

View File

@@ -38,13 +38,11 @@ import { ContentService } from '../../common/services/content.service';
import {
DataCellEvent,
DataColumn,
DataRowActionEvent,
DataSorting,
DataTableComponent,
DisplayMode,
ShowHeaderMode,
ObjectDataColumn,
PaginatedComponent,
AppConfigService,
DataColumnListComponent,
@@ -58,7 +56,9 @@ import {
AlfrescoApiService,
UserPreferenceValues,
DataRow,
DataTableService
DataTableService,
DataTableSchema,
DataColumn
} from '@alfresco/adf-core';
import { NodesApiService } from '../../common/services/nodes-api.service';
@@ -97,7 +97,7 @@ const BYTES_TO_MB_CONVERSION_VALUE = 1048576;
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-document-list' }
})
export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, AfterContentInit, PaginatedComponent, NavigableComponentInterface {
export class DocumentListComponent extends DataTableSchema implements OnInit, OnChanges, OnDestroy, AfterContentInit, PaginatedComponent, NavigableComponentInterface {
static SINGLE_CLICK_NAVIGATION: string = 'click';
static DOUBLE_CLICK_NAVIGATION: string = 'dblclick';
@@ -315,6 +315,14 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
@Input()
maxItems: number = this.DEFAULT_PAGINATION.maxItems;
/** Key of columns preset set in extension.json */
@Input()
columnsPresetKey?: string;
/** Limit of possible visible columns, including "$thumbnail" column if provided */
@Input()
maxColumnsVisible?: number;
/** Emitted when the user clicks a list node */
@Output()
nodeClick = new EventEmitter<NodeEntityEvent>();
@@ -371,7 +379,6 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
pagination: BehaviorSubject<PaginationModel> = new BehaviorSubject<PaginationModel>(this.DEFAULT_PAGINATION);
sortingSubject: BehaviorSubject<DataSorting[]> = new BehaviorSubject<DataSorting[]>(this.DEFAULT_SORTING);
private layoutPresets = {};
private rowMenuCache: { [key: string]: ContentActionModel[] } = {};
private loadingTimeout: any;
private onDestroy$ = new Subject<boolean>();
@@ -395,6 +402,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
private lockService: LockService,
private dialog: MatDialog
) {
super(appConfig, 'default', presetsDefaultModel);
this.nodeService.nodeUpdated.pipe(takeUntil(this.onDestroy$)).subscribe((node) => {
this.dataTableService.rowUpdate.next({ id: node.id, obj: { entry: node } });
});
@@ -421,10 +429,6 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
return null;
}
private get hasCustomLayout(): boolean {
return this.columnList?.columns?.length > 0;
}
private getDefaultSorting(): DataSorting {
let defaultSorting: DataSorting;
if (Array.isArray(this.sorting)) {
@@ -437,9 +441,6 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
return defaultSorting;
}
private getLayoutPreset(name: string = 'default'): DataColumn[] {
return (this.layoutPresets[name] || this.layoutPresets['default']).map((col) => new ObjectDataColumn(col));
}
isMobile(): boolean {
return !!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
@@ -477,34 +478,21 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
if (this.filterValue && Object.keys(this.filterValue).length > 0) {
this.showHeader = ShowHeaderMode.Always;
}
if (this.columnsPresetKey) {
this.setPresetKey(this.columnsPresetKey);
}
}
ngAfterContentInit() {
if (this.columnList) {
this.columnList.columns.changes.pipe(takeUntil(this.onDestroy$)).subscribe(() => this.setTableSchema());
this.columnList.columns.changes.pipe(takeUntil(this.onDestroy$)).subscribe(() => {
this.createColumns();
this.data.setColumns(this.columns);
});
}
this.setTableSchema();
this.createDatatableSchema();
this.data.setColumns(this.columns);
}
private setTableSchema() {
let schema: DataColumn[] = [];
if (this.hasCustomLayout) {
schema = this.columnList.columns.map((c) => c as DataColumn);
}
if (!this.data) {
this.data = new ShareDataTableAdapter(this.thumbnailService, this.contentService, schema, this.getDefaultSorting(), this.sortingMode);
} else if (schema && schema.length > 0) {
this.data.setColumns(schema);
}
const columns = this.data.getColumns();
if (!columns || columns.length === 0) {
this.setupDefaultColumns(this.currentFolderId);
}
}
ngOnChanges(changes: SimpleChanges) {
if (!changes['preselectNodes']) {
this.resetSelection();
@@ -718,10 +706,6 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
this.setLoadingState(true);
}
if (!this.hasCustomLayout) {
this.setupDefaultColumns(this.currentFolderId);
}
if (this.documentListService.isCustomSourceService(this.currentFolderId)) {
this.updateCustomSourceData(this.currentFolderId);
}
@@ -772,17 +756,6 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
return [`${this.additionalSorting.key} ${this.additionalSorting.direction}`, `${currentKey} ${currentDirection}`];
}
/**
* Creates a set of predefined columns.
* @param preset preset to use
*/
private setupDefaultColumns(preset: string = 'default'): void {
if (this.data) {
const columns = this.getLayoutPreset(preset);
this.data.setColumns(columns);
}
}
onPreviewFile(node: NodeEntry) {
if (node) {
const sizeInMB = node.entry?.content?.sizeInBytes / BYTES_TO_MB_CONVERSION_VALUE;
@@ -797,7 +770,9 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
}
}
}
onColumnsVisibilityChange(updatedColumns: Array<DataColumn>): void {
this.data.setColumns(updatedColumns);
}
onNodeClick(nodeEntry: NodeEntry) {
const domEvent = new CustomEvent('node-click', {
detail: {
@@ -932,10 +907,6 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
return canNavigateFolder;
}
private loadLayoutPresets(): void {
const externalSettings = this.appConfig.get('document-list.presets', null);
this.layoutPresets = externalSettings ? Object.assign({}, presetsDefaultModel, externalSettings) : presetsDefaultModel;
}
private onDataReady(nodePaging: NodePaging) {
this.ready.emit(nodePaging);

View File

@@ -43,6 +43,7 @@
class="adf-columns-selector-column-checkbox"
[attr.data-automation-id]="'adf-columns-selector-column-checkbox-' + column.title"
[checked]="!column.isHidden"
[disabled]="isCheckboxDisabled(column)"
(change)="changeColumnVisibility(column)">
<div class="adf-columns-selector-list-content">{{translatedTitle}}</div>
</mat-checkbox>

View File

@@ -156,19 +156,35 @@ describe('ColumnsSelectorComponent', () => {
expect(toggledColumnItem.isHidden).toBe(true);
});
it('should set proper default state for checkboxes', async () => {
menuOpenedTrigger.next();
fixture.detectChanges();
const checkboxes = await loader.getAllHarnesses(MatCheckboxHarness);
describe('checkboxes', () => {
it('should have set proper default state', async () => {
menuOpenedTrigger.next();
fixture.detectChanges();
expect(await checkboxes[0].isChecked()).toBe(true);
expect(await checkboxes[1].isChecked()).toBe(true);
expect(await checkboxes[2].isChecked()).toBe(true);
expect(await checkboxes[3].isChecked()).toBe(false);
const checkboxes = await loader.getAllHarnesses(MatCheckboxHarness);
expect(await checkboxes[0].isChecked()).toBe(true);
expect(await checkboxes[1].isChecked()).toBe(true);
expect(await checkboxes[2].isChecked()).toBe(true);
expect(await checkboxes[3].isChecked()).toBe(false);
});
it('should be disabled when visible columns limit is reached', async () => {
component.maxColumnsVisible = 4;
menuOpenedTrigger.next();
fixture.detectChanges();
const checkboxes = await loader.getAllHarnesses(MatCheckboxHarness);
expect(await checkboxes[0].isDisabled()).toBe(false);
expect(await checkboxes[1].isDisabled()).toBe(false);
expect(await checkboxes[2].isDisabled()).toBe(false);
expect(await checkboxes[3].isDisabled()).toBe(true);
});
});
it('should show hidden columns at the end of the list', async () => {
describe('sorting', () => {
const hiddenDataColumn: DataColumn = {
id: 'hiddenDataColumn',
title: 'hiddenDataColumn',
@@ -183,14 +199,27 @@ describe('ColumnsSelectorComponent', () => {
key: 'shownDataColumn',
type: 'text'
};
it('should show hidden columns at the end of the list by default', async () => {
component.columns = [hiddenDataColumn, shownDataColumn];
menuOpenedTrigger.next();
fixture.detectChanges();
component.columns = [hiddenDataColumn, shownDataColumn];
menuOpenedTrigger.next();
fixture.detectChanges();
const checkboxes = await loader.getAllHarnesses(MatCheckboxHarness);
const checkboxes = await loader.getAllHarnesses(MatCheckboxHarness);
expect(await checkboxes[0].getLabelText()).toBe(shownDataColumn.title);
expect(await checkboxes[1].getLabelText()).toBe(hiddenDataColumn.title);
});
expect(await checkboxes[0].getLabelText()).toBe(shownDataColumn.title);
expect(await checkboxes[1].getLabelText()).toBe(hiddenDataColumn.title);
it('should NOT show hidden columns at the end of the list if sorting is disabled', async () => {
component.columns = [hiddenDataColumn, shownDataColumn];
component.columnsSorting = false;
menuOpenedTrigger.next();
fixture.detectChanges();
const checkboxes = await loader.getAllHarnesses(MatCheckboxHarness);
expect(await checkboxes[0].getLabelText()).toBe(hiddenDataColumn.title);
expect(await checkboxes[1].getLabelText()).toBe(shownDataColumn.title);
});
});
});

View File

@@ -34,6 +34,12 @@ export class ColumnsSelectorComponent implements OnInit, OnDestroy {
@Input()
mainMenuTrigger: MatMenuTrigger;
@Input()
columnsSorting = true;
@Input()
maxColumnsVisible?: number;
@Output()
submitColumnsVisibility = new EventEmitter<DataColumn[]>();
@@ -47,7 +53,7 @@ export class ColumnsSelectorComponent implements OnInit, OnDestroy {
takeUntil(this.onDestroy$)
).subscribe(() => {
const columns = this.columns.map(column => ({...column}));
this.columnItems = this.sortColumns(columns);
this.columnItems = this.columnsSorting ? this.sortColumns(columns) : columns;
});
this.mainMenuTrigger.menuClosed.pipe(
@@ -82,6 +88,10 @@ export class ColumnsSelectorComponent implements OnInit, OnDestroy {
this.closeMenu();
}
isCheckboxDisabled(column: DataColumn): boolean {
return this.maxColumnsVisible && column.isHidden && this.maxColumnsVisible === this.columnItems.filter(dataColumn => !dataColumn.isHidden).length;
}
private sortColumns(columns: DataColumn[]): DataColumn[] {
const shownColumns = columns.filter(column => !column.isHidden);
const hiddenColumns = columns.filter(column => column.isHidden);

View File

@@ -49,7 +49,6 @@ export abstract class DataTableSchema<T = unknown> {
public createDatatableSchema(): void {
this.loadLayoutPresets();
if (!this.columns || this.columns.length === 0) {
this.createColumns();
this.columnsSchemaSubject$.next(true);
@@ -98,7 +97,7 @@ export abstract class DataTableSchema<T = unknown> {
}
public getSchemaFromConfig(presetColumn: string): DataColumn[] {
return presetColumn ? this.layoutPresets[presetColumn].map((col) => new ObjectDataColumn(col)) : [];
return presetColumn && this.layoutPresets[presetColumn] ? this.layoutPresets[presetColumn].map((col) => new ObjectDataColumn(col)) : [];
}
private getDefaultLayoutPreset(): DataColumn[] {

View File

@@ -44,6 +44,7 @@ export interface DocumentListPresetRef extends ExtensionElement {
template: string;
desktopOnly: boolean;
sortingKey: string;
isHidden?: boolean;
rules?: {
[key: string]: string;
visible?: string;