merge conflicts

This commit is contained in:
Daniel Herrero 2018-01-30 14:52:28 +01:00
commit 9218aaf059
380 changed files with 61813 additions and 35620 deletions

20
.vscode/launch.json vendored
View File

@ -1,26 +1,6 @@
{ {
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"name": "Iniciar",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/services/auth/server/server.js",
"stopOnEntry": false,
"args": [],
"cwd": "${workspaceRoot}",
"preLaunchTask": null,
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"
],
"env": {
"NODE_ENV": "development"
},
"console": "internalConsole",
"sourceMaps": false,
"outFiles": []
},
{ {
"name": "Asociar", "name": "Asociar",
"type": "node", "type": "node",

2
Jenkinsfile vendored
View File

@ -43,7 +43,7 @@ node
stage ("Stopping/Removing Docker") stage ("Stopping/Removing Docker")
{ {
sh "docker-compose down --rmi all" sh "docker-compose down --rmi 'all'"
} }
stage ("Generar dockers") stage ("Generar dockers")

View File

@ -1,53 +1,67 @@
# Project Title # Project Title
One Paragraph of project description goes here Salix is an Enterprise resource planning (ERP) integrated management of core business processes, in real-time and mediated by software and technology developed with the stack listed below.
Salix is also the scientific name of a beautifull tree! :)
### Prerequisites ### Prerequisites
For testing purposes you will need to install globally the following items: You will need to install globally the following items:
npm install -g karma $ npm install -g karma
npm install -g karma-cli $ npm install -g karma-cli
$ npm install -g gulp
## Getting Started // ### Installing $ npm install -g webpack
$ npm install -g nodemon
Pull from repo.
install nodejs v6 or above.
install nginx globally. install nginx globally.
Ask a senior dev for the datasources.development.json files required to run the project. ## Getting Started // ### Installing
Pull from repository.
install nodejs v6.
Ask a senior developer for the datasources.development.json files required to run the project.
on root run: on root run:
npm install $ npm install
gulp install $ gulp install
lauching nginx: lauching nginx:
./dev.sh $ ./dev.sh
launching frontend: launching frontend:
gulp client $ gulp client
or start nginx before client on sequence
$ gulp clientDev
launching backend: launching backend:
gulp services $ gulp services
or start the local database before services on sequence
$ gulp serivcesDev
Manually reset local fixtures:
$ gulp docker
to check docker images and containers status:
$ docker images
$ docker ps -a
## Running the tests ## Running the tests
for client-side unit tests run from project's root: for client-side unit tests run from project's root:
karma start $ karma start
for server-side unit tests run from project's root: for server-side unit tests run from project's root:
npm run testWatch or test for single run $ npm run test
### Break down into end to end tests ### Break down into end to end tests
on root run: Run local database plus e2e paths:
$ gulp e2e
gulp docker Just the e2e paths as the fixtures are untainted:
$ npm run e2e
wait 10 secs for db to be ready
npm run e2e
## Built With ## Built With

View File

@ -48,7 +48,6 @@ export default class Controller {
onLoginErr(json) { onLoginErr(json) {
this.loading = false; this.loading = false;
this.password = ''; this.password = '';
this.focusUser();
let message; let message;
@ -64,6 +63,7 @@ export default class Controller {
} }
this.showMessage(message); this.showMessage(message);
this.focusUser();
} }
focusUser() { focusUser() {
this.$.userField.select(); this.$.userField.select();

View File

@ -26,7 +26,7 @@
"client": "$ctrl.client" "client": "$ctrl.client"
}, },
"menu": { "menu": {
"description": "Datos básicos", "description": "Basic data",
"icon": "person" "icon": "person"
} }
}, { }, {
@ -37,7 +37,7 @@
"client": "$ctrl.client" "client": "$ctrl.client"
}, },
"menu": { "menu": {
"description": "Datos fiscales", "description": "Fiscal data",
"icon": "account_balance" "icon": "account_balance"
} }
}, { }, {
@ -64,7 +64,7 @@
"client": "$ctrl.client" "client": "$ctrl.client"
}, },
"menu": { "menu": {
"description": "Consignatarios", "description": "Addresses",
"icon": "local_shipping" "icon": "local_shipping"
} }
}, { }, {
@ -83,7 +83,7 @@
"client": "$ctrl.client" "client": "$ctrl.client"
}, },
"menu": { "menu": {
"description": "Acceso web", "description": "Web access",
"icon": "language" "icon": "language"
} }
}, { }, {
@ -99,13 +99,67 @@
"client": "$ctrl.client" "client": "$ctrl.client"
}, },
"menu": { "menu": {
"description": "Notas", "description": "Notes",
"icon": "insert_drive_file" "icon": "insert_drive_file"
} }
}, { }, {
"url": "/create", "url": "/create",
"state": "clientCard.notes.create", "state": "clientCard.notes.create",
"component": "vn-note-create" "component": "vn-note-create"
}, {
"url": "/credit",
"abstract": true,
"state": "clientCard.credit",
"component": "ui-view"
}, {
"url": "/list",
"state": "clientCard.credit.list",
"component": "vn-client-credit-list",
"params": {
"client": "$ctrl.client"
},
"menu": {
"description": "Credit",
"icon": "credit_card"
}
}, {
"url": "/create",
"state": "clientCard.credit.create",
"component": "vn-client-credit-create",
"params": {
"client": "$ctrl.client"
}
}, {
"url": "/greuge",
"abstract": true,
"state": "clientCard.greuge",
"component": "ui-view"
}, {
"url": "/list",
"state": "clientCard.greuge.list",
"component": "vn-client-greuge-list",
"params": {
"client": "$ctrl.client"
},
"menu": {
"description": "Greuge",
"icon": "work"
}
}, {
"url": "/create",
"state": "clientCard.greuge.create",
"component": "vn-client-greuge-create",
"params": {
"client": "$ctrl.client"
}
}, {
"url": "/mandate",
"state": "clientCard.mandate",
"component": "vn-client-mandate",
"menu": {
"description": "Mandate",
"icon": "pan_tool"
}
} }
] ]
} }

View File

@ -12,10 +12,10 @@
<vn-title>Address</vn-title> <vn-title>Address</vn-title>
<vn-horizontal> <vn-horizontal>
<vn-one> <vn-one>
<vn-check label="enabled" field="$ctrl.address.isEnabled"></vn-check> <vn-check label="Enabled" field="$ctrl.address.isEnabled"></vn-check>
</vn-one> </vn-one>
<vn-one> <vn-one>
<vn-check label="Is equalizated" field="$ctrl.address.isEqualizated"></vn-check> <vn-check label="Is equalizated" field="$ctrl.address.isEqualizated" vn-acl="administrative"></vn-check>
</vn-one> </vn-one>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>

View File

@ -5,29 +5,31 @@
<vn-horizontal> <vn-horizontal>
<vn-title vn-one>Addresses</vn-title> <vn-title vn-one>Addresses</vn-title>
</vn-horizontal> </vn-horizontal>
<vn-horizontal ng-repeat="i in index.model track by i.id" class="pad-medium-top" style="align-items: center;"> <vn-horizontal ng-repeat="i in index.model.items track by i.id" class="pad-medium-top" style="align-items: center;">
<vn-auto style="border-radius: .5em;" class="pad-small border-solid" <vn-one border-radius class="pad-small border-solid"
ng-class="{'bg-dark-item': i.isDefaultAddress,'bg-opacity-item': !i.isEnabled && !i.isDefaultAddress}"> ng-class="{'bg-dark-item': i.isDefaultAddress,'bg-opacity-item': !i.isEnabled && !i.isDefaultAddress}">
<vn-horizontal style="align-items: center;"> <vn-horizontal style="align-items: center;">
<vn-none pad-medium-h style="color:#FFA410;"> <vn-none pad-medium-h style="color:#FFA410;">
<i class="material-icons" ng-if="i.isDefaultAddress">star</i> <i class="material-icons" ng-if="i.isDefaultAddress">star</i>
<i class="material-icons pointer" ng-if="!i.isDefaultAddress&&i.isEnabled" vn-tooltip="Set as default" tooltip-position="left" ng-click="$ctrl.setDefault(i.id)">star_border</i> <i class="material-icons pointer" ng-if="!i.isDefaultAddress&&i.isEnabled" vn-tooltip="Set as default" tooltip-position="left" ng-click="$ctrl.setDefault(i.id)">star_border</i>
</vn-none> </vn-none>
<vn-auto> <vn-one>
<div><b>{{::i.consignee}}</b></div> <div><b>{{::i.consignee}}</b></div>
<div>{{::i.street}}</div> <div>{{::i.street}}</div>
<div>{{::i.city}}, {{::i.province}}</div> <div>{{::i.city}}, {{::i.province}}</div>
<div>{{::i.phone}}, {{::i.mobile}}</div> <div>{{::i.phone}}, {{::i.mobile}}</div>
</vn-auto> </vn-one>
<a vn-empty ui-sref="clientCard.addresses.edit({addressId: {{i.id}}})"> <a vn-auto ui-sref="clientCard.addresses.edit({addressId: {{i.id}}})">
<vn-icon-button icon="edit"></vn-icon-button> <vn-icon-button icon="edit"></vn-icon-button>
</a> </a>
</vn-horizontal> </vn-horizontal>
</vn-auto> </vn-one>
</vn-horizontal> </vn-horizontal>
</vn-vertical> </vn-vertical>
</vn-card> </vn-card>
<vn-paging index="index"></vn-paging>
<vn-paging index="index" total="index.model.total"></vn-paging>
<vn-float-button <vn-float-button
fixed-bottom-right fixed-bottom-right
ui-sref="clientCard.addresses.create" ui-sref="clientCard.addresses.create"

View File

@ -1,4 +1,3 @@
{ {
"Addresses": "Consignatarios",
"Set as default": "Establecer como predeterminado" "Set as default": "Establecer como predeterminado"
} }

View File

@ -1,5 +1,4 @@
{ {
"Basic data": "Datos básicos",
"Comercial Name": "Nombre comercial", "Comercial Name": "Nombre comercial",
"Tax number": "NIF/CIF", "Tax number": "NIF/CIF",
"Social name": "Razón social", "Social name": "Razón social",

View File

@ -19,28 +19,24 @@
label="Pay method"> label="Pay method">
</vn-autocomplete> </vn-autocomplete>
<vn-textfield vn-two label="IBAN" field="$ctrl.client.iban" vn-acl="administrative"></vn-textfield> <vn-textfield vn-two label="IBAN" field="$ctrl.client.iban" vn-acl="administrative"></vn-textfield>
<vn-textfield vn-one label="Vencimiento" field="$ctrl.client.dueDay" vn-acl="administrative"></vn-textfield> <vn-textfield vn-one label="Due day" field="$ctrl.client.dueDay" vn-acl="administrative"></vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield vn-one label="Crédito" field="$ctrl.client.credit" vn-acl="administrative"></vn-textfield>
<vn-textfield vn-one label="Crédito asegurado" field="$ctrl.client.creditInsurance" vn-acl="administrative"></vn-textfield>
</vn-horizontal> </vn-horizontal>
<vn-horizontal margin-medium-bottom> <vn-horizontal margin-medium-bottom>
<vn-one> <vn-one>
<vn-check label="Recibido core VNH" field="$ctrl.client.coreVnh" vn-acl="administrative"></vn-check> <vn-check label="Received core VNH" field="$ctrl.client.hasCoreVnh" vn-acl="administrative"></vn-check>
</vn-one> </vn-one>
<vn-one> <vn-one>
<vn-check label="Recibido core VNL" field="$ctrl.client.coreVnl" vn-acl="administrative"></vn-check> <vn-check label="Received core VNL" field="$ctrl.client.hasCoreVnl" vn-acl="administrative"></vn-check>
</vn-one> </vn-one>
<vn-one> <vn-one>
<vn-check label="Recibido B2B VNL" field="$ctrl.client.sepaVnl" vn-acl="administrative"></vn-check> <vn-check label="Received B2B VNL" field="$ctrl.client.hasSepaVnl" vn-acl="administrative"></vn-check>
</vn-one> </vn-one>
</vn-horizontal> </vn-horizontal>
</vn-vertical> </vn-vertical>
</vn-card> </vn-card>
<vn-button-bar> <vn-button-bar>
<vn-submit label="Guardar" vn-acl="administrative"></vn-submit> <vn-submit label="Save" vn-acl="administrative"></vn-submit>
</vn-button-bar> </vn-button-bar>
</form> </form>
<vn-dialog <vn-dialog

View File

@ -17,9 +17,6 @@ export default class Controller {
this.billData.payMethodFk = this.client.payMethodFk; this.billData.payMethodFk = this.client.payMethodFk;
this.billData.iban = this.client.iban; this.billData.iban = this.client.iban;
this.billData.dueDay = this.client.dueDay; this.billData.dueDay = this.client.dueDay;
this.billData.discount = this.client.discount;
this.billData.credit = this.client.credit;
this.billData.creditInsurance = this.client.creditInsurance;
} }
} }
submit() { submit() {
@ -43,7 +40,7 @@ export default class Controller {
} }
returnDialog(response) { returnDialog(response) {
if (response === 'ACCEPT') { if (response === 'ACCEPT') {
this.$http.post(`/mailer/manuscript/payment-update/${this.client.id}`).then( this.$http.post(`/mailer/notification/payment-update/${this.client.id}`).then(
() => { () => {
this.vnApp.showMessage(this.translate.instant('Notification sent!')); this.vnApp.showMessage(this.translate.instant('Notification sent!'));
} }

View File

@ -25,9 +25,6 @@ describe('Client', () => {
describe('copyData()', () => { describe('copyData()', () => {
it(`should define billData using client's data`, () => { it(`should define billData using client's data`, () => {
controller.client = { controller.client = {
credit: 1000000000000,
creditInsurance: null,
discount: 99,
dueDay: 0, dueDay: 0,
iban: null, iban: null,
payMethodFk: 1 payMethodFk: 1
@ -72,8 +69,8 @@ describe('Client', () => {
describe('returnDialog()', () => { describe('returnDialog()', () => {
it('should request to send notification email', () => { it('should request to send notification email', () => {
controller.client = {id: '123'}; controller.client = {id: '123'};
$httpBackend.when('POST', `/mailer/manuscript/payment-update/${controller.client.id}`).respond('done'); $httpBackend.when('POST', `/mailer/notification/payment-update/${controller.client.id}`).respond('done');
$httpBackend.expectPOST(`/mailer/manuscript/payment-update/${controller.client.id}`); $httpBackend.expectPOST(`/mailer/notification/payment-update/${controller.client.id}`);
controller.returnDialog('ACCEPT'); controller.returnDialog('ACCEPT');
$httpBackend.flush(); $httpBackend.flush();
}); });

View File

@ -10,5 +10,10 @@
"Yes, propagate": "Si, propagar", "Yes, propagate": "Si, propagar",
"Equivalent tax spreaded": "Recargo de equivalencia propagado", "Equivalent tax spreaded": "Recargo de equivalencia propagado",
"Invoice by address": "Facturar por consignatario", "Invoice by address": "Facturar por consignatario",
"Equalization tax": "Recargo de equivalencia" "Equalization tax": "Recargo de equivalencia",
"Due day": "Vencimiento",
"Received core VNH": "Recibido core VNH",
"Received core VNL": "Recibido core VNL",
"Received B2B VNL": "Recibido B2B VNL",
"SAVE": "GUARDAR"
} }

View File

@ -1,18 +1,16 @@
<vn-horizontal> <vn-main-block>
<mg-ajax <mg-ajax
path="/client/api/Clients/{{edit.params.id}}/card" path="/client/api/Clients/{{edit.params.id}}/card"
actions="$ctrl.client = edit.model" actions="$ctrl.client = edit.model"
options="mgEdit"> options="mgEdit">
</mg-ajax> </mg-ajax>
<vn-empty style="min-width: 18em; padding-left: 1em; padding-bottom: 1em;"> <vn-horizontal>
<vn-descriptor <vn-auto class="left-block">
client="$ctrl.client" <vn-descriptor client="$ctrl.client"></vn-descriptor>
active="$ctrl.client.active" <vn-left-menu></vn-left-menu>
class="display-block" > </vn-auto>
</vn-descriptor> <vn-one>
<vn-left-menu></vn-left-menu> <vn-vertical ui-view></vn-vertical>
</vn-empty> </vn-one>
<vn-auto> </vn-horizontal>
<vn-vertical style="max-width: 70em; margin: 0 auto;" ui-view></vn-vertical> </vn-main-block>
</vn-auto>
</vn-horizontal>

View File

@ -1,5 +1,4 @@
import ngModule from '../module'; import ngModule from '../module';
import './style.css';
export default class Controller { export default class Controller {
constructor() { constructor() {

View File

@ -1,3 +0,0 @@
vn-descriptor {
font-family: raleway-bold;
}

View File

@ -14,3 +14,8 @@ import './address-edit/address-edit';
import './notes/notes'; import './notes/notes';
import './note-create/note-create'; import './note-create/note-create';
import './web-access/web-access'; import './web-access/web-access';
import './credit-list/credit-list';
import './credit-create/credit-create';
import './greuge-list/greuge-list';
import './greuge-create/greuge-create';
import './mandate/mandate';

View File

@ -20,13 +20,20 @@
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-textfield vn-one label="Email" field="$ctrl.client.email" info="You can save multiple emails by chaining them using comma without spaces, example: user@domain.com,user2@domain.com the first email will be considered as the main"></vn-textfield> <vn-textfield vn-one label="Email" field="$ctrl.client.email" info="You can save multiple emails by chaining them using comma without spaces, example: user@domain.com,user2@domain.com the first email will be considered as the main"></vn-textfield>
<vn-one></vn-one> <vn-autocomplete vn-one
field="$ctrl.client.salesPersonFk"
url="/client/api/Clients/activeSalesPerson"
show-field="name"
value-field="id"
select-fields="surname"
label="Salesperson"
filter-search="{where: {or: [{name: {regexp: 'search'}}, {surname: {regexp: 'search'}}]}}"
></vn-autocomplete>
</vn-horizontal> </vn-horizontal>
</vn-vertical> </vn-vertical>
</vn-card> </vn-card>
<vn-button-bar> <vn-button-bar>
<vn-submit label="Create and edit"></vn-submit> <vn-submit label="Create"></vn-submit>
<vn-button label="Create" ng-click="watcher.submitBack()"></vn-button>
</vn-button-bar> </vn-button-bar>
</div> </div>
</form> </form>

View File

@ -0,0 +1,23 @@
<mg-ajax path="/client/api/Clients/{{patch.params.id}}" options="vnPatch"></mg-ajax>
<vn-watcher
vn-id="watcher"
data="$ctrl.client"
form="form"
save="patch">
</vn-watcher>
<form name="form" ng-submit="watcher.submitGo('clientCard.credit.list')" pad-medium>
<vn-card >
<vn-vertical pad-large>
<vn-title>Add credit</vn-title>
<vn-horizontal>
<vn-textfield vn-one label="Credit" field="$ctrl.client.credit" type="number" vn-focus></vn-textfield>
<vn-one></vn-one>
</vn-horizontal>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-submit label="Save"></vn-submit>
</vn-button-bar>
</form>

View File

@ -0,0 +1,8 @@
import ngModule from '../module';
ngModule.component('vnClientCreditCreate', {
template: require('./credit-create.html'),
bindings: {
client: '<'
}
});

View File

@ -0,0 +1,3 @@
{
"Add credit": "Añadir crédito"
}

View File

@ -0,0 +1,27 @@
<mg-ajax path="/client/api/ClientCredits/filter" options="vnIndexNonAuto"></mg-ajax>
<vn-card pad-medium>
<vn-vertical pad-large>
<vn-title vn-one margin-large-bottom>Credit</vn-title>
<vn-grid-header on-order="$ctrl.onOrder(field, order)">
<vn-column-header vn-one pad-medium-h field="amount" text="Credit"></vn-column-header>
<vn-column-header vn-two pad-medium-h field="created" text="Since" default-order="ASC"></vn-column-header>
<vn-column-header vn-two pad-medium-h field="employee.name" text="Employee" order-locked></vn-column-header>
</vn-grid-header>
<vn-one class="list list-content">
<vn-horizontal
vn-one class="list list-element text-center"
pad-small-bottom
ng-repeat="credit in index.model.instances track by credit.id">
<vn-one pad-medium-h>{{::credit.amount | number:2}} €</vn-one>
<vn-two pad-medium-h>{{::credit.created | date:'dd/MM/yyyy HH:mm' }}</vn-two>
<vn-two pad-medium-h>{{::credit.employee.name}} {{::credit.employee.surname}}</vn-two>
</vn-horizontal>
</vn-one>
<vn-one class="text-center pad-small-v" ng-if="index.model.count === 0" translate>No results</vn-one>
<vn-horizontal vn-one class="list list-footer"></vn-horizontal>
<vn-paging vn-one margin-large-top index="index" total="index.model.count"></vn-paging>
</vn-vertical>
</vn-card>
<a ui-sref="clientCard.credit.create" fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>

View File

@ -0,0 +1,9 @@
import ngModule from '../module';
import FilterClientList from '../filterClientList';
class ClientCreditList extends FilterClientList {}
ngModule.component('vnClientCreditList', {
template: require('./credit-list.html'),
controller: ClientCreditList
});

View File

@ -0,0 +1,5 @@
{
"Since" : "Desde",
"Employee" : "Empleado",
"No results": "Sin resultados"
}

View File

@ -1,15 +1,18 @@
<vn-card> <vn-card margin-medium-v>
<vn-vertical class="margin-medium" pad-medium-top pad-medium-bottom> <vn-vertical>
<vn-horizontal> <vn-auto class="descriptor-header pointer" ui-sref="clients">
<a vn-one ui-sref="clients"> <i class="material-icons">person</i>
<i class="material-icons descriptor-icon">person</i> </vn-auto>
</a> <vn-auto pad-medium>
<vn-vertical vn-two> <vn-vertical>
<div class="margin-none">{{::$ctrl.client.id}}</div> <vn-horizontal ng-repeat="(field, title) in $ctrl.fieldsToShow">
<div class="margin-none">{{$ctrl.client.name}}</div> <strong vn-auto>{{::title}}:</strong>
<div class="margin-none">{{$ctrl.client.phone}}</div> <vn-auto margin-small-left>
<vn-switch label="Activo" model="$ctrl.active"></vn-switch> <span ng-if="field.includes('credit')">{{$ctrl.client[field] || 0 | number:2}} €</span>
<span ng-if="!field.includes('credit')">{{$ctrl.client[field]}}</span>
</vn-auto>
</vn-horizontal>
</vn-vertical> </vn-vertical>
</vn-horizontal> </vn-auto>
</vn-vertical> </vn-vertical>
</vn-card> </vn-card>

View File

@ -1,26 +1,32 @@
import ngModule from '../module'; import ngModule from '../module';
export default class Controller { export default class Controller {
constructor($scope, $http) { constructor($translate) {
this.$http = $http; this.$translate = $translate;
// CLient fields to display
this.fields = ['id', 'name', 'phone', 'credit', 'creditInsurance'];
this.fieldsToShow = {};
} }
set active(value) {
if (this._active !== value && this._active !== undefined)
this.$http.put(`/client/api/Clients/${this.client.id}/activate`);
this._active = value; // concat 2 Arrays without duplicates
_concatFields(a, b) {
return a.concat(b.filter(item => a.indexOf(item) < 0));
} }
get active() {
return this._active; $onInit() {
let fields = (this.moreFields && this.moreFields instanceof Array) ? this._concatFields(this.fields, this.moreFields) : this.fields;
fields.forEach(field => {
this.fieldsToShow[field] = this.$translate.instant(field);
});
} }
} }
Controller.$inject = ['$scope', '$http']; Controller.$inject = ['$translate'];
ngModule.component('vnDescriptor', { ngModule.component('vnDescriptor', {
template: require('./descriptor.html'), template: require('./descriptor.html'),
controller: Controller, controller: Controller,
bindings: { bindings: {
client: '<', client: '<',
active: '<' moreFields: '<?'
} }
}); });

View File

@ -3,30 +3,32 @@ import './descriptor.js';
describe('Client', () => { describe('Client', () => {
describe('Component vnDescriptor', () => { describe('Component vnDescriptor', () => {
let $componentController; let $componentController;
let $scope; let $translate;
let controller; let controller;
beforeEach(() => { beforeEach(() => {
angular.mock.module('client'); angular.mock.module('client');
}); });
beforeEach(angular.mock.inject((_$componentController_, $rootScope) => { beforeEach(angular.mock.inject((_$componentController_, _$translate_) => {
$componentController = _$componentController_; $componentController = _$componentController_;
$scope = $rootScope.$new(); $translate = _$translate_;
controller = $componentController('vnDescriptor', {$scope: $scope}); controller = $componentController('vnDescriptor', {$translate: $translate});
})); }));
describe('set active', () => { describe('onInit()', () => {
it('should check if active is defined and diferent from the new value', () => { it('should create Object with basic fields', () => {
controller.client = {id: 1}; controller.client = {
id: 1,
name: "Peter Parker",
phone: null,
mobile: "666666",
credit: 300,
creditInsurance: null
};
controller.$onInit();
expect(controller._active).toBe(undefined); expect(controller.fieldsToShow.id).toBe('id');
controller.active = false;
expect(controller._active).toBe(false);
controller.active = true;
expect(controller._active).toBe(true);
}); });
}); });
}); });

View File

@ -0,0 +1,40 @@
// Generic object to list models, related to the client, with mgCrud
export default class FilterClientList {
constructor($scope, $timeout, $state) {
this.$ = $scope;
this.$timeout = $timeout;
this.$state = $state;
this.waitingMgCrud = 0;
this.clientFk = $state.params.id;
}
onOrder(field, order) {
this.filter(`${field} ${order}`);
}
filter(order) {
if (this.$.index && this.clientFk) {
this.waitingMgCrud = 0;
this.$.index.filter = {
page: 1,
size: 10,
clientFk: this.clientFk
};
if (order) {
this.$.index.filter.order = order;
}
this.$.index.accept();
} else if (!this.clientFk) {
throw new Error('Error: ClientFk not found');
} else if (this.waitingMgCrud > 3) {
throw new Error('Error: Magic Crud is not loaded');
} else {
this.waitingMgCrud++;
this.$timeout(() => {
this.filter(order);
}, 250);
}
}
}
FilterClientList.$inject = ['$scope', '$timeout', '$state'];

View File

@ -12,6 +12,7 @@
<vn-horizontal> <vn-horizontal>
<vn-textfield autofocus vn-two label="Social name" field="$ctrl.client.socialName" vn-acl="administrative"></vn-textfield> <vn-textfield autofocus vn-two label="Social name" field="$ctrl.client.socialName" vn-acl="administrative"></vn-textfield>
<vn-textfield vn-one label="Tax number" field="$ctrl.client.fi" vn-acl="administrative"></vn-textfield> <vn-textfield vn-one label="Tax number" field="$ctrl.client.fi" vn-acl="administrative"></vn-textfield>
<vn-check vn-one label="Is equalizated" field="$ctrl.client.isEqualizated" vn-acl="administrative"></vn-check>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-textfield vn-two label="Street" field="$ctrl.client.street" vn-focus vn-acl="administrative"></vn-textfield> <vn-textfield vn-two label="Street" field="$ctrl.client.street" vn-focus vn-acl="administrative"></vn-textfield>
@ -42,41 +43,42 @@
</vn-horizontal> </vn-horizontal>
<vn-horizontal margin-small-bottom> <vn-horizontal margin-small-bottom>
<vn-one> <vn-one>
<vn-check label="Equalization tax" field="$ctrl.client.equalizationTax" vn-acl="administrative" vn-acl="administrative"></vn-check> <vn-check label="Active" field="$ctrl.client.isActive" vn-acl="administrative"></vn-check>
</vn-one> </vn-one>
<vn-one> <vn-one>
<vn-check label="Invoice by address" field="$ctrl.client.hasToInvoiceByAddress" vn-acl="administrative" vn-acl="administrative"></vn-check> <vn-check label="Invoice by address" field="$ctrl.client.hasToInvoiceByAddress" vn-acl="administrative"></vn-check>
</vn-one>
<vn-one>
<vn-check label="Verified data" field="$ctrl.client.isTaxDataChecked" vn-acl="administrative"></vn-check>
</vn-one> </vn-one>
<vn-one></vn-one>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-one> <vn-one>
<vn-check label="Has to invoice" field="$ctrl.client.hasToInvoice" vn-acl="administrative"></vn-check> <vn-check label="Has to invoice" field="$ctrl.client.hasToInvoice" vn-acl="administrative"></vn-check>
</vn-one> </vn-one>
<vn-one> <vn-one>
<vn-check label="Invoice by mail" field="$ctrl.client.invoiceByEmail" vn-acl="administrative"></vn-check> <vn-check vn-one label="Invoice by mail" field="$ctrl.client.isToBeMailed" vn-acl="administrative"></vn-check>
</vn-one> </vn-one>
<vn-one> <vn-one>
<vn-check label="Vies" field="$ctrl.client.vies" vn-acl="administrative" vn-acl="administrative"></vn-check> <vn-check vn-one label="Vies" field="$ctrl.client.isVies" vn-acl="administrative"></vn-check>
</vn-one> </vn-one>
</vn-horizontal> </vn-horizontal>
</vn-vertical> </vn-vertical>
</vn-card> </vn-card>
<vn-button-bar> <vn-button-bar>
<vn-submit label="Save"></vn-submit> <vn-submit label="Save" vn-acl="administrative"></vn-submit>
</vn-button-bar> </vn-button-bar>
</form> </form>
<vn-dialog <vn-dialog
vn-id="propagate-equalizationTax" vn-id="propagate-isEqualizated"
on-response="$ctrl.returnDialogEt(response)" on-response="$ctrl.returnDialogEt(response)"
> >
<tpl-body> <tpl-body>
<vn-vertical> <vn-vertical>
<vn-one text-center translate>You changes the equivalent tax</vn-one> <vn-one text-center translate>You changes the equivalen
<vn-one text-center translate>Do you want to spread the change to their consignees?</vn-one> <vn-one text-center translate>Do you want to spread the change to their consig
</vn-vertical> </vn-vertical>
</tpl-body> </tpl-body>
<tpl-buttons> <tpl-buttons>

View File

@ -6,7 +6,7 @@ export default class ClientFiscalData {
this.$http = $http; this.$http = $http;
this.vnApp = vnApp; this.vnApp = vnApp;
this.translate = $translate; this.translate = $translate;
this.equalizationTax = undefined; this.isEqualizated = undefined;
this.copyData(); this.copyData();
} }
@ -16,7 +16,7 @@ export default class ClientFiscalData {
copyData() { copyData() {
if (this.client) { if (this.client) {
this.equalizationTax = this.client.equalizationTax; this.isEqualizated = this.client.isEqualizated;
} }
} }
@ -26,16 +26,16 @@ export default class ClientFiscalData {
} }
checkEtChanges() { checkEtChanges() {
let equals = this.equalizationTax == this.client.equalizationTax; let equals = this.isEqualizated == this.client.isEqualizated;
this.equalizationTax = this.client.equalizationTax; this.isEqualizated = this.client.isEqualizated;
if (!equals) if (!equals)
this.$.propagateEqualizationTax.show(); this.$.propagateIsEqualizated.show();
} }
returnDialogEt(response) { returnDialogEt(response) {
if (response === 'ACCEPT') { if (response === 'ACCEPT') {
this.$http.patch(`/client/api/Clients/${this.client.id}/addressesPropagateRe`, {isEqualizated: this.client.equalizationTax}).then( this.$http.patch(`/client/api/Clients/${this.client.id}/addressesPropagateRe`, {isEqualizated: this.client.isEqualizated}).then(
res => { res => {
if (res.data) if (res.data)
this.vnApp.showMessage(this.translate.instant('Equivalent tax spreaded')); this.vnApp.showMessage(this.translate.instant('Equivalent tax spreaded'));

View File

@ -0,0 +1,36 @@
<mg-ajax path="/client/api/greuges" options="vnPost"></mg-ajax>
<vn-watcher
vn-id="watcher"
data="$ctrl.greuge"
form="form"
save="post">
</vn-watcher>
<form pad-medium name="form" ng-submit="$ctrl.onSubmit()">
<vn-card>
<vn-vertical pad-medium>
<vn-title vn-one margin-large-bottom>Add Greuge</vn-title>
<vn-horizontal>
<vn-textfield vn-one margin-medium-right label="Amount" field="$ctrl.greuge.amount" type="number" step="1" vn-focus></vn-textfield>
<vn-date-picker vn-one
label="Date"
model="$ctrl.greuge.shipped"
ini-options="{enableTime: true, dateFormat: 'd-m-Y h:i', time_24hr: true}"
>
</vn-date-picker>
</vn-horizontal>
<vn-horizontal>
<vn-textfield vn-one margin-medium-right label="Comment" field="$ctrl.greuge.description"></vn-textfield>
<vn-autocomplete vn-one
field="$ctrl.greuge.greugeTypeFk"
url="/client/api/greugeTypes"
label="Type"
>
</vn-autocomplete>
</vn-horizontal>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-submit label="Save"></vn-submit>
</vn-button-bar>
</form>

View File

@ -0,0 +1,25 @@
import ngModule from '../module';
class ClientGreugeCreate {
constructor($scope, $state, $filter) {
this.$ = $scope;
this.$state = $state;
this.greuge = {
shipped: $filter('date')(new Date(), 'yyyy-MM-dd HH:mm')
};
}
onSubmit() {
this.greuge.clientFk = this.$state.params.id;
this.$.watcher.submit().then(
() => {
this.$state.go('clientCard.greuge.list');
}
);
}
}
ClientGreugeCreate.$inject = ['$scope', '$state', '$filter'];
ngModule.component('vnClientGreugeCreate', {
template: require('./greuge-create.html'),
controller: ClientGreugeCreate
});

View File

@ -0,0 +1,39 @@
import './greuge-create.js';
describe('Client', () => {
describe('Component vnClientGreugeCreate', () => {
let $componentController;
let $scope;
let $state;
let controller;
beforeEach(() => {
angular.mock.module('client');
});
beforeEach(angular.mock.inject((_$componentController_, $rootScope, _$state_) => {
$componentController = _$componentController_;
$scope = $rootScope.$new();
$state = _$state_;
$scope.watcher = {
submit: () => {
return {
then: callback => {
callback();
}
};
}
};
controller = $componentController('vnClientGreugeCreate', {$scope: $scope});
}));
describe('onSubmit()', () => {
it('should call the function go() on $state to go to the greuges list', () => {
spyOn($state, 'go');
controller.onSubmit();
expect(controller.$state.go).toHaveBeenCalledWith('clientCard.greuge.list');
});
});
});
});

View File

@ -0,0 +1,38 @@
<mg-ajax path="/client/api/greuges/filter" options="vnIndexNonAuto"></mg-ajax>
<mg-ajax path="/client/api/greuges/{{edit.params.id}}/sumAmount" options="mgEdit"></mg-ajax>
<vn-card pad-medium>
<vn-vertical pad-medium>
<vn-title vn-one margin-large-bottom>Greuge</vn-title>
<vn-grid-header on-order="$ctrl.onOrder(field, order)">
<vn-column-header vn-one pad-medium-h field="shipped" text="Date" default-order="ASC"></vn-column-header>
<vn-column-header vn-two pad-medium-h field="description" text="Comment"></vn-column-header>
<vn-column-header vn-one pad-medium-h field="amount" text="Amount"></vn-column-header>
<vn-column-header vn-one pad-medium-h field="greugeTypeFk" text="Type"></vn-column-header>
</vn-grid-header>
<vn-one class="list list-content">
<vn-horizontal
class="list list-element text-center"
pad-small-bottom
ng-repeat="greuge in index.model.instances track by greuge.id"
>
<vn-one pad-medium-h>{{::greuge.shipped | date:'dd/MM/yyyy HH:mm' }}</vn-one>
<vn-two pad-medium-h>{{::greuge.description}}</vn-two>
<vn-one pad-medium-h>{{::greuge.amount | number:2}} €</vn-one>
<vn-one pad-medium-h>{{::greuge.greugeType.name}}</vn-one>
</vn-horizontal>
</vn-one>
<vn-one class="text-center pad-small-v" ng-if="index.model.count === 0" translate>No results</vn-one>
<vn-horizontal vn-one class="list list-footer text-center">
<vn-one pad-medium-h></vn-one>
<vn-two pad-medium-h></vn-two>
<vn-one pad-medium-h ng-if="index.model.count > 0">{{edit.model.sumAmount | number:2}} €</vn-one>
<vn-one pad-medium-h></vn-one>
</vn-horizontal>
<vn-paging margin-large-top vn-one index="index" total="index.model.count"></vn-paging>
</vn-vertical>
</vn-card>
<a ui-sref="clientCard.greuge.create" fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>

View File

@ -0,0 +1,9 @@
import ngModule from '../module';
import FilterClientList from '../filterClientList';
class ClientGreugeList extends FilterClientList {}
ngModule.component('vnClientGreugeList', {
template: require('./greuge-list.html'),
controller: ClientGreugeList
});

View File

@ -0,0 +1,7 @@
{
"Date" : "Fecha",
"Comment" : "Comentario",
"Amount" : "Importe",
"Type": "Tipo",
"Add Greuge": "Añadir Greuge"
}

View File

@ -3,12 +3,13 @@
<div style="max-width: 40em; margin: 0 auto;"> <div style="max-width: 40em; margin: 0 auto;">
<vn-card> <vn-card>
<vn-horizontal pad-medium> <vn-horizontal pad-medium>
<vn-searchbar vn-auto <vn-searchbar vn-one
index="index" index="index"
on-search="$ctrl.search(index)" on-search="$ctrl.search(index)"
advanced="true" advanced="true"
search="$ctrl.model.search" popover="vn-client-search-panel"
popover="vn-client-search-panel"> ignore-keys = "['page', 'size', 'search']"
>
</vn-searchbar> </vn-searchbar>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>

View File

@ -7,7 +7,6 @@ export default class Controller {
this.model = {}; this.model = {};
} }
search(index) { search(index) {
index.filter.search = this.model.search;
index.accept(); index.accept();
} }
} }

View File

@ -18,19 +18,19 @@ describe('Client', () => {
expect(controller.model).toEqual({}); expect(controller.model).toEqual({});
}); });
describe('search()', () => { // describe('search()', () => {
it(`should set model's search to the search input`, () => { // it(`should set model's search to the search input`, () => {
controller.model.search = 'batman'; // controller.model.search = 'batman';
let index = { // let index = {
filter: {}, // filter: {},
accept: () => { // accept: () => {
return 'accepted'; // return 'accepted';
} // }
}; // };
controller.search(index); // controller.search(index);
expect(index.filter.search).toBe('batman'); // expect(index.filter.search).toBe('batman');
}); // });
}); // });
}); });
}); });

View File

@ -12,5 +12,5 @@ vn-item-client a:hover {
} }
.vn-item-client-name { .vn-item-client-name {
font-family: raleway-bold; font-family: vn-font-bold;
} }

View File

@ -1,7 +1,12 @@
{ {
"Active": "Activo",
"Client": "Cliente", "Client": "Cliente",
"Clients": "Clientes", "Clients": "Clientes",
"Basic data": "Datos básicos",
"Fiscal data": "Datos Fiscales", "Fiscal data": "Datos Fiscales",
"Addresses": "Consignatarios",
"Web access": "Acceso web",
"Notes": "Notas",
"Has to invoice": "Factura", "Has to invoice": "Factura",
"Invoice by mail": "Factura impresa", "Invoice by mail": "Factura impresa",
"Country": "País", "Country": "País",
@ -10,5 +15,11 @@
"Postcode": "Código postal", "Postcode": "Código postal",
"Province": "Provincia", "Province": "Provincia",
"Save": "Guardar", "Save": "Guardar",
"Pay method" : "Forma de pago" "Pay method" : "Forma de pago",
"Address": "Consignatario",
"Credit" : "Crédito",
"Secured credit": "Crédito asegurado",
"Verified data": "Datos comprobados",
"Mandate": "Mandato",
"Amount": "Importe"
} }

View File

@ -0,0 +1,5 @@
{
"Company": "Empresa",
"Register date": "Fecha alta",
"End date": "Fecha baja"
}

View File

@ -0,0 +1,31 @@
<mg-ajax path="/client/api/Mandates/filter" options="vnIndexNonAuto"></mg-ajax>
<vn-card pad-medium>
<vn-vertical pad-medium>
<vn-title vn-one margin-large-bottom>Mandate</vn-title>
<vn-grid-header on-order="$ctrl.onOrder(field, order)">
<vn-column-header vn-one pad-medium-h field="id" text="Id"></vn-column-header>
<vn-column-header vn-one pad-medium-h field="companyFk" text="Company"></vn-column-header>
<vn-column-header vn-one pad-medium-h field="mandateTypeFk" text="Type"></vn-column-header>
<vn-column-header vn-one pad-medium-h field="created" text="Register date" default-order="ASC"></vn-column-header>
<vn-column-header vn-one pad-medium-h field="finished" text="End date"></vn-column-header>
</vn-grid-header>
<vn-one class="list list-content">
<vn-horizontal
vn-one class="list list-element text-center"
pad-small-bottom
ng-repeat="mandate in index.model.instances track by mandate.id"
>
<vn-one pad-medium-h>{{::mandate.id}}</vn-one>
<vn-one pad-medium-h>{{::mandate.company.code}}</vn-one>
<vn-one pad-medium-h>{{::mandate.mandateType.name}}</vn-one>
<vn-one pad-medium-h>{{::mandate.created | date:'dd/MM/yyyy HH:mm' }}</vn-one>
<vn-one pad-medium-h>{{::mandate.finished | date:'dd/MM/yyyy HH:mm' || '-'}}</vn-one>
</vn-horizontal>
</vn-one>
<vn-one class="text-center pad-small-v" ng-if="index.model.count === 0" translate>No results</vn-one>
<vn-horizontal vn-one class="list list-footer"></vn-horizontal>
<vn-paging vn-one margin-large-top index="index" total="index.model.count"></vn-paging>
</vn-vertical>
</vn-card>

View File

@ -0,0 +1,7 @@
import ngModule from '../module';
import FilterClientList from '../filterClientList';
ngModule.component('vnClientMandate', {
template: require('./mandate.html'),
controller: FilterClientList
});

View File

@ -1,3 +0,0 @@
{
"Notes": "Notas"
}

View File

@ -1,13 +1,19 @@
<vn-card ng-show="$ctrl.observations.length" pad-medium> <vn-card ng-show="$ctrl.observations.length" pad-medium>
<vn-vertical pad-large> <vn-vertical pad-large>
<vn-title>Notes</vn-title> <vn-title>Notes</vn-title>
<vn-horizontal ng-repeat="n in $ctrl.observations" margin-small-bottom style="align-items: center;"> <vn-one
<vn-auto style="border-radius: .3em;" class="pad-small border-solid"> ng-repeat="n in $ctrl.observations"
<div class="notes-date">{{::n.created | date:'dd/MM/yyyy HH:mm'}}</div> pad-small border-solid
<div class="notes-date">{{::n.employee.name}}</div> border-radius
<div>{{::n.text}}</div> margin-small-bottom style="align-items: center;">
</vn-auto> <vn-horizontal>
</vn-horizontal> <vn-one >{{::n.employee.name}} {{::n.employee.surname}}</vn-one>
<vn-auto>{{::n.created | date:'dd/MM/yyyy HH:mm'}}</vn-auto>
</vn-horizontal>
<vn-horizontal>
<b>{{::n.text}}</b>
</vn-horizontal>
</vn-one>
</vn-vertical> </vn-vertical>
</vn-card> </vn-card>
<vn-float-button <vn-float-button

View File

@ -1,3 +0,0 @@
.notes-date {
font-family: raleway-bold;
}

View File

@ -1,28 +1,16 @@
import ngModule from '../module'; import ngModule from '../module';
export default class Controller { export default class Controller {
constructor(sessionStorage) { constructor() {
this.sessionStorage = sessionStorage;
// onSubmit() is defined by @vnSearchbar // onSubmit() is defined by @vnSearchbar
this.onSubmit = () => {}; this.onSubmit = () => {};
} }
onSearch() { onSearch() {
this.setStorageValue();
this.onSubmit(this.filter); this.onSubmit(this.filter);
} }
$onChanges() {
var value = this.sessionStorage.get('filter');
if (value !== undefined)
this.filter = value;
}
setStorageValue() {
this.sessionStorage.set('filter', this.filter);
}
} }
Controller.$inject = ['sessionStorage']; Controller.$inject = [];
ngModule.component('vnClientSearchPanel', { ngModule.component('vnClientSearchPanel', {
template: require('./search-panel.html'), template: require('./search-panel.html'),

View File

@ -16,26 +16,26 @@ describe('Client', () => {
controller = $componentController('vnClientSearchPanel', {sessionStorage: sessionStorage}); controller = $componentController('vnClientSearchPanel', {sessionStorage: sessionStorage});
})); }));
describe('onSearch()', () => { // describe('onSearch()', () => {
it('should call setStorageValue() and onSubmit()', () => { // it('should call setStorageValue() and onSubmit()', () => {
spyOn(controller, 'setStorageValue'); // spyOn(controller, 'setStorageValue');
spyOn(controller, 'onSubmit'); // spyOn(controller, 'onSubmit');
controller.setStorageValue(); // controller.setStorageValue();
controller.onSubmit(); // controller.onSubmit();
expect(controller.setStorageValue).toHaveBeenCalledWith(); // expect(controller.setStorageValue).toHaveBeenCalledWith();
expect(controller.onSubmit).toHaveBeenCalledWith(); // expect(controller.onSubmit).toHaveBeenCalledWith();
}); // });
}); // });
describe('$onChanges()', () => { // describe('$onChanges()', () => {
it('should set filter properties using the search values', () => { // it('should set filter properties using the search values', () => {
expect(controller.filter).not.toBeDefined(); // expect(controller.filter).not.toBeDefined();
spyOn(sessionStorage, 'get').and.returnValue({data: 'data'}); // spyOn(sessionStorage, 'get').and.returnValue({data: 'data'});
controller.$onChanges(); // controller.$onChanges();
expect(controller.filter).toBe(sessionStorage.get({data: 'data'})); // expect(controller.filter).toBe(sessionStorage.get({data: 'data'}));
}); // });
}); // });
}); });
}); });

View File

@ -1,7 +1,6 @@
{ {
"User": "Usuario", "User": "Usuario",
"Enable web access": "Habilitar acceso web", "Enable web access": "Habilitar acceso web",
"Web access": "Acceso web",
"New password": "Nueva contraseña", "New password": "Nueva contraseña",
"Repeat password": "Repetir contraseña", "Repeat password": "Repetir contraseña",
"Change password": "Cambiar contraseña" "Change password": "Cambiar contraseña"

View File

@ -1,6 +1,12 @@
<vn-vertical ng-click="$ctrl.showDropDown = true"> <vn-vertical ng-click="$ctrl.showDropDown = true" tabindex="0">
<vn-textfield vn-one label="{{$ctrl.label}}" model="$ctrl.displayValue" readonly="$ctrl.readonly"></vn-textfield> <vn-textfield vn-auto
<vn-drop-down vn-one label="{{$ctrl.label}}"
model="$ctrl.displayValue"
readonly="$ctrl.readonly"
tab-index="-1"
>
</vn-textfield>
<vn-drop-down vn-auto
items="$ctrl.items" items="$ctrl.items"
show="$ctrl.showDropDown" show="$ctrl.showDropDown"
selected="$ctrl.field" selected="$ctrl.field"
@ -11,5 +17,6 @@
filter-action="$ctrl.findItems(search)" filter-action="$ctrl.findItems(search)"
item-width="$ctrl.width" item-width="$ctrl.width"
multiple="$ctrl.multiple" multiple="$ctrl.multiple"
parent = "$ctrl.element"
><vn-item ng-transclude="tplItem">{{$parent.item.name}}</vn-item></vn-drop-down> ><vn-item ng-transclude="tplItem">{{$parent.item.name}}</vn-item></vn-drop-down>
</vn-vertical> </vn-vertical>

View File

@ -2,6 +2,7 @@ import {module} from '../module';
import Component from '../lib/component'; import Component from '../lib/component';
import copyObject from '../lib/copy'; import copyObject from '../lib/copy';
import './style.scss'; import './style.scss';
import { log } from 'util';
class Autocomplete extends Component { class Autocomplete extends Component {
constructor($element, $scope, $http, $timeout, $filter) { constructor($element, $scope, $http, $timeout, $filter) {
@ -27,6 +28,8 @@ class Autocomplete extends Component {
this._multiField = []; this._multiField = [];
this.readonly = true; this.readonly = true;
this.removeLoadMore = false; this.removeLoadMore = false;
this.form = null;
this.findForm = false;
} }
get showDropDown() { get showDropDown() {
@ -87,6 +90,7 @@ class Autocomplete extends Component {
if (this.multiple) { if (this.multiple) {
this.setMultiField(value[this.valueField]); this.setMultiField(value[this.valueField]);
} }
this.setDirtyForm();
} else { } else {
this.setValue(value); this.setValue(value);
} }
@ -284,6 +288,22 @@ class Autocomplete extends Component {
} }
} }
} }
_parentForm() {
this.findForm = true;
let formScope = this.$scope;
while (formScope && !formScope.form && formScope.$id > 1) {
formScope = formScope.$parent;
}
this.form = formScope ? formScope.form || null : null;
}
setDirtyForm() {
if (!this.form && !this.findForm) {
this._parentForm();
}
if (this.form) {
this.form.$setDirty();
}
}
$onInit() { $onInit() {
this.findMore = this.url && this.maxRow; this.findMore = this.url && this.maxRow;

View File

@ -18,18 +18,23 @@ ul.vn-autocomplete {
} }
&.load-more { &.load-more {
color: #ffa410; color: #ffa410;
font-weight: bold; font-family: vn-font-bold;
padding: .4em .8em; padding: .4em .8em;
} }
} }
} }
vn-autocomplete { vn-autocomplete {
position: relative;
vn-vertical {
outline:none;
}
.mdl-chip__action { .mdl-chip__action {
position: absolute; position: absolute;
top: 0px; top: 0px;
right: -6px; right: -6px;
margin: 22px 0px; margin: 22px 0px;
background-color: white; background: transparent;
} }
.material-icons { .material-icons {
font-size: 18px; font-size: 18px;

View File

@ -1,11 +1,22 @@
<div class="{{$ctrl.className}}" ng-mouseover="$ctrl.mouseIsOver = true" ng-mouseleave="$ctrl.mouseIsOver = false"> <div class="{{$ctrl.className}}" ng-mouseover="$ctrl.mouseIsOver = true" ng-mouseleave="$ctrl.mouseIsOver = false">
<vn-horizontal ng-if="$ctrl.text" class="orderly" ng-click="$ctrl.onClick()"> <vn-horizontal ng-if="$ctrl.text" class="orderly">
<vn-none class="title" translate> <vn-none
{{::$ctrl.text}} class="title"
ng-class="{'noDrop': $ctrl.orderLocked, 'pointer' : !$ctrl.orderLocked}"
ng-click="$ctrl.onClick($event)" translate>
{{::$ctrl.text}}
</vn-none> </vn-none>
<vn-none> <vn-none ng-if="!$ctrl.orderLocked">
<vn-icon icon="arrow_drop_down" ng-if="$ctrl.showArrow('DESC')"></vn-icon> <vn-icon
<vn-icon icon="arrow_drop_up" ng-if="$ctrl.showArrow('ASC')"></vn-icon> icon="arrow_drop_down"
ng-class="{'active': $ctrl.showArrow('DESC')}"
ng-click="$ctrl.onClick($event, 'DESC')"
></vn-icon>
<vn-icon
icon="arrow_drop_up"
ng-class="{'active': $ctrl.showArrow('ASC')}"
ng-click="$ctrl.onClick($event, 'ASC')"
></vn-icon>
</vn-none> </vn-none>
</vn-horizontal> </vn-horizontal>
<ng-transclude ng-if="!$ctrl.text"></ng-transclude> <ng-transclude ng-if="!$ctrl.text"></ng-transclude>

View File

@ -1,19 +1,29 @@
import {module} from '../module'; import {module} from '../module';
export default class ColumnHeader { export default class ColumnHeader {
constructor() { constructor($attrs) {
this.order = undefined; this.order = undefined;
this.mouseIsOver = false; this.mouseIsOver = false;
this.orderLocked = ($attrs.orderLocked !== undefined);
} }
onClick() { onClick(event, order) {
if (this.order === 'ASC') { if (!this.orderLocked) {
this.order = 'DESC'; if (order) {
} else { this.order = order;
this.order = 'ASC'; } else if (this.order === 'ASC') {
this.order = 'DESC';
} else {
this.order = 'ASC';
}
this.gridHeader.selectColum(this);
} }
this.gridHeader.selectColum(this); if (event)
event.preventDefault();
} }
showArrow(type) { showArrow(type) {
if (this.orderLocked)
return false;
let showArrow = (this.gridHeader && this.gridHeader.currentColumn && this.gridHeader.currentColumn.field === this.field && this.order === type); let showArrow = (this.gridHeader && this.gridHeader.currentColumn && this.gridHeader.currentColumn.field === this.field && this.order === type);
let showOther = (this.gridHeader && this.gridHeader.currentColumn && this.gridHeader.currentColumn.field === this.field && this.order !== type); let showOther = (this.gridHeader && this.gridHeader.currentColumn && this.gridHeader.currentColumn.field === this.field && this.order !== type);
if (type === 'DESC' && this.mouseIsOver && !showOther) { if (type === 'DESC' && this.mouseIsOver && !showOther) {
@ -22,13 +32,13 @@ export default class ColumnHeader {
return showArrow; return showArrow;
} }
$onInit() { $onInit() {
if (this.defaultOrder) { if (this.defaultOrder && !this.orderLocked) {
this.order = this.defaultOrder; this.order = this.defaultOrder;
this.onClick(); this.onClick();
} }
} }
} }
ColumnHeader.$inject = []; ColumnHeader.$inject = ['$attrs'];
module.component('vnColumnHeader', { module.component('vnColumnHeader', {
template: require('./column-header.html'), template: require('./column-header.html'),

View File

@ -3,6 +3,8 @@ import './column-header.js';
describe('Component vnColumnHeader', () => { describe('Component vnColumnHeader', () => {
let $componentController; let $componentController;
let controller; let controller;
let $event;
let $attrs;
beforeEach(() => { beforeEach(() => {
angular.mock.module('client'); angular.mock.module('client');
@ -10,14 +12,18 @@ describe('Component vnColumnHeader', () => {
beforeEach(angular.mock.inject(_$componentController_ => { beforeEach(angular.mock.inject(_$componentController_ => {
$componentController = _$componentController_; $componentController = _$componentController_;
controller = $componentController('vnColumnHeader', {}); $event = {
preventDefault: () => {}
};
$attrs = {};
controller = $componentController('vnColumnHeader', {$attrs});
})); }));
describe('onClick()', () => { describe('onClick()', () => {
it(`should change the ordenation to DESC (descendant) if it was ASC (ascendant)`, () => { it(`should change the ordenation to DESC (descendant) if it was ASC (ascendant)`, () => {
controller.gridHeader = {selectColum: () => {}}; controller.gridHeader = {selectColum: () => {}};
controller.order = 'ASC'; controller.order = 'ASC';
controller.onClick(); controller.onClick($event);
expect(controller.order).toEqual('DESC'); expect(controller.order).toEqual('DESC');
}); });
@ -25,7 +31,7 @@ describe('Component vnColumnHeader', () => {
it(`should change the ordenation to ASC (ascendant) if it wasnt ASC`, () => { it(`should change the ordenation to ASC (ascendant) if it wasnt ASC`, () => {
controller.gridHeader = {selectColum: () => {}}; controller.gridHeader = {selectColum: () => {}};
controller.order = 'DESC or any other value that might occur'; controller.order = 'DESC or any other value that might occur';
controller.onClick(); controller.onClick($event);
expect(controller.order).toEqual('ASC'); expect(controller.order).toEqual('ASC');
}); });
@ -34,7 +40,7 @@ describe('Component vnColumnHeader', () => {
controller.gridHeader = {selectColum: () => {}}; controller.gridHeader = {selectColum: () => {}};
controller.order = 'Change me!'; controller.order = 'Change me!';
spyOn(controller.gridHeader, 'selectColum'); spyOn(controller.gridHeader, 'selectColum');
controller.onClick(); controller.onClick($event);
expect(controller.gridHeader.selectColum).toHaveBeenCalledWith(controller); expect(controller.gridHeader.selectColum).toHaveBeenCalledWith(controller);
}); });

View File

@ -1,5 +1,5 @@
vn-confirm .dialog-title { vn-confirm .dialog-title {
color:#424242; color:#424242;
font-family: raleway-bold; font-family: vn-font-bold;
} }

View File

@ -4,7 +4,8 @@
ng-focus="$ctrl.hasFocus = true" ng-focus="$ctrl.hasFocus = true"
ng-blur="$ctrl.hasFocus = false" ng-blur="$ctrl.hasFocus = false"
ng-mouseenter="$ctrl.hasMouseIn = true" ng-mouseenter="$ctrl.hasMouseIn = true"
ng-mouseleave="$ctrl.hasMouseIn = false" ng-mouseleave="$ctrl.hasMouseIn = false"
ng-click="$ctrl.onClick()"
> >
<input type="text" <input type="text"
class="mdl-textfield__input" class="mdl-textfield__input"

View File

@ -18,16 +18,19 @@ export const formatEquivalence = {
}; };
class DatePicker extends Component { class DatePicker extends Component {
constructor($element, $translate, $filter, $timeout) { constructor($element, $translate, $filter, $timeout, $attrs) {
super($element); super($element);
this.input = $element[0].querySelector('input'); this.input = $element[0].querySelector('input');
this.$translate = $translate; this.$translate = $translate;
this.$filter = $filter; this.$filter = $filter;
this.$timeout = $timeout; this.$timeout = $timeout;
this.$attrs = $attrs;
this.enabled = true; this.enabled = true;
this._modelView = null; this._modelView = null;
this._model = undefined; this._model = undefined;
this._optionsChecked = false; this._optionsChecked = false;
this._waitingInit = 0;
this.hasFocus = false; this.hasFocus = false;
this.hasMouseIn = false; this.hasMouseIn = false;
componentHandler.upgradeElement($element[0].firstChild); componentHandler.upgradeElement($element[0].firstChild);
@ -37,12 +40,34 @@ class DatePicker extends Component {
return this._model; return this._model;
} }
set model(value) { set model(value) {
this._model = value; if (this._optionsChecked) {
if (value && !this.modelView) { this._waitingInit = 0;
let options = this._getOptions(); this._model = value;
let initialDateFormat = (options && options.dateFormat) ? options.dateFormat : 'Y-m-d'; if (value && !this.modelView) {
let format = this._formatFlat2Angular(initialDateFormat); let options = this._getOptions();
this.modelView = this.$filter('date')(value, format); let initialDateFormat;
if (options && options.dateFormat) {
initialDateFormat = options.dateFormat;
} else {
initialDateFormat = this.$translate.use() === 'es' ? 'd-m-Y' : 'Y-m-d';
if (options.enableTime) {
initialDateFormat += ' H:i:s';
}
}
let format = this._formatFlat2Angular(initialDateFormat);
this._modelView = this.$filter('date')(new Date(this._model), format);
this.mdlUpdate();
}
} else if (this._waitingInit < 4) {
this._waitingInit++;
this.$timeout(() => {
this.model = value;
}, 250);
} else {
this.model = null;
this.modelView = '';
this._waitingInit = 0;
} }
} }
get modelView() { get modelView() {
@ -52,17 +77,23 @@ class DatePicker extends Component {
this._modelView = value; this._modelView = value;
this.input.value = value; this.input.value = value;
this._setModel(value); this._setModel(value);
this.$timeout(() => { this.mdlUpdate();
this.mdlUpdate();
}, 500);
} }
onClear() { onClear() {
this.modelView = null; this.modelView = null;
} }
onClick() {
if (this.vp) {
this.vp.open();
}
}
mdlUpdate() { mdlUpdate() {
let mdlField = this.element.firstChild.MaterialTextfield; this.$timeout(() => {
if (mdlField) let mdlField = this.element.firstChild.MaterialTextfield;
mdlField.updateClasses_(); if (mdlField)
mdlField.updateClasses_();
}, 500);
} }
_formatFlat2Angular(string) { // change string Flatpickr format to angular format (d-m-Y -> dd-MM-yyyy) _formatFlat2Angular(string) { // change string Flatpickr format to angular format (d-m-Y -> dd-MM-yyyy)
@ -83,54 +114,59 @@ class DatePicker extends Component {
return parts.join('-'); return parts.join('-');
} }
_string2BackFormat(value) {
let formats = this.iniOptions.dateFormat.split(/[ZT.,/ :-]/);
let aux = value.split(/[ZT.,/ :-]/);
let date = {};
formats.forEach(
(k, i) => {
if (k.toLowerCase() === 'y') {
date.year = aux[i];
} else if (k === 'm' || k === 'n') {
date.month = aux[i];
} else if (k === 'd' || k === 'j') {
date.day = aux[i];
} else if (k.toLowerCase() === 'h') {
date.hour = aux[i];
} else if (k === 'i') {
date.minutes = aux[i];
} else if (k === 's') {
date.seccons = aux[i];
}
}
);
let dateStr = '';
let hourStr = '';
if (date.year && date.month && date.day) {
dateStr = `${date.year}-${date.month}-${date.day}`;
}
if (date.hour) {
hourStr = date.hour;
if (date.minutes) {
hourStr += ':' + date.minutes;
} else {
hourStr += ':00';
}
if (date.seccons) {
hourStr += ':' + date.seccons;
} else {
hourStr += ':00';
}
}
return `${dateStr} ${hourStr}`.trim();
}
_setModel(value) { _setModel(value) {
let model; let model;
let options = this._getOptions();
if (!value) { if (!value) {
model = undefined; model = undefined;
} else if (!this.iniOptions || (this.iniOptions.dateFormat && this.iniOptions.dateFormat.startsWith('Y-m-d'))) { } else if (!options || (options.dateFormat && options.dateFormat.startsWith('Y-m-d'))) {
model = value; model = value;
} else { } else {
let formats = this.iniOptions.dateFormat.split(/[ZT.,/ :-]/); model = this._string2BackFormat(value);
let aux = value.split(/[ZT.,/ :-]/);
let date = {};
formats.forEach(
(k, i) => {
if (k.toLowerCase() === 'y') {
date.year = aux[i];
} else if (k === 'm' || k === 'n') {
date.month = aux[i];
} else if (k === 'd' || k === 'j') {
date.day = aux[i];
} else if (k.toLowerCase() === 'h') {
date.hour = aux[i];
} else if (k === 'i') {
date.minutes = aux[i];
} else if (k === 's') {
date.seccons = aux[i];
}
}
);
let dateStr = '';
let hourStr = '';
if (date.year && date.month && date.day) {
dateStr = `${date.year}-${date.month}-${date.day}`;
}
if (date.hour) {
hourStr = date.hour;
if (date.minutes) {
hourStr += ':' + date.minutes;
} else {
hourStr += ':00';
}
if (date.seccons) {
hourStr += ':' + date.seccons;
} else {
hourStr += ':00';
}
}
model = `${dateStr} ${hourStr}`.trim();
} }
if (this.model !== model) { if (this.model !== model) {
@ -148,8 +184,8 @@ class DatePicker extends Component {
if (!this.iniOptions.locale) if (!this.iniOptions.locale)
this.iniOptions.locale = this.$translate.use(); this.iniOptions.locale = this.$translate.use();
if (!this.iniOptions.dateFormat && this.iniOptions.locale === 'es') if (!this.iniOptions.dateFormat)
this.iniOptions.dateFormat = 'd-m-Y'; this.iniOptions.dateFormat = this.iniOptions.locale === 'es' ? 'd-m-Y' : 'Y-m-d';
else if (this.iniOptions.dateFormat) { else if (this.iniOptions.dateFormat) {
let format = this.iniOptions.dateFormat.split(/[ZT.,/ :-]/); let format = this.iniOptions.dateFormat.split(/[ZT.,/ :-]/);
if (format.length <= 1) { if (format.length <= 1) {
@ -163,21 +199,44 @@ class DatePicker extends Component {
} }
); );
} }
if (this.$attrs.hasOwnProperty('today')) {
this.iniOptions.defaultDate = new Date();
}
this._optionsChecked = true; this._optionsChecked = true;
return this.iniOptions; return this.iniOptions;
} }
$onInit() { initPicker() {
this.iniOptions = this._getOptions(); this.iniOptions = this._getOptions();
this.isTimePicker = (this.iniOptions && this.iniOptions.enableTime && this.iniOptions.noCalendar); this.isTimePicker = (this.iniOptions && this.iniOptions.enableTime && this.iniOptions.noCalendar);
this.vp = new Flatpickr(this.input, this.iniOptions); this.vp = new Flatpickr(this.input, this.iniOptions);
if (this.iniOptions.defaultDate) {
this.modelView = this.vp.formatDate(this.iniOptions.defaultDate, this.iniOptions.dateFormat);
}
} }
$onDestroy() { destroyPicker() {
if (this.vp) if (this.vp)
this.vp.destroy(); this.vp.destroy();
this.vp = undefined;
}
$onChanges(objChange) {
if (objChange.iniOptions && objChange.iniOptions.currentValue) {
this.iniOptions = Object.assign(this.iniOptions, objChange.iniOptions.currentValue);
}
}
$onInit() {
this.initPicker();
}
$onDestroy() {
this.destroyPicker();
} }
} }
DatePicker.$inject = ['$element', '$translate', '$filter', '$timeout']; DatePicker.$inject = ['$element', '$translate', '$filter', '$timeout', '$attrs'];
module.component('vnDatePicker', { module.component('vnDatePicker', {
template: require('./datePicker.html'), template: require('./datePicker.html'),

View File

@ -2,9 +2,10 @@ import './datePicker.js';
describe('Component vnDatePicker', () => { describe('Component vnDatePicker', () => {
let $componentController; let $componentController;
let $scope; let $filter;
let $timeout; let $timeout;
let $element; let $element;
let $attrs;
let $translate; let $translate;
let controller; let controller;
@ -12,13 +13,14 @@ describe('Component vnDatePicker', () => {
angular.mock.module('client'); angular.mock.module('client');
}); });
beforeEach(angular.mock.inject((_$componentController_, $rootScope, _$timeout_, _$translate_) => { beforeEach(angular.mock.inject((_$componentController_, _$filter_, _$timeout_, _$translate_) => {
$componentController = _$componentController_; $componentController = _$componentController_;
$scope = $rootScope.$new(); $filter = _$filter_;
$timeout = _$timeout_; $timeout = _$timeout_;
$element = angular.element(`<div><input></div>`); $element = angular.element(`<div><input></div>`);
$translate = _$translate_; $translate = _$translate_;
controller = $componentController('vnDatePicker', {$scope, $element, $translate, $timeout}); $attrs = {};
controller = $componentController('vnDatePicker', {$element, $translate, $filter, $timeout, $attrs});
})); }));
describe('_formatFlat2Angular()', () => { describe('_formatFlat2Angular()', () => {
@ -36,6 +38,7 @@ describe('Component vnDatePicker', () => {
it(`should split the given string into parts`, () => { it(`should split the given string into parts`, () => {
controller.iniOptions = {dateFormat: 'd/m/Y'}; controller.iniOptions = {dateFormat: 'd/m/Y'};
controller._optionsChecked = true;
controller.model = '2017-12-23'; controller.model = '2017-12-23';
expect(controller.modelView).toBe('23-12-2017'); expect(controller.modelView).toBe('23-12-2017');

View File

@ -54,7 +54,7 @@
text-transform: uppercase; text-transform: uppercase;
font-size: 1.1em; font-size: 1.1em;
color: #ffa410; color: #ffa410;
font-weight: bold; font-family: vn-font-bold;
cursor: pointer; cursor: pointer;
padding: .5em; padding: .5em;
margin: -0.5em; margin: -0.5em;

View File

@ -1,6 +1,25 @@
import {module} from '../module'; import {module} from '../module';
function vnAcl(aclService, $timeout) { function vnAcl(aclService, $timeout) {
function getMaterialType(className) {
let type = '';
if (className) {
type = className.replace('mdl-', '').replace('__input', '');
type = type.charAt(0).toUpperCase() + type.slice(1);
}
return type;
}
function udateMaterial(input) {
if (input && input.className) {
let find = input.className.match(/mdl-[\w]+input/g);
if (find && find.length && find[0]) {
let type = getMaterialType(find[0]);
if (type && input.parentNode[`Material${type}`] && input.parentNode[`Material${type}`].updateClasses_) {
input.parentNode[`Material${type}`].updateClasses_();
}
}
}
}
return { return {
restrict: 'A', restrict: 'A',
priority: -1, priority: -1,
@ -18,6 +37,7 @@ function vnAcl(aclService, $timeout) {
if (input) { if (input) {
$timeout(() => { $timeout(() => {
input.setAttribute("disabled", "true"); input.setAttribute("disabled", "true");
udateMaterial(input);
}); });
$element[0].querySelectorAll('i, vn-drop-down').forEach(element => { $element[0].querySelectorAll('i, vn-drop-down').forEach(element => {
element.parentNode.removeChild(element); element.parentNode.removeChild(element);

View File

@ -3,3 +3,4 @@ import './focus';
import './dialog'; import './dialog';
import './validation'; import './validation';
import './acl'; import './acl';
import './onErrorSrc';

View File

@ -0,0 +1,17 @@
import {module} from '../module';
function onErrorSrc() {
return {
restrict: 'A',
link: (scope, element, attrs) => {
let imgError = '/static/images/no-image200x200.png';
element.bind('error', function() {
if (attrs.src != imgError) {
attrs.$set('src', imgError);
}
});
}
};
}
module.directive('onErrorSrc', onErrorSrc);

View File

@ -0,0 +1,186 @@
describe('Directive validation', () => {
let scope;
let element;
let compile;
beforeEach(() => {
angular.mock.module('client');
});
compile = (_element, validations, value) => {
inject(($compile, $rootScope, aclService, _$timeout_, $window) => {
$window.validations = validations;
scope = $rootScope.$new();
scope.user = {name: value};
element = angular.element(_element);
$compile(element)(scope);
scope.$digest();
});
};
it(`should throw an error if the vnValidation doesn't have the right syntax`, () => {
let html = `<input type="name" ng-model="user.name" vn-validation="user"/>`;
expect(() => {
compile(html, {});
}).toThrow(new Error(`vnValidation: Attribute must have this syntax: [entity].[field]`));
});
it('should throw an error if the window.validations aint defined', () => {
let html = `<input type="name" ng-model="user.name" vn-validation="user.name"/>`;
expect(() => {
compile(html, {});
}).toThrow(new Error(`vnValidation: Entity 'User' doesn't exist`));
});
describe('Validator presence()', () => {
it('should not validate the user name as it is an empty string', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'presence'}]}}};
compile(html, validations, 'Spiderman');
scope.user.name = '';
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
});
describe('Validator absence()', () => {
it('should not validate the entity as it should be an empty string', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'absence'}]}}};
compile(html, validations, 'Spiderman');
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
it('should validate the entity as it is an empty string', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'absence'}]}}};
compile(html, validations, '');
scope.$digest();
expect(element[0].classList).toContain('ng-valid');
expect(element[0].classList).not.toContain('ng-invalid');
});
});
describe('Validator length()', () => {
it('should not validate the user name as it should have min length of 15', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'length', min: 10, max: 50, is: 15}]}}};
compile(html, validations, 'fifteen!');
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
it('should validate the user name as it has length of 15', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'length', min: 10, max: 50, is: 15}]}}};
compile(html, validations, 'fifteen length!');
scope.$digest();
expect(element[0].classList).toContain('ng-valid');
expect(element[0].classList).not.toContain('ng-invalid');
});
it('should not validate the user name as it should have min length of 10', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'length', min: 10}]}}};
compile(html, validations, 'shortname');
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
it('should validate the user name as its length is greater then the minimum', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'length', min: 10}]}}};
compile(html, validations, 'verylongname');
scope.$digest();
expect(element[0].classList).toContain('ng-valid');
expect(element[0].classList).not.toContain('ng-invalid');
});
it('should not validate the user name as its length is greater then the maximum', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'length', max: 10}]}}};
compile(html, validations, 'toolongname');
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
});
describe('Validator numericality()', () => {
it('should not validate the phone number as it should a integer', () => {
let html = `<form><input type="text" ng-model="user.phone" vn-validation="user.phone"/></form>`;
let validations = {User: {validations: {phone: [{validation: 'numericality', is: 'what is this?'}]}}};
compile(html, validations, 'spiderman');
scope.user.phone = 'this is not a phone number!';
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
it('should validate the phone number as it an integer', () => {
let html = `<form><input type="text" ng-model="user.phone" vn-validation="user.phone"/></form>`;
let validations = {User: {validations: {phone: [{validation: 'numericality', is: 'what is this?'}]}}};
compile(html, validations, 'spiderman');
scope.user.phone = '555555555';
scope.$digest();
expect(element[0].classList).toContain('ng-valid');
expect(element[0].classList).not.toContain('ng-invalid');
});
});
describe('Validator inclusion()', () => {
it('should not validate the phone number as it is not an integer', () => {
let html = `<form><input type="text" ng-model="user.phone" vn-validation="user.phone"/></form>`;
let validations = {User: {validations: {phone: [{validation: 'inclusion'}]}}};
compile(html, validations, 'spiderman');
scope.user.phone = 'this is not a phone number!';
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
});
describe('Validator exclusion()', () => {
it('should validate the phone number as it is an integer', () => {
let html = `<form><input type="text" ng-model="user.phone" vn-validation="user.phone"/></form>`;
let validations = {User: {validations: {phone: [{validation: 'exclusion'}]}}};
compile(html, validations, 'spiderman');
scope.user.phone = '555555555';
scope.$digest();
expect(element[0].classList).toContain('ng-valid');
expect(element[0].classList).not.toContain('ng-invalid');
});
});
describe('Validator format()', () => {
it('should not validate the email number as it doesnt contain @', () => {
let html = `<form><input type="text" ng-model="user.email" vn-validation="user.email"/></form>`;
let validations = {User: {validations: {email: [{validation: 'format', with: '@'}]}}};
compile(html, validations, 'spiderman');
scope.user.email = 'userverdnatura.es';
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
});
});

View File

@ -29,7 +29,6 @@ export function directive(interpolate, compile, $window) {
throw new Error(`vnValidation: Entity '${entityName}' doesn't exist`); throw new Error(`vnValidation: Entity '${entityName}' doesn't exist`);
let validations = entity.validations[fieldName]; let validations = entity.validations[fieldName];
if (!validations || validations.length == 0) if (!validations || validations.length == 0)
return; return;
@ -40,7 +39,7 @@ export function directive(interpolate, compile, $window) {
let errorShown = false; let errorShown = false;
input.$options.$$options.allowInvalid = true; input.$options.$$options.allowInvalid = true;
input.$validators.entity = function(value) { input.$validators.entity = value => {
try { try {
validateAll(value, validations); validateAll(value, validations);
return true; return true;
@ -51,9 +50,9 @@ export function directive(interpolate, compile, $window) {
} }
}; };
scope.$watch(function() { scope.$watch(() => {
return (form.$submitted || input.$dirty) && input.$invalid; return (form.$submitted || input.$dirty) && input.$invalid;
}, function(value) { }, value => {
let parent = element.parent(); let parent = element.parent();
if (value) { if (value) {

View File

@ -1,13 +1,13 @@
<vn-vertical class="dropdown-body" ng-show="$ctrl.show"> <vn-vertical class="dropdown-body" ng-if="$ctrl.show">
<vn-one ng-show="$ctrl.filter" class="filter"> <vn-auto ng-show="$ctrl.filter" class="filter">
<vn-horizontal> <vn-horizontal>
<input vn-one placeholder="{{'Search' | translate}}" type="text" ng-model="$ctrl.search"/> <input vn-one placeholder="{{'Search' | translate}}" type="text" ng-model="$ctrl.search"/>
<vn-icon vn-none icon="clear" ng-click="$ctrl.clearSearch()"></vn-icon> <vn-icon vn-none icon="clear" ng-click="$ctrl.clearSearch()"></vn-icon>
</vn-horizontal> </vn-horizontal>
</vn-one> </vn-auto>
<vn-one> <vn-auto>
<ul class="dropdown"> <ul class="dropdown">
<li tabIndex="-1" <li
ng-repeat="item in $ctrl.itemsFiltered track by $index" ng-repeat="item in $ctrl.itemsFiltered track by $index"
ng-click="$ctrl.selectItem(item)" ng-click="$ctrl.selectItem(item)"
ng-class="{'active': $index === $ctrl.activeOption, 'checked': item.checked}" ng-class="{'active': $index === $ctrl.activeOption, 'checked': item.checked}"
@ -19,11 +19,10 @@
<li <li
ng-if="$ctrl.loadMore&&!$ctrl.removeLoadMore" ng-if="$ctrl.loadMore&&!$ctrl.removeLoadMore"
class="dropdown__loadMore" class="dropdown__loadMore"
tabIndex="-1"
ng-class="{'active': $ctrl.itemsFiltered.length === $ctrl.activeOption, 'noMore' : !$ctrl.showLoadMore}" ng-class="{'active': $ctrl.itemsFiltered.length === $ctrl.activeOption, 'noMore' : !$ctrl.showLoadMore}"
ng-click="$ctrl.loadItems()" ng-click="$ctrl.loadItems()"
translate="{{$ctrl.showLoadMore ? 'Show More' : 'No more results'}}" translate="{{$ctrl.showLoadMore ? 'Show More' : 'No more results'}}"
></li> ></li>
</ul> </ul>
</vn-one> </vn-auto>
</vn-vertical> </vn-vertical>

View File

@ -1,5 +1,6 @@
import {module} from '../module'; import {module} from '../module';
import './style.scss'; import './style.scss';
import validKey from '../lib/keyCodes';
export default class DropDown { export default class DropDown {
constructor($element, $filter, $timeout) { constructor($element, $filter, $timeout) {
@ -7,26 +8,36 @@ export default class DropDown {
this.$filter = $filter; this.$filter = $filter;
this.$timeout = $timeout; this.$timeout = $timeout;
this.parent = this.parent || $element[0].parentNode;
this._search = null; this._search = null;
this.itemsFiltered = []; this.itemsFiltered = [];
this._activeOption = -1; this._activeOption = -1;
this._focusingFilter = false; this._focusingFilter = false;
this._tryToShow = 0;
} }
get container() {
return this.$element[0].querySelector('ul.dropdown');
}
get show() { get show() {
return this._show; return this._show;
} }
set show(value) { set show(value) {
let oldValue = this.show; let oldValue = this.show;
this._show = value; // It wait up to 1 second if the dropdown opens but there is no data to show
if (value && !this._focusingFilter && oldValue !== value && this.filter) { if (value && !oldValue && !this.itemsFiltered.length && this._tryToShow < 4) {
let inputFilterSearch = this.$element[0].querySelector('input');
this._focusingFilter = true;
this.$timeout(() => { this.$timeout(() => {
inputFilterSearch.focus(); this._tryToShow++;
this._focusingFilter = false; this.show = true;
if (this.activeOption === -1) {
this.activeOption = 0;
}
}, 250); }, 250);
} else {
this._tryToShow = 0;
this._show = value;
this._toggleDropDown(value, oldValue);
} }
} }
@ -51,18 +62,92 @@ export default class DropDown {
set activeOption(value) { set activeOption(value) {
if (value < 0) { if (value < 0) {
value = 0; value = 0;
} else if (value >= this.items.length) { } else if (value >= this.itemsFiltered.length) {
value = this.showLoadMore ? this.items.length : this.items.length - 1; value = this.showLoadMore ? this.itemsFiltered.length : this.itemsFiltered.length - 1;
} }
this.$timeout(() => { this.$timeout(() => {
this._activeOption = value; this._activeOption = value;
// AutoLoad items with "scroll" (1st version): if (value && value >= this.itemsFiltered.length - 3 && !this.removeLoadMore) {
if (value && value >= this.items.length - 3 && !this.removeLoadMore) {
this.loadItems(); this.loadItems();
} }
}); });
} }
_toggleDropDown(value, oldValue) {
this.$timeout(() => {
this._eventScroll(value);
this._calculatePosition(value, oldValue);
});
}
_eventScroll(add, num) {
let count = num || 0;
if (add) {
if (this.container) {
this.container.addEventListener('scroll', e => this.loadFromScroll(e));
// this.$timeout(() => { // falla al entrar por primera vez xq pierde el foco y cierra el dropdown
// this._setFocusInFilterInput();
// });
} else if (count < 4) {
count++;
this.$timeout(() => { // wait angular ngIf
this._eventScroll(add, count);
}, 250);
}
} else if (this.container) {
this.container.removeEventListener('scroll', e => this.loadFromScroll(e));
}
}
_setFocusInFilterInput() {
let inputFilterSearch = this.$element[0].querySelector('input');
this._focusingFilter = true;
if (inputFilterSearch)
this.$timeout(() => {
inputFilterSearch.focus();
this._focusingFilter = false;
}, 250);
}
_background(create) {
let el = document.getElementById('ddBack');
if (el) {
el.parentNode.removeChild(el);
}
if (create) {
el = document.createElement('div');
el.id = 'ddBack';
document.body.appendChild(el);
}
}
_calculatePosition(value, oldValue) {
if (value && !oldValue) {
if (this.parent === undefined) {
this.parent = this.$element.parent().parent();
}
let parentRect = this.parent.getBoundingClientRect();
let elemetRect = this.$element[0].getBoundingClientRect();
let instOffset = parentRect.bottom + elemetRect.height;
if (instOffset >= window.innerHeight) {
this._background(true);
this.$element.addClass('fixed-dropDown');
this.$element.css('top', `${(parentRect.top - elemetRect.height)}px`);
this.$element.css('left', `${(parentRect.x)}px`);
this.$element.css('height', `${elemetRect.height}px`);
}
} else if (!value && oldValue) {
this.$element.removeAttr('style');
if (this.itemWidth) {
this.$element.css('width', this.itemWidth + 'px');
}
this.$element.removeClass('fixed-dropDown');
this._background();
}
}
filterItems() { filterItems() {
this.itemsFiltered = this.search ? this.$filter('filter')(this.items, this.search) : this.items; this.itemsFiltered = this.search ? this.$filter('filter')(this.items, this.search) : this.items;
} }
@ -73,18 +158,6 @@ export default class DropDown {
} }
} }
$onChanges(changesObj) {
if (changesObj.show && changesObj.top && changesObj.top.currentValue) {
this.$element.css('top', changesObj.top.currentValue + 'px');
}
if (changesObj.show && changesObj.itemWidth && changesObj.itemWidth.currentValue) {
this.$element.css('width', changesObj.itemWidth.currentValue + 'px');
}
if (changesObj.items) {
this.filterItems();
}
}
clearSearch() { clearSearch() {
this.search = null; this.search = null;
} }
@ -101,42 +174,55 @@ export default class DropDown {
onKeydown(event) { onKeydown(event) {
if (this.show) { if (this.show) {
switch (event.keyCode) { if (event.keyCode === 13) { // Enter
case 13: // Enter
this.$timeout(() => { this.$timeout(() => {
this.selectOption(); this.selectOption();
}); });
event.preventDefault(); event.preventDefault();
break; } else if (event.keyCode === 27) { // Escape
case 27: // Escape
this.clearSearch(); this.clearSearch();
break; } else if (event.keyCode === 38) { // Arrow up
case 38: // Arrow up
this.activeOption--; this.activeOption--;
this.$timeout(() => { this.$timeout(() => {
this.setScrollPosition(); this.setScrollPosition();
}, 100); }, 100);
break; } else if (event.keyCode === 40) { // Arrow down
case 40: // Arrow down
this.activeOption++; this.activeOption++;
this.$timeout(() => { this.$timeout(() => {
this.setScrollPosition(); this.setScrollPosition();
}, 100); }, 100);
break; } else if (event.keyCode === 35) { // End
default: this.activeOption = this.itemsFiltered.length - 1;
return; this.$timeout(() => {
this.setScrollPosition();
}, 100);
} else if (event.keyCode === 36) { // Start
this.activeOption = 0;
this.$timeout(() => {
this.setScrollPosition();
}, 100);
} else if (this.filter) {
let oldValue = this.search || '';
if (validKey(event)) {
this.search = oldValue + String.fromCharCode(event.keyCode);
} else if (event.keyCode === 8) { // backSpace
this.search = oldValue.slice(0, -1);
}
} /* else {
console.error(`Error: keyCode ${event.keyCode} not supported`);
} */
}
}
setScrollPosition() {
let child = this.$element[0].querySelector('ul.dropdown li.active');
if (child) {
let childRect = child.getBoundingClientRect();
let containerRect = this.container.getBoundingClientRect();
if (typeof child.scrollIntoView === 'function' && (childRect.top > containerRect.top + containerRect.height || childRect.top < containerRect.top)) {
child.scrollIntoView();
} }
} }
} }
setScrollPosition() {
let dropdown = this.$element[0].querySelector('ul.dropdown');
let child = dropdown ? dropdown.childNodes[this.activeOption] : null;
if (child && typeof child.scrollIntoView === 'function') {
child.scrollIntoView();
}
}
selectItem(item) { selectItem(item) {
this.selected = item; this.selected = item;
if (this.multiple) { if (this.multiple) {
@ -146,14 +232,26 @@ export default class DropDown {
this.show = false; this.show = false;
} }
} }
loadItems() { loadItems() {
if (this.showLoadMore && this.loadMore) { if (this.showLoadMore && this.loadMore) {
this.loadMore(); this.loadMore();
} }
this.show = true; this.show = true;
} }
loadFromScroll(e) {
let containerRect = e.target.getBoundingClientRect();
if (e.target.scrollHeight - e.target.scrollTop - containerRect.height <= 50) {
this.loadItems();
}
}
$onChanges(changesObj) {
if (changesObj.show && changesObj.itemWidth && changesObj.itemWidth.currentValue) {
this.$element.css('width', changesObj.itemWidth.currentValue + 'px');
}
if (changesObj.items) {
this.filterItems();
}
}
$onInit() { $onInit() {
if (this.parent) if (this.parent)
this.parent.addEventListener('keydown', e => this.onKeydown(e)); this.parent.addEventListener('keydown', e => this.onKeydown(e));
@ -179,7 +277,6 @@ module.component('vnDropDown', {
removeLoadMore: '<?', removeLoadMore: '<?',
filterAction: '&?', filterAction: '&?',
showLoadMore: '<?', showLoadMore: '<?',
top: '<?',
itemWidth: '<?', itemWidth: '<?',
parent: '<?', parent: '<?',
multiple: '<?' multiple: '<?'

View File

@ -17,6 +17,7 @@ describe('Component vnDropDown', () => {
$timeout = _$timeout_; $timeout = _$timeout_;
$filter = _$filter_; $filter = _$filter_;
controller = $componentController('vnDropDown', {$element, $timeout, $filter}); controller = $componentController('vnDropDown', {$element, $timeout, $filter});
controller.parent = angular.element('<vn-parent></vn-parent>')[0];
})); }));
describe('show() setter', () => { describe('show() setter', () => {
@ -62,43 +63,43 @@ describe('Component vnDropDown', () => {
it(`should set _activeOption as items.length if showLoadMore is defined if activeOption is bigger than items.length then call loadItems()`, () => { it(`should set _activeOption as items.length if showLoadMore is defined if activeOption is bigger than items.length then call loadItems()`, () => {
spyOn(controller, 'loadItems'); spyOn(controller, 'loadItems');
controller.showLoadMore = true; controller.showLoadMore = true;
controller.items = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}]; controller.itemsFiltered = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}];
controller.activeOption = 10; controller.activeOption = 10;
$timeout.flush(); $timeout.flush();
expect(controller._activeOption).toEqual(4); expect(controller.activeOption).toEqual(4);
expect(controller.loadItems).toHaveBeenCalledWith(); expect(controller.loadItems).toHaveBeenCalledWith();
}); });
it(`should set _activeOption as activeOption if showLoadMore is defined if activeOption is smaller than items.length then call loadItems()`, () => { it(`should set _activeOption as activeOption if showLoadMore is defined if activeOption is smaller than items.length then call loadItems()`, () => {
spyOn(controller, 'loadItems'); spyOn(controller, 'loadItems');
controller.showLoadMore = true; controller.showLoadMore = true;
controller.items = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}]; controller.itemsFiltered = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}];
controller.activeOption = 2; controller.activeOption = 2;
$timeout.flush(); $timeout.flush();
expect(controller._activeOption).toEqual(2); expect(controller.activeOption).toEqual(2);
expect(controller.loadItems).toHaveBeenCalledWith(); expect(controller.loadItems).toHaveBeenCalledWith();
}); });
it(`should set _activeOption as items.length -1 if showLoadMore is not defined then call loadItems()`, () => { it(`should set _activeOption as items.length -1 if showLoadMore is not defined then call loadItems()`, () => {
spyOn(controller, 'loadItems'); spyOn(controller, 'loadItems');
controller.showLoadMore = undefined; controller.showLoadMore = undefined;
controller.items = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}]; controller.itemsFiltered = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}];
controller.activeOption = 10; controller.activeOption = 10;
$timeout.flush(); $timeout.flush();
expect(controller._activeOption).toEqual(3); expect(controller.activeOption).toEqual(3);
expect(controller.loadItems).toHaveBeenCalledWith(); expect(controller.loadItems).toHaveBeenCalledWith();
}); });
it(`should define _activeOption as activeOption and never call loadItems()`, () => { it(`should define _activeOption as activeOption and never call loadItems()`, () => {
spyOn(controller, 'loadItems'); spyOn(controller, 'loadItems');
controller.items = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}, {id: 5, name: 'Doctor X'}]; controller.itemsFiltered = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}, {id: 5, name: 'Doctor X'}];
controller.activeOption = 1; controller.activeOption = 1;
$timeout.flush(); $timeout.flush();
expect(controller._activeOption).toEqual(1); expect(controller.activeOption).toEqual(1);
expect(controller.loadItems).not.toHaveBeenCalledWith(); expect(controller.loadItems).not.toHaveBeenCalledWith();
}); });
}); });
@ -139,14 +140,6 @@ describe('Component vnDropDown', () => {
}); });
describe('$onChanges()', () => { describe('$onChanges()', () => {
it(`should set the top css of the $element`, () => {
let argumentObject = {show: true, top: {currentValue: 100}};
spyOn(controller.$element, 'css');
controller.$onChanges(argumentObject);
expect(controller.$element.css).toHaveBeenCalledWith('top', '100px');
});
it(`should set the width css of the $element`, () => { it(`should set the width css of the $element`, () => {
let argumentObject = {show: true, itemWidth: {currentValue: 100}}; let argumentObject = {show: true, itemWidth: {currentValue: 100}};
spyOn(controller.$element, 'css'); spyOn(controller.$element, 'css');
@ -235,7 +228,7 @@ describe('Component vnDropDown', () => {
}); });
it(`should call clearSearch() Esc key is pressed and take off 1 from _activeOption`, () => { it(`should call clearSearch() Esc key is pressed and take off 1 from _activeOption`, () => {
controller.items = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}]; controller.itemsFiltered = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}];
spyOn(controller, 'setScrollPosition'); spyOn(controller, 'setScrollPosition');
controller._show = true; controller._show = true;
controller.element = document.createElement('div'); controller.element = document.createElement('div');
@ -250,7 +243,7 @@ describe('Component vnDropDown', () => {
}); });
it(`should call clearSearch() Esc key is pressed and add up 1 to _activeOption`, () => { it(`should call clearSearch() Esc key is pressed and add up 1 to _activeOption`, () => {
controller.items = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}]; controller.itemsFiltered = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}];
spyOn(controller, 'setScrollPosition'); spyOn(controller, 'setScrollPosition');
controller._show = true; controller._show = true;
controller.element = document.createElement('div'); controller.element = document.createElement('div');
@ -265,18 +258,19 @@ describe('Component vnDropDown', () => {
}); });
}); });
describe('setScrollPosition()', () => { // describe('setScrollPosition()', () => {
it(`should call child.scrollIntoView if defined `, () => { // it(`should call child.scrollIntoView if defined `, () => {
$element[0].firstChild.setAttribute('class', 'dropdown'); // $element[0].firstChild.setAttribute('class', 'dropdown');
let child = $element[0].firstChild.firstChild; // let child = $element[0].firstChild.firstChild;
child.scrollIntoView = () => {};
spyOn(child, 'scrollIntoView');
controller._activeOption = 0;
controller.setScrollPosition();
expect(child.scrollIntoView).toHaveBeenCalledWith(); // child.scrollIntoView = () => {};
}); // spyOn(child, 'scrollIntoView');
}); // controller._activeOption = 0;
// controller.setScrollPosition();
// expect(child.scrollIntoView).toHaveBeenCalledWith();
// });
// });
describe('selectItem()', () => { describe('selectItem()', () => {
it(`should pass item to selected and set controller._show to false`, () => { it(`should pass item to selected and set controller._show to false`, () => {
@ -293,26 +287,40 @@ describe('Component vnDropDown', () => {
controller.selectItem(item); controller.selectItem(item);
expect(controller.selected).toEqual(item); expect(controller.selected).toEqual(item);
expect(controller._show).toEqual(true); expect(controller._show).not.toBeDefined();
}); });
}); });
describe('loadItems()', () => { describe('loadItems()', () => {
it(`should set controller._show to true`, () => { it(`should set controller.show to true`, () => {
controller.show = false;
controller.itemsFiltered = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}];
controller.loadItems(); controller.loadItems();
expect(controller._show).toEqual(true); expect(controller.show).toEqual(true);
}); });
it(`should call loadMore() and then set controller._show to true`, () => { it(`should call loadMore() and then set controller._show to true if there are items`, () => {
controller.showLoadMore = () => {}; controller.showLoadMore = () => {};
controller.loadMore = () => {}; controller.loadMore = () => {};
spyOn(controller, 'loadMore'); spyOn(controller, 'loadMore');
controller.itemsFiltered = [{id: 1, name: 'Batman'}, {id: 2, name: 'Bruce'}, {id: 3, name: 'Logan'}, {id: 4, name: 'Wolverine'}];
controller.loadItems(); controller.loadItems();
expect(controller._show).toEqual(true); expect(controller._show).toEqual(true);
expect(controller.loadMore).toHaveBeenCalledWith(); expect(controller.loadMore).toHaveBeenCalledWith();
}); });
it(`should call loadMore() and then set controller._show to undefined if there are not items`, () => {
controller.showLoadMore = () => {};
controller.loadMore = () => {};
spyOn(controller, 'loadMore');
controller.itemsFiltered = [];
controller.loadItems();
expect(controller._show).not.toBeDefined();
expect(controller.loadMore).toHaveBeenCalledWith();
});
}); });
describe('$onInit()', () => { describe('$onInit()', () => {

View File

@ -4,6 +4,8 @@ vn-drop-down {
padding: 0 15px; padding: 0 15px;
margin-left: -15px; margin-left: -15px;
background: transparent; background: transparent;
max-height: 446px;
overflow: hidden;
.dropdown-body{ .dropdown-body{
background: white; background: white;
border: 1px solid #A7A7A7; border: 1px solid #A7A7A7;
@ -27,7 +29,7 @@ vn-drop-down {
padding: 0; padding: 0;
margin: 0; margin: 0;
background: white; background: white;
max-height: 400px; max-height: 378px;
overflow-y: auto; overflow-y: auto;
li { li {
outline: none; outline: none;
@ -56,3 +58,23 @@ vn-drop-down {
} }
} }
} }
#ddBack {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: transparent;
}
vn-drop-down.fixed-dropDown {
position: fixed;
margin: 0;
padding: 0;
.dropdown-body{
height: 100%;
ul{
border-bottom: 1px solid #A7A7A7;
}
}
}

View File

@ -2,7 +2,6 @@ vn-grid-header {
border-bottom: 3px solid #9D9D9D; border-bottom: 3px solid #9D9D9D;
font-weight: bold; font-weight: bold;
.orderly{ .orderly{
cursor: pointer;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
justify-content: center; justify-content: center;
@ -11,13 +10,18 @@ vn-grid-header {
} }
} }
vn-icon{ vn-icon{
line-height: 17px;
font-size: 17px;
margin: 0; margin: 0;
padding: 0; padding: 0;
display: inline;
i { i {
padding-top: 3px; padding: 0;
margin: -2px -17px 0 0;
opacity: 0.2;
cursor: pointer;
}
&.active {
i {
opacity: 1;
}
} }
} }
[min-none]{ [min-none]{

View File

@ -1,7 +1,7 @@
<div class="icon-menu"> <div class="icon-menu" ng-click="$ctrl.showDropDown = true">
<button class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored icon-square icon-menu__button"> <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored icon-square icon-menu__button">
<vn-icon vn-none icon="{{::$ctrl.icon}}"></vn-icon> <vn-icon vn-none icon="{{::$ctrl.icon}}"></vn-icon>
<vn-icon vn-none class="icon-menu__arrow_down" icon="keyboard_arrow_down" ng-click="$ctrl.showDropDown = true"></vn-icon> <vn-icon vn-none class="icon-menu__arrow_down" icon="keyboard_arrow_down"></vn-icon>
</button> </button>
<div ng-if="!$ctrl.findMore"> <div ng-if="!$ctrl.findMore">
<vn-drop-down <vn-drop-down

View File

@ -79,25 +79,32 @@ export default class IconMenu {
this.findMore = this.url && this.maxRow; this.findMore = this.url && this.maxRow;
this.mouseFocus = false;
this.focused = false;
this.$element.bind('mouseover', e => { this.$element.bind('mouseover', e => {
this.$timeout(() => { this.$timeout(() => {
this.showDropDown = true; this.mouseFocus = true;
this.showDropDown = this.focused;
}); });
}); });
this.$element.bind('mouseout', () => { this.$element.bind('mouseout', () => {
this.$timeout(() => { this.$timeout(() => {
this.showDropDown = false; this.mouseFocus = false;
this.showDropDown = this.focused;
}); });
}); });
this.$element.bind('focusin', e => { this.$element.bind('focusin', e => {
this.$timeout(() => { this.$timeout(() => {
this.focused = true;
this.showDropDown = true; this.showDropDown = true;
}); });
}); });
this.$element.bind('focusout', e => { this.$element.bind('focusout', e => {
this.$timeout(() => { this.$timeout(() => {
this.showDropDown = false; this.focused = false;
this.showDropDown = this.mouseFocus;
}); });
}); });
} }

View File

@ -7,8 +7,13 @@ import {module} from '../module';
* @property {Snackbar} snackbar The main object to show messages. * @property {Snackbar} snackbar The main object to show messages.
*/ */
export default class App { export default class App {
constructor($rootScope) {
this.loaderStatus = 0;
this.$rootScope = $rootScope;
this.timeout = window.snackbarTimeout || 2000;
}
show(message) { show(message) {
if (this.snackbar) this.snackbar.show({message: message}); if (this.snackbar) this.snackbar.show({message: message, timeout: this.timeout});
} }
showMessage(message) { showMessage(message) {
this.show(message); this.show(message);
@ -16,5 +21,17 @@ export default class App {
showError(message) { showError(message) {
this.show(`Error: ${message}`); this.show(`Error: ${message}`);
} }
pushLoader() {
this.loaderStatus++;
if (this.loaderStatus === 1)
this.$rootScope.loading = true;
}
popLoader() {
this.loaderStatus--;
if (this.loaderStatus === 0)
this.$rootScope.loading = false;
}
} }
App.$inject = ['$rootScope'];
module.service('vnApp', App); module.service('vnApp', App);

View File

@ -8,6 +8,14 @@ function index(mgIndex) {
} }
module.factory('vnIndex', index); module.factory('vnIndex', index);
nonAuto.$inject = ['mgIndex'];
function nonAuto(mgIndex) {
return Object.assign({}, mgIndex, {
auto: false
});
}
module.factory('vnIndexNonAuto', nonAuto);
successFactoryCreate.$inject = ['mgSuccessFactoryCreate']; successFactoryCreate.$inject = ['mgSuccessFactoryCreate'];
function successFactoryCreate(create) { function successFactoryCreate(create) {
return Object.assign({}, create, { return Object.assign({}, create, {

View File

@ -0,0 +1,9 @@
import {module} from '../module';
const isFullEmpty = item => {
return (item === null || item === undefined) || (typeof item === 'object' && !Object.keys(item).length);
};
export default isFullEmpty;
export const NAME = 'isFullEmpty';
module.value(NAME, isFullEmpty);

View File

@ -14,3 +14,4 @@ export {NAME as INTERPOLATE, Interpolate} from './interpolate';
export {NAME as COPY_OBJECT} from './copy'; export {NAME as COPY_OBJECT} from './copy';
export {NAME as EQUALS_OBJECT} from './equals'; export {NAME as EQUALS_OBJECT} from './equals';
export {NAME as GET_DATA_MODIFIED, factory as Modified} from './modified'; export {NAME as GET_DATA_MODIFIED, factory as Modified} from './modified';
export {NAME as VALID_KEY} from './keyCodes';

View File

@ -0,0 +1,13 @@
import Component from './component';
/**
* Component that host an input.
*/
export default class Input extends Component {
select() {
this.input.select();
}
focus() {
this.input.focus();
}
}

View File

@ -1,11 +1,10 @@
import {module} from '../module'; import {module} from '../module';
interceptor.$inject = ['$q', '$rootScope', '$window', 'vnApp', '$translate', '$cookies']; interceptor.$inject = ['$q', '$window', 'vnApp', '$translate', '$cookies'];
function interceptor($q, $rootScope, $window, vnApp, $translate, $cookies) { function interceptor($q, $window, vnApp, $translate, $cookies) {
$rootScope.loading = false;
return { return {
request: function(config) { request: function(config) {
$rootScope.loading = true; vnApp.pushLoader();
let token = $cookies.get('vnToken'); let token = $cookies.get('vnToken');
if (token) if (token)
@ -23,11 +22,11 @@ function interceptor($q, $rootScope, $window, vnApp, $translate, $cookies) {
case 'PATCH': case 'PATCH':
vnApp.showMessage($translate.instant('Data saved!')); vnApp.showMessage($translate.instant('Data saved!'));
} }
$rootScope.loading = false; vnApp.popLoader();
return response; return response;
}, },
responseError: function(rejection) { responseError: function(rejection) {
$rootScope.loading = false; vnApp.popLoader();
let data = rejection.data; let data = rejection.data;
let error; let error;

View File

@ -0,0 +1,19 @@
import {module} from '../module';
const validKey = key => {
let keycode = key.keyCode || key;
let valid =
(keycode > 47 && keycode < 58) || // number keys
(keycode > 64 && keycode < 91) || // letter keys
(keycode > 95 && keycode < 112) || // numpad keys
(keycode > 185 && keycode < 193) || // ;=,-./` (in order)
(keycode > 218 && keycode < 223); // [\]' (in order)
return valid;
};
export default validKey;
export const NAME = 'validKey';
module.value(NAME, validKey);

View File

@ -0,0 +1,50 @@
describe('Service acl', () => {
let aclService;
beforeEach(() => {
angular.mock.module('vnCore');
});
beforeEach(angular.mock.module($provide => {
$provide.value('aclConstant', {});
}));
beforeEach(inject((_aclService_, $httpBackend) => {
aclService = _aclService_;
}));
it("should return false as the service doesn't have roles", () => {
expect(aclService.routeHasPermission('http://www.verdnatura.es')).toBeFalsy();
});
it("should return true as the service has roles but the route has no acl", () => {
aclService.roles = {customer: true};
expect(aclService.routeHasPermission('http://www.verdnatura.es')).toBeTruthy();
});
it("should return false as the service roles have no length", () => {
aclService.roles = {};
let route = {url: 'http://www.verdnatura.es', acl: []};
expect(aclService.routeHasPermission(route)).toBeFalsy();
});
it("should call the service aclPermission() function and return false as the service has roles and the rote has acl without length", () => {
aclService.roles = {customer: true, employee: true};
let route = {url: 'http://www.verdnatura.es', acl: []};
spyOn(aclService, 'aclPermission').and.callThrough();
expect(aclService.routeHasPermission(route)).toBeFalsy();
expect(aclService.aclPermission).toHaveBeenCalledWith(route.acl);
});
it("should call the service aclPermission() function to return true as the service has roles matching with the ones in acl", () => {
aclService.roles = {customer: true, employee: true};
let route = {url: 'http://www.verdnatura.es', acl: ['customer']};
spyOn(aclService, 'aclPermission').and.callThrough();
expect(aclService.routeHasPermission(route)).toBeTruthy();
expect(aclService.aclPermission).toHaveBeenCalledWith(route.acl);
});
});

View File

@ -1,20 +1,21 @@
import {validator} from 'vendor'; import {validator} from 'vendor';
export const validators = { export const validators = {
presence: function(value, conf) { presence: value => {
if (validator.isEmpty(value)) if (validator.isEmpty(value ? String(value) : ''))
throw new Error(`Value can't be empty`); throw new Error(`Value can't be empty`);
}, },
absence: function(value, conf) { absence: value => {
if (!validator.isEmpty(value)) if (!validator.isEmpty(value))
throw new Error(`Value should be empty`); throw new Error(`Value should be empty`);
}, },
length: function(value, conf) { length: (value, conf) => {
let options = { let options = {
min: conf.min || conf.is, min: conf.min || conf.is,
max: conf.max || conf.is max: conf.max || conf.is
}; };
if (!validator.isLength(value, options)) { let val = value ? String(value) : '';
if (!validator.isLength(val, options)) {
if (conf.is) { if (conf.is) {
throw new Error(`Value should be ${conf.is} characters long`); throw new Error(`Value should be ${conf.is} characters long`);
} else if (conf.min && conf.max) { } else if (conf.min && conf.max) {
@ -26,26 +27,26 @@ export const validators = {
} }
} }
}, },
numericality: function(value, conf) { numericality: (value, conf) => {
if (conf.int) { if (conf.int) {
if (!validator.isInt(value)) if (!validator.isInt(value))
throw new Error(`Value should be integer`); throw new Error(`Value should be integer`);
} else if (!validator.isNumeric(value)) } else if (!validator.isNumeric(value))
throw new Error(`Value should be a number`); throw new Error(`Value should be a number`);
}, },
inclusion: function(value, conf) { inclusion: (value, conf) => {
if (!validator.isIn(value, conf.in)) if (!validator.isIn(value, conf.in))
throw new Error(`Invalid value`); throw new Error(`Invalid value`);
}, },
exclusion: function(value, conf) { exclusion: (value, conf) => {
if (validator.isIn(value, conf.in)) if (validator.isIn(value, conf.in))
throw new Error(`Invalid value`); throw new Error(`Invalid value`);
}, },
format: function(value, conf) { format: (value, conf) => {
if (!validator.matches(value, conf.with)) if (!validator.matches(value, conf.with))
throw new Error(`Invalid value`); throw new Error(`Invalid value`);
}, },
custom: function(value, conf) { custom: (value, conf) => {
if (!conf.bindedFunction(value)) if (!conf.bindedFunction(value))
throw new Error(`Invalid value`); throw new Error(`Invalid value`);
} }
@ -73,7 +74,7 @@ export function validate(value, conf) {
try { try {
checkNull(value, conf); checkNull(value, conf);
if (validator && value != null) if (validator) // && value != null ??
validator(value, conf); validator(value, conf);
} catch (e) { } catch (e) {
let message = conf.message ? conf.message : e.message; let message = conf.message ? conf.message : e.message;

View File

@ -34,7 +34,7 @@
.mdl-dialog{ .mdl-dialog{
width: 400px; width: 400px;
font-family: raleway-regular; font-family: vn-font;
line-height:60px; line-height:60px;
text-align: center; text-align: center;
} }

View File

@ -7,12 +7,18 @@ vn-textfield {
width: auto; width: auto;
top: 0px; top: 0px;
right: -6px; right: -6px;
margin: 22px 0px; margin: 21px 0px;
background-color: white; background: white;
opacity: 1;
z-index: 9999;
color: #aaa;
} }
.material-icons { .material-icons {
font-size: 18px; font-size: 18px;
float: right; float: right;
margin-right: 5px; margin-right: 5px;
} }
.material-icons:hover {
color: rgba(0,0,0, .87);
}
} }

View File

@ -1,11 +1,7 @@
<div <div
class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label" class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label"
tabindex="1"
ng-focus="$ctrl.hasFocus = true"
ng-blur="$ctrl.hasFocus = false"
ng-mouseenter="$ctrl.hasMouseIn = true" ng-mouseenter="$ctrl.hasMouseIn = true"
ng-mouseleave="$ctrl.hasMouseIn = false" ng-mouseleave="$ctrl.hasMouseIn = false">
>
<input <input
class="mdl-textfield__input" class="mdl-textfield__input"
type="{{$ctrl.type}}" type="{{$ctrl.type}}"
@ -14,10 +10,21 @@
vn-validation="{{$ctrl.rule}}" vn-validation="{{$ctrl.rule}}"
ng-disabled="$ctrl.disabled" ng-disabled="$ctrl.disabled"
ng-readonly="$ctrl.readonly" ng-readonly="$ctrl.readonly"
/> ng-focus="$ctrl.hasFocus = true"
ng-blur="$ctrl.hasFocus = false"
/>
<div class="mdl-chip__action"> <div class="mdl-chip__action">
<i class="material-icons" ng-if="$ctrl.hasInfo" vn-tooltip="{{$ctrl.info}}" tooltip-position="up">info_outline</i> <i class="material-icons"
<i class="material-icons pointer" ng-show="$ctrl.hasValue&&($ctrl.hasFocus||$ctrl.hasMouseIn)" ng-click="$ctrl.clear()">clear</i> ng-if="$ctrl.hasInfo"
vn-tooltip="{{$ctrl.info}}"
tooltip-position="up">
info_outline
</i>
<i class="material-icons pointer"
ng-show="$ctrl.hasValue && ($ctrl.hasFocus || $ctrl.hasMouseIn)"
ng-click="$ctrl.clear()">
clear
</i>
</div> </div>
<label class="mdl-textfield__label" translate>{{$ctrl.label}}</label> <label class="mdl-textfield__label" translate>{{$ctrl.label}}</label>
</div> </div>

View File

@ -1,9 +1,8 @@
import {module} from '../module'; import {module} from '../module';
import Component from '../lib/component'; import Input from '../lib/input';
import * as normalizerFactory from '../lib/inputAttrsNormalizer';
import './style.scss'; import './style.scss';
export default class TextfieldController extends Component { export default class Textfield extends Input {
constructor($element, $scope, $attrs, $timeout, normalizer) { constructor($element, $scope, $attrs, $timeout, normalizer) {
super($element); super($element);
@ -15,45 +14,42 @@ export default class TextfieldController extends Component {
this.$timeout = $timeout; this.$timeout = $timeout;
this._value = null; this._value = null;
this.type = this.$attrs.type || 'text'; this.type = $attrs.type || 'text';
this.showActions = false; this.showActions = false;
this.input = $element[0].querySelector('input'); this.input = $element[0].querySelector('input');
this.focus = false; this.hasInfo = Boolean($attrs.info);
this.hasInfo = Boolean(this.$attrs.info); this.info = $attrs.info || null;
this.info = this.$attrs.info || null;
this.hasFocus = false; this.hasFocus = false;
this.hasMouseIn = false; this.hasMouseIn = false;
componentHandler.upgradeElement($element[0].firstChild); componentHandler.upgradeElement($element[0].firstChild);
} }
get value() { get value() {
return this._value; return this._value;
} }
set value(value) { set value(value) {
this._value = (value === undefined || value === '') ? null : value; this._value = (value === undefined || value === '') ? null : value;
this.input.value = this._value; this.input.value = this._value;
this.hasValue = Boolean(this._value); this.hasValue = Boolean(this._value);
this.mdlUpdate(); this.mdlUpdate();
} }
set tabIndex(value) {
this.input.tabIndex = value;
}
mdlUpdate() { mdlUpdate() {
let mdlField = this.$element[0].firstChild.MaterialTextfield; let mdlField = this.$element[0].firstChild.MaterialTextfield;
if (mdlField) if (mdlField)
mdlField.updateClasses_(); mdlField.updateClasses_();
} }
clear() { clear() {
this.value = null; this.value = null;
this.input.focus(); this.input.focus();
} }
} }
Textfield.$inject = ['$element', '$scope', '$attrs', '$timeout', 'vnInputAttrsNormalizer'];
TextfieldController.$inject = ['$element', '$scope', '$attrs', '$timeout', normalizerFactory.NAME];
module.component('vnTextfield', { module.component('vnTextfield', {
template: require('./textfield.html'), template: require('./textfield.html'),
controller: TextfieldController, controller: Textfield,
bindings: { bindings: {
value: '=model', value: '=model',
label: '@?', label: '@?',
@ -61,6 +57,7 @@ module.component('vnTextfield', {
disabled: '<?', disabled: '<?',
readonly: '<?', readonly: '<?',
rule: '@?', rule: '@?',
type: '@?' type: '@?',
tabIndex: '@?'
} }
}); });

View File

@ -3,6 +3,7 @@ import Component from '../lib/component';
import getModifiedData from '../lib/modified'; import getModifiedData from '../lib/modified';
import copyObject from '../lib/copy'; import copyObject from '../lib/copy';
import isEqual from '../lib/equals'; import isEqual from '../lib/equals';
import isFullEmpty from '../lib/fullEmpty';
/** /**
* Component that checks for changes on a specific model property and * Component that checks for changes on a specific model property and
@ -11,12 +12,13 @@ import isEqual from '../lib/equals';
* properties are provided. * properties are provided.
*/ */
export default class Watcher extends Component { export default class Watcher extends Component {
constructor($element, $scope, $state, $transitions, $http, vnApp, $translate) { constructor($element, $scope, $state, $transitions, $http, vnApp, $translate, $attrs) {
super($element); super($element);
this.$scope = $scope; this.$scope = $scope;
this.$state = $state; this.$state = $state;
this.$http = $http; this.$http = $http;
this.$translate = $translate; this.$translate = $translate;
this.$attrs = $attrs;
this.vnApp = vnApp; this.vnApp = vnApp;
this.state = null; this.state = null;
@ -68,9 +70,9 @@ export default class Watcher extends Component {
* *
* @param {String} state The state name * @param {String} state The state name
*/ */
submitGo(state) { submitGo(state, params) {
return this.submit().then( return this.submit().then(
() => this.$state.go(state) () => this.$state.go(state, params || {})
); );
} }
/** /**
@ -92,10 +94,10 @@ export default class Watcher extends Component {
(resolve, reject) => this.noChanges(reject) (resolve, reject) => this.noChanges(reject)
); );
} }
let changedData = getModifiedData(this.data, this.orgData); let changedData = (this.$attrs.save && this.$attrs.save.toLowerCase() === 'post') ? this.copyInNewObject(this.data) : getModifiedData(this.data, this.orgData);
if (this.save) { if (this.save) {
this.save.model = changedData; this.save.model = changedData; // this.copyInNewObject(changedData);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.save.accept().then( this.save.accept().then(
json => this.writeData({data: json}, resolve), json => this.writeData({data: json}, resolve),
@ -154,7 +156,7 @@ export default class Watcher extends Component {
if (data && typeof data === 'object') { if (data && typeof data === 'object') {
Object.keys(data).forEach( Object.keys(data).forEach(
val => { val => {
if (data[val] !== "" && data[val] !== undefined && data[val] !== null) { if (!isFullEmpty(data[val])) {
if (typeof data[val] === 'object') { if (typeof data[val] === 'object') {
newCopy[val] = this.copyInNewObject(data[val]); newCopy[val] = this.copyInNewObject(data[val]);
} else { } else {
@ -179,6 +181,7 @@ export default class Watcher extends Component {
} }
dataChanged() { dataChanged() {
if (this.form && !this.form.$dirty) return false;
let newData = this.copyInNewObject(this.data); let newData = this.copyInNewObject(this.data);
return !isEqual(newData, this.orgData); return !isEqual(newData, this.orgData);
} }
@ -193,7 +196,7 @@ export default class Watcher extends Component {
} }
} }
Watcher.$inject = ['$element', '$scope', '$state', '$transitions', '$http', 'vnApp', '$translate']; Watcher.$inject = ['$element', '$scope', '$state', '$transitions', '$http', 'vnApp', '$translate', '$attrs'];
module.component('vnWatcher', { module.component('vnWatcher', {
template: require('./watcher.html'), template: require('./watcher.html'),

View File

@ -11,6 +11,7 @@ describe('Component vnWatcher', () => {
let vnApp; let vnApp;
let $translate; let $translate;
let controller; let controller;
let $attrs;
beforeEach(() => { beforeEach(() => {
angular.mock.module('client'); angular.mock.module('client');
@ -25,7 +26,10 @@ describe('Component vnWatcher', () => {
$transitions = _$transitions_; $transitions = _$transitions_;
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
$translate = _$translate_; $translate = _$translate_;
controller = $componentController('vnWatcher', {$scope, $element, $state, vnApp, $transitions, $httpBackend, $translate}); $attrs = {
save: "patch"
};
controller = $componentController('vnWatcher', {$scope, $element, $state, vnApp, $transitions, $httpBackend, $translate, $attrs});
})); }));
describe('$onInit()', () => { describe('$onInit()', () => {
@ -100,11 +104,11 @@ describe('Component vnWatcher', () => {
it(`should call controller.$state.go() function after calling controllers submit() function`, done => { it(`should call controller.$state.go() function after calling controllers submit() function`, done => {
spyOn(controller, 'submit').and.returnValue(Promise.resolve()); spyOn(controller, 'submit').and.returnValue(Promise.resolve());
spyOn(controller.$state, 'go'); spyOn(controller.$state, 'go');
let state = 'the state'; let state = 'the.State';
controller.submitGo(state) controller.submitGo(state)
.then(() => { .then(() => {
expect(controller.submit).toHaveBeenCalledWith(); expect(controller.submit).toHaveBeenCalledWith();
expect(controller.$state.go).toHaveBeenCalledWith(state); expect(controller.$state.go).toHaveBeenCalledWith(state, {});
done(); done();
}); });
}); });
@ -290,4 +294,3 @@ describe('Component vnWatcher', () => {
}); });
}); });
}); });
// 309

1
client/item/index.js Normal file
View File

@ -0,0 +1 @@
export * from './src/item';

106
client/item/routes.json Normal file
View File

@ -0,0 +1,106 @@
{
"module": "item",
"name": "Items",
"icon": "/static/images/icon_item.png",
"validations" : true,
"routes": [
{
"url": "/item",
"state": "item",
"abstract": true,
"component": "ui-view"
},
{
"url": "/list",
"state": "item.index",
"component": "vn-item-list",
"acl": ["developer"]
}, {
"url": "/create",
"state": "item.create",
"component": "vn-item-create"
}, {
"url": "/:id",
"state": "item.card",
"abstract": true,
"component": "vn-item-card"
}, {
"url" : "/data",
"state": "item.card.data",
"component": "vn-item-data",
"params": {
"item": "$ctrl.item"
},
"menu": {
"description": "Basic data",
"icon": "folder"
}
},{
"url" : "/tags",
"state": "item.card.tags",
"component": "vn-item-tags",
"params": {
"item": "$ctrl.item"
},
"menu": {
"description": "Tags",
"icon": "folder"
}
},{
"url" : "/history",
"state": "item.card.history",
"component": "vn-item-history",
"params": {
"item": "$ctrl.item"
},
"menu": {
"description": "History",
"icon": "folder"
}
},{
"url" : "/niche",
"state": "item.card.niche",
"component": "vn-item-niche",
"params": {
"item": "$ctrl.item"
},
"menu": {
"description": "Niche",
"icon": "folder"
}
},{
"url" : "/botanical",
"state": "item.card.botanical",
"component": "vn-item-botanical",
"params": {
"item": "$ctrl.item"
},
"menu": {
"description": "Botanical",
"icon": "folder"
}
},{
"url" : "/picture",
"state": "item.card.picture",
"component": "vn-item-picture",
"params": {
"item": "$ctrl.item"
},
"menu": {
"description": "Picture",
"icon": "folder"
}
}, {
"url" : "/barcode",
"state": "item.card.barcode",
"component": "vn-item-barcode",
"params": {
"item": "$ctrl.item"
},
"menu": {
"description": "Barcode",
"icon": "folder"
}
}
]
}

View File

@ -0,0 +1,5 @@
<vn-card>
<vn-vertical pad-large>
<vn-title>Item barcode</vn-title>
</vn-vertical>
</vn-card>

View File

@ -0,0 +1,5 @@
import ngModule from '../module';
ngModule.component('vnItemBarcode', {
template: require('./item-barcode.html')
});

View File

@ -0,0 +1,5 @@
<vn-card>
<vn-vertical pad-large>
<vn-title>Botanical</vn-title>
</vn-vertical>
</vn-card>

View File

@ -0,0 +1,5 @@
import ngModule from '../module';
ngModule.component('vnItemBotanical', {
template: require('./item-botanical.html')
});

View File

@ -0,0 +1,11 @@
<vn-main-block>
<vn-horizontal>
<vn-auto class="left-block">
<vn-item-descriptor item="$ctrl.item"></vn-item-descriptor>
<vn-left-menu></vn-left-menu>
</vn-auto>
<vn-one>
<vn-vertical margin-medium ui-view></vn-vertical>
</vn-one>
</vn-horizontal>
</vn-main-block>

View File

@ -0,0 +1,32 @@
import ngModule from '../module';
class ItemCard {
constructor($http, $state) {
this.$http = $http;
this.$state = $state;
this.item = {};
}
$onInit() {
let filter = {
include: [
{relation: "itemType"},
{relation: "origin"},
{relation: "ink"},
{relation: "producer"},
{relation: "intrastat"}
]
};
this.$http.get(`/item/api/Items/${this.$state.params.id}?filter=${JSON.stringify(filter)}`).then(
res => {
this.item = res.data;
}
);
}
}
ItemCard.$inject = ['$http', '$state'];
ngModule.component('vnItemCard', {
template: require('./item-card.html'),
controller: ItemCard
});

View File

@ -0,0 +1,57 @@
<mg-ajax path="/item/api/Items" options="vnPost"></mg-ajax>
<vn-watcher
vn-id="watcher"
data="$ctrl.item"
form="form"
save="post">
</vn-watcher>
<form name="form" ng-submit="$ctrl.onSubmit()" margin-medium>
<div style="max-width: 70em; margin: 0 auto;">
<vn-card>
<vn-vertical pad-large>
<vn-title>New item</vn-title>
<vn-horizontal>
<vn-textfield vn-one label="Name" field="$ctrl.item.name" vn-focus></vn-textfield>
<vn-autocomplete vn-one
url="/item/api/ItemTypes"
label="Type"
show-field="name"
value-field="id"
field="$ctrl.item.typeFk"
>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
url="/item/api/Intrastats"
label="Intrastat"
show-field="description"
value-field="id"
field="$ctrl.item.intrastatFk"
order="description ASC"
filter-search="{where: {description: {regexp: 'search'}} }"
>
<tpl-item>{{$parent.$parent.item.description}}</tpl-item>
</vn-autocomplete>
<vn-textfield vn-one label="Relevancy" field="$ctrl.item.relevancy" type="number"></vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
url="/item/api/Origins"
label="Origin"
show-field="name"
value-field="id"
field="$ctrl.item.originFk"
></vn-autocomplete>
<vn-one></vn-one>
</vn-horizontal>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-submit label="Create"></vn-submit>
</vn-button-bar>
</div>
</form>

View File

@ -0,0 +1,23 @@
import ngModule from '../module';
class ItemCreate {
constructor($scope, $state) {
this.$ = $scope;
this.$state = $state;
this.item = {
relevancy: 0
};
}
onSubmit() {
this.$.watcher.submit().then(
json => this.$state.go('item.card.data', {id: json.data.id})
);
}
}
ItemCreate.$inject = ['$scope', '$state'];
ngModule.component('vnItemCreate', {
template: require('./item-create.html'),
controller: ItemCreate
});

View File

@ -0,0 +1,65 @@
<mg-ajax
path="/item/api/Items/{{patch.params.id}}"
options="vnPatch"
override="{filter: {include: [{relation: 'itemType'}, {relation: 'origin'}, {relation: 'ink'}, {relation: 'producer'}, {relation: 'expence'}]}}"
>
</mg-ajax>
<vn-watcher
vn-id="watcher"
data="$ctrl.item"
form="form"
save="patch">
</vn-watcher>
<form name="form" ng-submit="watcher.submit()" ng-cloak>
<vn-card>
<vn-vertical pad-large>
<vn-title>Basic data</vn-title>
<vn-horizontal>
<vn-textfield vn-one label="Name" field="$ctrl.item.name" vn-focus></vn-textfield>
<vn-autocomplete vn-one
url="/item/api/ItemTypes"
label="Type"
show-field="name"
value-field="id"
field="$ctrl.item.typeFk"
>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
url="/item/api/Intrastats"
label="Intrastat"
show-field="description"
value-field="id"
field="$ctrl.item.intrastatFk"
order="description ASC"
filter-search="{where: {description: {regexp: 'search'}} }"
>
<tpl-item>{{$parent.$parent.item.description}}</tpl-item>
</vn-autocomplete>
<vn-textfield vn-one label="Relevancy" field="$ctrl.item.relevancy" type="number"></vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
url="/item/api/Origins"
label="Origin"
show-field="name"
value-field="id"
field="$ctrl.item.originFk"
></vn-autocomplete>
<vn-autocomplete vn-one
url="/item/api/Expences"
label="Expence"
field="$ctrl.item.expenceFk"
></vn-autocomplete>
</vn-horizontal>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-submit label="Save"></vn-submit>
</vn-button-bar>
</form>

View File

@ -0,0 +1,8 @@
import ngModule from '../module';
ngModule.component('vnItemData', {
template: require('./item-data.html'),
bindings: {
item: '<'
}
});

Some files were not shown because too many files have changed in this diff Show More