From 379f7438648f26d19b0f71795ce1b75f16aef131 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Wed, 13 Mar 2019 13:53:40 +0100 Subject: [PATCH] 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. --- pillar/api/nodes/__init__.py | 10 +- pillar/api/nodes/activities.py | 43 +++++++ pillar/api/utils/__init__.py | 5 +- src/scripts/js/es6/common/api/init.js | 3 +- src/scripts/js/es6/common/api/nodes.js | 77 +++++++++++- src/scripts/js/es6/common/api/users.js | 7 ++ src/scripts/js/es6/common/events/Nodes.js | 76 ++++++++++-- .../vuecomponents/comments/CommentEditor.js | 9 +- .../vuecomponents/comments/CommentTree.js | 33 ++++-- .../js/es6/common/vuecomponents/init.js | 12 +- .../mixins/BrowserHistoryState.js | 111 ++++++++++++++++++ .../vuecomponents/mixins/UnitOfWorkTracker.js | 5 + .../es6/common/vuecomponents/table/Table.js | 85 +++++++++++++- .../table/cells/renderer/CellProxy.js | 1 + .../vuecomponents/table/rows/RowObjectBase.js | 31 ++++- .../table/rows/RowObjectsSourceBase.js | 8 +- .../vuecomponents/table/rows/renderer/Row.js | 9 +- 17 files changed, 477 insertions(+), 48 deletions(-) create mode 100644 pillar/api/nodes/activities.py create mode 100644 src/scripts/js/es6/common/api/users.js create mode 100644 src/scripts/js/es6/common/vuecomponents/mixins/BrowserHistoryState.js diff --git a/pillar/api/nodes/__init__.py b/pillar/api/nodes/__init__.py index 60b36881..7478f11c 100644 --- a/pillar/api/nodes/__init__.py +++ b/pillar/api/nodes/__init__.py @@ -6,7 +6,7 @@ import pymongo.errors import werkzeug.exceptions as wz_exceptions from flask import current_app, Blueprint, request -from pillar.api.nodes import eve_hooks, comments +from pillar.api.nodes import eve_hooks, comments, activities from pillar.api.utils import str2id, jsonify from pillar.api.utils.authorization import check_permissions, require_login from pillar.web.utils import pretty_date @@ -86,6 +86,12 @@ def post_node_comment_vote(node_path: str, comment_path: str): return comments.post_node_comment_vote(node_id, comment_id, vote) +@blueprint.route('//activities', methods=['GET']) +def activities_for_node(node_path: str): + node_id = str2id(node_path) + return jsonify(activities.for_node(node_id)) + + @blueprint.route('/tagged/') @blueprint.route('/tagged/') def tagged(tag=''): @@ -264,3 +270,5 @@ def setup_app(app, url_prefix): app.on_deleted_item_nodes += eve_hooks.after_deleting_node app.register_api_blueprint(blueprint, url_prefix=url_prefix) + + activities.setup_app(app) diff --git a/pillar/api/nodes/activities.py b/pillar/api/nodes/activities.py new file mode 100644 index 00000000..9a821d52 --- /dev/null +++ b/pillar/api/nodes/activities.py @@ -0,0 +1,43 @@ +from eve.methods import get + +from pillar.api.utils import gravatar + + +def for_node(node_id): + activities, _, _, status, _ =\ + get('activities', + { + '$or': [ + {'object_type': 'node', + 'object': node_id}, + {'context_object_type': 'node', + 'context_object': node_id}, + ], + },) + + for act in activities['_items']: + act['actor_user'] = _user_info(act['actor_user']) + + return activities + + +def _user_info(user_id): + users, _, _, status, _ = get('users', {'_id': user_id}) + if len(users['_items']) > 0: + user = users['_items'][0] + user['gravatar'] = gravatar(user['email']) + + public_fields = {'full_name', 'username', 'gravatar'} + for field in list(user.keys()): + if field not in public_fields: + del user[field] + + return user + return {} + + +def setup_app(app): + global _user_info + + decorator = app.cache.memoize(timeout=300, make_name='%s.public_user_info' % __name__) + _user_info = decorator(_user_info) diff --git a/pillar/api/utils/__init__.py b/pillar/api/utils/__init__.py index 47225f75..997aa4b8 100644 --- a/pillar/api/utils/__init__.py +++ b/pillar/api/utils/__init__.py @@ -223,7 +223,8 @@ def doc_diff(doc1, doc2, *, falsey_is_equal=True, superkey: str = None): function won't report differences between DoesNotExist, False, '', and 0. """ - private_keys = {'_id', '_etag', '_deleted', '_updated', '_created'} + def is_private(key): + return str(key).startswith('_') def combine_key(some_key): """Combine this key with the superkey. @@ -244,7 +245,7 @@ def doc_diff(doc1, doc2, *, falsey_is_equal=True, superkey: str = None): if isinstance(doc1, dict) and isinstance(doc2, dict): for key in set(doc1.keys()).union(set(doc2.keys())): - if key in private_keys: + if is_private(key): continue val1 = doc1.get(key, DoesNotExist) diff --git a/src/scripts/js/es6/common/api/init.js b/src/scripts/js/es6/common/api/init.js index dcb2cdb4..5b4b8000 100644 --- a/src/scripts/js/es6/common/api/init.js +++ b/src/scripts/js/es6/common/api/init.js @@ -1,3 +1,4 @@ export { thenMarkdownToHtml } from './markdown' export { thenGetProject } from './projects' -export { thenGetNodes } from './nodes' +export { thenGetNodes, thenGetNode, thenGetNodeActivities, thenUpdateNode, thenDeleteNode } from './nodes' +export { thenGetProjectUsers } from './users' diff --git a/src/scripts/js/es6/common/api/nodes.js b/src/scripts/js/es6/common/api/nodes.js index d02c622b..188a92b1 100644 --- a/src/scripts/js/es6/common/api/nodes.js +++ b/src/scripts/js/es6/common/api/nodes.js @@ -3,7 +3,80 @@ function thenGetNodes(where, embedded={}, sort='') { let encodedEmbedded = encodeURIComponent(JSON.stringify(embedded)); let encodedSort = encodeURIComponent(sort); - return $.get(`/api/nodes?where=${encodedWhere}&embedded=${encodedEmbedded}&sort=${encodedSort}`); + return $.ajax({ + url: `/api/nodes?where=${encodedWhere}&embedded=${encodedEmbedded}&sort=${encodedSort}`, + cache: false, + }); } -export { thenGetNodes } +function thenGetNode(nodeId) { + return $.ajax({ + url: `/api/nodes/${nodeId}`, + cache: false, + }); +} + +function thenGetNodeActivities(nodeId, sort='[("_created", -1)]', max_results=20, page=1) { + let encodedSort = encodeURIComponent(sort); + return $.ajax({ + url: `/api/nodes/${nodeId}/activities?sort=${encodedSort}&max_results=${max_results}&page=${page}`, + cache: false, + }); +} + +function thenUpdateNode(node) { + let id = node['_id']; + let etag = node['_etag']; + + let nodeToSave = removePrivateKeys(node); + let data = JSON.stringify(nodeToSave); + return $.ajax({ + url: `/api/nodes/${id}`, + type: 'PUT', + data: data, + dataType: 'json', + contentType: 'application/json; charset=UTF-8', + headers: {'If-Match': etag}, + }).then(updatedInfo => { + return thenGetNode(updatedInfo['_id']) + .then(node => { + pillar.events.Nodes.triggerUpdated(node); + return node; + }) + }); +} + +function thenDeleteNode(node) { + let id = node['_id']; + let etag = node['_etag']; + + return $.ajax({ + url: `/api/nodes/${id}`, + type: 'DELETE', + headers: {'If-Match': etag}, + }).then(() => { + pillar.events.Nodes.triggerDeleted(id); + }); +} + +function removePrivateKeys(doc) { + function doRemove(d) { + for (const key in d) { + if (key.startsWith('_')) { + delete d[key]; + continue; + } + let val = d[key]; + if(typeof val === 'object') { + doRemove(val); + } + } + } + let docCopy = JSON.parse(JSON.stringify(doc)); + doRemove(docCopy); + delete docCopy['allowed_methods'] + + return docCopy; +} + +export { thenGetNodes, thenGetNode, thenGetNodeActivities, thenUpdateNode, thenDeleteNode } diff --git a/src/scripts/js/es6/common/api/users.js b/src/scripts/js/es6/common/api/users.js new file mode 100644 index 00000000..f07f8d40 --- /dev/null +++ b/src/scripts/js/es6/common/api/users.js @@ -0,0 +1,7 @@ +function thenGetProjectUsers(projectId) { + return $.ajax({ + url: `/api/p/users?project_id=${projectId}`, + }); +} + +export { thenGetProjectUsers } diff --git a/src/scripts/js/es6/common/events/Nodes.js b/src/scripts/js/es6/common/events/Nodes.js index 2be3dfe6..1af26120 100644 --- a/src/scripts/js/es6/common/events/Nodes.js +++ b/src/scripts/js/es6/common/events/Nodes.js @@ -1,3 +1,14 @@ +/** + * Helper class to trigger/listen to global events on new/updated/deleted nodes. + * + * @example + * function myCallback(event) { + * console.log('Updated node:', event.detail); + * } + * Nodes.onUpdated('5c1cc4a5a013573d9787164b', myCallback); + * Nodes.triggerUpdated(myUpdatedNode); + */ + class EventName { static parentCreated(parentId, node_type) { return `pillar:node:${parentId}:created-${node_type}`; @@ -16,74 +27,115 @@ class EventName { } } +function trigger(eventName, data) { + document.dispatchEvent(new CustomEvent(eventName, {detail: data})); +} + +function on(eventName, cb) { + document.addEventListener(eventName, cb); +} + +function off(eventName, cb) { + document.removeEventListener(eventName, cb); +} + class Nodes { + /** + * Trigger events that node has been created + * @param {Object} node + */ static triggerCreated(node) { if (node.parent) { - $('body').trigger( + trigger( EventName.parentCreated(node.parent, node.node_type), node); } - $('body').trigger( + trigger( EventName.globalCreated(node.node_type), node); } + /** + * Get notified when new nodes where parent === parentId and node_type === node_type + * @param {String} parentId + * @param {String} node_type + * @param {Function(Event)} cb + */ static onParentCreated(parentId, node_type, cb){ - $('body').on( + on( EventName.parentCreated(parentId, node_type), cb); } static offParentCreated(parentId, node_type, cb){ - $('body').off( + off( EventName.parentCreated(parentId, node_type), cb); } + /** + * Get notified when new nodes where node_type === node_type is created + * @param {String} node_type + * @param {Function(Event)} cb + */ static onCreated(node_type, cb){ - $('body').on( + on( EventName.globalCreated(node_type), cb); } static offCreated(node_type, cb){ - $('body').off( + off( EventName.globalCreated(node_type), cb); } static triggerUpdated(node) { - $('body').trigger( + trigger( EventName.updated(node._id), node); } + /** + * Get notified when node with _id === nodeId is updated + * @param {String} nodeId + * @param {Function(Event)} cb + */ static onUpdated(nodeId, cb) { - $('body').on( + on( EventName.updated(nodeId), cb); } static offUpdated(nodeId, cb) { - $('body').off( + off( EventName.updated(nodeId), cb); } + /** + * Notify that node has been deleted. + * @param {String} nodeId + */ static triggerDeleted(nodeId) { - $('body').trigger( + trigger( EventName.deleted(nodeId), nodeId); } + /** + * Listen to events of new nodes where _id === nodeId + * @param {String} nodeId + * @param {Function(Event)} cb + */ static onDeleted(nodeId, cb) { - $('body').on( + on( EventName.deleted(nodeId), cb); } static offDeleted(nodeId, cb) { - $('body').off( + off( EventName.deleted(nodeId), cb); } diff --git a/src/scripts/js/es6/common/vuecomponents/comments/CommentEditor.js b/src/scripts/js/es6/common/vuecomponents/comments/CommentEditor.js index b552d77e..fd048bde 100644 --- a/src/scripts/js/es6/common/vuecomponents/comments/CommentEditor.js +++ b/src/scripts/js/es6/common/vuecomponents/comments/CommentEditor.js @@ -202,7 +202,10 @@ Vue.component('comment-editor', { this.unitOfWork( this.thenSubmit() .fail((err) => {toastr.error(pillar.utils.messageFromError(err), 'Failed to submit comment')}) - ); + ) + .then(() => { + EventBus.$emit(Events.EDIT_DONE, this.comment._id); + }); }, thenSubmit() { if (this.mode === 'reply') { @@ -220,7 +223,6 @@ Vue.component('comment-editor', { return thenCreateComment(this.parentId, this.msg, this.attachmentsAsObject) .then((newComment) => { EventBus.$emit(Events.NEW_COMMENT, newComment); - EventBus.$emit(Events.EDIT_DONE, newComment.id ); this.cleanUp(); }) }, @@ -228,7 +230,6 @@ Vue.component('comment-editor', { return thenUpdateComment(this.comment.parent, this.comment.id, this.msg, this.attachmentsAsObject) .then((updatedComment) => { EventBus.$emit(Events.UPDATED_COMMENT, updatedComment); - EventBus.$emit(Events.EDIT_DONE, updatedComment.id); this.cleanUp(); }) }, @@ -327,4 +328,4 @@ Vue.component('comment-editor', { elInputField.style.cssText = `height:${ newInputHeight }px`; } } -}); \ No newline at end of file +}); diff --git a/src/scripts/js/es6/common/vuecomponents/comments/CommentTree.js b/src/scripts/js/es6/common/vuecomponents/comments/CommentTree.js index bf556366..377b47d1 100644 --- a/src/scripts/js/es6/common/vuecomponents/comments/CommentTree.js +++ b/src/scripts/js/es6/common/vuecomponents/comments/CommentTree.js @@ -91,6 +91,9 @@ Vue.component('comments-tree', { } else { $(document).trigger('pillar:workStop'); } + }, + parentId() { + this.fetchComments(); } }, created() { @@ -98,24 +101,31 @@ Vue.component('comments-tree', { EventBus.$on(Events.EDIT_DONE, this.showReplyComponent); EventBus.$on(Events.NEW_COMMENT, this.onNewComment); EventBus.$on(Events.UPDATED_COMMENT, this.onCommentUpdated); - this.unitOfWork( - thenGetComments(this.parentId) - .then((commentsTree) => { - this.nbrOfComments = commentsTree['nbr_of_comments']; - this.comments = commentsTree['comments']; - this.projectId = commentsTree['project']; - }) - .fail((err) => {toastr.error(pillar.utils.messageFromError(err), 'Failed to load comments')}) - .always(()=>this.showLoadingPlaceholder = false) - ); + this.fetchComments() }, beforeDestroy() { EventBus.$off(Events.BEFORE_SHOW_EDITOR, this.doHideEditors); EventBus.$off(Events.EDIT_DONE, this.showReplyComponent); EventBus.$off(Events.NEW_COMMENT, this.onNewComment); EventBus.$off(Events.UPDATED_COMMENT, this.onCommentUpdated); + if(this.isBusyWorking) { + $(document).trigger('pillar:workStop'); + } }, methods: { + fetchComments() { + this.showLoadingPlaceholder = true; + this.unitOfWork( + thenGetComments(this.parentId) + .then((commentsTree) => { + this.nbrOfComments = commentsTree['nbr_of_comments']; + this.comments = commentsTree['comments']; + this.projectId = commentsTree['project']; + }) + .fail((err) => {toastr.error(pillar.utils.messageFromError(err), 'Failed to load comments')}) + .always(()=>this.showLoadingPlaceholder = false) + ); + }, doHideEditors() { this.replyHidden = true; }, @@ -134,6 +144,7 @@ Vue.component('comments-tree', { parentArray = parentComment.replies; } parentArray.unshift(newComment); + this.$emit('new-comment'); }, onCommentUpdated(updatedComment) { let commentInTree = this.findComment(this.comments, (comment) => { @@ -154,4 +165,4 @@ Vue.component('comments-tree', { } } }, -}); \ No newline at end of file +}); diff --git a/src/scripts/js/es6/common/vuecomponents/init.js b/src/scripts/js/es6/common/vuecomponents/init.js index 66bd1ca8..5f42b6b3 100644 --- a/src/scripts/js/es6/common/vuecomponents/init.js +++ b/src/scripts/js/es6/common/vuecomponents/init.js @@ -1,17 +1,20 @@ import './comments/CommentTree' import './customdirectives/click-outside' import { UnitOfWorkTracker } from './mixins/UnitOfWorkTracker' +import { BrowserHistoryState, StateSaveMode } from './mixins/BrowserHistoryState' import { PillarTable } from './table/Table' import { CellPrettyDate } from './table/cells/renderer/CellPrettyDate' import { CellDefault } from './table/cells/renderer/CellDefault' 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 { RowBase, RowState } from './table/rows/RowObjectBase' import { RowFilter } from './table/filter/RowFilter' let mixins = { - UnitOfWorkTracker + UnitOfWorkTracker, + BrowserHistoryState, + StateSaveMode } let table = { @@ -28,11 +31,12 @@ let table = { }, rows: { RowObjectsSourceBase, - RowBase + RowBase, + RowState, }, filter: { RowFilter - } + }, } export { mixins, table } diff --git a/src/scripts/js/es6/common/vuecomponents/mixins/BrowserHistoryState.js b/src/scripts/js/es6/common/vuecomponents/mixins/BrowserHistoryState.js new file mode 100644 index 00000000..e89fac6b --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/mixins/BrowserHistoryState.js @@ -0,0 +1,111 @@ +/** + * Vue helper mixin to push app state into browser history. + * + * How to use: + * Override browserHistoryState so it return the state you want to store + * Override historyStateUrl so it return the url you want to store with your state + * Override applyHistoryState to apply your state + */ + +const StateSaveMode = Object.freeze({ + IGNORE: Symbol("ignore"), + PUSH: Symbol("push"), + REPLACE: Symbol("replace") +}); + +let BrowserHistoryState = { + created() { + window.onpopstate = this._popHistoryState; + }, + data() { + return { + _lastApplyedHistoryState: undefined + } + }, + computed: { + /** + * Override and return state object + * @returns {Object} state object + */ + browserHistoryState() { + return {}; + }, + /** + * Override and return url to this state + * @returns {String} url to state + */ + historyStateUrl() { + return '' + } + }, + watch: { + browserHistoryState(newState) { + if(JSON.stringify(newState) === JSON.stringify(window.history.state)) return; // Don't save state on apply + + let mode = this.stateSaveMode(newState, window.history.state); + switch(mode) { + case StateSaveMode.IGNORE: break; + case StateSaveMode.PUSH: + this._pushHistoryState(); + break; + case StateSaveMode.REPLACE: + this._replaceHistoryState(); + break; + default: + console.log('Unknown state save mode', mode); + } + + } + }, + methods: { + /** + * Override to apply your state + * @param {Object} newState The state object you returned in @function browserHistoryState + */ + applyHistoryState(newState) { + + }, + /** + * Override to + * @param {Object} newState + * @param {Object} oldState + * @returns {StateSaveMode} Enum value to instruct how state change should be handled + */ + stateSaveMode(newState, oldState) { + if (!oldState) { + // Initial state. Replace what we have so we can go back to this state + return StateSaveMode.REPLACE; + } + return StateSaveMode.PUSH; + }, + _pushHistoryState() { + let currentState = this.browserHistoryState; + if (!currentState) return; + + let url = this.historyStateUrl; + window.history.pushState( + currentState, + undefined, + url + ); + }, + _replaceHistoryState() { + let currentState = this.browserHistoryState; + if (!currentState) return; + + let url = this.historyStateUrl; + window.history.replaceState( + currentState, + undefined, + url + ); + }, + _popHistoryState(event) { + let newState = event.state; + if (!newState) return; + this.applyHistoryState(newState); + }, + }, +} + +export { BrowserHistoryState, StateSaveMode } diff --git a/src/scripts/js/es6/common/vuecomponents/mixins/UnitOfWorkTracker.js b/src/scripts/js/es6/common/vuecomponents/mixins/UnitOfWorkTracker.js index 79743fdb..ebc3a3c1 100644 --- a/src/scripts/js/es6/common/vuecomponents/mixins/UnitOfWorkTracker.js +++ b/src/scripts/js/es6/common/vuecomponents/mixins/UnitOfWorkTracker.js @@ -39,6 +39,11 @@ var UnitOfWorkTracker = { } } }, + beforeDestroy() { + if(this.unitOfWorkCounter !== 0) { + this.$emit('unit-of-work', -this.unitOfWorkCounter); + } + }, methods: { unitOfWork(promise) { this.unitOfWorkBegin(); diff --git a/src/scripts/js/es6/common/vuecomponents/table/Table.js b/src/scripts/js/es6/common/vuecomponents/table/Table.js index e97357d2..a9ef4960 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/Table.js +++ b/src/scripts/js/es6/common/vuecomponents/table/Table.js @@ -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 =`
- +
@@ -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; }, } }); diff --git a/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/CellProxy.js b/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/CellProxy.js index 51570895..c5838cdc 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/CellProxy.js +++ b/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/CellProxy.js @@ -6,6 +6,7 @@ const TEMPLATE =` :rowObject="rowObject" :column="column" :rawCellValue="rawCellValue" + @item-clicked="$emit('item-clicked', ...arguments)" /> `; diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectBase.js b/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectBase.js index a8fde3ed..a24c99b6 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectBase.js +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectBase.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectsSourceBase.js b/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectsSourceBase.js index 6b88d22a..cb3d4139 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectsSourceBase.js +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectsSourceBase.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/renderer/Row.js b/src/scripts/js/es6/common/vuecomponents/table/rows/renderer/Row.js index b5581311..a74758a9 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/rows/renderer/Row.js +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/renderer/Row.js @@ -1,14 +1,17 @@ import '../../cells/renderer/CellProxy' + const TEMPLATE =`
`; @@ -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; } - } + }, });