-
-
- {{$ctrl.statusText}}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$ctrl.statusText}}
+
-
\ No newline at end of file
+
diff --git a/client/core/src/components/drop-down/drop-down.js b/client/core/src/components/drop-down/drop-down.js
index c5e11a839..066e26810 100755
--- a/client/core/src/components/drop-down/drop-down.js
+++ b/client/core/src/components/drop-down/drop-down.js
@@ -4,45 +4,34 @@ import './style.scss';
import './model';
export default class DropDown extends Component {
- constructor($element, $scope, $transclude, $timeout, $http, $translate) {
+ constructor($element, $scope, $transclude, $timeout, $http) {
super($element, $scope);
this.$transclude = $transclude;
this.$timeout = $timeout;
- this.$translate = $translate;
this.valueField = 'id';
this.showField = 'name';
this._search = undefined;
- this._shown = false;
this._activeOption = -1;
this.showLoadMore = true;
this.showFilter = true;
- this.input = this.element.querySelector('.search');
- this.body = this.element.querySelector('.body');
- this.container = this.element.querySelector('ul');
- this.list = this.element.querySelector('.list');
-
this.docKeyDownHandler = e => this.onDocKeyDown(e);
- this.docFocusInHandler = e => this.onDocFocusIn(e);
+ }
- this.element.addEventListener('mousedown',
- e => this.onBackgroundMouseDown(e));
- this.element.addEventListener('focusin',
- e => this.onFocusIn(e));
- this.list.addEventListener('scroll',
- e => this.onScroll(e));
+ $postLink() {
+ this.input = this.element.querySelector('.search');
+ this.ul = this.element.querySelector('ul');
+ this.list = this.element.querySelector('.list');
+ this.list.addEventListener('scroll', e => this.onScroll(e));
}
get shown() {
- return this._shown;
+ return this.$.popover.shown;
}
set shown(value) {
- if (value)
- this.show();
- else
- this.hide();
+ this.$.popover.shown = value;
}
get search() {
@@ -97,62 +86,18 @@ export default class DropDown extends Component {
* @param {String} search The initial search term or %null
*/
show(search) {
- if (this._shown) return;
- this._shown = true;
- this._activeOption = -1;
this.search = search;
+ this._activeOption = -1;
this.buildList();
- this.element.style.display = 'block';
- this.list.scrollTop = 0;
- this.$timeout(() => this.$element.addClass('shown'), 40);
- this.document.addEventListener('keydown', this.docKeyDownHandler);
- this.document.addEventListener('focusin', this.docFocusInHandler);
- this.relocate();
- this.input.focus();
+ this.$.popover.parent = this.parent;
+ this.$.popover.show();
}
/**
* Hides the drop-down.
*/
hide() {
- if (!this._shown) return;
- this._shown = false;
- this.element.style.display = '';
- this.$element.removeClass('shown');
- this.document.removeEventListener('keydown', this.docKeyDownHandler);
- this.document.removeEventListener('focusin', this.docFocusInHandler);
- if (this.parent)
- this.parent.focus();
- }
-
- /**
- * Repositions the drop-down to a correct location relative to the parent.
- */
- relocate() {
- if (!this.parent) return;
-
- let style = this.body.style;
- style.width = '';
- style.height = '';
-
- let parentRect = this.parent.getBoundingClientRect();
- let bodyRect = this.body.getBoundingClientRect();
-
- let top = parentRect.top + parentRect.height;
- let height = bodyRect.height;
- let width = Math.max(bodyRect.width, parentRect.width);
-
- let margin = 10;
-
- if (top + height + margin > window.innerHeight)
- top = Math.max(parentRect.top - height, margin);
-
- style.top = `${top}px`;
- style.left = `${parentRect.left}px`;
- style.width = `${width}px`;
-
- if (height + margin * 2 > window.innerHeight)
- style.height = `${window.innerHeight - margin * 2}px`;
+ this.$.popover.hide();
}
/**
@@ -190,7 +135,7 @@ export default class DropDown extends Component {
let data = this.$.model.data;
if (option >= 0 && data && option < data.length) {
- this.activeLi = this.container.children[option];
+ this.activeLi = this.ul.children[option];
this.activeLi.className = 'active';
}
}
@@ -224,7 +169,7 @@ export default class DropDown extends Component {
}
if (!this.multiple)
- this.hide();
+ this.$.popover.hide();
}
refreshModel() {
@@ -260,22 +205,14 @@ export default class DropDown extends Component {
return undefined;
}
- onMouseDown(event) {
- this.lastMouseEvent = event;
+ onOpen() {
+ this.document.addEventListener('keydown', this.docKeyDownHandler);
+ this.list.scrollTop = 0;
+ this.input.focus();
}
- onBackgroundMouseDown(event) {
- if (event != this.lastMouseEvent)
- this.hide();
- }
-
- onFocusIn(event) {
- this.lastFocusEvent = event;
- }
-
- onDocFocusIn(event) {
- if (event !== this.lastFocusEvent)
- this.hide();
+ onClose() {
+ this.document.removeEventListener('keydown', this.docKeyDownHandler);
}
onClearClick() {
@@ -299,7 +236,7 @@ export default class DropDown extends Component {
onContainerClick(event) {
if (event.defaultPrevented) return;
- let index = getPosition(this.container, event);
+ let index = getPosition(this.ul, event);
if (index != -1) this.selectOption(index);
}
@@ -318,9 +255,6 @@ export default class DropDown extends Component {
case 13: // Enter
this.selectOption(option);
break;
- case 27: // Escape
- this.hide();
- break;
case 38: // Up
this.moveToOption(option <= 0 ? nOpts : option - 1);
break;
@@ -342,8 +276,7 @@ export default class DropDown extends Component {
}
buildList() {
- this.destroyScopes();
- this.scopes = [];
+ this.destroyList();
let hasTemplate = this.$transclude &&
this.$transclude.isSlotFilled('tplItem');
@@ -377,23 +310,26 @@ export default class DropDown extends Component {
}
}
- this.container.innerHTML = '';
- this.container.appendChild(fragment);
+ this.ul.appendChild(fragment);
this.activateOption(this._activeOption);
- this.relocate();
+ this.$.popover.relocate();
}
- destroyScopes() {
+ destroyList() {
+ this.ul.innerHTML = '';
+
if (this.scopes)
for (let scope of this.scopes)
scope.$destroy();
+
+ this.scopes = [];
}
$onDestroy() {
- this.destroyScopes();
+ this.destroyList();
}
}
-DropDown.$inject = ['$element', '$scope', '$transclude', '$timeout', '$http', '$translate'];
+DropDown.$inject = ['$element', '$scope', '$transclude', '$timeout', '$http'];
/**
* Gets the position of an event element relative to a parent.
diff --git a/client/core/src/components/drop-down/drop-down.spec.js b/client/core/src/components/drop-down/drop-down.spec.js
index fc4028fc8..f0bd93853 100644
--- a/client/core/src/components/drop-down/drop-down.spec.js
+++ b/client/core/src/components/drop-down/drop-down.spec.js
@@ -7,6 +7,7 @@ describe('Component vnDropDown', () => {
let $element;
let $scope;
let $httpBackend;
+ let $transitions;
let $q;
let $filter;
let controller;
@@ -15,18 +16,23 @@ describe('Component vnDropDown', () => {
angular.mock.module('client');
});
- beforeEach(angular.mock.inject((_$componentController_, $rootScope, _$timeout_, _$httpBackend_, _$q_, _$filter_) => {
+ beforeEach(angular.mock.inject((_$componentController_, $rootScope, _$timeout_, _$httpBackend_, _$q_, _$filter_, _$transitions_) => {
$componentController = _$componentController_;
$element = angular.element(`
${template}
`);
$timeout = _$timeout_;
+ $transitions = _$transitions_;
$q = _$q_;
$filter = _$filter_;
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', /\/locale\/\w+\/[a-z]{2}\.json/).respond({});
+ let popoverTemplate = require('../popover/popover.html');
+ let $popover = angular.element(`
${popoverTemplate}
`);
+ $scope.popover = $componentController('vnPopover', {$element: $popover, $scope, $timeout, $transitions});
$scope.model = $componentController('vnModel', {$httpBackend, $q, $filter});
controller = $componentController('vnDropDown', {$element, $scope, $transclude: null, $timeout, $httpBackend, $translate: null});
+ controller.$postLink();
controller.parent = angular.element('
')[0];
}));
diff --git a/client/core/src/components/drop-down/style.scss b/client/core/src/components/drop-down/style.scss
index 71bfdd296..94521aff8 100755
--- a/client/core/src/components/drop-down/style.scss
+++ b/client/core/src/components/drop-down/style.scss
@@ -1,30 +1,7 @@
vn-drop-down {
- z-index: 10;
- position: fixed;
- display: none;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
-
- &.shown {
- & > .body {
- transform: translateY(0);
- opacity: 1;
- }
- }
- & > .body {
- position: fixed;
- box-shadow: 0 .1em .4em rgba(1, 1, 1, .4);
- border-radius: .1em;
- background-color: white;
+ .dropdown {
display: flex;
flex-direction: column;
- transform: translateY(-.4em);
- opacity: 0;
- transition-property: opacity, transform;
- transition-duration: 250ms;
- transition-timing-function: ease-in-out;
& > .filter {
position: relative;
@@ -35,6 +12,7 @@ vn-drop-down {
box-sizing: border-box;
border: none;
border-bottom: 1px solid #ccc;
+ font-size: inherit;
padding: .6em;
}
& > vn-icon[icon=clear] {
@@ -64,10 +42,9 @@ vn-drop-down {
ul {
padding: 0;
margin: 0;
+ list-style-type: none;
}
li, .status {
- outline: none;
- list-style-type: none;
padding: .6em;
cursor: pointer;
white-space: nowrap;
diff --git a/client/core/src/components/multi-check/multi-check.scss b/client/core/src/components/multi-check/multi-check.scss
index 596a2e6f5..c253bbde6 100644
--- a/client/core/src/components/multi-check/multi-check.scss
+++ b/client/core/src/components/multi-check/multi-check.scss
@@ -2,10 +2,6 @@
vn-icon{
cursor: pointer;
}
- &:focus, &:active, &:hover{
- outline: none;
- border: none;
- }
.primaryCheckbox {
vn-icon{
font-size: 22px;
diff --git a/client/core/src/components/popover/popover.html b/client/core/src/components/popover/popover.html
new file mode 100644
index 000000000..3e3e6ca59
--- /dev/null
+++ b/client/core/src/components/popover/popover.html
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/client/core/src/components/popover/popover.js b/client/core/src/components/popover/popover.js
index 8430f037c..45a25918f 100644
--- a/client/core/src/components/popover/popover.js
+++ b/client/core/src/components/popover/popover.js
@@ -1,205 +1,220 @@
import ngModule from '../../module';
-import './style.css';
+import Component from '../../lib/component';
+import './style.scss';
-directive.$inject = ['vnPopover'];
-export function directive(vnPopover) {
- return {
- restrict: 'A',
- link: function($scope, $element, $attrs) {
- $element.on('click', function(event) {
- vnPopover.showComponent($attrs.vnDialog, $scope, $element);
- event.preventDefault();
- });
- }
- };
-}
-ngModule.directive('vnPopover', directive);
-
-export class Popover {
- constructor($document, $compile, $transitions) {
- this.document = $document[0];
- this.$compile = $compile;
+/**
+ * A simple popover.
+ */
+export default class Popover extends Component {
+ constructor($element, $scope, $timeout, $transitions) {
+ super($element, $scope);
+ this.$timeout = $timeout;
this.$transitions = $transitions;
- this.removeScope = false;
- this.popOpens = 0;
- }
- _init() {
- this.docMouseDownHandler = e => this.onDocMouseDown(e);
- this.document.addEventListener('mousedown', this.docMouseDownHandler);
+ this._shown = false;
+
this.docKeyDownHandler = e => this.onDocKeyDown(e);
+ this.docFocusInHandler = e => this.onDocFocusIn(e);
+
+ this.element.addEventListener('mousedown',
+ e => this.onBackgroundMouseDown(e));
+ this.element.addEventListener('focusin',
+ e => this.onFocusIn(e));
+
+ this.popover = this.element.querySelector('.popover');
+ this.popover.addEventListener('mousedown',
+ e => this.onMouseDown(e));
+
+ this.arrow = this.element.querySelector('.arrow');
+ this.content = this.element.querySelector('.content');
+ }
+
+ set child(value) {
+ this.content.appendChild(value);
+ }
+
+ get child() {
+ return this.content.firstChild;
+ }
+
+ get shown() {
+ return this._shown;
+ }
+
+ set shown(value) {
+ if (value)
+ this.show();
+ else
+ this.hide();
+ }
+
+ /**
+ * Shows the popover. If a parent is specified it is shown in a visible
+ * relative position to it.
+ */
+ show() {
+ if (this._shown) return;
+
+ this._shown = true;
+ this.element.style.display = 'block';
+ this.$timeout.cancel(this.showTimeout);
+ this.showTimeout = this.$timeout(() => {
+ this.$element.addClass('shown');
+ this.showTimeout = null;
+ }, 30);
+
this.document.addEventListener('keydown', this.docKeyDownHandler);
- this.deregisterCallback = this.$transitions.onStart({},
- () => this.hideAll());
+ this.document.addEventListener('focusin', this.docFocusInHandler);
+
+ this.deregisterCallback = this.$transitions.onStart({}, () => this.hide());
+ this.relocate();
+
+ if (this.onOpen)
+ this.onOpen();
}
- _destroy() {
- this.document.removeEventListener('mousedown', this.docMouseDownHandler);
+
+ /**
+ * Hides the popover.
+ */
+ hide() {
+ if (!this._shown) return;
+
+ this._shown = false;
+ this.$element.removeClass('shown');
+ this.$timeout.cancel(this.showTimeout);
+ this.showTimeout = this.$timeout(() => {
+ this.element.style.display = 'none';
+ this.showTimeout = null;
+ }, 250);
+
this.document.removeEventListener('keydown', this.docKeyDownHandler);
- this.docMouseDownHandler = null;
- this.docKeyDownHandler = null;
- this.deregisterCallback();
- }
- show(childElement, parent, popoverId) {
- this.childElement = childElement;
- let popover = this.document.createElement('div');
- this.popOpens++;
+ this.document.removeEventListener('focusin', this.docFocusInHandler);
- if (!popoverId) {
- popoverId = 'popover-' + this.popOpens;
- popover.id = popoverId;
- }
+ if (this.deregisterCallback)
+ this.deregisterCallback();
- popover.className = 'vn-popover';
- popover.addEventListener('mousedown',
- e => this.onPopoverMouseDown(e));
- popover.appendChild(childElement);
- this.popover = popover;
+ if (this.parent)
+ this.parent.focus();
- let style = popover.style;
-
- let spacing = 0;
- let screenMargin = 20;
- let dblMargin = screenMargin * 2;
-
- let width = popover.offsetWidth;
- let height = popover.offsetHeight;
- let innerWidth = window.innerWidth;
- let innerHeight = window.innerHeight;
-
- if (width + dblMargin > innerWidth) {
- width = innerWidth - dblMargin;
- style.width = width + 'px';
- }
-
- if (height + dblMargin > innerHeight) {
- height = innerHeight - dblMargin;
- style.height = height + 'px';
- }
-
- if (parent) {
- let parentNode = parent;
- let rect = parentNode.getBoundingClientRect();
- let left = rect.left;
- let top = rect.top + spacing + parentNode.offsetHeight;
-
- if (left + width > innerWidth)
- left -= (left + width) - innerWidth + margin;
-
- if (top + height > innerHeight)
- top -= height + parentNode.offsetHeight + spacing * 2;
-
- if (left < 0)
- left = screenMargin;
-
- if (top < 0)
- top = screenMargin;
-
- style.top = (top) + 'px';
- style.left = (left) + 'px';
- style.minWidth = (rect.width) + 'px';
- }
-
- this.document.body.appendChild(popover);
-
- if (this.popOpens === 1) {
- this._init();
- }
- return popoverId;
+ if (this.onClose)
+ this.onClose();
}
- showComponent(childComponent, $scope, parent) {
- let childElement = this.document.createElement(childComponent);
- let id = 'popover-' + this.popOpens;
- childElement.id = id;
- this.removeScope = true;
- this.$compile(childElement)($scope.$new());
- this.show(childElement, parent, id);
- return childElement;
- }
+ /**
+ * Repositions the popover to a correct location relative to the parent.
+ */
+ relocate() {
+ if (!(this.parent && this._shown)) return;
- _checkOpens() {
- this.popOpens = this.document.querySelectorAll('*[id^="popover-"]').length;
- if (this.popOpens === 0) {
- this._destroy();
- }
- }
+ let style = this.popover.style;
+ style.width = '';
+ style.height = '';
- _removeElement(val) {
- if (!val) return;
- let element = angular.element(val);
- let parent = val.parentNode;
- if (element.scope() && element.scope().$id > 1) {
- element.scope().$destroy();
- }
- element.remove();
- if (parent.className.indexOf('vn-popover') !== -1)
- this._removeElement(parent);
- }
+ let arrowStyle = this.arrow.style;
+ arrowStyle.top = '';
+ arrowStyle.bottom = '';
- hide(id) {
- let popover = this.document.querySelector(`#${id}`);
- if (popover) {
- this._removeElement(popover);
- }
- this._checkOpens();
- }
+ let parentRect = this.parent.getBoundingClientRect();
+ let popoverRect = this.popover.getBoundingClientRect();
+ let arrowRect = this.arrow.getBoundingClientRect();
- hideChilds(id) {
- let popovers = this.document.querySelectorAll('*[id^="popover-"]');
- let idNumber = parseInt(id.split('-')[1], 10);
- popovers.forEach(
- val => {
- if (parseInt(val.id.split('-')[1], 10) > idNumber)
- this._removeElement(val);
- }
- );
- this._checkOpens();
- }
+ let arrowHeight = Math.sqrt(Math.pow(arrowRect.height, 2) * 2) / 2;
- hideAll() {
- let popovers = this.document.querySelectorAll('*[id^="popover-"]');
- popovers.forEach(
- val => {
- this._removeElement(val);
- }
- );
- this._checkOpens();
- }
+ let top = parentRect.top + parentRect.height + arrowHeight;
+ let left = parentRect.left;
+ let height = popoverRect.height;
+ let width = Math.max(popoverRect.width, parentRect.width);
- _findPopOver(node) {
- while (node != null) {
- if (node.id && node.id.startsWith('popover-')) {
- return node.id;
- }
- node = node.parentNode;
- }
- return null;
- }
+ let margin = 10;
+ let showTop = top + height + margin > window.innerHeight;
- onDocMouseDown(event) {
- let targetId = this._findPopOver(event.target);
- if (targetId) {
- this.hideChilds(targetId);
- } else {
- this.hideAll();
- }
+ if (showTop)
+ top = Math.max(parentRect.top - height - arrowHeight, margin);
+ if (left + width + margin > window.innerWidth)
+ left = window.innerWidth - width - margin;
+
+ if (showTop)
+ arrowStyle.bottom = `0`;
+ else
+ arrowStyle.top = `0`;
+
+ arrowStyle.left = `${(parentRect.left - left) + parentRect.width / 2}px`;
+
+ style.top = `${top}px`;
+ style.left = `${left}px`;
+ style.width = `${width}px`;
+
+ if (height + margin * 2 + arrowHeight > window.innerHeight)
+ style.height = `${window.innerHeight - margin * 2 - arrowHeight}px`;
}
onDocKeyDown(event) {
- if (event.keyCode === 27) {
- let targetId = this._findPopOver(this.lastTarget);
- if (targetId) {
- this.hideChilds(targetId);
- } else {
- this.hideAll();
- }
- this.lastTarget = null;
+ if (event.defaultPrevented) return;
+
+ if (event.keyCode == 27) { // Esc
+ event.preventDefault();
+ this.hide();
+ this.$.$applyAsync();
}
}
- onPopoverMouseDown(event) {
- this.lastTarget = event.target;
+ onMouseDown(event) {
+ this.lastMouseEvent = event;
+ }
+
+ onBackgroundMouseDown(event) {
+ if (event != this.lastMouseEvent)
+ this.hide();
+ }
+
+ onFocusIn(event) {
+ this.lastFocusEvent = event;
+ }
+
+ onDocFocusIn(event) {
+ if (event !== this.lastFocusEvent)
+ this.hide();
}
}
-Popover.$inject = ['$document', '$compile', '$transitions'];
+Popover.$inject = ['$element', '$scope', '$timeout', '$transitions'];
-ngModule.service('vnPopover', Popover);
+ngModule.component('vnPopover', {
+ template: require('./popover.html'),
+ controller: Popover,
+ transclude: true,
+ bindings: {
+ onOpen: '&?',
+ onClose: '&?'
+ }
+});
+
+class PopoverService {
+ constructor($document, $compile, $transitions, $rootScope) {
+ this.$compile = $compile;
+ this.$rootScope = $rootScope;
+ this.$document = $document;
+ this.stack = [];
+ }
+ show(child, parent, $scope) {
+ let element = this.$compile('
')($scope || this.$rootScope)[0];
+ let popover = element.$ctrl;
+ popover.parent = parent;
+ popover.child = child;
+ popover.show();
+ popover.onClose = () => {
+ this.$document[0].body.removeChild(element);
+ if ($scope) $scope.$destroy();
+ };
+ this.$document[0].body.appendChild(element);
+ return popover;
+ }
+
+ showComponent(componentTag, $scope, parent) {
+ let $newScope = $scope.$new();
+ let childElement = this.$compile(`<${componentTag}/>`)($newScope)[0];
+ this.show(childElement, parent, $newScope);
+ return childElement;
+ }
+}
+PopoverService.$inject = ['$document', '$compile', '$transitions', '$rootScope'];
+
+ngModule.service('vnPopover', PopoverService);
diff --git a/client/core/src/components/popover/style.css b/client/core/src/components/popover/style.css
deleted file mode 100644
index 536359a2f..000000000
--- a/client/core/src/components/popover/style.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.vn-popover {
- position: fixed;
- box-shadow: 0 0 .4em rgba(1,1,1,.4);
- background-color: white;
- z-index: 100;
- border-radius: .1em;
- top: 0;
- left: 0;
-}
\ No newline at end of file
diff --git a/client/core/src/components/popover/style.scss b/client/core/src/components/popover/style.scss
new file mode 100644
index 000000000..f8323fb5e
--- /dev/null
+++ b/client/core/src/components/popover/style.scss
@@ -0,0 +1,43 @@
+vn-popover {
+ display: none;
+ z-index: 10;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+
+ opacity: 0;
+ transform: translateY(-.6em);
+ transition-property: opacity, transform;
+ transition-duration: 200ms;
+ transition-timing-function: ease-in-out;
+
+ &.shown {
+ transform: translateY(0);
+ opacity: 1;
+ }
+ & > .popover {
+ position: absolute;
+ display: flex;
+ box-shadow: 0 .1em .4em rgba(1, 1, 1, .4);
+
+ & > .arrow {
+ width: 1em;
+ height: 1em;
+ margin: -.5em;
+ background-color: white;
+ box-shadow: 0 .1em .4em rgba(1, 1, 1, .4);
+ position: absolute;
+ transform: rotate(45deg);
+ z-index: 0;
+ }
+ & > .content {
+ width: 100%;
+ border-radius: .1em;
+ overflow: auto;
+ background-color: white;
+ z-index: 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/client/core/src/components/textfield/style.scss b/client/core/src/components/textfield/style.scss
index 81e5ca74c..181ad8d8b 100644
--- a/client/core/src/components/textfield/style.scss
+++ b/client/core/src/components/textfield/style.scss
@@ -1,7 +1,4 @@
vn-textfield {
- div {
- outline: none; //remove chrome outline
- }
.mdl-chip__action {
position: absolute;
width: auto;
diff --git a/client/core/src/directives/index.js b/client/core/src/directives/index.js
index 02f922149..23392e5e0 100644
--- a/client/core/src/directives/index.js
+++ b/client/core/src/directives/index.js
@@ -1,6 +1,7 @@
import './id';
import './focus';
import './dialog';
+import './popover';
import './validation';
import './acl';
import './on-error-src';
diff --git a/client/core/src/directives/popover.js b/client/core/src/directives/popover.js
new file mode 100644
index 000000000..437d93783
--- /dev/null
+++ b/client/core/src/directives/popover.js
@@ -0,0 +1,26 @@
+import ngModule from '../module';
+import Popover from '../components/popover/popover';
+import {kebabToCamel} from '../lib/string';
+
+/**
+ * Directive used to open a popover.
+ *
+ * @return {Object} The directive
+ */
+export function directive() {
+ return {
+ restrict: 'A',
+ link: function($scope, $element, $attrs) {
+ $element.on('click', function(event) {
+ let popoverKey = kebabToCamel($attrs.vnPopover);
+ let popover = $scope[popoverKey];
+ if (popover instanceof Popover) {
+ popover.parent = $element[0];
+ popover.show();
+ }
+ event.preventDefault();
+ });
+ }
+ };
+}
+ngModule.directive('vnPopover', directive);
diff --git a/client/item/routes.json b/client/item/routes.json
index ac1c55820..7c0832f25 100644
--- a/client/item/routes.json
+++ b/client/item/routes.json
@@ -1,7 +1,7 @@
{
"module": "item",
"name": "Items",
- "icon": "/static/images/icon_item.png",
+ "icon": "inbox",
"validations" : true,
"routes": [
{
diff --git a/client/item/src/descriptor/item-descriptor.html b/client/item/src/descriptor/item-descriptor.html
index e9a225502..38002ed5a 100644
--- a/client/item/src/descriptor/item-descriptor.html
+++ b/client/item/src/descriptor/item-descriptor.html
@@ -18,7 +18,7 @@
+ vn-visible-by="administrative, salesAssistant">
diff --git a/client/production/routes.json b/client/production/routes.json
index 1c3abd874..159a80144 100644
--- a/client/production/routes.json
+++ b/client/production/routes.json
@@ -1,7 +1,7 @@
{
"module": "production",
"name": "Production",
- "icon": "/static/images/icon_production.png",
+ "icon": "local_florist",
"validations" : false,
"routes": [
{
diff --git a/client/salix/src/components/main-menu/main-menu.html b/client/salix/src/components/main-menu/main-menu.html
index 863585baa..72a4bdc06 100644
--- a/client/salix/src/components/main-menu/main-menu.html
+++ b/client/salix/src/components/main-menu/main-menu.html
@@ -6,29 +6,33 @@
-
+
+
+
-
+
+
+
vn-icon {
+ padding-right: .3em;
+ vertical-align: middle;
+ }
+ &:hover {
+ background-color: #FF9300;
+ opacity: 0.7 !important;
+ }
+ &:last-child {
+ margin-bottom: 0;
+ }
}
- i {
- float: left;
- padding-top: 13px;
- margin-right: 3px;
- }
- }
- li.mdl-menu__item:hover {
- background-color: #FF9300;
- opacity: 0.7 !important;
- }
- li.mdl-menu__item:last-child {
- margin-bottom: 0;
}
}
\ No newline at end of file
diff --git a/client/salix/src/styles/misc.scss b/client/salix/src/styles/misc.scss
index 34fc2063c..e20bda7ed 100644
--- a/client/salix/src/styles/misc.scss
+++ b/client/salix/src/styles/misc.scss
@@ -3,6 +3,21 @@
@import "colors";
@import "border";
+
+a:focus,
+input:focus,
+button:focus
+{
+ outline: none;
+}
+button::-moz-focus-inner,
+input[type=submit]::-moz-focus-inner,
+input[type=button]::-moz-focus-inner,
+input[type=reset]::-moz-focus-inner
+{
+ border: none;
+}
+
.form {
height: 100%;
box-sizing: border-box;
diff --git a/e2e/helpers/components_selectors.js b/e2e/helpers/components_selectors.js
index ea46353d6..488d002e0 100644
--- a/e2e/helpers/components_selectors.js
+++ b/e2e/helpers/components_selectors.js
@@ -8,7 +8,6 @@ export default {
vnSubmit: 'vn-submit > input',
vnTopbar: 'vn-topbar > header',
vnIcon: 'vn-icon',
- vnMainMenu: 'vn-main-menu > div',
vnModuleContainer: 'vn-module-container > a',
vnSearchBar: 'vn-searchbar > form > vn-horizontal',
vnFloatButton: 'vn-float-button > button',
diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js
index 1a839a0cb..62e0278ac 100644
--- a/e2e/helpers/selectors.js
+++ b/e2e/helpers/selectors.js
@@ -6,8 +6,8 @@ export default {
globalItems: {
logOutButton: `#logout`,
applicationsMenuButton: `#apps`,
- applicationsMenuVisible: `${components.vnMainMenu} .is-visible > div`,
- clientsButton: `${components.vnMainMenu} > div > ul > li:nth-child(1)`
+ applicationsMenuVisible: `vn-main-menu [vn-id="apps-menu"] ul`,
+ clientsButton: `vn-main-menu [vn-id="apps-menu"] ul > li:nth-child(1)`
},
moduleAccessView: {
clientsSectionButton: `${components.vnModuleContainer}[ui-sref="clients"]`,
diff --git a/e2e/paths/client-module/12_lock_of_verified_data.spec.js b/e2e/paths/client-module/12_lock_of_verified_data.spec.js
index a3d8a3dae..764476e62 100644
--- a/e2e/paths/client-module/12_lock_of_verified_data.spec.js
+++ b/e2e/paths/client-module/12_lock_of_verified_data.spec.js
@@ -280,4 +280,193 @@ describe('lock verified data path', () => {
});
});
});
+
+ describe('as salesAssistant', () => {
+ beforeAll(() => {
+ return nightmare
+ .waitToClick(selectors.globalItems.logOutButton)
+ .waitForLogin('salesAssistant');
+ });
+
+ it('should navigate to clients index', () => {
+ return nightmare
+ .waitToClick(selectors.globalItems.applicationsMenuButton)
+ .wait(selectors.globalItems.applicationsMenuVisible)
+ .waitToClick(selectors.globalItems.clientsButton)
+ .wait(selectors.clientsIndex.createClientButton)
+ .parsedUrl()
+ .then(url => {
+ expect(url.hash).toEqual('#!/clients');
+ });
+ });
+
+ it('should search again for the user Petter Parker', () => {
+ return nightmare
+ .wait(selectors.clientsIndex.searchResult)
+ .type(selectors.clientsIndex.searchClientInput, 'Petter Parker')
+ .click(selectors.clientsIndex.searchButton)
+ .waitForNumberOfElements(selectors.clientsIndex.searchResult, 1)
+ .countSearchResults(selectors.clientsIndex.searchResult)
+ .then(result => {
+ expect(result).toEqual(1);
+ });
+ });
+
+ it(`should click on the search result to access to the Petter Parkers fiscal data`, () => {
+ return nightmare
+ .waitForTextInElement(selectors.clientsIndex.searchResult, 'Petter Parker')
+ .waitToClick(selectors.clientsIndex.searchResult)
+ .waitToClick(selectors.clientFiscalData.fiscalDataButton)
+ .waitForURL('fiscal-data')
+ .parsedUrl()
+ .then(url => {
+ expect(url.hash).toContain('fiscal-data');
+ });
+ });
+
+ it(`should click on the fiscal data button`, () => {
+ return nightmare
+ .waitToClick(selectors.clientFiscalData.fiscalDataButton)
+ .waitForURL('fiscal-data')
+ .parsedUrl()
+ .then(url => {
+ expect(url.hash).toContain('fiscal-data');
+ });
+ });
+
+ it('should confirm verified data button is enabled for salesAssistant', () => {
+ return nightmare
+ .wait(selectors.clientFiscalData.verifiedDataCheckboxInput)
+ .evaluate(selector => {
+ return document.querySelector(selector).className;
+ }, 'body > vn-app > vn-vertical > vn-vertical > vn-client-card > vn-main-block > vn-horizontal > vn-one > vn-vertical > vn-client-fiscal-data > form > vn-card > div > vn-horizontal:nth-child(5) > vn-check:nth-child(3) > label')
+ .then(result => {
+ expect(result).not.toContain('is-disabled');
+ });
+ });
+
+ it('should uncheck the Verified data checkbox', () => {
+ return nightmare
+ .waitToClick(selectors.clientFiscalData.verifiedDataCheckboxInput)
+ .waitToClick(selectors.clientFiscalData.saveButton)
+ .waitForSnackbar()
+ .then(result => {
+ expect(result).toEqual('Data saved!');
+ });
+ });
+
+ it('should confirm Verified data checkbox is unchecked', () => {
+ return nightmare
+ .waitToClick(selectors.clientBasicData.basicDataButton)
+ .wait(selectors.clientBasicData.nameInput)
+ .waitToClick(selectors.clientFiscalData.fiscalDataButton)
+ .wait(selectors.clientFiscalData.verifiedDataCheckboxInput)
+ .evaluate(selector => {
+ return document.querySelector(selector).checked;
+ }, selectors.clientFiscalData.verifiedDataCheckboxInput)
+ .then(value => {
+ expect(value).toBeFalsy();
+ });
+ });
+
+ it('should again edit the social name', () => {
+ return nightmare
+ .wait(selectors.clientFiscalData.socialNameInput)
+ .clearInput(selectors.clientFiscalData.socialNameInput)
+ .type(selectors.clientFiscalData.socialNameInput, 'salesAssistant was here')
+ .click(selectors.clientFiscalData.saveButton)
+ .waitForSnackbar()
+ .then(result => {
+ expect(result).toEqual('Data saved!');
+ });
+ });
+
+ it('should confirm the social name have been edited once and for all', () => {
+ return nightmare
+ .waitToClick(selectors.clientBasicData.basicDataButton)
+ .wait(selectors.clientBasicData.nameInput)
+ .waitToClick(selectors.clientFiscalData.fiscalDataButton)
+ .wait(selectors.clientFiscalData.socialNameInput)
+ .getInputValue(selectors.clientFiscalData.socialNameInput)
+ .then(result => {
+ expect(result).toEqual('salesAssistant was here');
+ });
+ });
+ });
+
+ describe('as salesPerson third run', () => {
+ beforeAll(() => {
+ return nightmare
+ .waitToClick(selectors.globalItems.logOutButton)
+ .waitForLogin('salesPerson');
+ });
+
+ it('should again click on the Clients button of the top bar menu', () => {
+ return nightmare
+ .waitToClick(selectors.globalItems.applicationsMenuButton)
+ .wait(selectors.globalItems.applicationsMenuVisible)
+ .waitToClick(selectors.globalItems.clientsButton)
+ .wait(selectors.clientsIndex.createClientButton)
+ .parsedUrl()
+ .then(url => {
+ expect(url.hash).toEqual('#!/clients');
+ });
+ });
+
+ it('should once again search for the user Petter Parker', () => {
+ return nightmare
+ .wait(selectors.clientsIndex.searchResult)
+ .type(selectors.clientsIndex.searchClientInput, 'Petter Parker')
+ .click(selectors.clientsIndex.searchButton)
+ .waitForNumberOfElements(selectors.clientsIndex.searchResult, 1)
+ .countSearchResults(selectors.clientsIndex.searchResult)
+ .then(result => {
+ expect(result).toEqual(1);
+ });
+ });
+
+ it(`should click on the search result to access to the client's fiscal data`, () => {
+ return nightmare
+ .waitForTextInElement(selectors.clientsIndex.searchResult, 'Petter Parker')
+ .waitToClick(selectors.clientsIndex.searchResult)
+ .waitToClick(selectors.clientFiscalData.fiscalDataButton)
+ .waitForURL('fiscal-data')
+ .parsedUrl()
+ .then(url => {
+ expect(url.hash).toContain('fiscal-data');
+ });
+ });
+
+ it(`should click on the fiscal data button to start editing`, () => {
+ return nightmare
+ .waitToClick(selectors.clientFiscalData.fiscalDataButton)
+ .waitForURL('fiscal-data')
+ .parsedUrl()
+ .then(url => {
+ expect(url.hash).toContain('fiscal-data');
+ });
+ });
+
+ it('should confirm verified data button is enabled once again', () => {
+ return nightmare
+ .wait(selectors.clientFiscalData.verifiedDataCheckboxInput)
+ .evaluate(selector => {
+ return document.querySelector(selector).className;
+ }, 'body > vn-app > vn-vertical > vn-vertical > vn-client-card > vn-main-block > vn-horizontal > vn-one > vn-vertical > vn-client-fiscal-data > form > vn-card > div > vn-horizontal:nth-child(5) > vn-check:nth-child(3) > label')
+ .then(result => {
+ expect(result).toContain('is-disabled');
+ });
+ });
+
+ it('should confirm the form is enabled for salesPerson', () => {
+ return nightmare
+ .wait(selectors.clientFiscalData.socialNameInput)
+ .evaluate(selector => {
+ return document.querySelector(selector).className;
+ }, 'vn-textfield[field="$ctrl.client.socialName"] > div')
+ .then(result => {
+ expect(result).not.toContain('is-disabled');
+ });
+ });
+ });
});
diff --git a/gulpfile.js b/gulpfile.js
index 7c9be4d95..e1b3df473 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -63,11 +63,11 @@ gulp.task('services-only', async () => {
/**
* Runs the e2e tests, restoring the fixtures first.
*/
-gulp.task('e2e', ['docker-rebuild'], async () => {
+gulp.task('e2e', ['docker'], async () => {
await runSequenceP('e2e-only');
});
-gulp.task('smokes', ['docker-rebuild'], async () => {
+gulp.task('smokes', ['docker'], async () => {
await runSequenceP('smokes-only');
});
@@ -416,7 +416,7 @@ gulp.task('docker', async () => {
* Does the minium effort to start the docker, if it doesn't exists calls
* the 'docker-run' task, if it is started does nothing. Keep in mind that when
* you do not rebuild the docker you may be using an outdated version of it.
- * See the 'docker-rebuild' task for more info.
+ * See the 'docker' task for more info.
*/
gulp.task('docker-start', async () => {
let state;
diff --git a/services/loopback/common/methods/client/createWithUser.js b/services/loopback/common/methods/client/createWithUser.js
index 2378406b4..f5220a839 100644
--- a/services/loopback/common/methods/client/createWithUser.js
+++ b/services/loopback/common/methods/client/createWithUser.js
@@ -1,5 +1,3 @@
-let md5 = require('md5');
-
module.exports = function(Self) {
Self.remoteMethod('createWithUser', {
description: 'Creates both client and its web account',
@@ -18,43 +16,33 @@ module.exports = function(Self) {
}
});
- Self.createWithUser = (data, callback) => {
+ Self.createWithUser = async data => {
let firstEmail = data.email ? data.email.split(',')[0] : null;
let user = {
name: data.userName,
email: firstEmail,
- password: md5(parseInt(Math.random() * 100000000000000))
+ password: parseInt(Math.random() * 100000000000000)
};
let Account = Self.app.models.Account;
- Account.beginTransaction({}, (error, transaction) => {
- if (error) return callback(error);
+ let transaction = await Account.beginTransaction({});
- Account.create(user, {transaction}, (error, account) => {
- if (error) {
- transaction.rollback();
- return callback(error);
- }
-
- let client = {
- name: data.name,
- fi: data.fi,
- socialName: data.socialName,
- id: account.id,
- email: data.email,
- salesPersonFk: data.salesPersonFk
- };
-
- Self.create(client, {transaction}, (error, newClient) => {
- if (error) {
- transaction.rollback();
- return callback(error);
- }
-
- transaction.commit();
- callback(null, newClient);
- });
- });
- });
+ try {
+ let account = await Account.create(user, {transaction});
+ let client = {
+ id: account.id,
+ name: data.name,
+ fi: data.fi,
+ socialName: data.socialName,
+ email: data.email,
+ salesPersonFk: data.salesPersonFk
+ };
+ newClient = await Self.create(client, {transaction});
+ await transaction.commit();
+ return newClient;
+ } catch (e) {
+ transaction.rollback();
+ throw e;
+ }
};
};
diff --git a/services/loopback/common/methods/client/specs/createWithUser.spec.js b/services/loopback/common/methods/client/specs/createWithUser.spec.js
index eb41d0873..25f6789fb 100644
--- a/services/loopback/common/methods/client/specs/createWithUser.spec.js
+++ b/services/loopback/common/methods/client/specs/createWithUser.spec.js
@@ -48,45 +48,39 @@ describe('Client Create', () => {
.catch(catchErrors(done));
});
- it('should not be able to create a user if exists', done => {
- app.models.Client.findOne({where: {name: 'Charles Xavier'}})
- .then(client => {
- app.models.Account.findOne({where: {id: client.id}})
- .then(account => {
- let formerAccountData = {
- name: client.name,
- userName: account.name,
- email: client.email,
- fi: client.fi,
- socialName: client.socialName
- };
+ it('should not be able to create a user if exists', async() => {
+ let client = await app.models.Client.findOne({where: {name: 'Charles Xavier'}});
+ let account = await app.models.Account.findOne({where: {id: client.id}});
- app.models.Client.createWithUser(formerAccountData, (err, client) => {
- expect(err.details.codes.name[0]).toEqual('uniqueness');
- done();
- });
- });
- })
- .catch(catchErrors(done));
+ let formerAccountData = {
+ name: client.name,
+ userName: account.name,
+ email: client.email,
+ fi: client.fi,
+ socialName: client.socialName
+ };
+
+ try {
+ let client = await app.models.Client.createWithUser(formerAccountData);
+
+ expect(client).toBeNull();
+ } catch (err) {
+ expect(err.details.codes.name[0]).toEqual('uniqueness');
+ }
});
- it('should create a new account', done => {
- app.models.Client.createWithUser(newAccountData, (error, client) => {
- if (error) return catchErrors(done)(error);
- app.models.Account.findOne({where: {name: newAccountData.userName}})
- .then(account => {
- expect(account.name).toEqual(newAccountData.userName);
- app.models.Client.findOne({where: {name: newAccountData.name}})
- .then(client => {
- expect(client.id).toEqual(account.id);
- expect(client.name).toEqual(newAccountData.name);
- expect(client.email).toEqual(newAccountData.email);
- expect(client.fi).toEqual(newAccountData.fi);
- expect(client.socialName).toEqual(newAccountData.socialName);
- done();
- });
- })
- .catch(catchErrors(done));
- });
+ it('should create a new account', async() => {
+ let client = await app.models.Client.createWithUser(newAccountData);
+ let account = await app.models.Account.findOne({where: {name: newAccountData.userName}});
+
+ expect(account.name).toEqual(newAccountData.userName);
+
+ client = await app.models.Client.findOne({where: {name: newAccountData.name}});
+
+ expect(client.id).toEqual(account.id);
+ expect(client.name).toEqual(newAccountData.name);
+ expect(client.email).toEqual(newAccountData.email);
+ expect(client.fi).toEqual(newAccountData.fi);
+ expect(client.socialName).toEqual(newAccountData.socialName);
});
});
diff --git a/services/loopback/common/models/client.js b/services/loopback/common/models/client.js
index b09c38688..f55d23021 100644
--- a/services/loopback/common/models/client.js
+++ b/services/loopback/common/models/client.js
@@ -35,7 +35,7 @@ module.exports = function(Self) {
message: 'Correo electrónico inválido',
allowNull: true,
allowBlank: true,
- with: /^[\w|-|.]+@[\w|-]+(\.[\w|-]+)*(,[\w|-|.]+@[\w|-]+(\.[\w|-]+)*)*$/
+ with: /^[\w|.|-]+@[\w|-]+(\.[\w|-]+)*(,[\w|.|-]+@[\w|-]+(\.[\w|-]+)*)*$/
});
Self.validatesLengthOf('postcode', {
allowNull: true,