From 379f7438648f26d19b0f71795ce1b75f16aef131 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Wed, 13 Mar 2019 13:53:40 +0100 Subject: [PATCH 01/66] 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; } - } + }, }); From 01da240f54b159083b43c97e7b46e312d05d1f64 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Wed, 13 Mar 2019 15:27:16 +0100 Subject: [PATCH 02/66] Attract multi edit: Shift + mouse to select all between and hopefully now command button on Mac works for multiselect. --- .../es6/common/vuecomponents/table/Table.js | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/scripts/js/es6/common/vuecomponents/table/Table.js b/src/scripts/js/es6/common/vuecomponents/table/Table.js index a9ef4960..810b6fd0 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/Table.js +++ b/src/scripts/js/es6/common/vuecomponents/table/Table.js @@ -137,15 +137,24 @@ let PillarTable = Vue.component('pillar-table-base', { }, onItemClicked(clickEvent, itemId) { if(!this.canChangeSelectionCB()) return; - - if(this.isMultiSelectClick(clickEvent) && this.canMultiSelect) { + if(this.isMultiToggleClick(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 { + } else if(this.isSelectBetween(clickEvent) && this.canMultiSelect) { + if (this.selectedIds.length > 0) { + let betweenA = this.selectedIds[this.selectedIds.length -1]; + let betweenB = itemId; + this.selectedIds = this.rowsBetween(betweenA, betweenB).map(it => it.getId()); + + } else { + this.selectedIds = [itemId]; + } + } + else { if (this.selectedIds.length === 1 && this.selectedIds[0] === itemId) { this.selectedIds = []; } else { @@ -153,9 +162,26 @@ let PillarTable = Vue.component('pillar-table-base', { } } }, - isMultiSelectClick(clickEvent) { - return clickEvent.ctrlKey; + isSelectBetween(clickEvent) { + return clickEvent.shiftKey; }, + isMultiToggleClick(clickEvent) { + return clickEvent.ctrlKey || + clickEvent.metaKey; // Mac command key + }, + rowsBetween(id1, id2) { + let hasFoundFirst = false; + let hasFoundLast = false; + return this.visibleRowObjects.filter((it) => { + if (hasFoundLast) return false; + if (!hasFoundFirst) { + hasFoundFirst = [id1, id2].includes(it.getId()); + return hasFoundFirst; + } + hasFoundLast = [id1, id2].includes(it.getId()); + return true; + }) + } } }); From 4136da110f4859c98ebc813e04f7189d90b1335c Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Thu, 14 Mar 2019 10:30:23 +0100 Subject: [PATCH 03/66] Added comments and minor refactoring --- .../js/es6/common/vuecomponents/init.js | 6 +- .../es6/common/vuecomponents/table/Table.js | 62 ++++++++++++++++--- .../table/cells/renderer/CellDefault.js | 4 ++ .../table/cells/renderer/CellPrettyDate.js | 4 ++ .../table/cells/renderer/CellProxy.js | 21 ++++++- .../table/cells/renderer/HeadCell.js | 5 ++ .../vuecomponents/table/columns/ColumnBase.js | 13 +++- .../table/columns/ColumnFactoryBase.js | 9 ++- .../table/columns/renderer/Column.js | 10 --- .../table/filter/ColumnFilter.js | 27 +++++--- .../vuecomponents/table/filter/RowFilter.js | 3 + .../vuecomponents/table/rows/RowObjectBase.js | 33 +++++----- .../table/rows/RowObjectsSourceBase.js | 14 ++++- .../vuecomponents/table/rows/renderer/Head.js | 4 +- .../vuecomponents/table/rows/renderer/Row.js | 4 +- 15 files changed, 162 insertions(+), 57 deletions(-) delete mode 100644 src/scripts/js/es6/common/vuecomponents/table/columns/renderer/Column.js diff --git a/src/scripts/js/es6/common/vuecomponents/init.js b/src/scripts/js/es6/common/vuecomponents/init.js index 5f42b6b3..6cd6b180 100644 --- a/src/scripts/js/es6/common/vuecomponents/init.js +++ b/src/scripts/js/es6/common/vuecomponents/init.js @@ -2,13 +2,13 @@ import './comments/CommentTree' import './customdirectives/click-outside' import { UnitOfWorkTracker } from './mixins/UnitOfWorkTracker' 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 { 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, RowState } from './table/rows/RowObjectBase' +import { RowBase } from './table/rows/RowObjectBase' import { RowFilter } from './table/filter/RowFilter' let mixins = { @@ -19,6 +19,7 @@ let mixins = { let table = { PillarTable, + TableState, columns: { ColumnBase, ColumnFactoryBase, @@ -32,7 +33,6 @@ let table = { rows: { RowObjectsSourceBase, RowBase, - RowState, }, filter: { RowFilter diff --git a/src/scripts/js/es6/common/vuecomponents/table/Table.js b/src/scripts/js/es6/common/vuecomponents/table/Table.js index 810b6fd0..bc9149b2 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/Table.js +++ b/src/scripts/js/es6/common/vuecomponents/table/Table.js @@ -3,7 +3,25 @@ import './rows/renderer/Row' import './filter/ColumnFilter' import './filter/RowFilter' 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 =`
`; +/** + * 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', { template: TEMPLATE, mixins: [UnitOfWorkTracker], @@ -64,15 +96,18 @@ let PillarTable = Vue.component('pillar-table-base', { visibleRowObjects: [], rowsSource: {}, isInitialized: false, - compareRows: (row1, row2) => 0 + compareRowsCB: (row1, row2) => 0 } }, computed: { rowObjects() { return this.rowsSource.rowObjects || []; }, + /** + * Rows sorted with a column sorter + */ sortedRowObjects() { - return this.rowObjects.concat().sort(this.compareRows); + return this.rowObjects.concat().sort(this.compareRowsCB); }, rowAndChildObjects() { let all = []; @@ -105,19 +140,19 @@ let PillarTable = Vue.component('pillar-table-base', { let columnFactory = new this.$options.columnFactory(this.projectId); this.rowsSource = new this.$options.rowsSource(this.projectId); - let rowState = new RowState(this.selectedIds); + let tableState = new TableState(this.selectedIds); this.unitOfWork( Promise.all([ columnFactory.thenGetColumns(), - this.rowsSource.thenFetchObjects() + this.rowsSource.thenGetRowObjects() ]) .then((resp) => { this.columns = resp[0]; return this.rowsSource.thenInit(); }) .then(() => { - this.rowAndChildObjects.forEach(rowState.applyState.bind(rowState)); + this.rowAndChildObjects.forEach(tableState.applyRowState.bind(tableState)); this.isInitialized = true; }) ); @@ -133,10 +168,11 @@ let PillarTable = Vue.component('pillar-table-base', { function compareRows(r1, r2) { return column.compareRows(r1, r2) * direction; } - this.compareRows = compareRows; + this.compareRowsCB = compareRows; }, onItemClicked(clickEvent, itemId) { if(!this.canChangeSelectionCB()) return; + if(this.isMultiToggleClick(clickEvent) && this.canMultiSelect) { let slectedIdsWithoutClicked = this.selectedIds.filter(id => id !== itemId); if (slectedIdsWithoutClicked.length < this.selectedIds.length) { @@ -144,7 +180,7 @@ let PillarTable = Vue.component('pillar-table-base', { } else { this.selectedIds = [itemId, ...this.selectedIds]; } - } else if(this.isSelectBetween(clickEvent) && this.canMultiSelect) { + } else if(this.isSelectBetweenClick(clickEvent) && this.canMultiSelect) { if (this.selectedIds.length > 0) { let betweenA = this.selectedIds[this.selectedIds.length -1]; let betweenB = itemId; @@ -162,13 +198,19 @@ let PillarTable = Vue.component('pillar-table-base', { } } }, - isSelectBetween(clickEvent) { + isSelectBetweenClick(clickEvent) { return clickEvent.shiftKey; }, isMultiToggleClick(clickEvent) { return clickEvent.ctrlKey || clickEvent.metaKey; // Mac command key }, + /** + * Get visible rows between id1 and id2 + * @param {String} id1 + * @param {String} id2 + * @returns {Array(RowObjects)} + */ rowsBetween(id1, id2) { let hasFoundFirst = false; let hasFoundLast = false; @@ -185,4 +227,4 @@ let PillarTable = Vue.component('pillar-table-base', { } }); -export { PillarTable } +export { PillarTable, TableState } diff --git a/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/CellDefault.js b/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/CellDefault.js index e3c433be..8a9a71f1 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/CellDefault.js +++ b/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/CellDefault.js @@ -4,6 +4,10 @@ const TEMPLATE =`
`; +/** + * Default cell renderer. Takes raw cell value and formats it. + * Override for custom formatting of value. + */ let CellDefault = Vue.component('pillar-cell-default', { template: TEMPLATE, props: { diff --git a/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/CellPrettyDate.js b/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/CellPrettyDate.js index f158d552..dc884568 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/CellPrettyDate.js +++ b/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/CellPrettyDate.js @@ -1,5 +1,9 @@ import { CellDefault } from './CellDefault' +/** + * Formats raw values as "pretty date". + * Expects rawCellValue to be a date. + */ let CellPrettyDate = Vue.component('pillar-cell-pretty-date', { extends: CellDefault, computed: { 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 c5838cdc..ac3322de 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 @@ -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', { template: TEMPLATE, props: { - column: Object, - rowObject: Object + column: Object, // ColumnBase + rowObject: Object // RowObject }, computed: { + /** + * Raw unformated cell value + */ rawCellValue() { return this.column.getRawCellValue(this.rowObject) || ''; }, + /** + * Name of the cell render component to be rendered + */ cellRenderer() { return this.column.getCellRenderer(this.rowObject); }, + /** + * Css classes to apply to the cell + */ cellClasses() { return this.column.getCellClasses(this.rawCellValue, this.rowObject); }, + /** + * Cell tooltip + */ cellTitle() { return this.column.getCellTitle(this.rawCellValue, this.rowObject); } diff --git a/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/HeadCell.js b/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/HeadCell.js index 97facd37..3416b20c 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/HeadCell.js +++ b/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/HeadCell.js @@ -22,6 +22,11 @@ const TEMPLATE =`
`; +/** + * A cell in the Header of the table + * + * @emits sort(column,direction) When user clicks column sort arrows. + */ Vue.component('pillar-head-cell', { template: TEMPLATE, props: { diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js b/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js index 29cfa8f9..a3532e96 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js +++ b/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js @@ -1,5 +1,9 @@ import { CellDefault } from '../cells/renderer/CellDefault' +/** + * Column logic + */ + let nextColumnId = 0; export class ColumnBase { constructor(displayName, columnType) { @@ -13,13 +17,18 @@ export class ColumnBase { /** * - * @param {*} rowObject + * @param {RowObject} rowObject * @returns {String} Name of the Cell renderer component */ getCellRenderer(rowObject) { return CellDefault.options.name; } + /** + * + * @param {RowObject} rowObject + * @returns {*} Raw unformated value + */ getRawCellValue(rowObject) { // Should be overridden throw Error('Not implemented'); @@ -38,7 +47,7 @@ export class ColumnBase { /** * Object with css classes to use on the header cell - * @returns {Any} Object with css classes + * @returns {Object} Object with css classes */ getHeaderCellClasses() { // Should be overridden diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnFactoryBase.js b/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnFactoryBase.js index 18c1fa6a..878782e9 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnFactoryBase.js +++ b/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnFactoryBase.js @@ -1,10 +1,16 @@ +/** + * Provides the columns that are available in a table. + */ class ColumnFactoryBase{ constructor(projectId) { this.projectId = projectId; this.projectPromise; } - // Override this + /** + * To be overridden for your purposes + * @returns {Promise(ColumnBase)} The columns that are available in the table. + */ thenGetColumns() { throw Error('Not implemented') } @@ -19,3 +25,4 @@ class ColumnFactoryBase{ } export { ColumnFactoryBase } + diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/renderer/Column.js b/src/scripts/js/es6/common/vuecomponents/table/columns/renderer/Column.js deleted file mode 100644 index d22392bc..00000000 --- a/src/scripts/js/es6/common/vuecomponents/table/columns/renderer/Column.js +++ /dev/null @@ -1,10 +0,0 @@ -const TEMPLATE =` -
-`; - -Vue.component('pillar-table-column', { - template: TEMPLATE, - props: { - column: Object - }, -}); diff --git a/src/scripts/js/es6/common/vuecomponents/table/filter/ColumnFilter.js b/src/scripts/js/es6/common/vuecomponents/table/filter/ColumnFilter.js index 222e9532..0ca380a4 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/filter/ColumnFilter.js +++ b/src/scripts/js/es6/common/vuecomponents/table/filter/ColumnFilter.js @@ -25,14 +25,27 @@ const TEMPLATE =`
`; +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', { template: TEMPLATE, props: { - columns: Array, + columns: Array, // Instances of ColumnBase }, data() { return { - columnStates: [], + columnStates: [], // Instances of ColumnState } }, computed: { @@ -57,18 +70,16 @@ let Filter = Vue.component('pillar-table-column-filter', { setColumnStates() { return this.columns.reduce((states, c) => { if (!c.isMandatory) { - states.push({ - _id: c._id, - displayName: c.displayName, - isVisible: true, - }); + states.push( + new ColumnState(c._id, c.displayName, true) + ); } return states; }, []) }, isColumnStateVisible(column) { for (let state of this.columnStates) { - if (state._id === column._id) { + if (state.id === column._id) { return state.isVisible; } } diff --git a/src/scripts/js/es6/common/vuecomponents/table/filter/RowFilter.js b/src/scripts/js/es6/common/vuecomponents/table/filter/RowFilter.js index 098627a2..6282c200 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/filter/RowFilter.js +++ b/src/scripts/js/es6/common/vuecomponents/table/filter/RowFilter.js @@ -7,6 +7,9 @@ const TEMPLATE =` `; +/** + * @emits visibleRowObjectsChanged(rowObjects) When the what objects to be visible has changed. + */ let RowFilter = Vue.component('pillar-table-row-filter', { template: TEMPLATE, props: { 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 a24c99b6..02394e43 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectBase.js +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectBase.js @@ -1,26 +1,16 @@ -class RowState { - constructor(selectedIds) { - this.selectedIds = selectedIds || []; - } - - /** - * - * @param {RowBase} rowObject - */ - applyState(rowObject) { - rowObject.isSelected = this.selectedIds.includes(rowObject.getId()); - } -} - +/** + * Each object to be visualized in the table is wrapped in a RowBase object. Column cells interact with it, + */ class RowBase { constructor(underlyingObject) { this.underlyingObject = underlyingObject; this.isInitialized = false; - this.isVisible = true; this.isSelected = false; } - + /** + * Called after the row has been created to initalize async properties. Fetching child objects for instance + */ thenInit() { return this._thenInitImpl() .then(() => { @@ -28,6 +18,9 @@ class RowBase { }) } + /** + * Override to initialize async properties such as fetching child objects. + */ _thenInitImpl() { return Promise.resolve(); } @@ -44,15 +37,21 @@ class RowBase { return this.underlyingObject.properties; } + /** + * The css classes that should be applied to the row in the table + */ getRowClasses() { return { "is-busy": !this.isInitialized } } + /** + * A row could have children (shots has tasks for example). Children should also be instances of RowObject + */ getChildObjects() { return []; } } -export { RowBase, RowState } +export { RowBase } 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 cb3d4139..429b733a 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectsSourceBase.js +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectsSourceBase.js @@ -1,14 +1,24 @@ +/** + * The provider of RowObjects to a table. + * Extend to fit your purpose. + */ class RowObjectsSourceBase { constructor(projectId) { this.projectId = projectId; 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'); } + /** + * Inits all its row objects. + */ thenInit() { return Promise.all( this.rowObjects.map(it => it.thenInit()) diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/renderer/Head.js b/src/scripts/js/es6/common/vuecomponents/table/rows/renderer/Head.js index d71a88b2..d614f8e1 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/rows/renderer/Head.js +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/renderer/Head.js @@ -9,7 +9,9 @@ const TEMPLATE =` /> `; - +/** + * @emits sort(column,direction) When a column head has been clicked + */ Vue.component('pillar-table-head', { template: TEMPLATE, props: { 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 a74758a9..7f2e0e31 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 @@ -15,7 +15,9 @@ const TEMPLATE =` /> `; - +/** + * @emits item-clicked(mouseEvent,itemId) When a RowObject has been clicked + */ Vue.component('pillar-table-row', { template: TEMPLATE, props: { From ace091c9987deabe63d0b581ed2e249c853917b3 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Thu, 14 Mar 2019 10:50:46 +0100 Subject: [PATCH 04/66] Row selection before table fully inited failed If a row was selected before table was fully initialized it would be unselected once the row was fully initialized. --- src/scripts/js/es6/common/vuecomponents/table/Table.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/scripts/js/es6/common/vuecomponents/table/Table.js b/src/scripts/js/es6/common/vuecomponents/table/Table.js index bc9149b2..a4674f42 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/Table.js +++ b/src/scripts/js/es6/common/vuecomponents/table/Table.js @@ -152,6 +152,11 @@ let PillarTable = Vue.component('pillar-table-base', { return this.rowsSource.thenInit(); }) .then(() => { + let currentlySelectedIds = this.selectedItems.map(it => it._id); + if (currentlySelectedIds.length > 0) { + // User has clicked on a row while we inited the rows. Keep that selection! + tableState.selectedIds = currentlySelectedIds; + } this.rowAndChildObjects.forEach(tableState.applyRowState.bind(tableState)); this.isInitialized = true; }) From 58ff236a998b12af902b22d3c636da46dfd543f6 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Fri, 15 Mar 2019 10:18:23 +0100 Subject: [PATCH 05/66] Generalized table to not depend on project id --- .../js/es6/common/vuecomponents/table/Table.js | 11 ++++------- .../table/columns/ColumnFactoryBase.js | 13 ------------- .../table/rows/RowObjectsSourceBase.js | 3 +-- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/src/scripts/js/es6/common/vuecomponents/table/Table.js b/src/scripts/js/es6/common/vuecomponents/table/Table.js index a4674f42..a8eaed3a 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/Table.js +++ b/src/scripts/js/es6/common/vuecomponents/table/Table.js @@ -75,8 +75,6 @@ const TEMPLATE =` let PillarTable = Vue.component('pillar-table-base', { template: TEMPLATE, mixins: [UnitOfWorkTracker], - // columnFactory, - // rowsSource, props: { projectId: String, selectedIds: Array, @@ -94,7 +92,8 @@ let PillarTable = Vue.component('pillar-table-base', { columns: [], visibleColumns: [], visibleRowObjects: [], - rowsSource: {}, + rowsSource: undefined, // Override with your implementations of ColumnFactoryBase + columnFactory: undefined, // Override with your implementations of RowSource isInitialized: false, compareRowsCB: (row1, row2) => 0 } @@ -137,14 +136,11 @@ let PillarTable = Vue.component('pillar-table-base', { } }, created() { - let columnFactory = new this.$options.columnFactory(this.projectId); - this.rowsSource = new this.$options.rowsSource(this.projectId); - let tableState = new TableState(this.selectedIds); this.unitOfWork( Promise.all([ - columnFactory.thenGetColumns(), + this.columnFactory.thenGetColumns(), this.rowsSource.thenGetRowObjects() ]) .then((resp) => { @@ -160,6 +156,7 @@ let PillarTable = Vue.component('pillar-table-base', { this.rowAndChildObjects.forEach(tableState.applyRowState.bind(tableState)); this.isInitialized = true; }) + .catch((err) => {toastr.error(pillar.utils.messageFromError(err), 'Loading table failed')}) ); }, methods: { diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnFactoryBase.js b/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnFactoryBase.js index 878782e9..6612a023 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnFactoryBase.js +++ b/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnFactoryBase.js @@ -2,11 +2,6 @@ * Provides the columns that are available in a table. */ class ColumnFactoryBase{ - constructor(projectId) { - this.projectId = projectId; - this.projectPromise; - } - /** * To be overridden for your purposes * @returns {Promise(ColumnBase)} The columns that are available in the table. @@ -14,14 +9,6 @@ class ColumnFactoryBase{ thenGetColumns() { throw Error('Not implemented') } - - thenGetProject() { - if (this.projectPromise) { - return this.projectPromise; - } - this.projectPromise = pillar.api.thenGetProject(this.projectId); - return this.projectPromise; - } } export { ColumnFactoryBase } 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 429b733a..467d8e00 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectsSourceBase.js +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectsSourceBase.js @@ -3,8 +3,7 @@ * Extend to fit your purpose. */ class RowObjectsSourceBase { - constructor(projectId) { - this.projectId = projectId; + constructor() { this.rowObjects = []; } From cfff5ef18977f0c0a8f0b25a8e4f14b0c7c80c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Mon, 18 Mar 2019 14:18:54 +0100 Subject: [PATCH 06/66] Fixed redirects ignoring the 'next_after_login` session variable There were a few redirects (for example, trying to log in while already logged in) that would incorrectly redirect to the main page. They use the `next_after_login` session variable now. --- pillar/web/users/routes.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pillar/web/users/routes.py b/pillar/web/users/routes.py index 79136eb5..73073e4a 100644 --- a/pillar/web/users/routes.py +++ b/pillar/web/users/routes.py @@ -31,8 +31,10 @@ def check_oauth_provider(provider): @blueprint.route('/authorize/') def oauth_authorize(provider): - if not current_user.is_anonymous: - return redirect(url_for('main.homepage')) + if current_user.is_authenticated: + next_after_login = session.pop('next_after_login', None) or url_for('main.homepage') + log.debug('Redirecting user to %s', next_after_login) + return redirect(next_after_login) try: oauth = OAuthSignIn.get_provider(provider) @@ -52,8 +54,10 @@ def oauth_callback(provider): from pillar.api.utils.authentication import store_token from pillar.api.utils import utcnow + next_after_login = session.pop('next_after_login', None) or url_for('main.homepage') if current_user.is_authenticated: - return redirect(url_for('main.homepage')) + log.debug('Redirecting user to %s', next_after_login) + return redirect(next_after_login) oauth = OAuthSignIn.get_provider(provider) try: @@ -63,7 +67,7 @@ def oauth_callback(provider): raise wz_exceptions.Forbidden() if oauth_user.id is None: log.debug('Authentication failed for user with {}'.format(provider)) - return redirect(url_for('main.homepage')) + return redirect(next_after_login) # Find or create user user_info = {'id': oauth_user.id, 'email': oauth_user.email, 'full_name': ''} @@ -88,11 +92,8 @@ def oauth_callback(provider): # Check with Blender ID to update certain user roles. update_subscription() - next_after_login = session.pop('next_after_login', None) - if next_after_login: - log.debug('Redirecting user to %s', next_after_login) - return redirect(next_after_login) - return redirect(url_for('main.homepage')) + log.debug('Redirecting user to %s', next_after_login) + return redirect(next_after_login) @blueprint.route('/login') From 0ee1d0d3dad4b447f433dc85d46eeaf07376d440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Mon, 18 Mar 2019 14:42:00 +0100 Subject: [PATCH 07/66] Allow HTTP headers to be set for @require_login() error responses This makes the `require_login` decorator always return a Flask response. Previously it could also raise a `Forbidden` exception; now it returns a 403 Forbidden response in that case too. --- pillar/api/utils/authorization.py | 17 ++++++++++++++--- tests/test_api/test_auth.py | 28 ++++++++++++++++++---------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/pillar/api/utils/authorization.py b/pillar/api/utils/authorization.py index b7d8f4c9..1a55cdc0 100644 --- a/pillar/api/utils/authorization.py +++ b/pillar/api/utils/authorization.py @@ -289,7 +289,8 @@ def require_login(*, require_roles=set(), require_cap='', require_all=False, redirect_to_login=False, - error_view=None): + error_view=None, + error_headers: typing.Optional[typing.Dict[str, str]]=None): """Decorator that enforces users to authenticate. Optionally only allows access to users with a certain role and/or capability. @@ -313,6 +314,7 @@ def require_login(*, require_roles=set(), requests, and mimicks the flask_login behaviour. :param error_view: Callable that returns a Flask response object. This is sent back to the client instead of the default 403 Forbidden. + :param error_headers: HTTP headers to include in error responses. """ from flask import request, redirect, url_for, Response @@ -331,9 +333,18 @@ def require_login(*, require_roles=set(), def render_error() -> Response: if error_view is None: - abort(403) - resp: Response = error_view() + resp = Forbidden().get_response() + else: + resp = error_view() resp.status_code = 403 + if error_headers: + for header_name, header_value in error_headers.items(): + resp.headers.set(header_name, header_value) + + if 'Access-Control-Allow-Origin' in error_headers: + origin = request.headers.get('Origin', '') + resp.headers.set('Access-Control-Allow-Origin', origin) + return resp def decorator(func): diff --git a/tests/test_api/test_auth.py b/tests/test_api/test_auth.py index 49cad737..349da074 100644 --- a/tests/test_api/test_auth.py +++ b/tests/test_api/test_auth.py @@ -631,21 +631,25 @@ class RequireRolesTest(AbstractPillarTest): def test_some_roles_required(self): from pillar.api.utils.authorization import require_login - called = [False] + called = False @require_login(require_roles={'admin'}) def call_me(): - called[0] = True + nonlocal called + called = True + return None with self.app.test_request_context(): self.login_api_as(ObjectId(24 * 'a'), ['succubus']) - self.assertRaises(Forbidden, call_me) - self.assertFalse(called[0]) + resp = call_me() + self.assertEqual(403, resp.status_code) + self.assertFalse(called, 'Forbidden function should not have been called') with self.app.test_request_context(): self.login_api_as(ObjectId(24 * 'a'), ['admin']) - call_me() - self.assertTrue(called[0]) + resp = call_me() + self.assertIsNone(resp) + self.assertTrue(called) def test_all_roles_required(self): from pillar.api.utils.authorization import require_login @@ -659,17 +663,20 @@ class RequireRolesTest(AbstractPillarTest): with self.app.test_request_context(): self.login_api_as(ObjectId(24 * 'a'), ['admin']) - self.assertRaises(Forbidden, call_me) + resp = call_me() + self.assertEqual(403, resp.status_code) self.assertFalse(called[0]) with self.app.test_request_context(): self.login_api_as(ObjectId(24 * 'a'), ['service']) - self.assertRaises(Forbidden, call_me) + resp = call_me() + self.assertEqual(403, resp.status_code) self.assertFalse(called[0]) with self.app.test_request_context(): self.login_api_as(ObjectId(24 * 'a'), ['badger']) - self.assertRaises(Forbidden, call_me) + resp = call_me() + self.assertEqual(403, resp.status_code) self.assertFalse(called[0]) with self.app.test_request_context(): @@ -702,7 +709,8 @@ class RequireRolesTest(AbstractPillarTest): with self.app.test_request_context(): self.login_api_as(ObjectId(24 * 'a'), ['succubus']) - self.assertRaises(Forbidden, call_me) + resp = call_me() + self.assertEqual(403, resp.status_code) self.assertFalse(called[0]) with self.app.test_request_context(): From a104117618da0a4974fca622aab5864e1f234b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 19 Mar 2019 10:55:15 +0100 Subject: [PATCH 08/66] Added pillar.auth.cors.allow() decorator Use this decorator on Flask endpoints that should respond with CORS headers. These headers are sent in a reply when the browser sends an `Origin` request header; for more info see [1]. This commit rolls back the previous commit (0ee1d0d3), as this new approach with a separate decorator is both easier to use and less error-prone. [1] https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS --- pillar/api/utils/authorization.py | 12 +-- pillar/auth/cors.py | 48 +++++++++++ tests/test_auth/__init__.py | 0 tests/test_auth/test_cors.py | 127 ++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 pillar/auth/cors.py create mode 100644 tests/test_auth/__init__.py create mode 100644 tests/test_auth/test_cors.py diff --git a/pillar/api/utils/authorization.py b/pillar/api/utils/authorization.py index 1a55cdc0..9595114b 100644 --- a/pillar/api/utils/authorization.py +++ b/pillar/api/utils/authorization.py @@ -289,8 +289,7 @@ def require_login(*, require_roles=set(), require_cap='', require_all=False, redirect_to_login=False, - error_view=None, - error_headers: typing.Optional[typing.Dict[str, str]]=None): + error_view=None): """Decorator that enforces users to authenticate. Optionally only allows access to users with a certain role and/or capability. @@ -314,7 +313,6 @@ def require_login(*, require_roles=set(), requests, and mimicks the flask_login behaviour. :param error_view: Callable that returns a Flask response object. This is sent back to the client instead of the default 403 Forbidden. - :param error_headers: HTTP headers to include in error responses. """ from flask import request, redirect, url_for, Response @@ -337,14 +335,6 @@ def require_login(*, require_roles=set(), else: resp = error_view() resp.status_code = 403 - if error_headers: - for header_name, header_value in error_headers.items(): - resp.headers.set(header_name, header_value) - - if 'Access-Control-Allow-Origin' in error_headers: - origin = request.headers.get('Origin', '') - resp.headers.set('Access-Control-Allow-Origin', origin) - return resp def decorator(func): diff --git a/pillar/auth/cors.py b/pillar/auth/cors.py new file mode 100644 index 00000000..10515042 --- /dev/null +++ b/pillar/auth/cors.py @@ -0,0 +1,48 @@ +"""Support for adding CORS headers to responses.""" + +import functools + +import flask +import werkzeug.wrappers as wz_wrappers +import werkzeug.exceptions as wz_exceptions + + +def allow(*, allow_credentials=False): + """Flask endpoint decorator, adds CORS headers to the response. + + If the request has a non-empty 'Origin' header, the response header + 'Access-Control-Allow-Origin' is set to the value of that request header, + and some other CORS headers are set. + """ + def decorator(wrapped): + @functools.wraps(wrapped) + def wrapper(*args, **kwargs): + request_origin = flask.request.headers.get('Origin') + if not request_origin: + # No CORS headers requested, so don't bother touching the response. + return wrapped(*args, **kwargs) + + try: + response = wrapped(*args, **kwargs) + except wz_exceptions.HTTPException as ex: + response = ex.get_response() + else: + if isinstance(response, tuple): + response = flask.make_response(*response) + elif isinstance(response, str): + response = flask.make_response(response) + elif isinstance(response, wz_wrappers.Response): + pass + else: + raise TypeError(f'unknown response type {type(response)}') + + assert isinstance(response, wz_wrappers.Response) + + response.headers.set('Access-Control-Allow-Origin', request_origin) + response.headers.set('Access-Control-Allow-Headers', 'x-requested-with') + if allow_credentials: + response.headers.set('Access-Control-Allow-Credentials', 'true') + + return response + return wrapper + return decorator diff --git a/tests/test_auth/__init__.py b/tests/test_auth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_auth/test_cors.py b/tests/test_auth/test_cors.py new file mode 100644 index 00000000..9649ba32 --- /dev/null +++ b/tests/test_auth/test_cors.py @@ -0,0 +1,127 @@ +from pillar.tests import AbstractPillarTest + +import flask +import werkzeug.wrappers as wz_wrappers +import werkzeug.exceptions as wz_exceptions + + +class CorsWrapperTest(AbstractPillarTest): + def test_noncors_request(self): + from pillar.auth.cors import allow + + @allow() + def wrapped(a, b): + return f'{a} and {b}' + + with self.app.test_request_context(): + resp = wrapped('x', 'y') + + self.assertEqual('x and y', resp, 'Non-CORS request should not be modified') + + def test_string_response(self): + from pillar.auth.cors import allow + + @allow() + def wrapped(a, b): + return f'{a} and {b}' + + with self.app.test_request_context(headers={'Origin': 'http://jemoeder.nl:1234/'}): + resp = wrapped('x', 'y') + + self.assertIsInstance(resp, wz_wrappers.Response) + self.assertEqual(b'x and y', resp.data) + self.assertEqual(200, resp.status_code) + + self.assertEqual('http://jemoeder.nl:1234/', resp.headers['Access-Control-Allow-Origin']) + self.assertEqual('x-requested-with', resp.headers['Access-Control-Allow-Headers']) + self.assertNotIn('Access-Control-Allow-Credentials', resp.headers) + + def test_string_with_code_response(self): + from pillar.auth.cors import allow + + @allow() + def wrapped(a, b): + return f'{a} and {b}', 403 + + with self.app.test_request_context(headers={'Origin': 'http://jemoeder.nl:1234/'}): + resp = wrapped('x', 'y') + + self.assertIsInstance(resp, wz_wrappers.Response) + self.assertEqual(b'x and y', resp.data) + self.assertEqual(403, resp.status_code) + + self.assertEqual('http://jemoeder.nl:1234/', resp.headers['Access-Control-Allow-Origin']) + self.assertEqual('x-requested-with', resp.headers['Access-Control-Allow-Headers']) + self.assertNotIn('Access-Control-Allow-Credentials', resp.headers) + + def test_flask_response_object(self): + from pillar.auth.cors import allow + + @allow() + def wrapped(a, b): + return flask.Response(f'{a} and {b}', status=147, headers={'op-je': 'hoofd'}) + + with self.app.test_request_context(headers={'Origin': 'http://jemoeder.nl:1234/'}): + resp = wrapped('x', 'y') + + self.assertIsInstance(resp, wz_wrappers.Response) + self.assertEqual(b'x and y', resp.data) + self.assertEqual(147, resp.status_code) + self.assertEqual('hoofd', resp.headers['Op-Je']) + + self.assertEqual('http://jemoeder.nl:1234/', resp.headers['Access-Control-Allow-Origin']) + self.assertEqual('x-requested-with', resp.headers['Access-Control-Allow-Headers']) + self.assertNotIn('Access-Control-Allow-Credentials', resp.headers) + + def test_wz_exception(self): + from pillar.auth.cors import allow + + @allow() + def wrapped(a, b): + raise wz_exceptions.NotImplemented('nee') + + with self.app.test_request_context(headers={'Origin': 'http://jemoeder.nl:1234/'}): + resp = wrapped('x', 'y') + + self.assertIsInstance(resp, wz_wrappers.Response) + self.assertIn(b'nee', resp.data) + self.assertEqual(501, resp.status_code) + + self.assertEqual('http://jemoeder.nl:1234/', resp.headers['Access-Control-Allow-Origin']) + self.assertEqual('x-requested-with', resp.headers['Access-Control-Allow-Headers']) + self.assertNotIn('Access-Control-Allow-Credentials', resp.headers) + + def test_flask_abort(self): + from pillar.auth.cors import allow + + @allow() + def wrapped(a, b): + raise flask.abort(401) + + with self.app.test_request_context(headers={'Origin': 'http://jemoeder.nl:1234/'}): + resp = wrapped('x', 'y') + + self.assertIsInstance(resp, wz_wrappers.Response) + self.assertEqual(401, resp.status_code) + + self.assertEqual('http://jemoeder.nl:1234/', resp.headers['Access-Control-Allow-Origin']) + self.assertEqual('x-requested-with', resp.headers['Access-Control-Allow-Headers']) + self.assertNotIn('Access-Control-Allow-Credentials', resp.headers) + + def test_with_credentials(self): + from pillar.auth.cors import allow + + @allow(allow_credentials=True) + def wrapped(a, b): + return f'{a} and {b}' + + with self.app.test_request_context(headers={'Origin': 'http://jemoeder.nl:1234/'}): + resp = wrapped('x', 'y') + + self.assertIsInstance(resp, wz_wrappers.Response) + self.assertEqual(b'x and y', resp.data) + self.assertEqual(200, resp.status_code) + + self.assertEqual('http://jemoeder.nl:1234/', resp.headers['Access-Control-Allow-Origin']) + self.assertEqual('x-requested-with', resp.headers['Access-Control-Allow-Headers']) + self.assertEqual('true', resp.headers['Access-Control-Allow-Credentials']) From 66e6ba146721214220e961e6a3a2f0c017b16339 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Wed, 20 Mar 2019 15:12:19 +0100 Subject: [PATCH 09/66] Move table css from attract to pillar repo --- src/styles/base.sass | 1 + src/styles/components/_pillar_table.sass | 146 +++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 src/styles/components/_pillar_table.sass diff --git a/src/styles/base.sass b/src/styles/base.sass index 6a5ac813..db61fb0c 100644 --- a/src/styles/base.sass +++ b/src/styles/base.sass @@ -69,6 +69,7 @@ @import components/tooltip @import components/checkbox @import components/overlay +@import components/pillar_table /* Top level, standalone stylesheets (not starting with _ so not meant for importing) * should not have pure styling here. diff --git a/src/styles/components/_pillar_table.sass b/src/styles/components/_pillar_table.sass new file mode 100644 index 00000000..366931a9 --- /dev/null +++ b/src/styles/components/_pillar_table.sass @@ -0,0 +1,146 @@ +$thumbnail-max-width: 110px +$thumbnail-max-height: calc(110px * (9/16)) + +.pillar-table-container + background-color: white + height: 100% + +.pillar-table + display: flex + flex-direction: column + width: 100% + height: 95% // TODO: Investigate why some rows are outside screen if 100% + +.pillar-table-head + display: flex + flex-direction: row + position: relative + box-shadow: 0 5 $color-background-dark + + .cell-content + display: flex + flex-direction: row + align-items: center + height: 100% + font-size: .9em + + .column-sort + display: flex + opacity: 0 + flex-direction: column + + .sort-action + &:hover + background-color: $color-background-active + &:hover + .column-sort + opacity: 1 + +.pillar-table-row-group + display: block + overflow-y: auto + height: 100% + + .pillar-table-row:nth-child(odd) + background-color: $color-background-dark + + .pillar-table-row + display: flex + flex-direction: row + transition: all 250ms ease-in-out + background-color: $color-background-light + cursor: pointer + + &:hover + background-color: $color-background-active + + &.is-busy + +stripes-animate + +stripes(transparent, rgba($color-background-active, .6), -45deg, 4em) + animation-duration: 4s + + &.is-corrupt + background-color: $color-warning + + &.active + border-left: 0.5em solid $color-background-active + +.pillar-cell + display: flex + flex-direction: column + flex-grow: 1 + flex-basis: 0 + overflow: hidden + white-space: nowrap + text-overflow: ellipsis + justify-content: center + + &.highlight + background-color: rgba($color-background-active, .4) + + &.warning + background-color: rgba($color-warning, .4) + + &.header-cell + text-transform: capitalize + color: $color-text-dark-secondary + + &.thumbnail + flex: 0 + flex-basis: $thumbnail-max-width + text-align: center + + img + max-width: $thumbnail-max-width + height: auto + + a + overflow: hidden + text-overflow: ellipsis + + @include status-color-property(background-color, '', '') + + +.pillar-table-menu + display: flex + flex-direction: row + + .settings-menu + display: flex + flex-direction: column + position: absolute + background-color: white + list-style: none + margin: 0 + padding: 0 + text-transform: capitalize + z-index: $z-index-base + 1 + box-shadow: 0 2px 5px rgba(black, .4) + + .pillar-table-row-filter + display: flex + flex-direction: row + + .pillar-table-actions + margin-left: auto + + .action + cursor: pointer + vertical-align: middle + color: $color-primary + border: none + background: none + + &:hover + text-decoration-line: underline + + .pillar-table-column-filter + margin-left: auto + .settings-menu + right: 0em + +.pillar-table-row-item + display: inline-block + +.pillar-table-row-enter, .pillar-table-row-leave-to + opacity: 0 From 6bae6a39dfb85e5b9e862f424c0b06d2d76b2a6e Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Wed, 20 Mar 2019 15:14:50 +0100 Subject: [PATCH 10/66] Mark pillar table rows as corrupt if init fails --- .../common/vuecomponents/table/rows/RowObjectBase.js | 12 ++++++++++-- .../common/vuecomponents/table/rows/renderer/Row.js | 4 +--- 2 files changed, 11 insertions(+), 5 deletions(-) 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 02394e43..e1e12ad7 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectBase.js +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/RowObjectBase.js @@ -5,6 +5,7 @@ class RowBase { constructor(underlyingObject) { this.underlyingObject = underlyingObject; this.isInitialized = false; + this.isCorrupt = false; this.isSelected = false; } @@ -14,7 +15,12 @@ class RowBase { thenInit() { return this._thenInitImpl() .then(() => { - this.isInitialized = true + this.isInitialized = true; + }) + .catch((err) => { + console.warn(err); + this.isCorrupt = true; + throw err; }) } @@ -42,7 +48,9 @@ class RowBase { */ getRowClasses() { return { - "is-busy": !this.isInitialized + "active": this.isSelected, + "is-busy": !this.isInitialized, + "is-corrupt": this.isCorrupt } } 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 7f2e0e31..660fd745 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 @@ -26,9 +26,7 @@ Vue.component('pillar-table-row', { }, computed: { rowClasses() { - let classes = this.rowObject.getRowClasses() - classes['active'] = this.rowObject.isSelected; - return classes; + return this.rowObject.getRowClasses(); } }, }); From dfd61c8bd8afc84b5a15b17ca913bb73b592ea47 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Wed, 20 Mar 2019 15:18:50 +0100 Subject: [PATCH 11/66] Update pillar table props --- src/scripts/js/es6/common/vuecomponents/table/Table.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/scripts/js/es6/common/vuecomponents/table/Table.js b/src/scripts/js/es6/common/vuecomponents/table/Table.js index a8eaed3a..b5cc958d 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/Table.js +++ b/src/scripts/js/es6/common/vuecomponents/table/Table.js @@ -76,8 +76,10 @@ let PillarTable = Vue.component('pillar-table-base', { template: TEMPLATE, mixins: [UnitOfWorkTracker], props: { - projectId: String, - selectedIds: Array, + selectedIds: { + type: Array, + default: [] + }, canChangeSelectionCB: { type: Function, default: () => true From 5aed4ceff7cec12341d221a66deb5a32992690f1 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Wed, 20 Mar 2019 15:19:37 +0100 Subject: [PATCH 12/66] Avoid emitting duplicate selectedItemsChanged --- src/scripts/js/es6/common/vuecomponents/table/Table.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/scripts/js/es6/common/vuecomponents/table/Table.js b/src/scripts/js/es6/common/vuecomponents/table/Table.js index b5cc958d..ebf03ba0 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/Table.js +++ b/src/scripts/js/es6/common/vuecomponents/table/Table.js @@ -129,7 +129,11 @@ let PillarTable = Vue.component('pillar-table-base', { }); }, selectedItems(newValue, oldValue) { - this.$emit('selectItemsChanged', newValue); + // Deep compare to avoid spamming un needed events + let hasChanged = JSON.stringify(newValue ) === JSON.stringify(oldValue); + if (!hasChanged) { + this.$emit('selectItemsChanged', newValue); + } }, isInitialized(newValue) { if (newValue) { From 6c4e6088d3acc87c5e1e8cb7463caeea73b8c141 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Thu, 21 Mar 2019 01:03:59 +0100 Subject: [PATCH 13/66] UI: Vertically center badges under comment avatar. --- src/styles/_comments.sass | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/styles/_comments.sass b/src/styles/_comments.sass index 5a3799b8..3b40497f 100644 --- a/src/styles/_comments.sass +++ b/src/styles/_comments.sass @@ -402,6 +402,11 @@ $comments-width-max: 710px width: 16px height: 16px + .comment-avatar + @extend .d-flex + @extend .flex-column + @extend .align-items-center + .user-avatar img border-radius: 50% From 022fc9a1b20de03a43f01d65a9db6b769b32cc9c Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Fri, 22 Mar 2019 14:06:17 +0100 Subject: [PATCH 14/66] Removed possibility to toggle selected in table --- src/scripts/js/es6/common/vuecomponents/table/Table.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/scripts/js/es6/common/vuecomponents/table/Table.js b/src/scripts/js/es6/common/vuecomponents/table/Table.js index ebf03ba0..8c4ffe0b 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/Table.js +++ b/src/scripts/js/es6/common/vuecomponents/table/Table.js @@ -199,11 +199,7 @@ let PillarTable = Vue.component('pillar-table-base', { } } else { - if (this.selectedIds.length === 1 && this.selectedIds[0] === itemId) { - this.selectedIds = []; - } else { - this.selectedIds = [itemId]; - } + this.selectedIds = [itemId]; } }, isSelectBetweenClick(clickEvent) { From 375182a7818e2c5f9fa1c748554bb1a8a3b981e8 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Fri, 22 Mar 2019 14:06:54 +0100 Subject: [PATCH 15/66] Add css class per task type to table columns --- .../vuecomponents/table/columns/ColumnBase.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js b/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js index a3532e96..c2c93851 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js +++ b/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js @@ -45,15 +45,24 @@ export class ColumnBase { return ''; } + /** + * Object with css classes to use on the column + * @returns {Object} Object with css classes + */ + getColumnClasses() { + // Should be overridden + let classes = {} + classes[this.columnType] = true; + return classes; + } + /** * Object with css classes to use on the header cell * @returns {Object} Object with css classes */ getHeaderCellClasses() { // Should be overridden - let classes = {} - classes[this.columnType] = true; - return classes; + return this.getColumnClasses(); } /** @@ -64,8 +73,7 @@ export class ColumnBase { */ getCellClasses(rawCellValue, rowObject) { // Should be overridden - let classes = {} - classes[this.columnType] = true; + let classes = this.getColumnClasses(); classes['highlight'] = !!this.isHighLighted; return classes; } From 072a1793e452e28a011f2d59868d765e8c2ab5a1 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Fri, 22 Mar 2019 14:07:29 +0100 Subject: [PATCH 16/66] Add missing tooltips in table --- .../common/vuecomponents/table/cells/renderer/HeadCell.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/HeadCell.js b/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/HeadCell.js index 3416b20c..aed74b34 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/HeadCell.js +++ b/src/scripts/js/es6/common/vuecomponents/table/cells/renderer/HeadCell.js @@ -5,7 +5,11 @@ const TEMPLATE =` @mouseleave="onMouseLeave" >
- {{ column.displayName }} +
+ {{ column.displayName }} +
From 5cba6f53f5f287f12669c570c636d484e5bf88e8 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Fri, 22 Mar 2019 14:10:18 +0100 Subject: [PATCH 17/66] Make sure sort buttons is always clickable Hide part overflow of column label if there is not enough room --- src/styles/components/_pillar_table.sass | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/styles/components/_pillar_table.sass b/src/styles/components/_pillar_table.sass index 366931a9..da3e4070 100644 --- a/src/styles/components/_pillar_table.sass +++ b/src/styles/components/_pillar_table.sass @@ -41,6 +41,9 @@ $thumbnail-max-height: calc(110px * (9/16)) overflow-y: auto height: 100% + .pillar-cell + padding-left: 0.3em + .pillar-table-row:nth-child(odd) background-color: $color-background-dark @@ -74,6 +77,7 @@ $thumbnail-max-height: calc(110px * (9/16)) white-space: nowrap text-overflow: ellipsis justify-content: center + min-width: 2em &.highlight background-color: rgba($color-background-active, .4) @@ -85,6 +89,9 @@ $thumbnail-max-height: calc(110px * (9/16)) text-transform: capitalize color: $color-text-dark-secondary + .header-label + overflow: hidden + &.thumbnail flex: 0 flex-basis: $thumbnail-max-width From 97cda1ef6b050dfe4c4f81356ac7efa0e5e8f973 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Tue, 26 Mar 2019 15:21:15 +0100 Subject: [PATCH 18/66] UI: Fix hidden fields showing up in project edit. The 'hidden' class got renamed to d-none in Bootstrap 4. --- src/templates/projects/edit.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/projects/edit.pug b/src/templates/projects/edit.pug index 81ff8432..a40b77ff 100644 --- a/src/templates/projects/edit.pug +++ b/src/templates/projects/edit.pug @@ -78,7 +78,7 @@ | {% endif %} | {% else %} - | {{ field(class='hidden') }} + | {{ field(class='d-none') }} | {% endif %} | {% endif %} From cd94eb237f54375a2d8c15c8969345dc10dd878b Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Tue, 26 Mar 2019 17:45:33 +0100 Subject: [PATCH 19/66] Cleanup: One indentation level too much. --- src/styles/components/_timeline.sass | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/styles/components/_timeline.sass b/src/styles/components/_timeline.sass index f16468c1..a7b17857 100644 --- a/src/styles/components/_timeline.sass +++ b/src/styles/components/_timeline.sass @@ -20,6 +20,6 @@ body.homepage top: 2.5rem body.is-mobile - .timeline - .js-asset-list - @extend .card-deck-vertical + .timeline + .js-asset-list + @extend .card-deck-vertical From 4977829da7c7cd5cd4771b994ad7e65d7802eb31 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Tue, 26 Mar 2019 18:31:54 +0100 Subject: [PATCH 20/66] Cleanup: Remove legacy Bootstrap 3 minified CSS file. * Our Pillar apps now use Bootstrap 4. * Pillar builds its own CSS from Bootstrap 4 components (from node_modules) --- .../web/static/assets/css/vendor/bootstrap.min.css | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 pillar/web/static/assets/css/vendor/bootstrap.min.css diff --git a/pillar/web/static/assets/css/vendor/bootstrap.min.css b/pillar/web/static/assets/css/vendor/bootstrap.min.css deleted file mode 100644 index 8a770ee6..00000000 --- a/pillar/web/static/assets/css/vendor/bootstrap.min.css +++ /dev/null @@ -1,14 +0,0 @@ -/*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -/*! - * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=3f4d4ab5b209f6196ab3) - * Config saved to config.json and https://gist.github.com/3f4d4ab5b209f6196ab3 - *//*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,*:before,*:after{background:transparent !important;color:#000 !important;-webkit-box-shadow:none !important;box-shadow:none !important;text-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{background-color:#fcf8e3;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover,a.text-primary:focus{color:#286090}.text-success{color:#3c763d}a.text-success:hover,a.text-success:focus{color:#2b542c}.text-info{color:#31708f}a.text-info:hover,a.text-info:focus{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover,a.text-warning:focus{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover,a.text-danger:focus{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover,a.bg-primary:focus{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover,a.bg-success:focus{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover,a.bg-info:focus{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover,a.bg-warning:focus{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover,a.bg-danger:focus{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:bold;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:0.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:34px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:30px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-left:-20px;margin-top:4px \9}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}@media (min-width:768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.333333px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:focus,.btn-default.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#fff;background-color:#398439;border-color:#255625}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#337ab7;font-weight:normal;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height, visibility;-o-transition-property:height, visibility;transition-property:height, visibility;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);-webkit-background-clip:padding-box;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#337ab7}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:4px;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-right-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#337ab7;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:3;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;color:#fff;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{text-decoration:none;color:#555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{background-color:#eee;color:#777;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-left:15px;padding-right:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.modal-open{overflow:hidden}.modal{display:none;overflow:hidden;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);-o-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform 0.3s ease-out;-o-transition:-o-transform 0.3s ease-out;transition:transform 0.3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);-webkit-background-clip:padding-box;background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.42857143px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:12px;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;right:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:14px;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,0.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,0.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1200px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1200px){.visible-lg-block{display:block !important}}@media (min-width:1200px){.visible-lg-inline{display:inline !important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media (min-width:1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}} \ No newline at end of file From 69806d96a92c8fe49680ab10d3e09bdca2605bf0 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Wed, 27 Mar 2019 11:04:39 +0100 Subject: [PATCH 21/66] UI: Narrower column for text in jumbotron component. Leaves some room to see the image on the right. --- src/templates/mixins/components.pug | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/templates/mixins/components.pug b/src/templates/mixins/components.pug index 4413cc75..3ec25172 100644 --- a/src/templates/mixins/components.pug +++ b/src/templates/mixins/components.pug @@ -11,7 +11,7 @@ mixin jumbotron(title, text, image, url) href=url)&attributes(attributes) .container .row - .col-md-9 + .col-md-8 if title .display-4.text-uppercase.font-weight-bold =title @@ -24,7 +24,7 @@ mixin jumbotron(title, text, image, url) .jumbotron.text-white(style='background-image: url(' + image + ');')&attributes(attributes) .container .row - .col-md-9 + .col-md-6 if title .display-4.text-uppercase.font-weight-bold =title From 4cd182e2d26c81e401f5d01150dbe51553a8bde5 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Wed, 27 Mar 2019 11:19:11 +0100 Subject: [PATCH 22/66] Cleanup: spaces to tabs. --- src/styles/components/_timeline.sass | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/styles/components/_timeline.sass b/src/styles/components/_timeline.sass index a7b17857..20ba858b 100644 --- a/src/styles/components/_timeline.sass +++ b/src/styles/components/_timeline.sass @@ -1,25 +1,25 @@ .timeline - .group - opacity: 0 - animation: fade-in 500ms forwards + .group + opacity: 0 + animation: fade-in 500ms forwards - .group-date - color: rgba($color-text, .4) + .group-date + color: rgba($color-text, .4) - .group-title - @extend .border-bottom - @extend .bg-white - @extend .text-uppercase - @extend .font-weight-bold - a - color: $color-text + .group-title + @extend .border-bottom + @extend .bg-white + @extend .text-uppercase + @extend .font-weight-bold + a + color: $color-text body.homepage - .timeline - .sticky-top - top: 2.5rem + .timeline + .sticky-top + top: 2.5rem body.is-mobile - .timeline - .js-asset-list - @extend .card-deck-vertical + .timeline + .js-asset-list + @extend .card-deck-vertical From 85706fc26488f9c6387a867c5440a61cb51b8ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Wed, 27 Mar 2019 11:58:43 +0100 Subject: [PATCH 23/66] Updated bug report URLs The project was apparently moved. The issues are closed, too, though, so we could at some point check whether our workarounds can be removed. --- pillar/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pillar/__init__.py b/pillar/__init__.py index f1578ecf..63c7cc36 100644 --- a/pillar/__init__.py +++ b/pillar/__init__.py @@ -790,7 +790,7 @@ class PillarServer(BlinkerCompatibleEve): return 'basic ' + base64.b64encode('%s:%s' % (username, subclient_id)) def post_internal(self, resource: str, payl=None, skip_validation=False): - """Workaround for Eve issue https://github.com/nicolaiarocci/eve/issues/810""" + """Workaround for Eve issue https://github.com/pyeve/eve/issues/810""" from eve.methods.post import post_internal url = self.config['URLS'][resource] @@ -800,7 +800,7 @@ class PillarServer(BlinkerCompatibleEve): def put_internal(self, resource: str, payload=None, concurrency_check=False, skip_validation=False, **lookup): - """Workaround for Eve issue https://github.com/nicolaiarocci/eve/issues/810""" + """Workaround for Eve issue https://github.com/pyeve/eve/issues/810""" from eve.methods.put import put_internal url = self.config['URLS'][resource] @@ -811,7 +811,7 @@ class PillarServer(BlinkerCompatibleEve): def patch_internal(self, resource: str, payload=None, concurrency_check=False, skip_validation=False, **lookup): - """Workaround for Eve issue https://github.com/nicolaiarocci/eve/issues/810""" + """Workaround for Eve issue https://github.com/pyeve/eve/issues/810""" from eve.methods.patch import patch_internal url = self.config['URLS'][resource] @@ -822,7 +822,7 @@ class PillarServer(BlinkerCompatibleEve): def delete_internal(self, resource: str, concurrency_check=False, suppress_callbacks=False, **lookup): - """Workaround for Eve issue https://github.com/nicolaiarocci/eve/issues/810""" + """Workaround for Eve issue https://github.com/pyeve/eve/issues/810""" from eve.methods.delete import deleteitem_internal url = self.config['URLS'][resource] From 237c135c31b4df942cc7570909018a26ebcc3482 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Wed, 27 Mar 2019 12:06:59 +0100 Subject: [PATCH 24/66] UI Timeline: support for dark backgrounds. Simply place the +timeline(project_id) mixin inside a div with a 'timeline-dark' class. --- src/styles/components/_timeline.sass | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/styles/components/_timeline.sass b/src/styles/components/_timeline.sass index 20ba858b..359dc2ee 100644 --- a/src/styles/components/_timeline.sass +++ b/src/styles/components/_timeline.sass @@ -23,3 +23,16 @@ body.is-mobile .timeline .js-asset-list @extend .card-deck-vertical + +.timeline-dark + .group-date + color: $color-text-light-hint + + .group-title + @extend .bg-transparent + border-bottom-color: rgba($color-background, .2) !important + color: rgba($color-background, .6) + + blockquote + background-color: rgba($color-background, .1) + box-shadow: inset 5px 0 0 rgba($color-background, .2) From 9c1e3452523cbbe1a41652bbb59ba75c8f670646 Mon Sep 17 00:00:00 2001 From: Francesco Siddi Date: Wed, 27 Mar 2019 11:56:33 +0100 Subject: [PATCH 25/66] Newline at end of file --- pillar/api/node_types/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pillar/api/node_types/utils.py b/pillar/api/node_types/utils.py index 73c7d4a2..d4f857c9 100644 --- a/pillar/api/node_types/utils.py +++ b/pillar/api/node_types/utils.py @@ -31,4 +31,4 @@ def markdown_fields(field: str, **kwargs) -> dict: 'default': field, # Name of the field containing the markdown. Will be input to the coerce function. 'coerce': 'markdown', } - } \ No newline at end of file + } From 1525ceafd5b581dd594a316b381c98a60d6c682f Mon Sep 17 00:00:00 2001 From: Francesco Siddi Date: Wed, 27 Mar 2019 12:11:57 +0100 Subject: [PATCH 26/66] Fix for find_markdown_fields project hook Original commit 3b59d3ee9aacae517b06bf25346efa3f2dae0fe7 Breaking commit 32e25ce129612010a4c14dfee0d21d1a93666108 The breaking commit was actually meant to remove the need for this hook logic entirely, by relying on a custom validator instead. This works for nodes, but it currently does not work for projects. The issue needs to be further investigated via T63006. --- pillar/api/projects/hooks.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/pillar/api/projects/hooks.py b/pillar/api/projects/hooks.py index 4f9eb9d7..d27359ba 100644 --- a/pillar/api/projects/hooks.py +++ b/pillar/api/projects/hooks.py @@ -253,26 +253,30 @@ def parse_markdown(project, original=None): schema = current_app.config['DOMAIN']['projects']['schema'] def find_markdown_fields(schema, project): - """Find and process all makrdown validated fields.""" - for k, v in schema.items(): - if not isinstance(v, dict): + """Find and process all Markdown coerced fields. + + - look for fields with a 'coerce': 'markdown' property + - parse the name of the field and generate the sibling field name (__html -> ) + - parse the content of the field as markdown and save it in __html + """ + for field_name, field_value in schema.items(): + if not isinstance(field_value, dict): + continue + if field_value.get('coerce') != 'markdown': + continue + if field_name not in project: continue - if v.get('validator') == 'markdown': - # If there is a match with the validator: markdown pair, assign the sibling - # property (following the naming convention __html) - # the processed value. - if k in project: - html = pillar.markdown.markdown(project[k]) - field_name = pillar.markdown.cache_field_name(k) - project[field_name] = html - if isinstance(project, dict) and k in project: - find_markdown_fields(v, project[k]) + # Construct markdown source field name (strip the leading '_' and the trailing '_html') + source_field_name = field_name[1:-5] + html = pillar.markdown.markdown(project[source_field_name]) + project[field_name] = html + + if isinstance(project, dict) and field_name in project: + find_markdown_fields(field_value, project[field_name]) find_markdown_fields(schema, project) - return 'ok' - def parse_markdowns(items): for item in items: From db11b03c39496ac46fa90f6d243897a613fd3095 Mon Sep 17 00:00:00 2001 From: Francesco Siddi Date: Wed, 27 Mar 2019 12:12:09 +0100 Subject: [PATCH 27/66] Fix typo --- pillar/api/custom_field_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pillar/api/custom_field_validation.py b/pillar/api/custom_field_validation.py index 4796676f..e2a24661 100644 --- a/pillar/api/custom_field_validation.py +++ b/pillar/api/custom_field_validation.py @@ -161,7 +161,7 @@ class ValidateCustomFields(Validator): """ Cache markdown as html. - :param markdown_field: name of the field containing mark down + :param markdown_field: name of the field containing Markdown :return: html string """ my_log = log.getChild('_normalize_coerce_markdown') From 87ff68175092f5305c789332c82edfa5bd311220 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Wed, 27 Mar 2019 14:09:48 +0100 Subject: [PATCH 28/66] UI: Font-size tweak to node description for blog and project. --- src/styles/_project.sass | 10 ++++++++++ src/styles/blog.sass | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/styles/_project.sass b/src/styles/_project.sass index fefdc82a..15b08f8b 100644 --- a/src/styles/_project.sass +++ b/src/styles/_project.sass @@ -238,6 +238,16 @@ ul.project-edit-tools border: none +#project_context + .node-details-description + font: + size: 1.2em + weight: 200 + + img + margin-bottom: 2rem + margin-top: 2rem + /* The actual navigation tree container */ #project_tree +media-xs diff --git a/src/styles/blog.sass b/src/styles/blog.sass index f92a91b4..748591a2 100644 --- a/src/styles/blog.sass +++ b/src/styles/blog.sass @@ -51,6 +51,16 @@ @import _comments @import _notifications +body.blog + .node-details-description + font: + size: 1.3em + weight: 200 + + img + margin-bottom: 2rem + margin-top: 2rem + #blog_post-edit-form padding: 20px From 898379d0d360045ca942ce59b84194093b4161ec Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Wed, 27 Mar 2019 14:11:05 +0100 Subject: [PATCH 29/66] UI: Font-size tweak for node description in timeline. --- src/styles/components/_timeline.sass | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/styles/components/_timeline.sass b/src/styles/components/_timeline.sass index 359dc2ee..4d291f6d 100644 --- a/src/styles/components/_timeline.sass +++ b/src/styles/components/_timeline.sass @@ -14,6 +14,15 @@ a color: $color-text + .node-details-description + font: + size: 1.2em + weight: 200 + + img + margin-bottom: 2rem + margin-top: 2rem + body.homepage .timeline .sticky-top From 1f671a2375074443b68e8ea7f578556984a53cb9 Mon Sep 17 00:00:00 2001 From: Francesco Siddi Date: Wed, 27 Mar 2019 14:22:33 +0100 Subject: [PATCH 30/66] Update package-lock.json The current packages where failing to build libsass on macOS. --- package-lock.json | 469 ++++++++++++++++++++++------------------------ 1 file changed, 226 insertions(+), 243 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5b48ec3b..4bcb6f75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1170,38 +1170,6 @@ "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "argparse": { @@ -1826,9 +1794,9 @@ }, "dependencies": { "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true } } @@ -2873,18 +2841,6 @@ "requires": { "lru-cache": "^4.0.1", "which": "^1.2.9" - }, - "dependencies": { - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - } } }, "crypto-browserify": { @@ -4121,7 +4077,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4142,12 +4099,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4162,17 +4121,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4289,7 +4251,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4301,6 +4264,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4315,6 +4279,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4322,12 +4287,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4346,6 +4313,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4426,7 +4394,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4438,6 +4407,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4523,7 +4493,8 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4559,6 +4530,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4578,6 +4550,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4621,12 +4594,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -4640,14 +4615,6 @@ "inherits": "~2.0.0", "mkdirp": ">=0.5 0", "rimraf": "2" - }, - "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - } } }, "function-bind": { @@ -4670,14 +4637,15 @@ "string-width": "^1.0.1", "strip-ansi": "^3.0.1", "wide-align": "^1.1.0" - }, - "dependencies": { - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - } + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "requires": { + "globule": "^1.0.0" } }, "get-assigned-identifiers": { @@ -4827,6 +4795,17 @@ "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", "dev": true }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, "glogg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.1.tgz", @@ -4836,6 +4815,12 @@ "sparkles": "^1.0.0" } }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -6360,6 +6345,12 @@ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -8089,9 +8080,9 @@ "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" }, "js-base64": { - "version": "2.4.9", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz", - "integrity": "sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", "dev": true }, "js-levenshtein": { @@ -8469,6 +8460,12 @@ } } }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", @@ -8542,6 +8539,16 @@ "signal-exit": "^3.0.0" } }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, "lru-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", @@ -8671,14 +8678,6 @@ "read-pkg-up": "^1.0.1", "redent": "^1.0.0", "trim-newlines": "^1.0.0" - }, - "dependencies": { - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - } } }, "merge": { @@ -8800,6 +8799,15 @@ "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", "dev": true }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", @@ -8997,35 +9005,6 @@ "which": "1" }, "dependencies": { - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "semver": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", @@ -9078,9 +9057,9 @@ } }, "node-sass": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.3.tgz", - "integrity": "sha512-XzXyGjO+84wxyH7fV6IwBOTrEBe2f0a6SBze9QWWYR/cL74AcQUks2AsqcCZenl/Fp/JVbuEaLpgrLtocwBUww==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.11.0.tgz", + "integrity": "sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==", "dev": true, "requires": { "async-foreach": "^0.1.3", @@ -9098,59 +9077,94 @@ "nan": "^2.10.0", "node-gyp": "^3.8.0", "npmlog": "^4.0.0", - "request": "2.87.0", + "request": "^2.88.0", "sass-graph": "^2.2.4", "stdout-stream": "^1.4.0", "true-case-path": "^1.0.2" }, "dependencies": { - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", "dev": true, "requires": { - "globule": "^1.0.0" + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "globule": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", - "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", - "dev": true, - "requires": { - "glob": "~7.1.1", - "lodash": "~4.17.10", - "minimatch": "~3.0.2" - } - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" } } } @@ -9251,6 +9265,12 @@ "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", "dev": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -9922,6 +9942,12 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", + "dev": true + }, "public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -10247,6 +10273,32 @@ "read-pkg": "^1.0.0" } }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", @@ -10737,35 +10789,6 @@ "wrap-ansi": "^2.0.0" } }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "yargs": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", @@ -11212,38 +11235,6 @@ "dev": true, "requires": { "readable-stream": "^2.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "stealthy-require": { @@ -12049,31 +12040,6 @@ "dev": true, "requires": { "glob": "^7.1.2" - }, - "dependencies": { - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } } }, "tsml": { @@ -12298,6 +12264,23 @@ "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", "dev": true }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", From 64cb7abcba386fa41ea5f608869ccc1008b71086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Wed, 27 Mar 2019 12:09:09 +0100 Subject: [PATCH 31/66] Removed unused imports --- pillar/web/nodes/routes.py | 4 ---- tests/test_web/test_jstree.py | 2 -- 2 files changed, 6 deletions(-) diff --git a/pillar/web/nodes/routes.py b/pillar/web/nodes/routes.py index 78b015b5..1c8a5818 100644 --- a/pillar/web/nodes/routes.py +++ b/pillar/web/nodes/routes.py @@ -1,5 +1,4 @@ import os -import json import logging from datetime import datetime @@ -18,15 +17,12 @@ from flask import request from flask import jsonify from flask import abort from flask_login import current_user -from flask_wtf.csrf import validate_csrf import werkzeug.exceptions as wz_exceptions from wtforms import SelectMultipleField from flask_login import login_required from jinja2.exceptions import TemplateNotFound -from pillar.api.utils.authorization import check_permissions -from pillar.web.utils import caching from pillar.markdown import markdown from pillar.web.nodes.forms import get_node_form from pillar.web.nodes.forms import process_node_form diff --git a/tests/test_web/test_jstree.py b/tests/test_web/test_jstree.py index 6925bfa7..34d1acba 100644 --- a/tests/test_web/test_jstree.py +++ b/tests/test_web/test_jstree.py @@ -1,5 +1,3 @@ -from unittest import mock - from bson import ObjectId from dateutil.parser import parse from flask import Markup From f6056f4f7e241975acd089224142e95b30dc9917 Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Wed, 27 Mar 2019 15:51:19 +0100 Subject: [PATCH 32/66] UI: New mixin component for listing categories. For e.g. Blender Cloud's Learn, Libraries, etc. --- src/templates/mixins/components.pug | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/templates/mixins/components.pug b/src/templates/mixins/components.pug index 3ec25172..8b66064f 100644 --- a/src/templates/mixins/components.pug +++ b/src/templates/mixins/components.pug @@ -84,4 +84,18 @@ mixin timeline(projectid, sortdirection) i.pi-spin.spin +//- Category listing (Learn, Libraries, etc). +mixin category_list_item(title, text, url, image) + .row.pb-2.my-2 + .col-md-3 + a(href=url, title=title) + img.img-fluid(alt=title, src=image) + .col-md-9 + a(href=url, title=title) + h3.font-weight-bold + =title + .lead + =text + if block + block From 465f1eb87e1d01efa2365349edbeede7875802fe Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Thu, 28 Mar 2019 10:29:13 +0100 Subject: [PATCH 33/66] 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. --- .../js/es6/common/vuecomponents/init.js | 16 +- .../es6/common/vuecomponents/menu/DropDown.js | 2 +- .../es6/common/vuecomponents/table/Table.js | 67 +++++++- .../vuecomponents/table/columns/ColumnBase.js | 3 +- .../table/columns/filter/ColumnFilter.js | 130 +++++++++++++++ .../table/filter/ColumnFilter.js | 91 ----------- .../vuecomponents/table/filter/RowFilter.js | 48 ------ .../table/rows/filter/EnumFilter.js | 153 ++++++++++++++++++ .../table/rows/filter/NameFilter.js | 35 ++++ .../table/rows/filter/RowFilter.js | 25 +++ .../table/rows/filter/StatusFilter.js | 48 ++++++ .../table/rows/filter/TextFilter.js | 86 ++++++++++ src/styles/components/_pillar_table.sass | 10 ++ 13 files changed, 562 insertions(+), 152 deletions(-) create mode 100644 src/scripts/js/es6/common/vuecomponents/table/columns/filter/ColumnFilter.js delete mode 100644 src/scripts/js/es6/common/vuecomponents/table/filter/ColumnFilter.js delete mode 100644 src/scripts/js/es6/common/vuecomponents/table/filter/RowFilter.js create mode 100644 src/scripts/js/es6/common/vuecomponents/table/rows/filter/EnumFilter.js create mode 100644 src/scripts/js/es6/common/vuecomponents/table/rows/filter/NameFilter.js create mode 100644 src/scripts/js/es6/common/vuecomponents/table/rows/filter/RowFilter.js create mode 100644 src/scripts/js/es6/common/vuecomponents/table/rows/filter/StatusFilter.js create mode 100644 src/scripts/js/es6/common/vuecomponents/table/rows/filter/TextFilter.js diff --git a/src/scripts/js/es6/common/vuecomponents/init.js b/src/scripts/js/es6/common/vuecomponents/init.js index 6cd6b180..86fd612f 100644 --- a/src/scripts/js/es6/common/vuecomponents/init.js +++ b/src/scripts/js/es6/common/vuecomponents/init.js @@ -9,7 +9,11 @@ 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 { RowFilter } from './table/filter/RowFilter' +import { RowFilter } from './table/rows/filter/RowFilter' +import { EnumFilter } from './table/rows/filter/EnumFilter' +import { StatusFilter } from './table/rows/filter/StatusFilter' +import { TextFilter } from './table/rows/filter/TextFilter' +import { NameFilter } from './table/rows/filter/NameFilter' let mixins = { UnitOfWorkTracker, @@ -31,12 +35,16 @@ let table = { } }, rows: { + filter: { + RowFilter, + EnumFilter, + StatusFilter, + TextFilter, + NameFilter + }, RowObjectsSourceBase, RowBase, }, - filter: { - RowFilter - }, } export { mixins, table } diff --git a/src/scripts/js/es6/common/vuecomponents/menu/DropDown.js b/src/scripts/js/es6/common/vuecomponents/menu/DropDown.js index e38d9e41..fec7e058 100644 --- a/src/scripts/js/es6/common/vuecomponents/menu/DropDown.js +++ b/src/scripts/js/es6/common/vuecomponents/menu/DropDown.js @@ -1,6 +1,6 @@ const TEMPLATE =`
-
diff --git a/src/scripts/js/es6/common/vuecomponents/table/Table.js b/src/scripts/js/es6/common/vuecomponents/table/Table.js index 8c4ffe0b..8167dc18 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/Table.js +++ b/src/scripts/js/es6/common/vuecomponents/table/Table.js @@ -1,8 +1,9 @@ import './rows/renderer/Head' import './rows/renderer/Row' -import './filter/ColumnFilter' -import './filter/RowFilter' +import './columns/filter/ColumnFilter' +import './rows/filter/RowFilter' import {UnitOfWorkTracker} from '../mixins/UnitOfWorkTracker' +import {RowFilter} from './rows/filter/RowFilter' /** * Table State @@ -23,6 +24,19 @@ class TableState { } } +class ComponentState { + /** + * Serializable state of this component. + * + * @param {Object} rowFilter + * @param {Object} columnFilter + */ + constructor(rowFilter, columnFilter) { + this.rowFilter = rowFilter; + this.columnFilter = columnFilter + } +} + const TEMPLATE =`
@@ -71,6 +90,7 @@ const TEMPLATE =` * * @emits isInitialized When all rows has been fetched, and are initialized. * @emits selectItemsChanged(selectedItems) When selected rows has changed. + * @emits componentStateChanged(newState) When table state changed. Filtered rows, visible columns... */ let PillarTable = Vue.component('pillar-table-base', { template: TEMPLATE, @@ -88,15 +108,23 @@ let PillarTable = Vue.component('pillar-table-base', { type: Boolean, default: true }, + componentState: { + // Instance of ComponentState + type: Object, + default: undefined + } }, data: function() { return { columns: [], visibleColumns: [], visibleRowObjects: [], - rowsSource: undefined, // Override with your implementations of ColumnFactoryBase - columnFactory: undefined, // Override with your implementations of RowSource + rowsSource: undefined, // Override with your implementations of RowSource + columnFactory: undefined, // Override with your implementations of ColumnFactoryBase + rowFilterConfig: undefined, isInitialized: false, + rowFilterState: (this.componentState || {}).rowFilter, + columnFilterState: (this.componentState || {}).columnFilter, compareRowsCB: (row1, row2) => 0 } }, @@ -120,6 +148,15 @@ let PillarTable = Vue.component('pillar-table-base', { selectedItems() { return this.rowAndChildObjects.filter(it => it.isSelected) .map(it => it.underlyingObject); + }, + currentComponentState() { + if (this.isInitialized) { + return new ComponentState( + this.rowFilterState, + this.columnFilterState + ); + } + return undefined; } }, watch: { @@ -130,8 +167,8 @@ let PillarTable = Vue.component('pillar-table-base', { }, selectedItems(newValue, oldValue) { // Deep compare to avoid spamming un needed events - let hasChanged = JSON.stringify(newValue ) === JSON.stringify(oldValue); - if (!hasChanged) { + let hasChanged = JSON.stringify(newValue ) !== JSON.stringify(oldValue); + if (hasChanged) { this.$emit('selectItemsChanged', newValue); } }, @@ -139,6 +176,15 @@ let PillarTable = Vue.component('pillar-table-base', { if (newValue) { this.$emit('isInitialized'); } + }, + currentComponentState(newValue, oldValue) { + if (this.isInitialized) { + // Deep compare to avoid spamming un needed events + let hasChanged = JSON.stringify(newValue ) !== JSON.stringify(oldValue); + if (hasChanged) { + this.$emit('componentStateChanged', newValue); + } + } } }, created() { @@ -169,9 +215,15 @@ let PillarTable = Vue.component('pillar-table-base', { onVisibleColumnsChanged(visibleColumns) { this.visibleColumns = visibleColumns; }, + onColumnFilterStateChanged(newComponentState) { + this.columnFilterState = newComponentState; + }, onVisibleRowObjectsChanged(visibleRowObjects) { this.visibleRowObjects = visibleRowObjects; }, + onRowFilterStateChanged(newComponentState) { + this.rowFilterState = newComponentState; + }, onSort(column, direction) { function compareRows(r1, r2) { return column.compareRows(r1, r2) * direction; @@ -228,6 +280,9 @@ let PillarTable = Vue.component('pillar-table-base', { return true; }) } + }, + components: { + 'pillar-table-row-filter': RowFilter } }); diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js b/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js index c2c93851..c6b4f91d 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js +++ b/src/scripts/js/es6/common/vuecomponents/table/columns/ColumnBase.js @@ -4,13 +4,12 @@ import { CellDefault } from '../cells/renderer/CellDefault' * Column logic */ -let nextColumnId = 0; export class ColumnBase { constructor(displayName, columnType) { - this._id = nextColumnId++; this.displayName = displayName; this.columnType = columnType; this.isMandatory = false; + this.includedByDefault = true; this.isSortable = true; this.isHighLighted = 0; } diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/filter/ColumnFilter.js b/src/scripts/js/es6/common/vuecomponents/table/columns/filter/ColumnFilter.js new file mode 100644 index 00000000..04e0eff4 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/columns/filter/ColumnFilter.js @@ -0,0 +1,130 @@ +import '../../../menu/DropDown' + +const TEMPLATE =` +
+ + + +
    + Columns: +
  • + + {{ c.displayName }} +
  • +
+
+
+`; + +class ColumnState{ + constructor() { + this.displayName; + this.isVisible; + this.isMandatory; + } + + static createDefault(column) { + let state = new ColumnState; + state.displayName = column.displayName; + state.isVisible = !!column.includedByDefault; + state.isMandatory = !!column.isMandatory; + return state; + } +} + +class ComponentState { + /** + * Serializable state of this component. + * + * @param {Array} selected The columns that should be visible + */ + constructor(selected) { + this.selected = selected; + } +} + +/** + * Component to select what columns to render in the table. + * + * @emits visibleColumnsChanged(columns) When visible columns has changed + * @emits componentStateChanged(newState) When column filter state changed. + */ +let Filter = Vue.component('pillar-table-column-filter', { + template: TEMPLATE, + props: { + columns: Array, // Instances of ColumnBase + componentState: Object, // Instance of ComponentState + }, + data() { + return { + columnStates: this.createInitialColumnStates(), // Instances of ColumnState + } + }, + computed: { + visibleColumns() { + return this.columns.filter((candidate) => { + return candidate.isMandatory || this.isColumnStateVisible(candidate); + }); + }, + columnFilterState() { + return new ComponentState(this.visibleColumns.map(it => it.displayName)); + } + }, + watch: { + columns() { + this.columnStates = this.createInitialColumnStates(); + }, + visibleColumns(visibleColumns) { + this.$emit('visibleColumnsChanged', visibleColumns); + }, + columnFilterState(newValue) { + this.$emit('componentStateChanged', newValue); + } + }, + created() { + this.$emit('visibleColumnsChanged', this.visibleColumns); + }, + methods: { + createInitialColumnStates() { + let columnStateCB = ColumnState.createDefault; + if (this.componentState && this.componentState.selected) { + let selected = this.componentState.selected; + columnStateCB = (column) => { + let state = ColumnState.createDefault(column); + state.isVisible = selected.includes(column.displayName); + return state; + } + } + + return this.columns.reduce((states, c) => { + if(!c.isMandatory) { + states.push(columnStateCB(c)); + } + return states; + }, []); + }, + isColumnStateVisible(column) { + for (let state of this.columnStates) { + if (state.displayName === column.displayName) { + return state.isVisible; + } + } + return false; + }, + toggleColumn(column) { + column.isVisible = !column.isVisible; + } + }, +}); + +export { Filter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/filter/ColumnFilter.js b/src/scripts/js/es6/common/vuecomponents/table/filter/ColumnFilter.js deleted file mode 100644 index 0ca380a4..00000000 --- a/src/scripts/js/es6/common/vuecomponents/table/filter/ColumnFilter.js +++ /dev/null @@ -1,91 +0,0 @@ -import '../../menu/DropDown' - -const TEMPLATE =` -
- - - -
    - Columns: -
  • - - {{ c.displayName }} -
  • -
-
-
-`; - -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', { - template: TEMPLATE, - props: { - columns: Array, // Instances of ColumnBase - }, - data() { - return { - columnStates: [], // Instances of ColumnState - } - }, - computed: { - visibleColumns() { - return this.columns.filter((candidate) => { - return candidate.isMandatory || this.isColumnStateVisible(candidate); - }); - } - }, - watch: { - columns() { - this.columnStates = this.setColumnStates(); - }, - visibleColumns(visibleColumns) { - this.$emit('visibleColumnsChanged', visibleColumns); - } - }, - created() { - this.$emit('visibleColumnsChanged', this.visibleColumns); - }, - methods: { - setColumnStates() { - return this.columns.reduce((states, c) => { - if (!c.isMandatory) { - states.push( - new ColumnState(c._id, c.displayName, true) - ); - } - return states; - }, []) - }, - isColumnStateVisible(column) { - for (let state of this.columnStates) { - if (state.id === column._id) { - return state.isVisible; - } - } - return false; - }, - }, -}); - -export { Filter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/filter/RowFilter.js b/src/scripts/js/es6/common/vuecomponents/table/filter/RowFilter.js deleted file mode 100644 index 6282c200..00000000 --- a/src/scripts/js/es6/common/vuecomponents/table/filter/RowFilter.js +++ /dev/null @@ -1,48 +0,0 @@ -const TEMPLATE =` -
- -
-`; - -/** - * @emits visibleRowObjectsChanged(rowObjects) When the what objects to be visible has changed. - */ -let RowFilter = Vue.component('pillar-table-row-filter', { - template: TEMPLATE, - props: { - rowObjects: Array - }, - data() { - return { - nameQuery: '', - } - }, - computed: { - nameQueryLoweCase() { - return this.nameQuery.toLowerCase(); - }, - visibleRowObjects() { - return this.rowObjects.filter((row) => { - return this.filterByName(row); - }); - } - }, - watch: { - visibleRowObjects(visibleRowObjects) { - this.$emit('visibleRowObjectsChanged', visibleRowObjects); - } - }, - created() { - this.$emit('visibleRowObjectsChanged', this.visibleRowObjects); - }, - methods: { - filterByName(rowObject) { - return rowObject.getName().toLowerCase().indexOf(this.nameQueryLoweCase) !== -1; - }, - }, -}); - -export { RowFilter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/filter/EnumFilter.js b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/EnumFilter.js new file mode 100644 index 00000000..12e75f44 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/EnumFilter.js @@ -0,0 +1,153 @@ +const TEMPLATE =` + + + +
    +
  • + {{ label }}: +
  • +
  • + Toggle All +
  • +
  • +
  • + {{ val.displayName }} +
  • +
+
+`; + +class EnumState{ + constructor(displayName, value, isVisible) { + this.displayName = displayName; + this.value = value; + this.isVisible = isVisible; + } +} + +class ComponentState { + /** + * Serializable state of this component. + * + * @param {Array} selected The enums that should be visible + */ + constructor(selected) { + this.selected = selected; + } +} + +/** + * Filter row objects based on enumeratable values. + * + * @emits visibleRowObjectsChanged(rowObjects) When the objects to be visible has changed. + * @emits componentStateChanged(newState) When row filter state changed. + */ +let EnumFilter = { + template: TEMPLATE, + props: { + label: String, + availableValues: Array, // Array with valid values [{value: abc, displayName: xyz},...] + componentState: Object, // Instance of ComponentState. + valueExtractorCB: { + // Callback to extract enumvalue from a rowObject + type: Function, + default: (rowObject) => {throw Error("Not Implemented")} + }, + rowObjects: Array, + }, + data() { + return { + enumVisibilities: this.initEnumVisibilities(), + } + }, + computed: { + visibleRowObjects() { + return this.rowObjects.filter((row) => { + return this.shouldBeVisible(row); + }); + }, + includesRows() { + for (const key in this.enumVisibilities) { + if(!this.enumVisibilities[key].isVisible) return false; + } + return true; + }, + enumButtonClasses() { + return { + 'filter-active': !this.includesRows + } + }, + currentComponentState() { + let visibleEnums = []; + for (const key in this.enumVisibilities) { + const enumState = this.enumVisibilities[key]; + if (enumState.isVisible) { + visibleEnums.push(enumState.value); + } + } + + return new ComponentState(visibleEnums); + } + }, + watch: { + visibleRowObjects(visibleRowObjects) { + this.$emit('visibleRowObjectsChanged', visibleRowObjects); + }, + currentComponentState(newValue) { + this.$emit('componentStateChanged', newValue); + } + }, + created() { + this.$emit('visibleRowObjectsChanged', this.visibleRowObjects); + }, + methods: { + shouldBeVisible(rowObject) { + let value = this.valueExtractorCB(rowObject); + if (typeof this.enumVisibilities[value] === 'undefined') { + console.warn(`RowObject ${rowObject.getId()} has an invalid ${this.label} enum: ${value}`) + return true; + } + return this.enumVisibilities[value].isVisible; + }, + initEnumVisibilities() { + let initialValueCB = () => true; + if (this.componentState && this.componentState.selected) { + initialValueCB = (val) => { + return this.componentState.selected.includes(val.value); + }; + } + + return this.availableValues.reduce((agg, val)=> { + agg[val.value] = new EnumState(val.displayName, val.value, initialValueCB(val)); + return agg; + }, {}); + }, + toggleEnum(value) { + this.enumVisibilities[value].isVisible = !this.enumVisibilities[value].isVisible; + }, + toggleAll() { + let newValue = !this.includesRows; + for (const key in this.enumVisibilities) { + this.enumVisibilities[key].isVisible = newValue; + } + } + }, +}; + +export { EnumFilter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/filter/NameFilter.js b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/NameFilter.js new file mode 100644 index 00000000..53e2a33b --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/NameFilter.js @@ -0,0 +1,35 @@ +import {TextFilter} from './TextFilter' + +const TEMPLATE =` + +`; +/** + * Filter row objects based on there name. + * + * @emits visibleRowObjectsChanged(rowObjects) When the objects to be visible has changed. + * @emits componentStateChanged(newState) When row filter state changed. + */ +let NameFilter = { + template: TEMPLATE, + props: { + componentState: Object, // Instance of object that componentStateChanged emitted. To restore previous state. + rowObjects: Array, + }, + methods: { + extractName(rowObject) { + return rowObject.getName(); + }, + }, + components: { + 'text-filter': TextFilter, + }, +}; + +export { NameFilter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/filter/RowFilter.js b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/RowFilter.js new file mode 100644 index 00000000..be8dcd37 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/RowFilter.js @@ -0,0 +1,25 @@ +import {NameFilter} from './NameFilter' + +const TEMPLATE =` +
+ +
+`; + +let RowFilter = { + template: TEMPLATE, + props: { + rowObjects: Array, + componentState: Object + }, + components: { + 'name-filter': NameFilter + } +}; + +export { RowFilter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/filter/StatusFilter.js b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/StatusFilter.js new file mode 100644 index 00000000..7361449a --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/StatusFilter.js @@ -0,0 +1,48 @@ +import {EnumFilter} from './EnumFilter' + +const TEMPLATE =` + +`; +/** + * Filter row objects based on there status. + * + * @emits visibleRowObjectsChanged(rowObjects) When the objects to be visible has changed. + * @emits componentStateChanged(newState) When row filter state changed. + */ +let StatusFilter = { + template: TEMPLATE, + props: { + availableStatuses: Array, // Array with valid values ['abc', 'xyz'] + componentState: Object, // Instance of object that componentStateChanged emitted. To restore previous state. + rowObjects: Array, + }, + computed: { + availableEnumValues() { + let statusCopy = this.availableStatuses.concat().sort() + return statusCopy.map(status =>{ + return { + value: status, + displayName: status.replace(/-|_/g, ' ') // Replace -(dash) and _(underscore) with space + } + }); + } + }, + methods: { + extractStatus(rowObject) { + return rowObject.getStatus(); + }, + }, + components: { + 'enum-filter': EnumFilter, + }, +}; + +export { StatusFilter } diff --git a/src/scripts/js/es6/common/vuecomponents/table/rows/filter/TextFilter.js b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/TextFilter.js new file mode 100644 index 00000000..bab98d14 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/rows/filter/TextFilter.js @@ -0,0 +1,86 @@ +const TEMPLATE =` + +`; + +class ComponentState { + /** + * Serializable state of this component. + * + * @param {String} textQuery + */ + constructor(textQuery) { + this.textQuery = textQuery; + } +} + +/** + * Component to filter rowobjects by a text value + * + * @emits visibleRowObjectsChanged(rowObjects) When the objects to be visible has changed. + * @emits componentStateChanged(newState) When row filter state changed. Filter query... + */ +let TextFilter = { + template: TEMPLATE, + props: { + label: String, + rowObjects: Array, + componentState: { + // Instance of ComponentState + type: Object, + default: undefined + }, + valueExtractorCB: { + // Callback to extract text to filter from a rowObject + type: Function, + default: (rowObject) => {throw Error("Not Implemented")} + } + }, + data() { + return { + textQuery: (this.componentState || {}).textQuery || '', + } + }, + computed: { + textQueryLoweCase() { + return this.textQuery.toLowerCase(); + }, + visibleRowObjects() { + return this.rowObjects.filter((row) => { + return this.filterByText(row); + }); + }, + textInputClasses() { + return { + 'filter-active': this.textQuery.length > 0 + }; + }, + currentComponentState() { + return new ComponentState(this.textQuery); + }, + placeholderText() { + return `Filter by ${this.label}`; + } + }, + watch: { + visibleRowObjects(visibleRowObjects) { + this.$emit('visibleRowObjectsChanged', visibleRowObjects); + }, + currentComponentState(newValue) { + this.$emit('componentStateChanged', newValue); + } + }, + created() { + this.$emit('visibleRowObjectsChanged', this.visibleRowObjects); + }, + methods: { + filterByText(rowObject) { + return (this.valueExtractorCB(rowObject) || '').toLowerCase().indexOf(this.textQueryLoweCase) !== -1; + }, + }, +}; + +export { TextFilter } diff --git a/src/styles/components/_pillar_table.sass b/src/styles/components/_pillar_table.sass index da3e4070..5a6e1d97 100644 --- a/src/styles/components/_pillar_table.sass +++ b/src/styles/components/_pillar_table.sass @@ -112,6 +112,9 @@ $thumbnail-max-height: calc(110px * (9/16)) display: flex flex-direction: row + .action + cursor: pointer + .settings-menu display: flex flex-direction: column @@ -123,10 +126,17 @@ $thumbnail-max-height: calc(110px * (9/16)) text-transform: capitalize z-index: $z-index-base + 1 box-shadow: 0 2px 5px rgba(black, .4) + user-select: none .pillar-table-row-filter display: flex flex-direction: row + + input.filter-active + background-color: rgba($color-info, .50) + + .pi-filter.filter-active + color: $color-info .pillar-table-actions margin-left: auto From 4499f911dec5fb400454970ab48434af0e62d338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Wed, 27 Mar 2019 13:57:21 +0100 Subject: [PATCH 34/66] Node breadcrumbs Breadcrumbs are served as JSON at `/nodes/{node ID}/breadcrumbs`, with the top-level parent listed first and the node itself listed last: {breadcrumbs: [ ... {_id: "parentID", name: "The Parent Node", node_type: "group", url: "/p/project/parentID"}, {_id: "deadbeefbeefbeefbeeffeee", name: "The Node Itself", node_type: "asset", url: "/p/project/nodeID", _self: true}, ]} When a parent node is missing, it has a breadcrumb like this: {_id: "deadbeefbeefbeefbeeffeee", _exists': false, name': '-unknown-'} Of course this will be the first in the breadcrumbs list, as we won't be able to determine the parent of a deleted/non-existing node. Breadcrumbs are rendered with Vue.js in Blender Cloud (not in Pillar); see projects/view.pug. --- pillar/web/nodes/routes.py | 89 ++++++++++++ src/scripts/js/es6/common/events/Nodes.js | 21 +++ .../vuecomponents/breadcrumbs/Breadcrumbs.js | 49 +++++++ .../js/es6/common/vuecomponents/init.js | 1 + tests/test_web/test_nodes.py | 131 ++++++++++++++++++ 5 files changed, 291 insertions(+) create mode 100644 src/scripts/js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js create mode 100644 tests/test_web/test_nodes.py diff --git a/pillar/web/nodes/routes.py b/pillar/web/nodes/routes.py index 1c8a5818..7bff2113 100644 --- a/pillar/web/nodes/routes.py +++ b/pillar/web/nodes/routes.py @@ -604,5 +604,94 @@ def url_for_node(node_id=None, node=None): return finders.find_url_for_node(node) +@blueprint.route("//breadcrumbs") +def breadcrumbs(node_id: str): + """Return breadcrumbs for the given node, as JSON. + + Note that a missing parent is still returned in the breadcrumbs, + but with `{_exists: false, name: '-unknown-'}`. + + The breadcrumbs start with the top-level parent, and end with the node + itself (marked by {_self: true}). Returns JSON like this: + + {breadcrumbs: [ + ..., + {_id: "parentID", + name: "The Parent Node", + node_type: "group", + url: "/p/project/parentID"}, + {_id: "deadbeefbeefbeefbeeffeee", + _self: true, + name: "The Node Itself", + node_type: "asset", + url: "/p/project/nodeID"}, + ]} + + When a parent node is missing, it has a breadcrumb like this: + + {_id: "deadbeefbeefbeefbeeffeee", + _exists': false, + name': '-unknown-'} + """ + + api = system_util.pillar_api() + is_self = True + + def make_crumb(some_node: None) -> dict: + """Construct a breadcrumb for this node.""" + nonlocal is_self + + crumb = { + '_id': some_node._id, + 'name': some_node.name, + 'node_type': some_node.node_type, + 'url': finders.find_url_for_node(some_node), + } + if is_self: + crumb['_self'] = True + is_self = False + return crumb + + def make_missing_crumb(some_node_id: None) -> dict: + """Construct 'missing parent' breadcrumb.""" + + return { + '_id': some_node_id, + '_exists': False, + 'name': '-unknown-', + } + + # The first node MUST exist. + try: + node = Node.find(node_id, api=api) + except ResourceNotFound: + log.warning('breadcrumbs(node_id=%r): Unable to find node', node_id) + raise wz_exceptions.NotFound(f'Unable to find node {node_id}') + except ForbiddenAccess: + log.warning('breadcrumbs(node_id=%r): access denied to current user', node_id) + raise wz_exceptions.Forbidden(f'No access to node {node_id}') + + crumbs = [] + while True: + crumbs.append(make_crumb(node)) + + child_id = node._id + node_id = node.parent + if not node_id: + break + + # If a subsequent node doesn't exist any more, include that in the breadcrumbs. + # Forbidden nodes are handled as if they don't exist. + try: + node = Node.find(node_id, api=api) + except (ResourceNotFound, ForbiddenAccess): + log.warning('breadcrumbs: Unable to find node %r but it is marked as parent of %r', + node_id, child_id) + crumbs.append(make_missing_crumb(node_id)) + break + + return jsonify({'breadcrumbs': list(reversed(crumbs))}) + + # Import of custom modules (using the same nodes decorator) from .custom import groups, storage, posts diff --git a/src/scripts/js/es6/common/events/Nodes.js b/src/scripts/js/es6/common/events/Nodes.js index 1af26120..cc0b7f63 100644 --- a/src/scripts/js/es6/common/events/Nodes.js +++ b/src/scripts/js/es6/common/events/Nodes.js @@ -25,6 +25,10 @@ class EventName { static deleted(nodeId) { return `pillar:node:${nodeId}:deleted`; } + + static loaded() { + return `pillar:node:loaded`; + } } function trigger(eventName, data) { @@ -139,6 +143,23 @@ class Nodes { EventName.deleted(nodeId), cb); } + + static triggerLoaded(nodeId) { + trigger(EventName.loaded(), {nodeId: nodeId}); + } + + /** + * Listen to events of nodes being loaded for display + * @param {Function(Event)} cb + */ + static onLoaded(cb) { + on(EventName.loaded(), cb); + } + + static offLoaded(cb) { + off(EventName.loaded(), cb); + } + } export { Nodes } diff --git a/src/scripts/js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js b/src/scripts/js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js new file mode 100644 index 00000000..51314662 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js @@ -0,0 +1,49 @@ +const TEMPLATE = ` + +` + +Vue.component("node-breadcrumbs", { + template: TEMPLATE, + created() { + this.loadBreadcrumbs(); + pillar.events.Nodes.onLoaded(event => { + this.nodeId = event.detail.nodeId; + }); + }, + props: { + nodeId: String, + }, + data() { return { + breadcrumbs: [], + }}, + watch: { + nodeId() { + this.loadBreadcrumbs(); + }, + }, + methods: { + loadBreadcrumbs() { + // The node ID may not exist (when at project level, for example). + if (!this.nodeId) { + this.breadcrumbs = []; + return; + } + + $.get(`/nodes/${this.nodeId}/breadcrumbs`) + .done(data => { + this.breadcrumbs = data.breadcrumbs; + }) + .fail(error => { + toastr.error(xhrErrorResponseMessage(error), "Unable to load breadcrumbs"); + }) + ; + }, + }, +}); diff --git a/src/scripts/js/es6/common/vuecomponents/init.js b/src/scripts/js/es6/common/vuecomponents/init.js index 86fd612f..26a30795 100644 --- a/src/scripts/js/es6/common/vuecomponents/init.js +++ b/src/scripts/js/es6/common/vuecomponents/init.js @@ -1,3 +1,4 @@ +import './breadcrumbs/Breadcrumbs' import './comments/CommentTree' import './customdirectives/click-outside' import { UnitOfWorkTracker } from './mixins/UnitOfWorkTracker' diff --git a/tests/test_web/test_nodes.py b/tests/test_web/test_nodes.py new file mode 100644 index 00000000..292ced59 --- /dev/null +++ b/tests/test_web/test_nodes.py @@ -0,0 +1,131 @@ +import typing + +from bson import ObjectId +import flask + +from pillar.tests import AbstractPillarTest + + +class BreadcrumbsTest(AbstractPillarTest): + def setUp(self, **kwargs): + super().setUp(**kwargs) + self.project_id, self.project = self.ensure_project_exists() + + def _create_group(self, + parent_id: typing.Optional[ObjectId], + name: str) -> ObjectId: + node = { + 'name': name, + 'description': '', + 'node_type': 'group', + 'user': self.project['user'], + 'properties': {'status': 'published'}, + 'project': self.project_id, + } + if parent_id: + node['parent'] = parent_id + return self.create_node(node) + + def test_happy(self) -> ObjectId: + # Create the nodes we expect to be returned in the breadcrumbs. + top_group_node_id = self._create_group(None, 'Top-level node') + group_node_id = self._create_group(top_group_node_id, 'Group node') + + fid, _ = self.ensure_file_exists() + node_id = self.create_node({ + 'name': 'Asset node', + 'parent': group_node_id, + 'description': '', + 'node_type': 'asset', + 'user': self.project['user'], + 'properties': {'status': 'published', 'file': fid}, + 'project': self.project_id, + }) + + # Create some siblings that should not be returned. + self._create_group(None, 'Sibling of top node') + self._create_group(top_group_node_id, 'Sibling of group node') + self._create_group(group_node_id, 'Sibling of asset node') + + expected = {'breadcrumbs': [ + {'_id': str(top_group_node_id), + 'name': 'Top-level node', + 'node_type': 'group', + 'url': f'/p/{self.project["url"]}/{top_group_node_id}'}, + {'_id': str(group_node_id), + 'name': 'Group node', + 'node_type': 'group', + 'url': f'/p/{self.project["url"]}/{group_node_id}'}, + {'_id': str(node_id), + '_self': True, + 'name': 'Asset node', + 'node_type': 'asset', + 'url': f'/p/{self.project["url"]}/{node_id}'}, + ]} + + with self.app.app_context(): + url = flask.url_for('nodes.breadcrumbs', node_id=str(node_id)) + + actual = self.get(url).json + self.assertEqual(expected, actual) + + return node_id + + def test_missing_parent(self): + # Note that this group node doesn't exist in the database: + group_node_id = ObjectId(3 * 'deadbeef') + + fid, _ = self.ensure_file_exists() + node_id = self.create_node({ + 'name': 'Asset node', + 'parent': group_node_id, + 'description': '', + 'node_type': 'asset', + 'user': self.project['user'], + 'properties': {'status': 'published', 'file': fid}, + 'project': self.project_id, + }) + + expected = {'breadcrumbs': [ + {'_id': str(group_node_id), + '_exists': False, + 'name': '-unknown-'}, + {'_id': str(node_id), + '_self': True, + 'name': 'Asset node', + 'node_type': 'asset', + 'url': f'/p/{self.project["url"]}/{node_id}'}, + ]} + + with self.app.app_context(): + url = flask.url_for('nodes.breadcrumbs', node_id=str(node_id)) + + actual = self.get(url).json + self.assertEqual(expected, actual) + + def test_missing_node(self): + with self.app.app_context(): + url = flask.url_for('nodes.breadcrumbs', node_id=3 * 'deadbeef') + self.get(url, expected_status=404) + + def test_permissions(self): + # Use the same test case as the happy case. + node_id = self.test_happy() + + # Tweak the project to make it private. + with self.app.app_context(): + proj_coll = self.app.db('projects') + result = proj_coll.update_one({'_id': self.project_id}, + {'$set': {'permissions.world': []}}) + self.assertEqual(1, result.modified_count) + self.project = self.fetch_project_from_db(self.project_id) + + with self.app.app_context(): + url = flask.url_for('nodes.breadcrumbs', node_id=str(node_id)) + + # Anonymous access should be forbidden. + self.get(url, expected_status=403) + + # Authorized access should work, though. + self.create_valid_auth_token(self.project['user'], token='user-token') + self.get(url, auth_token='user-token') From c089b0b6033dacaa3b2e3089213722a89e887757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Thu, 28 Mar 2019 12:34:40 +0100 Subject: [PATCH 35/66] Added little clarification --- src/scripts/js/es6/common/events/Nodes.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/scripts/js/es6/common/events/Nodes.js b/src/scripts/js/es6/common/events/Nodes.js index cc0b7f63..fbf81f7c 100644 --- a/src/scripts/js/es6/common/events/Nodes.js +++ b/src/scripts/js/es6/common/events/Nodes.js @@ -5,7 +5,9 @@ * function myCallback(event) { * console.log('Updated node:', event.detail); * } + * // Register a callback: * Nodes.onUpdated('5c1cc4a5a013573d9787164b', myCallback); + * // When changing the node, notify the listeners: * Nodes.triggerUpdated(myUpdatedNode); */ From f73b7e5c41feff1d44d9c073fc6e4d60e29c4d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Thu, 28 Mar 2019 12:35:21 +0100 Subject: [PATCH 36/66] Corrected comment --- src/scripts/js/es6/common/events/Nodes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/js/es6/common/events/Nodes.js b/src/scripts/js/es6/common/events/Nodes.js index fbf81f7c..b7650c2b 100644 --- a/src/scripts/js/es6/common/events/Nodes.js +++ b/src/scripts/js/es6/common/events/Nodes.js @@ -130,7 +130,7 @@ class Nodes { } /** - * Listen to events of new nodes where _id === nodeId + * Listen to events of nodes being deleted where _id === nodeId * @param {String} nodeId * @param {Function(Event)} cb */ From ff43fa19fd1393673a69a974c95d520c569facd1 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Thu, 28 Mar 2019 12:48:15 +0100 Subject: [PATCH 37/66] Add Created and Updated column --- .../js/es6/common/vuecomponents/init.js | 6 +- .../vuecomponents/table/columns/Created.js | 58 +++++++++++++++++++ .../vuecomponents/table/columns/Updated.js | 58 +++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 src/scripts/js/es6/common/vuecomponents/table/columns/Created.js create mode 100644 src/scripts/js/es6/common/vuecomponents/table/columns/Updated.js diff --git a/src/scripts/js/es6/common/vuecomponents/init.js b/src/scripts/js/es6/common/vuecomponents/init.js index 26a30795..b24bc409 100644 --- a/src/scripts/js/es6/common/vuecomponents/init.js +++ b/src/scripts/js/es6/common/vuecomponents/init.js @@ -7,6 +7,8 @@ import { PillarTable, TableState } from './table/Table' import { CellPrettyDate } from './table/cells/renderer/CellPrettyDate' import { CellDefault } from './table/cells/renderer/CellDefault' import { ColumnBase } from './table/columns/ColumnBase' +import { Created } from './table/columns/Created' +import { Updated } from './table/columns/Updated' import { ColumnFactoryBase } from './table/columns/ColumnFactoryBase' import { RowObjectsSourceBase } from './table/rows/RowObjectsSourceBase' import { RowBase } from './table/rows/RowObjectBase' @@ -18,7 +20,7 @@ import { NameFilter } from './table/rows/filter/NameFilter' let mixins = { UnitOfWorkTracker, - BrowserHistoryState, + BrowserHistoryState, StateSaveMode } @@ -27,6 +29,8 @@ let table = { TableState, columns: { ColumnBase, + Created, + Updated, ColumnFactoryBase, }, cells: { diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/Created.js b/src/scripts/js/es6/common/vuecomponents/table/columns/Created.js new file mode 100644 index 00000000..49d998b1 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/columns/Created.js @@ -0,0 +1,58 @@ +import { CellPrettyDate } from '../cells/renderer/CellPrettyDate' +import {ColumnBase} from './ColumnBase' + +/** + * Column showing the objects _created prettyfied + */ + +export class Created extends ColumnBase{ + constructor() { + super('Created', 'row-created'); + this.includedByDefault = false; + } + + /** + * + * @param {RowObject} rowObject + * @returns {String} Name of the Cell renderer component + */ + getCellRenderer(rowObject) { + return CellPrettyDate.options.name; + } + + /** + * + * @param {RowObject} rowObject + * @returns {DateString} + */ + getRawCellValue(rowObject) { + return rowObject.underlyingObject['_created']; + } + + /** + * Cell tooltip + * @param {Any} rawCellValue + * @param {RowObject} rowObject + * @returns {String} + */ + getCellTitle(rawCellValue, rowObject) { + return rawCellValue; + } + + /** + * Compare two rows to sort them. Can be overridden for more complex situations. + * + * @param {RowObject} rowObject1 + * @param {RowObject} rowObject2 + * @returns {Number} -1, 0, 1 + */ + compareRows(rowObject1, rowObject2) { + let dueDateStr1 = this.getRawCellValue(rowObject1); + let dueDateStr2 = this.getRawCellValue(rowObject2); + if (dueDateStr1 === dueDateStr2) return 0; + if (dueDateStr1 && dueDateStr2) { + return new Date(dueDateStr1) < new Date(dueDateStr2) ? -1 : 1; + } + return dueDateStr1 ? -1 : 1; + } +} diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/Updated.js b/src/scripts/js/es6/common/vuecomponents/table/columns/Updated.js new file mode 100644 index 00000000..855ae48d --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/columns/Updated.js @@ -0,0 +1,58 @@ +import { CellPrettyDate } from '../cells/renderer/CellPrettyDate' +import {ColumnBase} from './ColumnBase' + +/** + * Column showing the objects _updated prettyfied + */ + +export class Updated extends ColumnBase{ + constructor() { + super('Updated', 'row-updated'); + this.includedByDefault = false; + } + + /** + * + * @param {RowObject} rowObject + * @returns {String} Name of the Cell renderer component + */ + getCellRenderer(rowObject) { + return CellPrettyDate.options.name; + } + + /** + * + * @param {RowObject} rowObject + * @returns {DateString} + */ + getRawCellValue(rowObject) { + return rowObject.underlyingObject['_updated']; + } + + /** + * Cell tooltip + * @param {Any} rawCellValue + * @param {RowObject} rowObject + * @returns {String} + */ + getCellTitle(rawCellValue, rowObject) { + return rawCellValue; + } + + /** + * Compare two rows to sort them. Can be overridden for more complex situations. + * + * @param {RowObject} rowObject1 + * @param {RowObject} rowObject2 + * @returns {Number} -1, 0, 1 + */ + compareRows(rowObject1, rowObject2) { + let dueDateStr1 = this.getRawCellValue(rowObject1); + let dueDateStr2 = this.getRawCellValue(rowObject2); + if (dueDateStr1 === dueDateStr2) return 0; + if (dueDateStr1 && dueDateStr2) { + return new Date(dueDateStr1) < new Date(dueDateStr2) ? -1 : 1; + } + return dueDateStr1 ? -1 : 1; + } +} From 5c8181ae41bd1e7ebc0c7f8efd8748cec4351c55 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Thu, 28 Mar 2019 14:36:30 +0100 Subject: [PATCH 38/66] Refactored Date columns to have a common base --- .../js/es6/common/vuecomponents/init.js | 2 + .../vuecomponents/table/columns/Created.js | 43 +------------------ .../table/columns/DateColumnBase.js | 41 ++++++++++++++++++ .../vuecomponents/table/columns/Updated.js | 43 +------------------ 4 files changed, 47 insertions(+), 82 deletions(-) create mode 100644 src/scripts/js/es6/common/vuecomponents/table/columns/DateColumnBase.js diff --git a/src/scripts/js/es6/common/vuecomponents/init.js b/src/scripts/js/es6/common/vuecomponents/init.js index b24bc409..cccff5c6 100644 --- a/src/scripts/js/es6/common/vuecomponents/init.js +++ b/src/scripts/js/es6/common/vuecomponents/init.js @@ -9,6 +9,7 @@ import { CellDefault } from './table/cells/renderer/CellDefault' import { ColumnBase } from './table/columns/ColumnBase' import { Created } from './table/columns/Created' import { Updated } from './table/columns/Updated' +import { DateColumnBase } from './table/columns/DateColumnBase' import { ColumnFactoryBase } from './table/columns/ColumnFactoryBase' import { RowObjectsSourceBase } from './table/rows/RowObjectsSourceBase' import { RowBase } from './table/rows/RowObjectBase' @@ -31,6 +32,7 @@ let table = { ColumnBase, Created, Updated, + DateColumnBase, ColumnFactoryBase, }, cells: { diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/Created.js b/src/scripts/js/es6/common/vuecomponents/table/columns/Created.js index 49d998b1..db4026c9 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/columns/Created.js +++ b/src/scripts/js/es6/common/vuecomponents/table/columns/Created.js @@ -1,25 +1,13 @@ -import { CellPrettyDate } from '../cells/renderer/CellPrettyDate' -import {ColumnBase} from './ColumnBase' +import {DateColumnBase} from './DateColumnBase' /** * Column showing the objects _created prettyfied */ - -export class Created extends ColumnBase{ +export class Created extends DateColumnBase{ constructor() { super('Created', 'row-created'); this.includedByDefault = false; } - - /** - * - * @param {RowObject} rowObject - * @returns {String} Name of the Cell renderer component - */ - getCellRenderer(rowObject) { - return CellPrettyDate.options.name; - } - /** * * @param {RowObject} rowObject @@ -28,31 +16,4 @@ export class Created extends ColumnBase{ getRawCellValue(rowObject) { return rowObject.underlyingObject['_created']; } - - /** - * Cell tooltip - * @param {Any} rawCellValue - * @param {RowObject} rowObject - * @returns {String} - */ - getCellTitle(rawCellValue, rowObject) { - return rawCellValue; - } - - /** - * Compare two rows to sort them. Can be overridden for more complex situations. - * - * @param {RowObject} rowObject1 - * @param {RowObject} rowObject2 - * @returns {Number} -1, 0, 1 - */ - compareRows(rowObject1, rowObject2) { - let dueDateStr1 = this.getRawCellValue(rowObject1); - let dueDateStr2 = this.getRawCellValue(rowObject2); - if (dueDateStr1 === dueDateStr2) return 0; - if (dueDateStr1 && dueDateStr2) { - return new Date(dueDateStr1) < new Date(dueDateStr2) ? -1 : 1; - } - return dueDateStr1 ? -1 : 1; - } } diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/DateColumnBase.js b/src/scripts/js/es6/common/vuecomponents/table/columns/DateColumnBase.js new file mode 100644 index 00000000..afb84f6c --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/table/columns/DateColumnBase.js @@ -0,0 +1,41 @@ +import { CellPrettyDate } from '../cells/renderer/CellPrettyDate' +import { ColumnBase } from './ColumnBase' + +/** + * Column showing a pretty date + */ +export class DateColumnBase extends ColumnBase{ + /** + * + * @param {RowObject} rowObject + * @returns {String} Name of the Cell renderer component + */ + getCellRenderer(rowObject) { + return CellPrettyDate.options.name; + } + + /** + * Cell tooltip + * @param {Any} rawCellValue + * @param {RowObject} rowObject + * @returns {String} + */ + getCellTitle(rawCellValue, rowObject) { + return rawCellValue; + } + + /** + * @param {RowObject} rowObject1 + * @param {RowObject} rowObject2 + * @returns {Number} -1, 0, 1 + */ + compareRows(rowObject1, rowObject2) { + let dueDateStr1 = this.getRawCellValue(rowObject1); + let dueDateStr2 = this.getRawCellValue(rowObject2); + if (dueDateStr1 === dueDateStr2) return 0; + if (dueDateStr1 && dueDateStr2) { + return new Date(dueDateStr1) < new Date(dueDateStr2) ? -1 : 1; + } + return dueDateStr1 ? -1 : 1; + } +} diff --git a/src/scripts/js/es6/common/vuecomponents/table/columns/Updated.js b/src/scripts/js/es6/common/vuecomponents/table/columns/Updated.js index 855ae48d..4b711fa3 100644 --- a/src/scripts/js/es6/common/vuecomponents/table/columns/Updated.js +++ b/src/scripts/js/es6/common/vuecomponents/table/columns/Updated.js @@ -1,25 +1,13 @@ -import { CellPrettyDate } from '../cells/renderer/CellPrettyDate' -import {ColumnBase} from './ColumnBase' +import {DateColumnBase} from './DateColumnBase' /** * Column showing the objects _updated prettyfied */ - -export class Updated extends ColumnBase{ +export class Updated extends DateColumnBase{ constructor() { super('Updated', 'row-updated'); this.includedByDefault = false; } - - /** - * - * @param {RowObject} rowObject - * @returns {String} Name of the Cell renderer component - */ - getCellRenderer(rowObject) { - return CellPrettyDate.options.name; - } - /** * * @param {RowObject} rowObject @@ -28,31 +16,4 @@ export class Updated extends ColumnBase{ getRawCellValue(rowObject) { return rowObject.underlyingObject['_updated']; } - - /** - * Cell tooltip - * @param {Any} rawCellValue - * @param {RowObject} rowObject - * @returns {String} - */ - getCellTitle(rawCellValue, rowObject) { - return rawCellValue; - } - - /** - * Compare two rows to sort them. Can be overridden for more complex situations. - * - * @param {RowObject} rowObject1 - * @param {RowObject} rowObject2 - * @returns {Number} -1, 0, 1 - */ - compareRows(rowObject1, rowObject2) { - let dueDateStr1 = this.getRawCellValue(rowObject1); - let dueDateStr2 = this.getRawCellValue(rowObject2); - if (dueDateStr1 === dueDateStr2) return 0; - if (dueDateStr1 && dueDateStr2) { - return new Date(dueDateStr1) < new Date(dueDateStr2) ? -1 : 1; - } - return dueDateStr1 ? -1 : 1; - } } From a3b8a8933c609746305e0e5ca52a94f887ee636b Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Thu, 28 Mar 2019 16:03:24 +0100 Subject: [PATCH 39/66] Breadcrumbs: Use element in last item (_self). To be able to style it similarly to the links, but without a link. --- .../js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js b/src/scripts/js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js index 51314662..5987c593 100644 --- a/src/scripts/js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js +++ b/src/scripts/js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js @@ -3,7 +3,7 @@ const TEMPLATE = `
From d5a4c247b0cb4a641c77c69b8197d85a6cfa612a Mon Sep 17 00:00:00 2001 From: Pablo Vazquez Date: Thu, 28 Mar 2019 16:03:50 +0100 Subject: [PATCH 40/66] Breadcrumbs: Initial styling. --- src/styles/components/_breadcrumbs.sass | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/styles/components/_breadcrumbs.sass diff --git a/src/styles/components/_breadcrumbs.sass b/src/styles/components/_breadcrumbs.sass new file mode 100644 index 00000000..3fb7f7b0 --- /dev/null +++ b/src/styles/components/_breadcrumbs.sass @@ -0,0 +1,43 @@ +.breadcrumbs + @extend .bg-dark + @extend .text-secondary + flex: 1 + font-size: $font-size-xs + + ul + @extend .d-flex + @extend .list-unstyled + @extend .m-0 + @extend .align-items-center + + li + @extend .position-relative + @extend .pr-1 + + // Triangle indicator on the right of items. + &:after + content: '\e83a' + font-family: "pillar-font" + position: absolute + right: 0 + top: $font-size-xs / 2.2 + + // Remove indicator on the last item. + &:last-child + &:after + display: none + + a, span + @extend .d-block + @extend .py-1 + @extend .px-2 + + a + @extend .text-white + + span + cursor: default + + // .breadcrumbs-container + &-container + @extend .d-flex From 1fd17303a53016c15f685deebda65bea8e71be90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Thu, 28 Mar 2019 16:37:46 +0100 Subject: [PATCH 41/66] Breadcrumbs: emit 'navigate' event when clicking on the link Clicking on the breadcrumb link now doesn't follow the link any more, but by keeping it as a link users can still open in a new tab. --- .../js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/scripts/js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js b/src/scripts/js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js index 5987c593..2a372c05 100644 --- a/src/scripts/js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js +++ b/src/scripts/js/es6/common/vuecomponents/breadcrumbs/Breadcrumbs.js @@ -2,7 +2,7 @@ const TEMPLATE = `