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.
This commit is contained in:
2019-03-28 10:29:13 +01:00
parent 67d1e05d10
commit a7c1f5aa39
7 changed files with 160 additions and 102 deletions

View File

@@ -13,8 +13,10 @@ const TEMPLATE =`
:project="project" :project="project"
:selectedIds="selectedIds" :selectedIds="selectedIds"
:canChangeSelectionCB="canChangeSelectionCB" :canChangeSelectionCB="canChangeSelectionCB"
:componentState="initialTableState"
@selectItemsChanged="onSelectItemsChanged" @selectItemsChanged="onSelectItemsChanged"
@isInitialized="onTableInitialized" @isInitialized="onTableInitialized"
@componentStateChanged="onTableStateChanged"
/> />
</div> </div>
<div class="col-splitter"/> <div class="col-splitter"/>
@@ -27,6 +29,23 @@ const TEMPLATE =`
</div> </div>
`; `;
class ComponentState {
/**
* Serializable state of this component.
*
* @param {Object} tableState
*/
constructor(tableState) {
this.tableState = tableState;
}
}
/**
* Component wrapping a table for selecting attract_task/asset/shot nodes, and a editor to edit the selected node(s).
* Selected row filters and visible columns are stored in localStorage per project/context. This makes the settings
* sticky between sessions in the same browser.
* Selected nodes are stored in window.history. This makes it possible to move back/forward in browser and the selection
* will change accordingly.
*/
Vue.component('attract-app', { Vue.component('attract-app', {
template: TEMPLATE, template: TEMPLATE,
mixins: [BrowserHistoryState], mixins: [BrowserHistoryState],
@@ -98,7 +117,22 @@ Vue.component('attract-app', {
return `/attract/${projectUrl}/${this.contextType}/${selected._id}`; return `/attract/${projectUrl}/${this.contextType}/${selected._id}`;
} }
} }
},
stateStorageKey() {
return `attract.${this.projectId}.${this.contextType}`;
},
initialAppState() {
let stateJsonStr;
try {
stateJsonStr = localStorage.getItem(this.stateStorageKey);
} catch (error) {
// Log and ignore.
console.warn('Unable to restore state:', error);
}
return stateJsonStr ? JSON.parse(stateJsonStr) : undefined;
},
initialTableState() {
return this.initialAppState ? this.initialAppState.tableState : undefined;
} }
}, },
watch: { watch: {
@@ -127,6 +161,20 @@ Vue.component('attract-app', {
onTableInitialized() { onTableInitialized() {
this.isTableInited = true; this.isTableInited = true;
}, },
/**
* Save table state to localStorage per project and context
* @param {Object} newState
*/
onTableStateChanged(newState) {
let appState = new ComponentState(newState);
let stateJsonStr = JSON.stringify(appState);
try {
localStorage.setItem(this.stateStorageKey, stateJsonStr);
} catch (error) {
// Log and ignore.
console.warn('Unable to save state:', error);
}
},
canChangeSelectionCB() { canChangeSelectionCB() {
if(this.isEditing) { if(this.isEditing) {
let retval = confirm("You have unsaved data. Do you want to discard it?"); let retval = confirm("You have unsaved data. Do you want to discard it?");

View File

@@ -1,7 +1,7 @@
let PillarTable = pillar.vuecomponents.table.PillarTable; let PillarTable = pillar.vuecomponents.table.PillarTable;
import {AssetColumnFactory} from './columns/AssetColumnFactory' import {AssetColumnFactory} from './columns/AssetColumnFactory'
import {AssetRowsSource} from './rows/AssetRowsSource' import {AssetRowsSource} from './rows/AssetRowsSource'
import {RowFilter} from '../attracttable/filter/RowFilter' import {RowFilter} from '../attracttable/rows/filter/RowFilter'
const TEMPLATE =` const TEMPLATE =`
<div class="pillar-table-actions"> <div class="pillar-table-actions">
@@ -41,6 +41,18 @@ let AssetsTable = Vue.component('attract-assets-table', {
return { return {
columnFactory: new AssetColumnFactory(this.project), columnFactory: new AssetColumnFactory(this.project),
rowsSource: new AssetRowsSource(this.project._id), rowsSource: new AssetRowsSource(this.project._id),
rowFilterConfig: {validStatuses: this.getValidStatuses()}
}
},
methods: {
getValidStatuses() {
for (const it of this.project.node_types) {
if(it.name === 'attract_asset'){
return it.dyn_schema.status.allowed;
}
}
console.warn('Did not find allowed statuses for node type attract_shot');
return [];
} }
}, },
components: { components: {

View File

@@ -1,98 +0,0 @@
let RowFilterBase = pillar.vuecomponents.table.filter.RowFilter;
const TEMPLATE =`
<div class="pillar-table-row-filter">
<input
placeholder="Filter by name"
v-model="nameQuery"
/>
<pillar-dropdown>
<i class="pi-filter"
slot="button"
title="Row filter"
/>
<ul class="settings-menu"
slot="menu"
>
<li>
Status:
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.final"
/> Final
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.approved"
/> Approved
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.cbb"
/> Cbb
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.review"
/> Review
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.in_progress"
/> In Progress
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.todo"
/> Todo
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.on_hold"
/> On Hold
</li>
</ul>
</pillar-dropdown>
</div>
`;
let RowFilter = {
extends: RowFilterBase,
template: TEMPLATE,
props: {
rowObjects: Array
},
data() {
return {
showAssetStatus: {
todo: true,
in_progress: true,
on_hold: true,
approved: true,
cbb: true,
final: true,
review: true,
},
}
},
computed: {
nameQueryLoweCase() {
return this.nameQuery.toLowerCase();
},
visibleRowObjects() {
return this.rowObjects.filter((row) => {
if (!this.hasShowStatus(row)) return false;
return this.filterByName(row);
});
}
},
methods: {
hasShowStatus(rowObject) {
let status = rowObject.getProperties().status;
return !(this.showAssetStatus[status] === false); // To handle invalid statuses
},
},
};
export { RowFilter }

View File

@@ -9,6 +9,10 @@ class AttractRowBase extends RowBase {
onRowUpdated(event) { onRowUpdated(event) {
this.underlyingObject = event.detail; this.underlyingObject = event.detail;
} }
getStatus() {
return this.underlyingObject.properties.status;
}
} }
export { AttractRowBase } export { AttractRowBase }

View File

@@ -0,0 +1,68 @@
let NameFilter = pillar.vuecomponents.table.rows.filter.NameFilter;
let StatusFilter = pillar.vuecomponents.table.rows.filter.StatusFilter;
const TEMPLATE =`
<div class="pillar-table-row-filter">
<name-filter
:rowObjects="rowObjects"
:componentState="(componentState || {}).nameFilter"
@visibleRowObjectsChanged="onNameFiltered"
@componentStateChanged="onNameFilterStateChanged"
/>
<status-filter
:availableStatuses="availableStatuses"
:rowObjects="nameFilteredRowObjects"
:componentState="(componentState || {}).statusFilter"
@visibleRowObjectsChanged="$emit('visibleRowObjectsChanged', ...arguments)"
@componentStateChanged="onStatusFilterStateChanged"
/>
</div>
`;
let RowFilter = {
template: TEMPLATE,
props: {
rowObjects: Array,
componentState: Object,
config: Object
},
data() {
return {
availableStatuses: this.config.validStatuses,
nameFilteredRowObjects: [],
nameFilterState: (this.componentState || {}).nameFilter,
statusFilterState: (this.componentState || {}).statusFilter,
}
},
methods: {
onNameFiltered(visibleRowObjects) {
this.nameFilteredRowObjects = visibleRowObjects;
},
onNameFilterStateChanged(stateObj) {
this.nameFilterState = stateObj;
},
onStatusFilterStateChanged(stateObj) {
this.statusFilterState = stateObj;
}
},
computed: {
currentComponentState() {
return {
nameFilter: this.nameFilterState,
statusFilter: this.statusFilterState,
};
}
},
watch: {
currentComponentState(newValue) {
this.$emit('componentStateChanged', newValue);
}
},
components: {
'name-filter': NameFilter,
'status-filter': StatusFilter
}
};
export { RowFilter }

View File

@@ -1,7 +1,7 @@
let PillarTable = pillar.vuecomponents.table.PillarTable; let PillarTable = pillar.vuecomponents.table.PillarTable;
import {ShotsColumnFactory} from './columns/ShotsColumnFactory' import {ShotsColumnFactory} from './columns/ShotsColumnFactory'
import {ShotRowsSource} from './rows/ShotRowsSource' import {ShotRowsSource} from './rows/ShotRowsSource'
import {RowFilter} from '../attracttable/filter/RowFilter' import {RowFilter} from '../attracttable/rows/filter/RowFilter'
let ShotsTable = Vue.component('attract-shots-table', { let ShotsTable = Vue.component('attract-shots-table', {
extends: PillarTable, extends: PillarTable,
@@ -12,6 +12,18 @@ let ShotsTable = Vue.component('attract-shots-table', {
return { return {
columnFactory: new ShotsColumnFactory(this.project), columnFactory: new ShotsColumnFactory(this.project),
rowsSource: new ShotRowsSource(this.project._id), rowsSource: new ShotRowsSource(this.project._id),
rowFilterConfig: {validStatuses: this.getValidStatuses()}
}
},
methods: {
getValidStatuses() {
for (const it of this.project.node_types) {
if(it.name === 'attract_shot'){
return it.dyn_schema.status.allowed;
}
}
console.warn('Did not find allowed statuses for node type attract_shot');
return [];
} }
}, },
components: { components: {

View File

@@ -1,7 +1,7 @@
let PillarTable = pillar.vuecomponents.table.PillarTable; let PillarTable = pillar.vuecomponents.table.PillarTable;
import {TasksColumnFactory} from './columns/TasksColumnFactory' import {TasksColumnFactory} from './columns/TasksColumnFactory'
import {TaskRowsSource} from './rows/TaskRowsSource' import {TaskRowsSource} from './rows/TaskRowsSource'
import {RowFilter} from '../attracttable/filter/RowFilter' import {RowFilter} from '../attracttable/rows/filter/RowFilter'
const TEMPLATE =` const TEMPLATE =`
<div class="pillar-table-actions"> <div class="pillar-table-actions">
@@ -41,6 +41,18 @@ let TasksTable = Vue.component('attract-tasks-table', {
return { return {
columnFactory: new TasksColumnFactory(this.project), columnFactory: new TasksColumnFactory(this.project),
rowsSource: new TaskRowsSource(this.project._id), rowsSource: new TaskRowsSource(this.project._id),
rowFilterConfig: {validStatuses: this.getValidStatuses()}
}
},
methods: {
getValidStatuses() {
for (const it of this.project.node_types) {
if(it.name === 'attract_task'){
return it.dyn_schema.status.allowed;
}
}
console.warn('Did not find allowed statuses for node type attract_task');
return [];
} }
}, },
components: { components: {