From 465f1eb87e1d01efa2365349edbeede7875802fe Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Thu, 28 Mar 2019 10:29:13 +0100 Subject: [PATCH] Store filter/column settings in localStorage The filter and column settings in tables are stored per project and context in the browsers localStorage. This makes the table keep the settings even if the browser is refreshed or restarted. The table emits a "componentStateChanged" event containing the tables current state (filter/column settings) which then is saved by the top level component. --- .../js/es6/common/vuecomponents/init.js | 16 +- .../es6/common/vuecomponents/menu/DropDown.js | 2 +- .../es6/common/vuecomponents/table/Table.js | 67 +++++++- .../vuecomponents/table/columns/ColumnBase.js | 3 +- .../table/columns/filter/ColumnFilter.js | 130 +++++++++++++++ .../table/filter/ColumnFilter.js | 91 ----------- .../vuecomponents/table/filter/RowFilter.js | 48 ------ .../table/rows/filter/EnumFilter.js | 153 ++++++++++++++++++ .../table/rows/filter/NameFilter.js | 35 ++++ .../table/rows/filter/RowFilter.js | 25 +++ .../table/rows/filter/StatusFilter.js | 48 ++++++ .../table/rows/filter/TextFilter.js | 86 ++++++++++ src/styles/components/_pillar_table.sass | 10 ++ 13 files changed, 562 insertions(+), 152 deletions(-) create mode 100644 src/scripts/js/es6/common/vuecomponents/table/columns/filter/ColumnFilter.js delete mode 100644 src/scripts/js/es6/common/vuecomponents/table/filter/ColumnFilter.js delete mode 100644 src/scripts/js/es6/common/vuecomponents/table/filter/RowFilter.js create mode 100644 src/scripts/js/es6/common/vuecomponents/table/rows/filter/EnumFilter.js create mode 100644 src/scripts/js/es6/common/vuecomponents/table/rows/filter/NameFilter.js create mode 100644 src/scripts/js/es6/common/vuecomponents/table/rows/filter/RowFilter.js create mode 100644 src/scripts/js/es6/common/vuecomponents/table/rows/filter/StatusFilter.js create mode 100644 src/scripts/js/es6/common/vuecomponents/table/rows/filter/TextFilter.js diff --git a/src/scripts/js/es6/common/vuecomponents/init.js b/src/scripts/js/es6/common/vuecomponents/init.js index 6cd6b180..86fd612f 100644 --- a/src/scripts/js/es6/common/vuecomponents/init.js +++ b/src/scripts/js/es6/common/vuecomponents/init.js @@ -9,7 +9,11 @@ import { ColumnBase } from './table/columns/ColumnBase' import { ColumnFactoryBase } from './table/columns/ColumnFactoryBase' import { RowObjectsSourceBase } from './table/rows/RowObjectsSourceBase' import { RowBase } from './table/rows/RowObjectBase' -import { RowFilter } from './table/filter/RowFilter' +import { RowFilter } from './table/rows/filter/RowFilter' +import { EnumFilter } from './table/rows/filter/EnumFilter' +import { StatusFilter } from './table/rows/filter/StatusFilter' +import { TextFilter } from './table/rows/filter/TextFilter' +import { NameFilter } from './table/rows/filter/NameFilter' let mixins = { UnitOfWorkTracker, @@ -31,12 +35,16 @@ let table = { } }, rows: { + filter: { + RowFilter, + EnumFilter, + StatusFilter, + TextFilter, + NameFilter + }, RowObjectsSourceBase, RowBase, }, - filter: { - RowFilter - }, } export { mixins, table } diff --git a/src/scripts/js/es6/common/vuecomponents/menu/DropDown.js b/src/scripts/js/es6/common/vuecomponents/menu/DropDown.js index e38d9e41..fec7e058 100644 --- a/src/scripts/js/es6/common/vuecomponents/menu/DropDown.js +++ b/src/scripts/js/es6/common/vuecomponents/menu/DropDown.js @@ -1,6 +1,6 @@ const TEMPLATE =`
-
diff --git a/src/scripts/js/es6/common/vuecomponents/table/Table.js b/src/scripts/js/es6/common/vuecomponents/table/Table.js index 8c4ffe0b..8167dc18 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/Table.js +++ b/src/scripts/js/es6/common/vuecomponents/table/Table.js @@ -1,8 +1,9 @@ import './rows/renderer/Head' import './rows/renderer/Row' -import './filter/ColumnFilter' -import './filter/RowFilter' +import './columns/filter/ColumnFilter' +import './rows/filter/RowFilter' import {UnitOfWorkTracker} from '../mixins/UnitOfWorkTracker' +import {RowFilter} from './rows/filter/RowFilter' /** * Table State @@ -23,6 +24,19 @@ class TableState { } } +class ComponentState { + /** + * Serializable state of this component. + * + * @param {Object} rowFilter + * @param {Object} columnFilter + */ + constructor(rowFilter, columnFilter) { + this.rowFilter = rowFilter; + this.columnFilter = columnFilter + } +} + const TEMPLATE =`
@@ -71,6 +90,7 @@ const TEMPLATE =` * * @emits isInitialized When all rows has been fetched, and are initialized. * @emits selectItemsChanged(selectedItems) When selected rows has changed. + * @emits componentStateChanged(newState) When table state changed. Filtered rows, visible columns... */ let PillarTable = Vue.component('pillar-table-base', { template: TEMPLATE, @@ -88,15 +108,23 @@ let PillarTable = Vue.component('pillar-table-base', { type: Boolean, default: true }, + componentState: { + // Instance of ComponentState + type: Object, + default: undefined + } }, data: function() { return { columns: [], visibleColumns: [], visibleRowObjects: [], - rowsSource: undefined, // Override with your implementations of ColumnFactoryBase - columnFactory: undefined, // Override with your implementations of RowSource + rowsSource: undefined, // Override with your implementations of RowSource + columnFactory: undefined, // Override with your implementations of ColumnFactoryBase + rowFilterConfig: undefined, isInitialized: false, + rowFilterState: (this.componentState || {}).rowFilter, + columnFilterState: (this.componentState || {}).columnFilter, compareRowsCB: (row1, row2) => 0 } }, @@ -120,6 +148,15 @@ let PillarTable = Vue.component('pillar-table-base', { selectedItems() { return this.rowAndChildObjects.filter(it => it.isSelected) .map(it => it.underlyingObject); + }, + currentComponentState() { + if (this.isInitialized) { + return new ComponentState( + this.rowFilterState, + this.columnFilterState + ); + } + return undefined; } }, watch: { @@ -130,8 +167,8 @@ let PillarTable = Vue.component('pillar-table-base', { }, selectedItems(newValue, oldValue) { // Deep compare to avoid spamming un needed events - let hasChanged = JSON.stringify(newValue ) === JSON.stringify(oldValue); - if (!hasChanged) { + let hasChanged = JSON.stringify(newValue ) !== JSON.stringify(oldValue); + if (hasChanged) { this.$emit('selectItemsChanged', newValue); } }, @@ -139,6 +176,15 @@ let PillarTable = Vue.component('pillar-table-base', { if (newValue) { this.$emit('isInitialized'); } + }, + currentComponentState(newValue, oldValue) { + if (this.isInitialized) { + // Deep compare to avoid spamming un needed events + let hasChanged = JSON.stringify(newValue ) !== JSON.stringify(oldValue); + if (hasChanged) { + this.$emit('componentStateChanged', newValue); + } + } } }, created() { @@ -169,9 +215,15 @@ let PillarTable = Vue.component('pillar-table-base', { onVisibleColumnsChanged(visibleColumns) { this.visibleColumns = visibleColumns; }, + onColumnFilterStateChanged(newComponentState) { + this.columnFilterState = newComponentState; + }, onVisibleRowObjectsChanged(visibleRowObjects) { this.visibleRowObjects = visibleRowObjects; }, + onRowFilterStateChanged(newComponentState) { + this.rowFilterState = newComponentState; + }, onSort(column, direction) { function compareRows(r1, r2) { return column.compareRows(r1, r2) * direction; @@ -228,6 +280,9 @@ let PillarTable = Vue.component('pillar-table-base', { return true; }) } + }, + components: { + 'pillar-table-row-filter': RowFilter } }); diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js b/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js index c2c93851..c6b4f91d 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js +++ b/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js @@ -4,13 +4,12 @@ import { CellDefault } from '../cells/renderer/CellDefault' * Column logic */ -let nextColumnId = 0; export class ColumnBase { constructor(displayName, columnType) { - this._id = nextColumnId++; this.displayName = displayName; this.columnType = columnType; this.isMandatory = false; + this.includedByDefault = true; this.isSortable = true; this.isHighLighted = 0; } diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/filter/ColumnFilter.js b/src/scripts/js/es6/common/vuecomponents/table/columns/filter/ColumnFilter.js new file mode 100644 index 00000000..04e0eff4 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/columns/filter/ColumnFilter.js @@ -0,0 +1,130 @@ +import '../../../menu/DropDown' + +const TEMPLATE =` +
+ + + +
    + Columns: +
  • + + {{ c.displayName }} +
  • +
+
+
+`; + +class ColumnState{ + constructor() { + this.displayName; + this.isVisible; + this.isMandatory; + } + + static createDefault(column) { + let state = new ColumnState; + state.displayName = column.displayName; + state.isVisible = !!column.includedByDefault; + state.isMandatory = !!column.isMandatory; + return state; + } +} + +class ComponentState { + /** + * Serializable state of this component. + * + * @param {Array} selected The columns that should be visible + */ + constructor(selected) { + this.selected = selected; + } +} + +/** + * Component to select what columns to render in the table. + * + * @emits visibleColumnsChanged(columns) When visible columns has changed + * @emits componentStateChanged(newState) When column filter state changed. + */ +let Filter = Vue.component('pillar-table-column-filter', { + template: TEMPLATE, + props: { + columns: Array, // Instances of ColumnBase + componentState: Object, // Instance of ComponentState + }, + data() { + return { + columnStates: this.createInitialColumnStates(), // Instances of ColumnState + } + }, + computed: { + visibleColumns() { + return this.columns.filter((candidate) => { + return candidate.isMandatory || this.isColumnStateVisible(candidate); + }); + }, + columnFilterState() { + return new ComponentState(this.visibleColumns.map(it => it.displayName)); + } + }, + watch: { + columns() { + this.columnStates = this.createInitialColumnStates(); + }, + visibleColumns(visibleColumns) { + this.$emit('visibleColumnsChanged', visibleColumns); + }, + columnFilterState(newValue) { + this.$emit('componentStateChanged', newValue); + } + }, + created() { + this.$emit('visibleColumnsChanged', this.visibleColumns); + }, + methods: { + createInitialColumnStates() { + let columnStateCB = ColumnState.createDefault; + if (this.componentState && this.componentState.selected) { + let selected = this.componentState.selected; + columnStateCB = (column) => { + let state = ColumnState.createDefault(column); + state.isVisible = selected.includes(column.displayName); + return state; + } + } + + return this.columns.reduce((states, c) => { + if(!c.isMandatory) { + states.push(columnStateCB(c)); + } + return states; + }, []); + }, + isColumnStateVisible(column) { + for (let state of this.columnStates) { + if (state.displayName === column.displayName) { + return state.isVisible; + } + } + return false; + }, + toggleColumn(column) { + column.isVisible = !column.isVisible; + } + }, +}); + +export { Filter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/filter/ColumnFilter.js b/src/scripts/js/es6/common/vuecomponents/table/filter/ColumnFilter.js deleted file mode 100644 index 0ca380a4..00000000 --- a/src/scripts/js/es6/common/vuecomponents/table/filter/ColumnFilter.js +++ /dev/null @@ -1,91 +0,0 @@ -import '../../menu/DropDown' - -const TEMPLATE =` -
- - - -
    - Columns: -
  • - - {{ c.displayName }} -
  • -
-
-
-`; - -class ColumnState{ - constructor(id, displayName, isVisible) { - this.id = id; - this.displayName = displayName; - this.isVisible = isVisible; - } -} - -/** - * Component to select what columns to render in the table. - * - * @emits visibleColumnsChanged(columns) When visible columns has changed - */ -let Filter = Vue.component('pillar-table-column-filter', { - template: TEMPLATE, - props: { - columns: Array, // Instances of ColumnBase - }, - data() { - return { - columnStates: [], // Instances of ColumnState - } - }, - computed: { - visibleColumns() { - return this.columns.filter((candidate) => { - return candidate.isMandatory || this.isColumnStateVisible(candidate); - }); - } - }, - watch: { - columns() { - this.columnStates = this.setColumnStates(); - }, - visibleColumns(visibleColumns) { - this.$emit('visibleColumnsChanged', visibleColumns); - } - }, - created() { - this.$emit('visibleColumnsChanged', this.visibleColumns); - }, - methods: { - setColumnStates() { - return this.columns.reduce((states, c) => { - if (!c.isMandatory) { - states.push( - new ColumnState(c._id, c.displayName, true) - ); - } - return states; - }, []) - }, - isColumnStateVisible(column) { - for (let state of this.columnStates) { - if (state.id === column._id) { - return state.isVisible; - } - } - return false; - }, - }, -}); - -export { Filter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/filter/RowFilter.js b/src/scripts/js/es6/common/vuecomponents/table/filter/RowFilter.js deleted file mode 100644 index 6282c200..00000000 --- a/src/scripts/js/es6/common/vuecomponents/table/filter/RowFilter.js +++ /dev/null @@ -1,48 +0,0 @@ -const TEMPLATE =` -
- -
-`; - -/** - * @emits visibleRowObjectsChanged(rowObjects) When the what objects to be visible has changed. - */ -let RowFilter = Vue.component('pillar-table-row-filter', { - template: TEMPLATE, - props: { - rowObjects: Array - }, - data() { - return { - nameQuery: '', - } - }, - computed: { - nameQueryLoweCase() { - return this.nameQuery.toLowerCase(); - }, - visibleRowObjects() { - return this.rowObjects.filter((row) => { - return this.filterByName(row); - }); - } - }, - watch: { - visibleRowObjects(visibleRowObjects) { - this.$emit('visibleRowObjectsChanged', visibleRowObjects); - } - }, - created() { - this.$emit('visibleRowObjectsChanged', this.visibleRowObjects); - }, - methods: { - filterByName(rowObject) { - return rowObject.getName().toLowerCase().indexOf(this.nameQueryLoweCase) !== -1; - }, - }, -}); - -export { RowFilter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/filter/EnumFilter.js b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/EnumFilter.js new file mode 100644 index 00000000..12e75f44 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/EnumFilter.js @@ -0,0 +1,153 @@ +const TEMPLATE =` + + + +
    +
  • + {{ label }}: +
  • +
  • + Toggle All +
  • +
  • +
  • + {{ val.displayName }} +
  • +
+
+`; + +class EnumState{ + constructor(displayName, value, isVisible) { + this.displayName = displayName; + this.value = value; + this.isVisible = isVisible; + } +} + +class ComponentState { + /** + * Serializable state of this component. + * + * @param {Array} selected The enums that should be visible + */ + constructor(selected) { + this.selected = selected; + } +} + +/** + * Filter row objects based on enumeratable values. + * + * @emits visibleRowObjectsChanged(rowObjects) When the objects to be visible has changed. + * @emits componentStateChanged(newState) When row filter state changed. + */ +let EnumFilter = { + template: TEMPLATE, + props: { + label: String, + availableValues: Array, // Array with valid values [{value: abc, displayName: xyz},...] + componentState: Object, // Instance of ComponentState. + valueExtractorCB: { + // Callback to extract enumvalue from a rowObject + type: Function, + default: (rowObject) => {throw Error("Not Implemented")} + }, + rowObjects: Array, + }, + data() { + return { + enumVisibilities: this.initEnumVisibilities(), + } + }, + computed: { + visibleRowObjects() { + return this.rowObjects.filter((row) => { + return this.shouldBeVisible(row); + }); + }, + includesRows() { + for (const key in this.enumVisibilities) { + if(!this.enumVisibilities[key].isVisible) return false; + } + return true; + }, + enumButtonClasses() { + return { + 'filter-active': !this.includesRows + } + }, + currentComponentState() { + let visibleEnums = []; + for (const key in this.enumVisibilities) { + const enumState = this.enumVisibilities[key]; + if (enumState.isVisible) { + visibleEnums.push(enumState.value); + } + } + + return new ComponentState(visibleEnums); + } + }, + watch: { + visibleRowObjects(visibleRowObjects) { + this.$emit('visibleRowObjectsChanged', visibleRowObjects); + }, + currentComponentState(newValue) { + this.$emit('componentStateChanged', newValue); + } + }, + created() { + this.$emit('visibleRowObjectsChanged', this.visibleRowObjects); + }, + methods: { + shouldBeVisible(rowObject) { + let value = this.valueExtractorCB(rowObject); + if (typeof this.enumVisibilities[value] === 'undefined') { + console.warn(`RowObject ${rowObject.getId()} has an invalid ${this.label} enum: ${value}`) + return true; + } + return this.enumVisibilities[value].isVisible; + }, + initEnumVisibilities() { + let initialValueCB = () => true; + if (this.componentState && this.componentState.selected) { + initialValueCB = (val) => { + return this.componentState.selected.includes(val.value); + }; + } + + return this.availableValues.reduce((agg, val)=> { + agg[val.value] = new EnumState(val.displayName, val.value, initialValueCB(val)); + return agg; + }, {}); + }, + toggleEnum(value) { + this.enumVisibilities[value].isVisible = !this.enumVisibilities[value].isVisible; + }, + toggleAll() { + let newValue = !this.includesRows; + for (const key in this.enumVisibilities) { + this.enumVisibilities[key].isVisible = newValue; + } + } + }, +}; + +export { EnumFilter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/filter/NameFilter.js b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/NameFilter.js new file mode 100644 index 00000000..53e2a33b --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/NameFilter.js @@ -0,0 +1,35 @@ +import {TextFilter} from './TextFilter' + +const TEMPLATE =` + +`; +/** + * Filter row objects based on there name. + * + * @emits visibleRowObjectsChanged(rowObjects) When the objects to be visible has changed. + * @emits componentStateChanged(newState) When row filter state changed. + */ +let NameFilter = { + template: TEMPLATE, + props: { + componentState: Object, // Instance of object that componentStateChanged emitted. To restore previous state. + rowObjects: Array, + }, + methods: { + extractName(rowObject) { + return rowObject.getName(); + }, + }, + components: { + 'text-filter': TextFilter, + }, +}; + +export { NameFilter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/filter/RowFilter.js b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/RowFilter.js new file mode 100644 index 00000000..be8dcd37 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/RowFilter.js @@ -0,0 +1,25 @@ +import {NameFilter} from './NameFilter' + +const TEMPLATE =` +
+ +
+`; + +let RowFilter = { + template: TEMPLATE, + props: { + rowObjects: Array, + componentState: Object + }, + components: { + 'name-filter': NameFilter + } +}; + +export { RowFilter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/filter/StatusFilter.js b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/StatusFilter.js new file mode 100644 index 00000000..7361449a --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/StatusFilter.js @@ -0,0 +1,48 @@ +import {EnumFilter} from './EnumFilter' + +const TEMPLATE =` + +`; +/** + * Filter row objects based on there status. + * + * @emits visibleRowObjectsChanged(rowObjects) When the objects to be visible has changed. + * @emits componentStateChanged(newState) When row filter state changed. + */ +let StatusFilter = { + template: TEMPLATE, + props: { + availableStatuses: Array, // Array with valid values ['abc', 'xyz'] + componentState: Object, // Instance of object that componentStateChanged emitted. To restore previous state. + rowObjects: Array, + }, + computed: { + availableEnumValues() { + let statusCopy = this.availableStatuses.concat().sort() + return statusCopy.map(status =>{ + return { + value: status, + displayName: status.replace(/-|_/g, ' ') // Replace -(dash) and _(underscore) with space + } + }); + } + }, + methods: { + extractStatus(rowObject) { + return rowObject.getStatus(); + }, + }, + components: { + 'enum-filter': EnumFilter, + }, +}; + +export { StatusFilter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/filter/TextFilter.js b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/TextFilter.js new file mode 100644 index 00000000..bab98d14 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/TextFilter.js @@ -0,0 +1,86 @@ +const TEMPLATE =` + +`; + +class ComponentState { + /** + * Serializable state of this component. + * + * @param {String} textQuery + */ + constructor(textQuery) { + this.textQuery = textQuery; + } +} + +/** + * Component to filter rowobjects by a text value + * + * @emits visibleRowObjectsChanged(rowObjects) When the objects to be visible has changed. + * @emits componentStateChanged(newState) When row filter state changed. Filter query... + */ +let TextFilter = { + template: TEMPLATE, + props: { + label: String, + rowObjects: Array, + componentState: { + // Instance of ComponentState + type: Object, + default: undefined + }, + valueExtractorCB: { + // Callback to extract text to filter from a rowObject + type: Function, + default: (rowObject) => {throw Error("Not Implemented")} + } + }, + data() { + return { + textQuery: (this.componentState || {}).textQuery || '', + } + }, + computed: { + textQueryLoweCase() { + return this.textQuery.toLowerCase(); + }, + visibleRowObjects() { + return this.rowObjects.filter((row) => { + return this.filterByText(row); + }); + }, + textInputClasses() { + return { + 'filter-active': this.textQuery.length > 0 + }; + }, + currentComponentState() { + return new ComponentState(this.textQuery); + }, + placeholderText() { + return `Filter by ${this.label}`; + } + }, + watch: { + visibleRowObjects(visibleRowObjects) { + this.$emit('visibleRowObjectsChanged', visibleRowObjects); + }, + currentComponentState(newValue) { + this.$emit('componentStateChanged', newValue); + } + }, + created() { + this.$emit('visibleRowObjectsChanged', this.visibleRowObjects); + }, + methods: { + filterByText(rowObject) { + return (this.valueExtractorCB(rowObject) || '').toLowerCase().indexOf(this.textQueryLoweCase) !== -1; + }, + }, +}; + +export { TextFilter } diff --git a/src/styles/components/_pillar_table.sass b/src/styles/components/_pillar_table.sass index da3e4070..5a6e1d97 100644 --- a/src/styles/components/_pillar_table.sass +++ b/src/styles/components/_pillar_table.sass @@ -112,6 +112,9 @@ $thumbnail-max-height: calc(110px * (9/16)) display: flex flex-direction: row + .action + cursor: pointer + .settings-menu display: flex flex-direction: column @@ -123,10 +126,17 @@ $thumbnail-max-height: calc(110px * (9/16)) text-transform: capitalize z-index: $z-index-base + 1 box-shadow: 0 2px 5px rgba(black, .4) + user-select: none .pillar-table-row-filter display: flex flex-direction: row + + input.filter-active + background-color: rgba($color-info, .50) + + .pi-filter.filter-active + color: $color-info .pillar-table-actions margin-left: auto