diff --git a/packages/oui-datagrid/README.md b/packages/oui-datagrid/README.md index e22f1cf7..5e202d28 100644 --- a/packages/oui-datagrid/README.md +++ b/packages/oui-datagrid/README.md @@ -186,6 +186,43 @@ Using this attribute, a new column property `hidden` is available. All the properties of a column also become dynamic. +### Customizable columns + +```html:preview + + + + + + + {{$row.parents.mother.lastName}}, {{$row.parents.mother.firstName}} + + + {{$row.parents.father.lastName}}, {{$row.parents.father.firstName}} + + + {{$value}} + + + + + Birth: {{$row.birth}} + + + + + Phone: {{$row.phone}} + + +``` + ### Pagination By default the page size is 25. @@ -550,26 +587,51 @@ call `rows-loader` and then a `row-loader` call for each line. ### oui-datagrid -| Attribute | Type | Binding | One-time binding | Values | Default | Description | -| ---- | ---- | ---- | ---- | ---- | ---- | ---- | -| `id` | string | @? | | | | id of the datagrid | -| `page-size` | number | @? | | | 25 | maximum number of rows to show on each pages | -| `rows` | array | { const column = {}; + if (hasAttribute(columnElement, "name")) { + column.name = getAttribute(columnElement, "name"); + } + if (hasAttribute(columnElement, "property")) { const propertyValue = getAttribute(columnElement, "property"); - column.name = propertyValue; + column.name = column.name || propertyValue; column.getValue = this.$parse(propertyValue); // A column can be sorted only if it has a "property" attribute. @@ -57,6 +61,14 @@ export default class DatagridColumnBuilder { column.typeOptions = this.$parse(column["type-options"])($scope); } + if (hasAttribute(columnElement, "prevent-customization")) { + column.preventCustomization = true; + } + + if (hasAttribute(columnElement, "hidden")) { + column.hidden = true; + } + if (hasAttribute(columnElement, "title")) { const titleValue = getAttribute(columnElement, "title"); @@ -96,9 +108,11 @@ export default class DatagridColumnBuilder { angular.forEach(columnsDescription, columnDescription => { const column = {}; + column.name = columnDescription.name; + const propertyValue = columnDescription.property; if (propertyValue) { - column.name = propertyValue; + column.name = column.name || propertyValue; column.getValue = this.$parse(propertyValue); // A column can be sorted only if it has a "property" attribute. @@ -122,6 +136,8 @@ export default class DatagridColumnBuilder { column.typeOptions = this.$parse(column.typeOptions)($scope); } + column.preventCustomization = columnDescription.preventCustomization; + column.title = columnDescription.title; if (!column.sortProperty) { @@ -153,6 +169,7 @@ export default class DatagridColumnBuilder { template: actionColumnElement.outerHTML }; column.compiledTemplate = this._getColumnTemplate(column); + column.alwaysVisible = true; return column; } diff --git a/packages/oui-datagrid/src/datagrid.controller.js b/packages/oui-datagrid/src/datagrid.controller.js index a1fe7fb6..1037a922 100644 --- a/packages/oui-datagrid/src/datagrid.controller.js +++ b/packages/oui-datagrid/src/datagrid.controller.js @@ -1,3 +1,5 @@ +import { addBooleanParameter } from "@oui-angular/common/component-utils"; +import { find } from "lodash"; import { hasProperty } from "./util"; import template from "./datagrid.html"; @@ -14,10 +16,12 @@ const cssSortableDesc = "oui-datagrid__header_sortable-desc"; const checkScrollOnRefreshDataDelay = 1000; export default class DatagridController { - constructor ($compile, $element, $transclude, $q, $scope, $window, $timeout, ouiDatagridPaging, - ouiDatagridColumnBuilder, ouiDatagridConfiguration, ouiDatagridService) { + constructor ($attrs, $compile, $element, $transclude, $q, $scope, $window, $timeout, + ouiDatagridPaging, ouiDatagridColumnBuilder, ouiDatagridConfiguration, + ouiDatagridService) { "ngInject"; + this.$attrs = $attrs; this.$compile = $compile; this.$element = $element; this.$transclude = $transclude; @@ -71,6 +75,8 @@ export default class DatagridController { } $postLink () { + addBooleanParameter(this, "customizable"); + this.$compile(template)(this.$scope, (clone) => { this.$element.append(clone); }); @@ -97,7 +103,7 @@ export default class DatagridController { } // Manage responsiveness - if (this.hasActionMenu) { + if (this.hasActionMenu || this.customizable) { this.scrollablePanel = this.$element[0].querySelector(".oui-datagrid-responsive-container__overflow-container"); if (this.scrollablePanel) { angular.element(this.$window).on("resize", this.checkScroll); @@ -116,6 +122,10 @@ export default class DatagridController { if (changes.columnsDescription && !changes.columnsDescription.isFirstChange()) { this.buildColumns(); } + + if (changes.columnsParameters && !changes.columnsParameters.isFirstChange()) { + this.buildColumns(); + } } $doCheck () { @@ -146,16 +156,28 @@ export default class DatagridController { this.ouiDatagridColumnBuilder.build(this.columnElements, this.getParentScope()); if (this.actionColumnElements.length) { - builtColumns.columns.push(this.ouiDatagridColumnBuilder.buildActionColumn(this.actionColumnElements[0])); + this.actionColumn = this.ouiDatagridColumnBuilder.buildActionColumn(this.actionColumnElements[0]); this.hasActionMenu = true; } if (this.extraTopElements.length) { - this.hasExtraTopContent = true; this.extraTopCompiledTemplate = this.$compile(`
${this.extraTopElements[0].innerHTML}
`); + this.hasExtraTopContent = true; } - this.columns = builtColumns.columns.filter(column => !column.hidden); + this.availableColumns = angular.copy(builtColumns.columns) + .map(column => { // Override default with custom columns + const customColumn = find(this.columnsParameters, { + name: column.name + }); + if (customColumn) { + column.hidden = customColumn.hidden; + } + return column; + }); + + this.columns = this.availableColumns + .filter(column => !column.hidden); this.columns.forEach(column => { if (column.title) { @@ -176,6 +198,32 @@ export default class DatagridController { return builtColumns; } + onColumnsChange (columns) { + this.availableColumns = angular.copy(columns); + this.columns = columns.filter(column => !column.hidden); + + const columnsParameters = this.availableColumns + .filter(column => column.name) + .map(column => { + const cleanColumn = { + name: column.name + }; + + if (column.hidden) { + cleanColumn.hidden = true; + } + + return cleanColumn; + }); + + if (this.id) { + this.onColumnsParametersChange({ + id: this.id, + columns: columnsParameters + }); + } + } + getParentScope () { return this.$scope.$parent; } diff --git a/packages/oui-datagrid/src/datagrid.directive.js b/packages/oui-datagrid/src/datagrid.directive.js index baed5937..1e4f200b 100644 --- a/packages/oui-datagrid/src/datagrid.directive.js +++ b/packages/oui-datagrid/src/datagrid.directive.js @@ -10,10 +10,13 @@ export default () => { scope: { id: "@?", columnsDescription: " { // Transclude can't be used here otherwise transcluded diff --git a/packages/oui-datagrid/src/datagrid.html b/packages/oui-datagrid/src/datagrid.html index 21bcdb7f..b0bc1dc0 100644 --- a/packages/oui-datagrid/src/datagrid.html +++ b/packages/oui-datagrid/src/datagrid.html @@ -19,11 +19,11 @@
@@ -31,10 +31,16 @@ + ng-click="::$ctrl.sort(column)"> + + + + @@ -43,15 +49,22 @@ + data-title="{{column.title}}"> + column="column"> + + + + @@ -62,14 +75,14 @@ - diff --git a/packages/oui-datagrid/src/index.js b/packages/oui-datagrid/src/index.js index 09e0002d..1c54bf56 100644 --- a/packages/oui-datagrid/src/index.js +++ b/packages/oui-datagrid/src/index.js @@ -3,8 +3,9 @@ import Datagrid from "./datagrid.directive"; import DatagridColumnBuilder from "./datagrid-column-builder.service"; import DatagridExtraTop from "./extra-top/extra-top.component"; import DatagridPaging from "./paging/datagrid-paging.service"; -import DatagridProvider from "./datagrid.provider.js"; -import DatagridService from "./datagrid.service.js"; +import DatagridParameters from "./parameters/datagrid-parameters.component"; +import DatagridProvider from "./datagrid.provider"; +import DatagridService from "./datagrid.service"; angular .module("oui.datagrid", [ @@ -20,4 +21,5 @@ angular .component("ouiDatagridExtraTop", DatagridExtraTop) .service("ouiDatagridPaging", DatagridPaging) .provider("ouiDatagridConfiguration", DatagridProvider) - .service("ouiDatagridService", DatagridService); + .service("ouiDatagridService", DatagridService) + .component("ouiDatagridParameters", DatagridParameters); diff --git a/packages/oui-datagrid/src/index.spec.js b/packages/oui-datagrid/src/index.spec.js index 8535a0c2..9bc55f1d 100644 --- a/packages/oui-datagrid/src/index.spec.js +++ b/packages/oui-datagrid/src/index.spec.js @@ -22,10 +22,13 @@ describe("ouiDatagrid", () => { const isSortableDescCell = element => element.hasClass("oui-datagrid__header_sortable-desc"); const getActionMenu = element => angular.element(element[0].querySelectorAll("oui-action-menu")); const isStickyCell = element => element.hasClass("oui-datagrid__cell-sticky"); + const getDatagridParameters = element => element.find("oui-datagrid-parameters"); + const getColumnsInDatagridParameters = element => angular.element(element[0].querySelectorAll("oui-datagrid-parameters .oui-datagrid-parameters__column")); beforeEach(angular.mock.module("oui.datagrid")); beforeEach(angular.mock.module("oui.test-utils")); beforeEach(angular.mock.module("oui.action-menu")); + beforeEach(angular.mock.module("oui.checkbox")); beforeEach(inject((_TestUtils_, _$rootScope_, _$timeout_, _ouiDatagridService_) => { TestUtils = _TestUtils_; @@ -727,6 +730,196 @@ describe("ouiDatagrid", () => { }); }); + describe("Datagrid customization", () => { + describe("Display", () => { + it("should not show parameters icon if not enabled", () => { + const element = TestUtils.compileTemplate(` + + + + + `, { + rows: fakeData.slice(0, 5) + }); + + const datagridParameters = getDatagridParameters(element); + + expect(datagridParameters.length).toEqual(0); + }); + + it("should show parameters icon if enabled", () => { + const element = TestUtils.compileTemplate(` + + + + + `, { + rows: fakeData.slice(0, 5) + }); + + const datagridParameters = getDatagridParameters(element); + expect(datagridParameters.length).toEqual(1); + }); + + it("should show parameters icon if enabled and with action-menu", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `, { + rows: fakeData.slice(0, 5) + }); + + const datagridParameters = getDatagridParameters(element); + expect(datagridParameters.length).toEqual(1); + }); + + it("should only show not hidden columns", () => { + const element = TestUtils.compileTemplate(` + + + + + `, { + rows: fakeData.slice(0, 5) + }); + + const headers = getHeaderRow(element)[0].querySelectorAll(".oui-datagrid__header"); + expect(headers.length).toEqual(1); + }); + + it("should show visible and hidden columns in parameters", () => { + const element = TestUtils.compileTemplate(` + + + + + `, { + rows: fakeData.slice(0, 5) + }); + + const columnsParameters = getColumnsInDatagridParameters(element); + expect(columnsParameters.length).toBe(2); + }); + + it("should override base columns with custom columns", () => { + const element = TestUtils.compileTemplate(` + + + + + `, { + rows: fakeData.slice(0, 5), + columnsParameters: [{ + name: "firstName", + hidden: true + }, { + name: "lastName" + }] + }); + + const headers = getHeaderRow(element)[0].querySelectorAll(".oui-datagrid__header"); + expect(headers.length).toEqual(1); + }); + + it("should change columns if columns parameters is updated", () => { + const columnsParameters = [{ + name: "firstName", + hidden: true + }, { + name: "lastName" + }]; + const element = TestUtils.compileTemplate(` + + + + + `, { + rows: fakeData.slice(0, 5), + columnsParameters + }); + + let headers = getHeaderRow(element)[0].querySelectorAll(".oui-datagrid__header"); + expect(headers.length).toEqual(1); + + const scope = element.scope(); + const newColumnsParameters = angular.copy(columnsParameters); + newColumnsParameters[0].hidden = false; + scope.$ctrl.columnsParameters = newColumnsParameters; + scope.$digest(); + + headers = getHeaderRow(element)[0].querySelectorAll(".oui-datagrid__header"); + expect(headers.length).toEqual(2); + }); + }); + + describe("Events", () => { + const columns = [{ + name: "firstName" + }, { + name: "lastName" + }]; + + it("should not call change handler if no id is defined", () => { + const changeHandler = jasmine.createSpy(); + const element = TestUtils.compileTemplate(` + + + + + `, { + rows: fakeData.slice(0, 5), + changeHandler + }); + + const controller = element.controller("oui-datagrid"); + + const changedColumns = angular.copy(columns); + changedColumns[0].hidden = true; + controller.onColumnsChange(changedColumns); + + expect(changeHandler).not.toHaveBeenCalled(); + }); + + it("should call change handler", () => { + const id = "datagridId"; + const changeHandler = jasmine.createSpy(); + const element = TestUtils.compileTemplate(` + + + + + `, { + rows: fakeData.slice(0, 5), + changeHandler + }); + + const controller = element.controller("oui-datagrid"); + + const changedColumns = angular.copy(columns); + changedColumns[0].hidden = true; + controller.onColumnsChange(changedColumns); + + expect(changeHandler).toHaveBeenCalledWith(id, changedColumns); + }); + }); + }); + it("should display a pagination component", () => { const element = TestUtils.compileTemplate(` @@ -1033,9 +1226,9 @@ describe("ouiDatagrid", () => { // Check data-sortable and data-title const $headerRow = getHeaderRow(element); - expect(getHeaderCell($headerRow, 0).html()).toEqual("First name"); + expect(getHeaderCell($headerRow, 0).text().trim()).toEqual("First name"); expect(isSortableAscCell(getHeaderCell($headerRow, 0))).toBe(true); - expect(getHeaderCell($headerRow, 1).html()).toEqual("Last name"); + expect(getHeaderCell($headerRow, 1).text().trim()).toEqual("Last name"); expect(isSortableCell(getHeaderCell($headerRow, 1))).toBe(true); // Check data-property diff --git a/packages/oui-datagrid/src/parameters/datagrid-parameters.component.js b/packages/oui-datagrid/src/parameters/datagrid-parameters.component.js new file mode 100644 index 00000000..6a359f16 --- /dev/null +++ b/packages/oui-datagrid/src/parameters/datagrid-parameters.component.js @@ -0,0 +1,11 @@ +import controller from "./datagrid-parameters.controller"; +import template from "./datagrid-parameters.html"; + +export default { + bindings: { + columns: "<", + onChange: "&" + }, + controller, + template +}; diff --git a/packages/oui-datagrid/src/parameters/datagrid-parameters.controller.js b/packages/oui-datagrid/src/parameters/datagrid-parameters.controller.js new file mode 100644 index 00000000..4da4dd29 --- /dev/null +++ b/packages/oui-datagrid/src/parameters/datagrid-parameters.controller.js @@ -0,0 +1,37 @@ +export default class DatagridParametersController { + constructor ($element, $timeout) { + "ngInject"; + + this.$element = $element; + this.$timeout = $timeout; + } + + $onChanges () { + this.computeColumns(); + } + + $postLink () { + this.$timeout(() => { + this.$element.addClass("oui-datagrid-parameters"); + }); + } + + computeColumns () { + this.customizedColumns = angular.copy(this.columns) + .map(column => { + column.visible = !column.hidden; + return column; + }); + } + + onColumnChange (columnIndex, isVisible) { + this.changedColumns = angular.copy(this.customizedColumns); + this.changedColumns.forEach((column, index) => { + if (index === columnIndex) { + column.hidden = !isVisible; + } + delete column.visible; + }); + this.onChange({ columns: this.changedColumns }); + } +} diff --git a/packages/oui-datagrid/src/parameters/datagrid-parameters.html b/packages/oui-datagrid/src/parameters/datagrid-parameters.html new file mode 100644 index 00000000..3f9b5e0f --- /dev/null +++ b/packages/oui-datagrid/src/parameters/datagrid-parameters.html @@ -0,0 +1,22 @@ + + + +
+
+
Columns Display
+
+
+
+ +
+
+
+
+
diff --git a/packages/oui-datagrid/src/parameters/datagrid-parameters.spec.js b/packages/oui-datagrid/src/parameters/datagrid-parameters.spec.js new file mode 100644 index 00000000..e8ee5d97 --- /dev/null +++ b/packages/oui-datagrid/src/parameters/datagrid-parameters.spec.js @@ -0,0 +1,126 @@ +describe("ouiDatagridParameters", () => { + let $timeout; + let TestUtils; + + const getColumns = element => element[0].querySelectorAll(".oui-datagrid-parameters__column"); + const getCheckbox = (columnsElements, index) => angular.element(columnsElements[index]).find("input"); + + beforeEach(angular.mock.module("oui.datagrid")); + beforeEach(angular.mock.module("oui.test-utils")); + beforeEach(angular.mock.module("oui.dropdown")); + beforeEach(angular.mock.module("oui.checkbox")); + + beforeEach(inject((_$timeout_, _TestUtils_) => { + $timeout = _$timeout_; + TestUtils = _TestUtils_; + })); + + describe("Component", () => { + const columns = [{ + name: "foo", + title: "Foo" + }, { + name: "bar", + title: "Bar", + hidden: true + }, { + name: "baz", + title: "Baz" + }, { + name: "qux", + title: "Qux", + preventCustomization: true + }]; + + describe("Display", () => { + let element; + + beforeEach(() => { + element = TestUtils.compileTemplate(` + + `, { columns }); + }); + + it("should display the component", () => { + const columnsElements = getColumns(element); + + $timeout.flush(); + + expect(element.hasClass("oui-datagrid-parameters")).toBe(true); + expect(columnsElements.length).toEqual(columns.length); + expect(angular.element(columnsElements[0]).text()).toContain(columns[0].title); + }); + + it("should display a checked column when visible", () => { + const columnsElements = getColumns(element); + const checkedColumn = getCheckbox(columnsElements, 0); + expect(checkedColumn.prop("checked")).toBe(true); + }); + + it("should display an unchecked column when hidden", () => { + const columnsElements = getColumns(element); + const uncheckedColumn = getCheckbox(columnsElements, 1); + expect(uncheckedColumn.prop("checked")).toBe(false); + }); + + it("should display a disabled checkbox column when customization is prevented", () => { + const columnsElements = getColumns(element); + const disabledColumn = getCheckbox(columnsElements, 3); + expect(disabledColumn.prop("disabled")).toBe(true); + }); + }); + + describe("Events", () => { + let changeHandler; + let element; + + beforeEach(() => { + changeHandler = jasmine.createSpy("change"); + element = TestUtils.compileTemplate(` + + `, { + columns, + changeHandler + }); + }); + + it("should trigger onChange when column is toggled hidden", () => { + const columnsElements = getColumns(element); + const $checkbox1 = getCheckbox(columnsElements, 0); + const $checkbox2 = getCheckbox(columnsElements, 1); + const $checkbox3 = getCheckbox(columnsElements, 2); + const expected = angular.copy(columns); + expected[0].hidden = true; + + $checkbox1.prop("checked", false); + $checkbox1.triggerHandler("click"); + + // 2nd checkbox should stay unchecked + expect($checkbox2.prop("checked")).toBe(false); + + // 3rd checkbox should stay checked + expect($checkbox3.prop("checked")).toBe(true); + + expect(changeHandler).toHaveBeenCalledWith(expected); + }); + + it("should trigger onChange when column is toggled visible", () => { + const columnsElements = getColumns(element); + const $checkbox1 = getCheckbox(columnsElements, 0); + const $checkbox2 = getCheckbox(columnsElements, 1); + const expected = angular.copy(columns); + expected[1].hidden = false; + + $checkbox2.prop("checked", true); + $checkbox2.triggerHandler("click"); + + // 1st checkbox should stay checked + expect($checkbox1.prop("checked")).toBe(true); + expect(changeHandler).toHaveBeenCalledWith(expected); + }); + }); + }); +});