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