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; } - } + }, });