Added comments and minor refactoring

This commit is contained in:
2019-03-14 10:30:23 +01:00
parent 01da240f54
commit 4136da110f
15 changed files with 162 additions and 57 deletions

View File

@@ -2,13 +2,13 @@ import './comments/CommentTree'
import './customdirectives/click-outside' import './customdirectives/click-outside'
import { UnitOfWorkTracker } from './mixins/UnitOfWorkTracker' import { UnitOfWorkTracker } from './mixins/UnitOfWorkTracker'
import { BrowserHistoryState, StateSaveMode } from './mixins/BrowserHistoryState' import { BrowserHistoryState, StateSaveMode } from './mixins/BrowserHistoryState'
import { PillarTable } from './table/Table' import { PillarTable, TableState } from './table/Table'
import { CellPrettyDate } from './table/cells/renderer/CellPrettyDate' import { CellPrettyDate } from './table/cells/renderer/CellPrettyDate'
import { CellDefault } from './table/cells/renderer/CellDefault' import { CellDefault } from './table/cells/renderer/CellDefault'
import { ColumnBase } from './table/columns/ColumnBase' import { ColumnBase } from './table/columns/ColumnBase'
import { ColumnFactoryBase } from './table/columns/ColumnFactoryBase' import { ColumnFactoryBase } from './table/columns/ColumnFactoryBase'
import { RowObjectsSourceBase } from './table/rows/RowObjectsSourceBase' import { RowObjectsSourceBase } from './table/rows/RowObjectsSourceBase'
import { RowBase, RowState } from './table/rows/RowObjectBase' import { RowBase } from './table/rows/RowObjectBase'
import { RowFilter } from './table/filter/RowFilter' import { RowFilter } from './table/filter/RowFilter'
let mixins = { let mixins = {
@@ -19,6 +19,7 @@ let mixins = {
let table = { let table = {
PillarTable, PillarTable,
TableState,
columns: { columns: {
ColumnBase, ColumnBase,
ColumnFactoryBase, ColumnFactoryBase,
@@ -32,7 +33,6 @@ let table = {
rows: { rows: {
RowObjectsSourceBase, RowObjectsSourceBase,
RowBase, RowBase,
RowState,
}, },
filter: { filter: {
RowFilter RowFilter

View File

@@ -3,7 +3,25 @@ import './rows/renderer/Row'
import './filter/ColumnFilter' import './filter/ColumnFilter'
import './filter/RowFilter' import './filter/RowFilter'
import {UnitOfWorkTracker} from '../mixins/UnitOfWorkTracker' import {UnitOfWorkTracker} from '../mixins/UnitOfWorkTracker'
import {RowState} from './rows/RowObjectBase'
/**
* Table State
*
* Used to restore a table to a given state.
*/
class TableState {
constructor(selectedIds) {
this.selectedIds = selectedIds || [];
}
/**
* Apply state to row
* @param {RowBase} rowObject
*/
applyRowState(rowObject) {
rowObject.isSelected = this.selectedIds.includes(rowObject.getId());
}
}
const TEMPLATE =` const TEMPLATE =`
<div class="pillar-table-container" <div class="pillar-table-container"
@@ -40,6 +58,20 @@ const TEMPLATE =`
</div> </div>
`; `;
/**
* The table renders RowObject instances for the rows, and ColumnBase instances for the Columns.
* Extend the table to fit your needs.
*
* Usage:
* Extend RowBase to wrap the data you want in your row
* Extend ColumnBase once per column type you need
* Extend RowObjectsSourceBase to fetch and initialize your rows
* Extend ColumnFactoryBase to create the rows for your table
* Extend This Table with your ColumnFactory and RowSource
*
* @emits isInitialized When all rows has been fetched, and are initialized.
* @emits selectItemsChanged(selectedItems) When selected rows has changed.
*/
let PillarTable = Vue.component('pillar-table-base', { let PillarTable = Vue.component('pillar-table-base', {
template: TEMPLATE, template: TEMPLATE,
mixins: [UnitOfWorkTracker], mixins: [UnitOfWorkTracker],
@@ -64,15 +96,18 @@ let PillarTable = Vue.component('pillar-table-base', {
visibleRowObjects: [], visibleRowObjects: [],
rowsSource: {}, rowsSource: {},
isInitialized: false, isInitialized: false,
compareRows: (row1, row2) => 0 compareRowsCB: (row1, row2) => 0
} }
}, },
computed: { computed: {
rowObjects() { rowObjects() {
return this.rowsSource.rowObjects || []; return this.rowsSource.rowObjects || [];
}, },
/**
* Rows sorted with a column sorter
*/
sortedRowObjects() { sortedRowObjects() {
return this.rowObjects.concat().sort(this.compareRows); return this.rowObjects.concat().sort(this.compareRowsCB);
}, },
rowAndChildObjects() { rowAndChildObjects() {
let all = []; let all = [];
@@ -105,19 +140,19 @@ let PillarTable = Vue.component('pillar-table-base', {
let columnFactory = new this.$options.columnFactory(this.projectId); let columnFactory = new this.$options.columnFactory(this.projectId);
this.rowsSource = new this.$options.rowsSource(this.projectId); this.rowsSource = new this.$options.rowsSource(this.projectId);
let rowState = new RowState(this.selectedIds); let tableState = new TableState(this.selectedIds);
this.unitOfWork( this.unitOfWork(
Promise.all([ Promise.all([
columnFactory.thenGetColumns(), columnFactory.thenGetColumns(),
this.rowsSource.thenFetchObjects() this.rowsSource.thenGetRowObjects()
]) ])
.then((resp) => { .then((resp) => {
this.columns = resp[0]; this.columns = resp[0];
return this.rowsSource.thenInit(); return this.rowsSource.thenInit();
}) })
.then(() => { .then(() => {
this.rowAndChildObjects.forEach(rowState.applyState.bind(rowState)); this.rowAndChildObjects.forEach(tableState.applyRowState.bind(tableState));
this.isInitialized = true; this.isInitialized = true;
}) })
); );
@@ -133,10 +168,11 @@ let PillarTable = Vue.component('pillar-table-base', {
function compareRows(r1, r2) { function compareRows(r1, r2) {
return column.compareRows(r1, r2) * direction; return column.compareRows(r1, r2) * direction;
} }
this.compareRows = compareRows; this.compareRowsCB = compareRows;
}, },
onItemClicked(clickEvent, itemId) { onItemClicked(clickEvent, itemId) {
if(!this.canChangeSelectionCB()) return; if(!this.canChangeSelectionCB()) return;
if(this.isMultiToggleClick(clickEvent) && this.canMultiSelect) { if(this.isMultiToggleClick(clickEvent) && this.canMultiSelect) {
let slectedIdsWithoutClicked = this.selectedIds.filter(id => id !== itemId); let slectedIdsWithoutClicked = this.selectedIds.filter(id => id !== itemId);
if (slectedIdsWithoutClicked.length < this.selectedIds.length) { if (slectedIdsWithoutClicked.length < this.selectedIds.length) {
@@ -144,7 +180,7 @@ let PillarTable = Vue.component('pillar-table-base', {
} else { } else {
this.selectedIds = [itemId, ...this.selectedIds]; this.selectedIds = [itemId, ...this.selectedIds];
} }
} else if(this.isSelectBetween(clickEvent) && this.canMultiSelect) { } else if(this.isSelectBetweenClick(clickEvent) && this.canMultiSelect) {
if (this.selectedIds.length > 0) { if (this.selectedIds.length > 0) {
let betweenA = this.selectedIds[this.selectedIds.length -1]; let betweenA = this.selectedIds[this.selectedIds.length -1];
let betweenB = itemId; let betweenB = itemId;
@@ -162,13 +198,19 @@ let PillarTable = Vue.component('pillar-table-base', {
} }
} }
}, },
isSelectBetween(clickEvent) { isSelectBetweenClick(clickEvent) {
return clickEvent.shiftKey; return clickEvent.shiftKey;
}, },
isMultiToggleClick(clickEvent) { isMultiToggleClick(clickEvent) {
return clickEvent.ctrlKey || return clickEvent.ctrlKey ||
clickEvent.metaKey; // Mac command key clickEvent.metaKey; // Mac command key
}, },
/**
* Get visible rows between id1 and id2
* @param {String} id1
* @param {String} id2
* @returns {Array(RowObjects)}
*/
rowsBetween(id1, id2) { rowsBetween(id1, id2) {
let hasFoundFirst = false; let hasFoundFirst = false;
let hasFoundLast = false; let hasFoundLast = false;
@@ -185,4 +227,4 @@ let PillarTable = Vue.component('pillar-table-base', {
} }
}); });
export { PillarTable } export { PillarTable, TableState }

View File

@@ -4,6 +4,10 @@ const TEMPLATE =`
</div> </div>
`; `;
/**
* Default cell renderer. Takes raw cell value and formats it.
* Override for custom formatting of value.
*/
let CellDefault = Vue.component('pillar-cell-default', { let CellDefault = Vue.component('pillar-cell-default', {
template: TEMPLATE, template: TEMPLATE,
props: { props: {

View File

@@ -1,5 +1,9 @@
import { CellDefault } from './CellDefault' import { CellDefault } from './CellDefault'
/**
* Formats raw values as "pretty date".
* Expects rawCellValue to be a date.
*/
let CellPrettyDate = Vue.component('pillar-cell-pretty-date', { let CellPrettyDate = Vue.component('pillar-cell-pretty-date', {
extends: CellDefault, extends: CellDefault,
computed: { computed: {

View File

@@ -10,22 +10,39 @@ const TEMPLATE =`
/> />
`; `;
/**
* Renders the cell that the column requests.
*
* @emits item-clicked(mouseEvent,itemId) Re-emits if real cell is emitting it
*/
let CellProxy = Vue.component('pillar-cell-proxy', { let CellProxy = Vue.component('pillar-cell-proxy', {
template: TEMPLATE, template: TEMPLATE,
props: { props: {
column: Object, column: Object, // ColumnBase
rowObject: Object rowObject: Object // RowObject
}, },
computed: { computed: {
/**
* Raw unformated cell value
*/
rawCellValue() { rawCellValue() {
return this.column.getRawCellValue(this.rowObject) || ''; return this.column.getRawCellValue(this.rowObject) || '';
}, },
/**
* Name of the cell render component to be rendered
*/
cellRenderer() { cellRenderer() {
return this.column.getCellRenderer(this.rowObject); return this.column.getCellRenderer(this.rowObject);
}, },
/**
* Css classes to apply to the cell
*/
cellClasses() { cellClasses() {
return this.column.getCellClasses(this.rawCellValue, this.rowObject); return this.column.getCellClasses(this.rawCellValue, this.rowObject);
}, },
/**
* Cell tooltip
*/
cellTitle() { cellTitle() {
return this.column.getCellTitle(this.rawCellValue, this.rowObject); return this.column.getCellTitle(this.rawCellValue, this.rowObject);
} }

View File

@@ -22,6 +22,11 @@ const TEMPLATE =`
</div> </div>
`; `;
/**
* A cell in the Header of the table
*
* @emits sort(column,direction) When user clicks column sort arrows.
*/
Vue.component('pillar-head-cell', { Vue.component('pillar-head-cell', {
template: TEMPLATE, template: TEMPLATE,
props: { props: {

View File

@@ -1,5 +1,9 @@
import { CellDefault } from '../cells/renderer/CellDefault' import { CellDefault } from '../cells/renderer/CellDefault'
/**
* Column logic
*/
let nextColumnId = 0; let nextColumnId = 0;
export class ColumnBase { export class ColumnBase {
constructor(displayName, columnType) { constructor(displayName, columnType) {
@@ -13,13 +17,18 @@ export class ColumnBase {
/** /**
* *
* @param {*} rowObject * @param {RowObject} rowObject
* @returns {String} Name of the Cell renderer component * @returns {String} Name of the Cell renderer component
*/ */
getCellRenderer(rowObject) { getCellRenderer(rowObject) {
return CellDefault.options.name; return CellDefault.options.name;
} }
/**
*
* @param {RowObject} rowObject
* @returns {*} Raw unformated value
*/
getRawCellValue(rowObject) { getRawCellValue(rowObject) {
// Should be overridden // Should be overridden
throw Error('Not implemented'); throw Error('Not implemented');
@@ -38,7 +47,7 @@ export class ColumnBase {
/** /**
* Object with css classes to use on the header cell * Object with css classes to use on the header cell
* @returns {Any} Object with css classes * @returns {Object} Object with css classes
*/ */
getHeaderCellClasses() { getHeaderCellClasses() {
// Should be overridden // Should be overridden

View File

@@ -1,10 +1,16 @@
/**
* Provides the columns that are available in a table.
*/
class ColumnFactoryBase{ class ColumnFactoryBase{
constructor(projectId) { constructor(projectId) {
this.projectId = projectId; this.projectId = projectId;
this.projectPromise; this.projectPromise;
} }
// Override this /**
* To be overridden for your purposes
* @returns {Promise(ColumnBase)} The columns that are available in the table.
*/
thenGetColumns() { thenGetColumns() {
throw Error('Not implemented') throw Error('Not implemented')
} }
@@ -19,3 +25,4 @@ class ColumnFactoryBase{
} }
export { ColumnFactoryBase } export { ColumnFactoryBase }

View File

@@ -1,10 +0,0 @@
const TEMPLATE =`
<div class="pillar-table-column"/>
`;
Vue.component('pillar-table-column', {
template: TEMPLATE,
props: {
column: Object
},
});

View File

@@ -25,14 +25,27 @@ const TEMPLATE =`
</div> </div>
`; `;
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', { let Filter = Vue.component('pillar-table-column-filter', {
template: TEMPLATE, template: TEMPLATE,
props: { props: {
columns: Array, columns: Array, // Instances of ColumnBase
}, },
data() { data() {
return { return {
columnStates: [], columnStates: [], // Instances of ColumnState
} }
}, },
computed: { computed: {
@@ -57,18 +70,16 @@ let Filter = Vue.component('pillar-table-column-filter', {
setColumnStates() { setColumnStates() {
return this.columns.reduce((states, c) => { return this.columns.reduce((states, c) => {
if (!c.isMandatory) { if (!c.isMandatory) {
states.push({ states.push(
_id: c._id, new ColumnState(c._id, c.displayName, true)
displayName: c.displayName, );
isVisible: true,
});
} }
return states; return states;
}, []) }, [])
}, },
isColumnStateVisible(column) { isColumnStateVisible(column) {
for (let state of this.columnStates) { for (let state of this.columnStates) {
if (state._id === column._id) { if (state.id === column._id) {
return state.isVisible; return state.isVisible;
} }
} }

View File

@@ -7,6 +7,9 @@ const TEMPLATE =`
</div> </div>
`; `;
/**
* @emits visibleRowObjectsChanged(rowObjects) When the what objects to be visible has changed.
*/
let RowFilter = Vue.component('pillar-table-row-filter', { let RowFilter = Vue.component('pillar-table-row-filter', {
template: TEMPLATE, template: TEMPLATE,
props: { props: {

View File

@@ -1,26 +1,16 @@
class RowState {
constructor(selectedIds) {
this.selectedIds = selectedIds || [];
}
/** /**
* * Each object to be visualized in the table is wrapped in a RowBase object. Column cells interact with it,
* @param {RowBase} rowObject
*/ */
applyState(rowObject) {
rowObject.isSelected = this.selectedIds.includes(rowObject.getId());
}
}
class RowBase { class RowBase {
constructor(underlyingObject) { constructor(underlyingObject) {
this.underlyingObject = underlyingObject; this.underlyingObject = underlyingObject;
this.isInitialized = false; this.isInitialized = false;
this.isVisible = true;
this.isSelected = false; this.isSelected = false;
} }
/**
* Called after the row has been created to initalize async properties. Fetching child objects for instance
*/
thenInit() { thenInit() {
return this._thenInitImpl() return this._thenInitImpl()
.then(() => { .then(() => {
@@ -28,6 +18,9 @@ class RowBase {
}) })
} }
/**
* Override to initialize async properties such as fetching child objects.
*/
_thenInitImpl() { _thenInitImpl() {
return Promise.resolve(); return Promise.resolve();
} }
@@ -44,15 +37,21 @@ class RowBase {
return this.underlyingObject.properties; return this.underlyingObject.properties;
} }
/**
* The css classes that should be applied to the row in the table
*/
getRowClasses() { getRowClasses() {
return { return {
"is-busy": !this.isInitialized "is-busy": !this.isInitialized
} }
} }
/**
* A row could have children (shots has tasks for example). Children should also be instances of RowObject
*/
getChildObjects() { getChildObjects() {
return []; return [];
} }
} }
export { RowBase, RowState } export { RowBase }

View File

@@ -1,14 +1,24 @@
/**
* The provider of RowObjects to a table.
* Extend to fit your purpose.
*/
class RowObjectsSourceBase { class RowObjectsSourceBase {
constructor(projectId) { constructor(projectId) {
this.projectId = projectId; this.projectId = projectId;
this.rowObjects = []; this.rowObjects = [];
} }
// Override this /**
thenFetchObjects() { * Should be overriden to fetch and create the row objects to we rendered in the table. The Row objects should be stored in
* this.rowObjects
*/
thenGetRowObjects() {
throw Error('Not implemented'); throw Error('Not implemented');
} }
/**
* Inits all its row objects.
*/
thenInit() { thenInit() {
return Promise.all( return Promise.all(
this.rowObjects.map(it => it.thenInit()) this.rowObjects.map(it => it.thenInit())

View File

@@ -9,7 +9,9 @@ const TEMPLATE =`
/> />
</div> </div>
`; `;
/**
* @emits sort(column,direction) When a column head has been clicked
*/
Vue.component('pillar-table-head', { Vue.component('pillar-table-head', {
template: TEMPLATE, template: TEMPLATE,
props: { props: {

View File

@@ -15,7 +15,9 @@ const TEMPLATE =`
/> />
</div> </div>
`; `;
/**
* @emits item-clicked(mouseEvent,itemId) When a RowObject has been clicked
*/
Vue.component('pillar-table-row', { Vue.component('pillar-table-row', {
template: TEMPLATE, template: TEMPLATE,
props: { props: {