Skip to content

Commit

Permalink
feat(tree): add Collapse/Expand All comands in context menu
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Apr 5, 2020
1 parent 43735a6 commit 0b58d5e
Show file tree
Hide file tree
Showing 16 changed files with 305 additions and 97 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ npm run test:watch
- [ ] Dynamically Add Columns
- [ ] Grid Presets
- [ ] Pagination
- [ ] Tree View
- [ ] Tree Data
- [x] add Grid Demo
- [x] add Collapse/Expand All into Context Menu
- [ ] Search Filter on any Column
- [ ] Sorting from any Column

Expand Down
101 changes: 97 additions & 4 deletions packages/common/src/extensions/__tests__/contextMenuExtension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { ContextMenuExtension } from '../contextMenuExtension';
import { ExtensionUtility } from '../extensionUtility';
import { Formatters } from '../../formatters';
import { SharedService } from '../../services/shared.service';
import { DelimiterType, FileType, } from '../../enums/index';
import { Column, GridOption, MenuCommandItem } from '../../interfaces/index';
import { TranslateServiceStub } from '../../../../../test/translateServiceStub';
// import { ExcelExportService, ExportService } from '../../services';
Expand All @@ -23,15 +22,18 @@ const dataViewStub = {
collapseAllGroups: jest.fn(),
expandAllGroups: jest.fn(),
refresh: jest.fn(),
getItems: jest.fn(),
getGrouping: jest.fn(),
setGrouping: jest.fn(),
setItems: jest.fn(),
};

const gridStub = {
autosizeColumns: jest.fn(),
getColumnIndex: jest.fn(),
getColumns: jest.fn(),
getOptions: jest.fn(),
invalidate: jest.fn(),
registerPlugin: jest.fn(),
setColumns: jest.fn(),
setActiveCell: jest.fn(),
Expand Down Expand Up @@ -385,7 +387,7 @@ describe('contextMenuExtension', () => {
]);
});

it('should not have the "clear-grouping" menu command when "enableGrouping" and "hideClearAllGrouping" are set', () => {
it('should NOT have the "clear-grouping" menu command when "enableGrouping" and "hideClearAllGrouping" are set', () => {
const copyGridOptionsMock = { ...gridOptionsMock, enableGrouping: true, contextMenu: { hideCopyCellValueCommand: true, hideCollapseAllGroups: true, hideExpandAllGroups: true, hideClearAllGrouping: true } } as GridOption;
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
extension.register();
Expand All @@ -402,7 +404,17 @@ describe('contextMenuExtension', () => {
]);
});

it('should not have the "collapse-all-groups" menu command when "enableGrouping" and "hideClearAllGrouping" are set', () => {
it('should have the "collapse-all-groups" menu command when "enableTreeData" is set', () => {
const copyGridOptionsMock = { ...gridOptionsMock, enableTreeData: true, contextMenu: { hideCopyCellValueCommand: true, hideClearAllGrouping: true, hideCollapseAllGroups: false, hideExpandAllGroups: true } } as GridOption;
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
extension.register();
expect(SharedService.prototype.gridOptions.contextMenu.commandItems).toEqual([
{ divider: true, command: '', positionOrder: 54 },
{ action: expect.anything(), iconCssClass: 'fa fa-compress', title: 'Réduire tous les groupes', disabled: false, command: 'collapse-all-groups', positionOrder: 56, itemUsabilityOverride: expect.anything() }
]);
});

it('should NOT have the "collapse-all-groups" menu command when "enableGrouping" and "hideClearAllGrouping" are set', () => {
const copyGridOptionsMock = { ...gridOptionsMock, enableGrouping: true, contextMenu: { hideCopyCellValueCommand: true, hideClearAllGrouping: true, hideCollapseAllGroups: true, hideExpandAllGroups: true } } as GridOption;
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
extension.register();
Expand All @@ -419,13 +431,34 @@ describe('contextMenuExtension', () => {
]);
});

it('should not have the "expand-all-groups" menu command when "enableGrouping" and "hideClearAllGrouping" are set', () => {
it('should have the "expand-all-groups" menu command when "enableTreeData" is set', () => {
const copyGridOptionsMock = { ...gridOptionsMock, enableTreeData: true, contextMenu: { hideCopyCellValueCommand: true, hideClearAllGrouping: true, hideCollapseAllGroups: true, hideExpandAllGroups: false } } as GridOption;
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
extension.register();
expect(SharedService.prototype.gridOptions.contextMenu.commandItems).toEqual([
{ divider: true, command: '', positionOrder: 54 },
{ action: expect.anything(), iconCssClass: 'fa fa-expand', title: 'Étendre tous les groupes', disabled: false, command: 'expand-all-groups', positionOrder: 57, itemUsabilityOverride: expect.anything() }
]);
});

it('should NOT have the "expand-all-groups" menu command when "enableGrouping" and "hideClearAllGrouping" are set', () => {
const copyGridOptionsMock = { ...gridOptionsMock, enableGrouping: true, contextMenu: { hideCopyCellValueCommand: true, hideClearAllGrouping: true, hideCollapseAllGroups: true, hideExpandAllGroups: true } } as GridOption;
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
extension.register();
expect(SharedService.prototype.gridOptions.contextMenu.commandItems).toEqual([{ divider: true, command: '', positionOrder: 54 }]);
});

it('should have 2 Grouping commands (collapse, expand) when enableTreeData is set and none of the hidden flags are set', () => {
const copyGridOptionsMock = { ...gridOptionsMock, enableTreeData: true, contextMenu: { hideCopyCellValueCommand: true } } as GridOption;
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
extension.register();
expect(SharedService.prototype.gridOptions.contextMenu.commandItems).toEqual([
{ divider: true, command: '', positionOrder: 54 },
{ action: expect.anything(), iconCssClass: 'fa fa-compress', title: 'Réduire tous les groupes', disabled: false, command: 'collapse-all-groups', positionOrder: 56, itemUsabilityOverride: expect.anything() },
{ action: expect.anything(), iconCssClass: 'fa fa-expand', title: 'Étendre tous les groupes', disabled: false, command: 'expand-all-groups', positionOrder: 57, itemUsabilityOverride: expect.anything() }
]);
});

it('should have all 3 Grouping commands (clear, collapse, expand) when grouping is enabled and none of the hidden flags are set', () => {
const copyGridOptionsMock = { ...gridOptionsMock, enableGrouping: true, contextMenu: { hideCopyCellValueCommand: true } } as GridOption;
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
Expand Down Expand Up @@ -727,6 +760,22 @@ describe('contextMenuExtension', () => {
expect(dataviewSpy).toHaveBeenCalledWith();
});

it('should call "collapseAllGroups" from the DataView when Tree Data is enabled and the command triggered is "collapse-all-groups"', () => {
jest.spyOn(SharedService.prototype.dataView, 'getItems').mockReturnValueOnce(columnsMock);
const dataSetItemsSpy = jest.spyOn(SharedService.prototype.dataView, 'setItems');
const copyGridOptionsMock = { ...gridOptionsMock, enableTreeData: true, contextMenu: { hideCollapseAllGroups: false } } as GridOption;
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
extension.register();

const menuItemCommand = copyGridOptionsMock.contextMenu.commandItems.find((item: MenuCommandItem) => item.command === 'collapse-all-groups') as MenuCommandItem;
menuItemCommand.action(new CustomEvent('change'), { command: 'collapse-all-groups', cell: 0, row: 0 } as any);

expect(dataSetItemsSpy).toHaveBeenCalledWith([
{ __collapsed: true, field: 'field1', id: 'field1', nameKey: 'TITLE', width: 100 },
{ __collapsed: true, field: 'field2', id: 'field2', width: 75 }
]);
});

it('should call "expandAllGroups" from the DataView when Grouping is enabled and the command triggered is "expand-all-groups"', () => {
const dataviewSpy = jest.spyOn(SharedService.prototype.dataView, 'expandAllGroups');
const copyGridOptionsMock = { ...gridOptionsMock, enableGrouping: true, contextMenu: { hideExpandAllGroups: false } } as GridOption;
Expand All @@ -739,6 +788,22 @@ describe('contextMenuExtension', () => {
expect(dataviewSpy).toHaveBeenCalledWith();
});

it('should call "expandAllGroups" from the DataView when Tree Data is enabled and the command triggered is "expand-all-groups"', () => {
jest.spyOn(SharedService.prototype.dataView, 'getItems').mockReturnValueOnce(columnsMock);
const dataSetItemsSpy = jest.spyOn(SharedService.prototype.dataView, 'setItems');
const copyGridOptionsMock = { ...gridOptionsMock, enableTreeData: true, contextMenu: { hideExpandAllGroups: false } } as GridOption;
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
extension.register();

const menuItemCommand = copyGridOptionsMock.contextMenu.commandItems.find((item: MenuCommandItem) => item.command === 'expand-all-groups') as MenuCommandItem;
menuItemCommand.action(new CustomEvent('change'), { command: 'expand-all-groups', cell: 0, row: 0 } as any);

expect(dataSetItemsSpy).toHaveBeenCalledWith([
{ __collapsed: false, field: 'field1', id: 'field1', nameKey: 'TITLE', width: 100 },
{ __collapsed: false, field: 'field2', id: 'field2', width: 75 }
]);
});

it('should expect "itemUsabilityOverride" callback on all the Grouping command to return False when there are NO Groups in the grid', () => {
const copyGridOptionsMock = { ...gridOptionsMock, enableGrouping: true, contextMenu: { hideClearAllGrouping: false } } as GridOption;
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
Expand Down Expand Up @@ -776,6 +841,34 @@ describe('contextMenuExtension', () => {
expect(isExpandCommandUsable).toBe(true);
expect(dataviewSpy).toHaveBeenCalled();
});

it('should expect "itemUsabilityOverride" callback on all the Tree Data Grouping command to return Tree (collapse, expand) at all time even when there are NO Groups in the grid', () => {
const copyGridOptionsMock = { ...gridOptionsMock, enableTreeData: true, contextMenu: { hideClearAllGrouping: false } } as GridOption;
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
extension.register();

const menuCollapseCommand = copyGridOptionsMock.contextMenu.commandItems.find((item: MenuCommandItem) => item.command === 'collapse-all-groups') as MenuCommandItem;
const isCollapseCommandUsable = menuCollapseCommand.itemUsabilityOverride({ cell: 2, row: 2, grid: gridStub, } as any);
const menuExpandCommand = copyGridOptionsMock.contextMenu.commandItems.find((item: MenuCommandItem) => item.command === 'expand-all-groups') as MenuCommandItem;
const isExpandCommandUsable = menuExpandCommand.itemUsabilityOverride({ cell: 2, row: 2, grid: gridStub, } as any);

expect(isCollapseCommandUsable).toBe(true);
expect(isExpandCommandUsable).toBe(true);
});

it('should expect "itemUsabilityOverride" callback on all the Tree Data Grouping command to return True (collapse, expand) when there are Groups defined in the grid', () => {
const copyGridOptionsMock = { ...gridOptionsMock, enableTreeData: true, contextMenu: { hideClearAllGrouping: false } } as GridOption;
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
extension.register();

const menuCollapseCommand = copyGridOptionsMock.contextMenu.commandItems.find((item: MenuCommandItem) => item.command === 'collapse-all-groups') as MenuCommandItem;
const isCollapseCommandUsable = menuCollapseCommand.itemUsabilityOverride({ cell: 2, row: 2, grid: gridStub, } as any);
const menuExpandCommand = copyGridOptionsMock.contextMenu.commandItems.find((item: MenuCommandItem) => item.command === 'expand-all-groups') as MenuCommandItem;
const isExpandCommandUsable = menuExpandCommand.itemUsabilityOverride({ cell: 2, row: 2, grid: gridStub, } as any);

expect(isCollapseCommandUsable).toBe(true);
expect(isExpandCommandUsable).toBe(true);
});
});

describe('translateContextMenu method', () => {
Expand Down
46 changes: 38 additions & 8 deletions packages/common/src/extensions/contextMenuExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,6 @@ export class ContextMenuExtension implements Extension {

/**
* Create the Action Cell Menu and expose all the available hooks that user can subscribe (onCommand, onBeforeMenuShow, ...)
* @param grid
* @param dataView
* @param columnDefinitions
*/
register(): any {
if (this.sharedService.gridOptions && this.sharedService.gridOptions.enableTranslate && (!this.translaterService || !this.translaterService.translate)) {
Expand Down Expand Up @@ -180,6 +177,7 @@ export class ContextMenuExtension implements Extension {
const gridOptions = this.sharedService && this.sharedService.gridOptions || {};
const contextMenu = gridOptions && gridOptions.contextMenu;
const dataView = this.sharedService && this.sharedService.dataView;
const grid = this.sharedService && this.sharedService.grid;

// show context menu: Copy (cell value)
if (contextMenu && !contextMenu.hideCopyCellValueCommand) {
Expand Down Expand Up @@ -274,12 +272,18 @@ export class ContextMenuExtension implements Extension {
}

// -- Grouping Commands
if (gridOptions && (gridOptions.enableGrouping || gridOptions.enableDraggableGrouping)) {
if (gridOptions && (gridOptions.enableGrouping || gridOptions.enableDraggableGrouping || gridOptions.enableTreeData)) {
const columnDefinitions = this.sharedService.columnDefinitions || [];
let columnWithTreeData: Column;
if (gridOptions && gridOptions.enableTreeData && columnDefinitions) {
columnWithTreeData = columnDefinitions.find((col: Column) => col && col.treeData);
}

// add a divider (separator) between the top sort commands and the other clear commands
menuCustomItems.push({ divider: true, command: '', positionOrder: 54 });

// show context menu: Clear Grouping
if (gridOptions && contextMenu && !contextMenu.hideClearAllGrouping) {
// show context menu: Clear Grouping (except for Tree Data which shouldn't have this feature)
if (gridOptions && !gridOptions.enableTreeData && contextMenu && !contextMenu.hideClearAllGrouping) {
const commandName = 'clear-grouping';
if (!originalCustomItems.find((item: MenuCommandItem) => item.hasOwnProperty('command') && item.command === commandName)) {
menuCustomItems.push(
Expand Down Expand Up @@ -311,8 +315,21 @@ export class ContextMenuExtension implements Extension {
disabled: false,
command: commandName,
positionOrder: 56,
action: () => dataView.collapseAllGroups(),
action: () => {
if (gridOptions.enableTreeData) {
const dataset: any[] = dataView.getItems() || [];
const collapsedPropName = columnWithTreeData?.treeData?.collapsedPropName || '__collapsed';
dataset.forEach((item: any) => item[collapsedPropName] = true);
dataView.setItems(dataset);
grid.invalidate();
} else {
dataView.collapseAllGroups();
}
},
itemUsabilityOverride: () => {
if (gridOptions.enableTreeData) {
return true;
}
// only enable the command when there's an actually grouping in play
const groupingArray = dataView && dataView.getGrouping && dataView.getGrouping();
return Array.isArray(groupingArray) && groupingArray.length > 0;
Expand All @@ -333,8 +350,21 @@ export class ContextMenuExtension implements Extension {
disabled: false,
command: commandName,
positionOrder: 57,
action: () => dataView.expandAllGroups(),
action: () => {
if (gridOptions.enableTreeData) {
const dataset: any[] = dataView.getItems() || [];
const collapsedPropName = columnWithTreeData?.treeData?.collapsedPropName || '__collapsed';
dataset.forEach((item: any) => item[collapsedPropName] = false);
dataView.setItems(dataset);
grid.invalidate();
} else {
dataView.expandAllGroups();
}
},
itemUsabilityOverride: () => {
if (gridOptions.enableTreeData) {
return true;
}
// only enable the command when there's an actually grouping in play
const groupingArray = dataView && dataView.getGrouping && dataView.getGrouping();
return Array.isArray(groupingArray) && groupingArray.length > 0;
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/formatters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export const Formatters = {
/** Takes a boolean value, cast it to upperCase string and finally translates it (i18n). */
translateBoolean: translateBooleanFormatter,

/** Formatter that must be use with a Tree View column */
/** Formatter that must be use with a Tree Data column */
tree: treeFormatter,

/** Takes a value and displays it all uppercase */
Expand Down
11 changes: 6 additions & 5 deletions packages/common/src/formatters/treeFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { Column, Formatter } from './../interfaces/index';

export const treeFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any, grid: any) => {
const treeLevelPropName = columnDef.treeView?.levelPropName || '__treeLevel';
const indentMarginLeft = columnDef.treeView?.indentMarginLeft || 15;
const dataView = grid.getData();
const treeLevelPropName = columnDef.treeData?.levelPropName || '__treeLevel';
const indentMarginLeft = columnDef.treeData?.indentMarginLeft || 15;
const dataView = grid && grid.getData();

if (!dataContext.hasOwnProperty(treeLevelPropName)) {
throw new Error('You must provide a valid Tree View column, it seems that there are no tree level found in this row');
throw new Error('You must provide a valid Tree Data column, it seems that there are no tree level found in this row');
}

if (dataView && dataView.getIdxById && dataView.getItemByIdx) {
if (value === null || value === undefined || dataContext === undefined) { return ''; }
value = value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const identifierPropName = dataView.getIdPropertyName() || 'id';
const spacer = `<span style="display:inline-block;height:1px;width:${indentMarginLeft * dataContext[treeLevelPropName]}px"></span>`;
const spacer = `<span style="display:inline-block; width:${indentMarginLeft * dataContext[treeLevelPropName]}px;"></span>`;
const idx = dataView.getIdxById(dataContext[identifierPropName]);
const nextItemRow = dataView.getItemByIdx(idx + 1);

Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/interfaces/column.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,8 @@ export interface Column {
/** Custom Tooltip that can ben shown to the column */
toolTip?: string;

/** Tree View options */
treeView?: {
/** Tree Data options */
treeData?: {
childrenPropName?: string;
collapsedPropName?: string;
identifierPropName?: string;
Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/interfaces/gridOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@ export interface GridOption {
/** Do we want to enable localization translation (i18n)? */
enableTranslate?: boolean;

/** Do we want to enable Tree View grid? */
enableTreeView?: boolean;
/** Do we want to enable Tree Data grid? */
enableTreeData?: boolean;

/**
* Event naming style for the exposed SlickGrid & Component Events
Expand Down
Loading

0 comments on commit 0b58d5e

Please sign in to comment.