Attract multi edit: Edit multiple tasks/shots/assets at the same time
For the user: Ctrl + L-Mouse to select multiple tasks/shots/assets and then edit the nodes as before. When multiple items are selected a chain icon can be seen in editor next to the fields. If the chain is broken it indicates that the values are not the same on all the selected items. When a field has been edited it will be marked with a green background color. The items are saved one by one in parallel. This means that one item could fail to be saved, while the others get updated. For developers: The editor and activities has been ported to Vue. The table and has been updated to support multi select. MultiEditEngine is the core of the multi edit. It keeps track of what values differs and what has been edited.
This commit is contained in:
@@ -3,6 +3,7 @@ import './rows/renderer/Row'
|
||||
import './filter/ColumnFilter'
|
||||
import './filter/RowFilter'
|
||||
import {UnitOfWorkTracker} from '../mixins/UnitOfWorkTracker'
|
||||
import {RowState} from './rows/RowObjectBase'
|
||||
|
||||
const TEMPLATE =`
|
||||
<div class="pillar-table-container"
|
||||
@@ -10,10 +11,12 @@ const TEMPLATE =`
|
||||
>
|
||||
<div class="pillar-table-menu">
|
||||
<pillar-table-row-filter
|
||||
:rowObjects="rowObjects"
|
||||
:rowObjects="sortedRowObjects"
|
||||
@visibleRowObjectsChanged="onVisibleRowObjectsChanged"
|
||||
/>
|
||||
<pillar-table-actions/>
|
||||
<pillar-table-actions
|
||||
@item-clicked="onItemClicked"
|
||||
/>
|
||||
<pillar-table-column-filter
|
||||
:columns="columns"
|
||||
@visibleColumnsChanged="onVisibleColumnsChanged"
|
||||
@@ -30,6 +33,7 @@ const TEMPLATE =`
|
||||
:columns="visibleColumns"
|
||||
:rowObject="rowObject"
|
||||
:key="rowObject.getId()"
|
||||
@item-clicked="onItemClicked"
|
||||
/>
|
||||
</transition-group>
|
||||
</div>
|
||||
@@ -42,31 +46,79 @@ let PillarTable = Vue.component('pillar-table-base', {
|
||||
// columnFactory,
|
||||
// rowsSource,
|
||||
props: {
|
||||
projectId: String
|
||||
projectId: String,
|
||||
selectedIds: Array,
|
||||
canChangeSelectionCB: {
|
||||
type: Function,
|
||||
default: () => true
|
||||
},
|
||||
canMultiSelect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
columns: [],
|
||||
visibleColumns: [],
|
||||
visibleRowObjects: [],
|
||||
rowsSource: {}
|
||||
rowsSource: {},
|
||||
isInitialized: false,
|
||||
compareRows: (row1, row2) => 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rowObjects() {
|
||||
return this.rowsSource.rowObjects || [];
|
||||
},
|
||||
sortedRowObjects() {
|
||||
return this.rowObjects.concat().sort(this.compareRows);
|
||||
},
|
||||
rowAndChildObjects() {
|
||||
let all = [];
|
||||
for (const row of this.rowObjects) {
|
||||
all.push(row, ...row.getChildObjects());
|
||||
}
|
||||
return all;
|
||||
},
|
||||
selectedItems() {
|
||||
return this.rowAndChildObjects.filter(it => it.isSelected)
|
||||
.map(it => it.underlyingObject);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedIds(newValue) {
|
||||
this.rowAndChildObjects.forEach(item => {
|
||||
item.isSelected = newValue.includes(item.getId());
|
||||
});
|
||||
},
|
||||
selectedItems(newValue, oldValue) {
|
||||
this.$emit('selectItemsChanged', newValue);
|
||||
},
|
||||
isInitialized(newValue) {
|
||||
if (newValue) {
|
||||
this.$emit('isInitialized');
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
let columnFactory = new this.$options.columnFactory(this.projectId);
|
||||
this.rowsSource = new this.$options.rowsSource(this.projectId);
|
||||
|
||||
let rowState = new RowState(this.selectedIds);
|
||||
|
||||
this.unitOfWork(
|
||||
Promise.all([
|
||||
columnFactory.thenGetColumns(),
|
||||
this.rowsSource.thenInit()
|
||||
this.rowsSource.thenFetchObjects()
|
||||
])
|
||||
.then((resp) => {
|
||||
this.columns = resp[0];
|
||||
return this.rowsSource.thenInit();
|
||||
})
|
||||
.then(() => {
|
||||
this.rowAndChildObjects.forEach(rowState.applyState.bind(rowState));
|
||||
this.isInitialized = true;
|
||||
})
|
||||
);
|
||||
},
|
||||
@@ -81,7 +133,28 @@ let PillarTable = Vue.component('pillar-table-base', {
|
||||
function compareRows(r1, r2) {
|
||||
return column.compareRows(r1, r2) * direction;
|
||||
}
|
||||
this.rowObjects.sort(compareRows);
|
||||
this.compareRows = compareRows;
|
||||
},
|
||||
onItemClicked(clickEvent, itemId) {
|
||||
if(!this.canChangeSelectionCB()) return;
|
||||
|
||||
if(this.isMultiSelectClick(clickEvent) && this.canMultiSelect) {
|
||||
let slectedIdsWithoutClicked = this.selectedIds.filter(id => id !== itemId);
|
||||
if (slectedIdsWithoutClicked.length < this.selectedIds.length) {
|
||||
this.selectedIds = slectedIdsWithoutClicked;
|
||||
} else {
|
||||
this.selectedIds = [itemId, ...this.selectedIds];
|
||||
}
|
||||
} else {
|
||||
if (this.selectedIds.length === 1 && this.selectedIds[0] === itemId) {
|
||||
this.selectedIds = [];
|
||||
} else {
|
||||
this.selectedIds = [itemId];
|
||||
}
|
||||
}
|
||||
},
|
||||
isMultiSelectClick(clickEvent) {
|
||||
return clickEvent.ctrlKey;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
@@ -6,6 +6,7 @@ const TEMPLATE =`
|
||||
:rowObject="rowObject"
|
||||
:column="column"
|
||||
:rawCellValue="rawCellValue"
|
||||
@item-clicked="$emit('item-clicked', ...arguments)"
|
||||
/>
|
||||
`;
|
||||
|
||||
|
@@ -1,11 +1,34 @@
|
||||
class RowState {
|
||||
constructor(selectedIds) {
|
||||
this.selectedIds = selectedIds || [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {RowBase} rowObject
|
||||
*/
|
||||
applyState(rowObject) {
|
||||
rowObject.isSelected = this.selectedIds.includes(rowObject.getId());
|
||||
}
|
||||
}
|
||||
|
||||
class RowBase {
|
||||
constructor(underlyingObject) {
|
||||
this.underlyingObject = underlyingObject;
|
||||
this.isInitialized = false;
|
||||
this.isVisible = true;
|
||||
this.isSelected = false;
|
||||
}
|
||||
|
||||
|
||||
thenInit() {
|
||||
this.isInitialized = true
|
||||
return this._thenInitImpl()
|
||||
.then(() => {
|
||||
this.isInitialized = true
|
||||
})
|
||||
}
|
||||
|
||||
_thenInitImpl() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -26,6 +49,10 @@ class RowBase {
|
||||
"is-busy": !this.isInitialized
|
||||
}
|
||||
}
|
||||
|
||||
getChildObjects() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export { RowBase }
|
||||
export { RowBase, RowState }
|
||||
|
@@ -5,9 +5,15 @@ class RowObjectsSourceBase {
|
||||
}
|
||||
|
||||
// Override this
|
||||
thenInit() {
|
||||
thenFetchObjects() {
|
||||
throw Error('Not implemented');
|
||||
}
|
||||
|
||||
thenInit() {
|
||||
return Promise.all(
|
||||
this.rowObjects.map(it => it.thenInit())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { RowObjectsSourceBase }
|
||||
|
@@ -1,14 +1,17 @@
|
||||
import '../../cells/renderer/CellProxy'
|
||||
|
||||
|
||||
const TEMPLATE =`
|
||||
<div class="pillar-table-row"
|
||||
:class="rowClasses"
|
||||
@click.prevent.stop="$emit('item-clicked', arguments[0], rowObject.getId())"
|
||||
>
|
||||
<pillar-cell-proxy
|
||||
v-for="c in columns"
|
||||
:rowObject="rowObject"
|
||||
:column="c"
|
||||
:key="c._id"
|
||||
@item-clicked="$emit('item-clicked', ...arguments)"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -21,7 +24,9 @@ Vue.component('pillar-table-row', {
|
||||
},
|
||||
computed: {
|
||||
rowClasses() {
|
||||
return this.rowObject.getRowClasses();
|
||||
let classes = this.rowObject.getRowClasses()
|
||||
classes['active'] = this.rowObject.isSelected;
|
||||
return classes;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user