From 4ea5993dcc54c98a01c551db45efdb741019f701 Mon Sep 17 00:00:00 2001 From: bernat Date: Tue, 10 Nov 2020 08:39:34 +0100 Subject: [PATCH 01/22] user image --- back/methods/image/download.js | 94 ++++++++++++++++++ back/methods/image/specs/download.spec.js | 20 ++++ back/models/image-collection.json | 7 +- back/models/image.js | 2 + back/models/image.json | 8 ++ .../{10250 => 10250-toqueDeQueda}/00-ACL.sql | 2 +- .../10250-toqueDeQueda/00-imageCollection.sql | 5 + db/dump/dumpedFixtures.sql | 2 +- db/dump/fixtures.sql | 7 ++ front/salix/components/layout/index.html | 12 ++- front/salix/components/layout/index.js | 8 ++ front/salix/components/layout/index.spec.js | 15 +++ front/salix/components/layout/style.scss | 9 ++ .../salix/components/user-popover/index.html | 4 +- front/salix/components/user-popover/index.js | 9 +- .../components/user-popover/index.spec.js | 8 ++ .../salix/components/user-popover/style.scss | 4 + storage/image/user/160x160/9.png | Bin 0 -> 18980 bytes 18 files changed, 205 insertions(+), 11 deletions(-) create mode 100644 back/methods/image/download.js create mode 100644 back/methods/image/specs/download.spec.js rename db/changes/{10250 => 10250-toqueDeQueda}/00-ACL.sql (82%) create mode 100644 db/changes/10250-toqueDeQueda/00-imageCollection.sql create mode 100644 storage/image/user/160x160/9.png diff --git a/back/methods/image/download.js b/back/methods/image/download.js new file mode 100644 index 000000000..76cd3c109 --- /dev/null +++ b/back/methods/image/download.js @@ -0,0 +1,94 @@ +const UserError = require('vn-loopback/util/user-error'); +const fs = require('fs-extra'); + +module.exports = Self => { + Self.remoteMethod('download', { + description: 'Get the user image', + accessType: 'READ', + accepts: [ + { + arg: 'collection', + type: 'String', + description: 'The image collection', + http: {source: 'path'} + }, + { + arg: 'size', + type: 'String', + description: 'The image size', + http: {source: 'path'} + }, + { + arg: 'id', + type: 'Number', + description: 'The user id', + http: {source: 'path'} + } + + ], + returns: [ + { + arg: 'body', + type: 'file', + root: true + }, + { + arg: 'Content-Type', + type: 'String', + http: {target: 'header'} + }, + { + arg: 'Content-Disposition', + type: 'String', + http: {target: 'header'} + } + ], + http: { + path: `/:collection/:size/:id/download`, + verb: 'GET' + } + }); + + Self.download = async function(collection, size, id) { + const filter = { + where: { + collectionFk: collection, + name: id}, + include: { + relation: 'collection', + scope: { + fields: ['name', 'readRoleFk'], + include: { + relation: 'readRole' + } + } + } + }; + const image = await Self.app.models.Image.findOne(filter); + if (!image) return; + + const imageRole = image.collection().readRole().name; + const hasRole = await Self.app.models.Account.hasRole(id, imageRole); + if (!hasRole) + throw new UserError(`You don't have enough privileges`); + + let file; + let env = process.env.NODE_ENV; + if (env && env != 'development') { + file = { + path: `/var/lib/salix/image/${collection}/${size}/${id}.png`, + contentType: 'image/png', + name: `${id}.png` + }; + } else { + file = { + path: `${process.cwd()}/storage/image/${collection}/${size}/${id}.png`, + contentType: 'image/png', + name: `${id}.png` + }; + } + await fs.access(file.path); + let stream = fs.createReadStream(file.path); + return [stream, file.contentType, `filename="${file.name}"`]; + }; +}; diff --git a/back/methods/image/specs/download.spec.js b/back/methods/image/specs/download.spec.js new file mode 100644 index 000000000..43152a8f1 --- /dev/null +++ b/back/methods/image/specs/download.spec.js @@ -0,0 +1,20 @@ +const app = require('vn-loopback/server/server'); + +describe('image download()', () => { + const collection = 'user'; + const size = '160x160'; + + it('should return the image content-type of the user', async() => { + const userId = 9; + const image = await app.models.Image.download(collection, size, userId); + + expect(image[1]).toEqual('image/png'); + }); + + it(`should don't return an image if the user don't have it`, async() => { + const userId = 8; + const image = await app.models.Image.download(collection, size, userId); + + expect(image).toBeUndefined(); + }); +}); diff --git a/back/models/image-collection.json b/back/models/image-collection.json index 2234766c9..75faaf722 100644 --- a/back/models/image-collection.json +++ b/back/models/image-collection.json @@ -43,7 +43,12 @@ "model": "ImageCollectionSize", "foreignKey": "collectionFk", "property": "id" - } + }, + "readRole": { + "type": "belongsTo", + "model": "Role", + "foreignKey": "readRoleFk" + } }, "acls": [ { diff --git a/back/models/image.js b/back/models/image.js index 113bc7084..f6f4cf5db 100644 --- a/back/models/image.js +++ b/back/models/image.js @@ -3,6 +3,8 @@ const sharp = require('sharp'); const path = require('path'); module.exports = Self => { + require('../methods/image/download')(Self); + Self.getPath = function() { return '/var/lib/salix/image'; }; diff --git a/back/models/image.json b/back/models/image.json index 5b8c76cf1..047e5f5e4 100644 --- a/back/models/image.json +++ b/back/models/image.json @@ -29,6 +29,14 @@ "default": 0 } }, + "relations": { + "collection": { + "type": "belongsTo", + "model": "ImageCollection", + "foreignKey": "collectionFk", + "primaryKey": "name" + } + }, "acls": [ { "accessType": "READ", diff --git a/db/changes/10250/00-ACL.sql b/db/changes/10250-toqueDeQueda/00-ACL.sql similarity index 82% rename from db/changes/10250/00-ACL.sql rename to db/changes/10250-toqueDeQueda/00-ACL.sql index c9d8fd456..7765086d2 100644 --- a/db/changes/10250/00-ACL.sql +++ b/db/changes/10250-toqueDeQueda/00-ACL.sql @@ -1,2 +1,2 @@ -INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('supplier', '*', 'WRITE', 'ALLOW', 'ROLE', 'administrative'); +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('Supplier', '*', 'WRITE', 'ALLOW', 'ROLE', 'administrative'); INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('SupplierContact', '*', 'WRITE', 'ALLOW', 'ROLE', 'administrative'); diff --git a/db/changes/10250-toqueDeQueda/00-imageCollection.sql b/db/changes/10250-toqueDeQueda/00-imageCollection.sql new file mode 100644 index 000000000..bb5b1262e --- /dev/null +++ b/db/changes/10250-toqueDeQueda/00-imageCollection.sql @@ -0,0 +1,5 @@ + +ALTER TABLE `hedera`.`imageCollection` +ADD COLUMN `readRoleFk` VARCHAR(45) NULL DEFAULT NULL AFTER `column`; + +update `hedera`.`imageCollection` set `readRoleFk` = 1 where id = 6; \ No newline at end of file diff --git a/db/dump/dumpedFixtures.sql b/db/dump/dumpedFixtures.sql index 70e5d9b83..9f3f08b7c 100644 --- a/db/dump/dumpedFixtures.sql +++ b/db/dump/dumpedFixtures.sql @@ -443,7 +443,7 @@ USE `hedera`; LOCK TABLES `imageCollection` WRITE; /*!40000 ALTER TABLE `imageCollection` DISABLE KEYS */; -INSERT INTO `imageCollection` VALUES (1,'catalog','Artículo',3840,2160,'Item','image','vn','item','image'),(4,'link','Enlace',200,200,'Link','image','hedera','link','image'),(5,'news','Noticias',800,1200,'New','image','hedera','news','image'); +INSERT INTO `imageCollection` VALUES (1,'catalog','Artículo',3840,2160,'Item','image','vn','item','image'),(4,'link','Enlace',200,200,'Link','image','hedera','link','image'),(5,'news','Noticias',800,1200,'New','image','hedera','news','image'),('6','user','Usuario','800','1200','Account','image','account','user','image'); /*!40000 ALTER TABLE `imageCollection` ENABLE KEYS */; UNLOCK TABLES; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 31d9be163..1f8759c64 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -2141,3 +2141,10 @@ INSERT INTO `vn`.`campaign`(`code`, `dated`) ('allSaints', CONCAT(YEAR(DATE_ADD(CURDATE(), INTERVAL -2 YEAR)), '-11-01')), ('allSaints', CONCAT(YEAR(DATE_ADD(CURDATE(), INTERVAL -3 YEAR)), '-11-01')); +INSERT INTO `hedera`.`image`(`collectionFk`, `name`) + VALUES + ('user', 9); + +INSERT INTO `hedera`.`imageCollectionSize`(`id`, `collectionFk`,`width`, `height`) + VALUES + (1, 4, 160, 160); \ No newline at end of file diff --git a/front/salix/components/layout/index.html b/front/salix/components/layout/index.html index 8c716b84b..84e9fe566 100644 --- a/front/salix/components/layout/index.html +++ b/front/salix/components/layout/index.html @@ -31,12 +31,14 @@ ng-if="$ctrl.rightMenu" ng-click="$ctrl.rightMenu.show()"> - + - + translate-attr="{title: 'Account'}" + on-error-src/> + + diff --git a/front/salix/components/layout/index.js b/front/salix/components/layout/index.js index c6bc80734..986f61622 100644 --- a/front/salix/components/layout/index.js +++ b/front/salix/components/layout/index.js @@ -18,6 +18,14 @@ export class Layout extends Component { window.localStorage.currentUserWorkerId = json.data.id; }); } + + getImageUrl() { + if (!this.$.$root.user) return; + + const userId = this.$.$root.user.id; + const token = this.vnToken.token; + return `/api/Images/user/160x160/${userId}/download?access_token=${token}`; + } } Layout.$inject = ['$element', '$scope', 'vnModules']; diff --git a/front/salix/components/layout/index.spec.js b/front/salix/components/layout/index.spec.js index 18aca1f01..71dbb9192 100644 --- a/front/salix/components/layout/index.spec.js +++ b/front/salix/components/layout/index.spec.js @@ -22,4 +22,19 @@ describe('Component vnLayout', () => { expect(controller.$.$root.user.name).toEqual('batman'); }); }); + + describe('getImageUrl()', () => { + it('should return the url image if the user is defined', () => { + controller.$.$root.user = {id: 1}; + const url = controller.getImageUrl(); + + expect(url).not.toBe(3); + }); + + it('should return undefined if the user is not defined', () => { + const url = controller.getImageUrl(); + + expect(url).not.toBeDefined(); + }); + }); }); diff --git a/front/salix/components/layout/style.scss b/front/salix/components/layout/style.scss index 2b879040f..05858b3b1 100644 --- a/front/salix/components/layout/style.scss +++ b/front/salix/components/layout/style.scss @@ -109,6 +109,14 @@ vn-layout { } } } + img { + width: 40px; + border-radius: 50%; + } + .buttonAccount { + background: none; + border: none; + } @media screen and (max-width: $mobile-width) { & > vn-topbar { & > .start > .logo { @@ -147,3 +155,4 @@ vn-layout { font-size: 1.5rem; height: auto; } + diff --git a/front/salix/components/user-popover/index.html b/front/salix/components/user-popover/index.html index 4b02edd9f..9bd0f1411 100644 --- a/front/salix/components/user-popover/index.html +++ b/front/salix/components/user-popover/index.html @@ -13,7 +13,9 @@
- +
diff --git a/front/salix/components/user-popover/index.js b/front/salix/components/user-popover/index.js index c2fb6a130..e4d7b4466 100644 --- a/front/salix/components/user-popover/index.js +++ b/front/salix/components/user-popover/index.js @@ -3,12 +3,13 @@ import './style.scss'; import config from '../../config.json'; class Controller { - constructor($, $translate, vnConfig, vnAuth) { + constructor($, $translate, vnConfig, vnAuth, vnToken) { Object.assign(this, { $, $translate, vnConfig, vnAuth, + vnToken, lang: $translate.use(), langs: [] }); @@ -77,8 +78,12 @@ class Controller { this.$.companies.refresh(); this.$.popover.show(event.target); } + + getImageUrl(userId) { + return '/api/Images/user/160x160/' + userId + '/download?access_token=' + this.vnToken.token; + } } -Controller.$inject = ['$scope', '$translate', 'vnConfig', 'vnAuth']; +Controller.$inject = ['$scope', '$translate', 'vnConfig', 'vnAuth', 'vnToken']; ngModule.vnComponent('vnUserPopover', { template: require('./index.html'), diff --git a/front/salix/components/user-popover/index.spec.js b/front/salix/components/user-popover/index.spec.js index a0f1a953c..7afcf0104 100644 --- a/front/salix/components/user-popover/index.spec.js +++ b/front/salix/components/user-popover/index.spec.js @@ -57,5 +57,13 @@ describe('Salix', () => { expect(controller.companyFk).toBe(4); }); }); + + describe('getImageUrl()', () => { + it('should return de url image', () => { + const url = controller.getImageUrl(); + + expect(url).not.toBeDefined(); + }); + }); }); }); diff --git a/front/salix/components/user-popover/style.scss b/front/salix/components/user-popover/style.scss index 5f17ed293..9050c6654 100644 --- a/front/salix/components/user-popover/style.scss +++ b/front/salix/components/user-popover/style.scss @@ -11,6 +11,10 @@ font-size: 5rem; color: $color-font-bg-marginal; } + img { + width: 80px; + border-radius: 50%; + } & > div { display: flex; flex-direction: column; diff --git a/storage/image/user/160x160/9.png b/storage/image/user/160x160/9.png new file mode 100644 index 0000000000000000000000000000000000000000..2085c85f30f78df1bfb0f9445e7edcbd18453de3 GIT binary patch literal 18980 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 literal 0 HcmV?d00001 From c673f2130ac847b37db0b3514790983a5e1ec0f1 Mon Sep 17 00:00:00 2001 From: bernat Date: Tue, 10 Nov 2020 11:33:01 +0100 Subject: [PATCH 02/22] fix e2e --- back/methods/image/download.js | 2 +- back/methods/image/specs/download.spec.js | 4 ++-- e2e/helpers/selectors.js | 2 +- front/salix/components/user-popover/index.spec.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/back/methods/image/download.js b/back/methods/image/download.js index 76cd3c109..62be72496 100644 --- a/back/methods/image/download.js +++ b/back/methods/image/download.js @@ -65,7 +65,7 @@ module.exports = Self => { } }; const image = await Self.app.models.Image.findOne(filter); - if (!image) return; + if (!image) return false; const imageRole = image.collection().readRole().name; const hasRole = await Self.app.models.Account.hasRole(id, imageRole); diff --git a/back/methods/image/specs/download.spec.js b/back/methods/image/specs/download.spec.js index 43152a8f1..243871992 100644 --- a/back/methods/image/specs/download.spec.js +++ b/back/methods/image/specs/download.spec.js @@ -11,10 +11,10 @@ describe('image download()', () => { expect(image[1]).toEqual('image/png'); }); - it(`should don't return an image if the user don't have it`, async() => { + it(`should return false if the user don't have image`, async() => { const userId = 8; const image = await app.models.Image.download(collection, size, userId); - expect(image).toBeUndefined(); + expect(image).toBeFalse(); }); }); diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 3df6eec8c..e79477ef4 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -2,7 +2,7 @@ export default { globalItems: { applicationsMenuButton: '#apps', - userMenuButton: '#user', + userMenuButton: 'div.side.end img', logoutButton: '#logout', applicationsMenuVisible: '.modules-menu', clientsButton: '.modules-menu [ui-sref="client.index"]', diff --git a/front/salix/components/user-popover/index.spec.js b/front/salix/components/user-popover/index.spec.js index 7afcf0104..7a088fc51 100644 --- a/front/salix/components/user-popover/index.spec.js +++ b/front/salix/components/user-popover/index.spec.js @@ -62,7 +62,7 @@ describe('Salix', () => { it('should return de url image', () => { const url = controller.getImageUrl(); - expect(url).not.toBeDefined(); + expect(url).toBeDefined(); }); }); }); From 44d3cf748b578c792d57b7bda5ae13fcc2b0fc26 Mon Sep 17 00:00:00 2001 From: bernat Date: Tue, 10 Nov 2020 11:44:41 +0100 Subject: [PATCH 03/22] change id in ticketLastState model --- modules/ticket/back/models/ticket-last-state.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/ticket/back/models/ticket-last-state.json b/modules/ticket/back/models/ticket-last-state.json index 890d800af..6c8bc66d5 100644 --- a/modules/ticket/back/models/ticket-last-state.json +++ b/modules/ticket/back/models/ticket-last-state.json @@ -11,11 +11,10 @@ "type": "string" }, "ticketFk": { - "id": 1, + "id": true, "type": "Number" }, "ticketTrackingFk": { - "id": 2, "type": "Number" } }, From 75ffa108fb55cca40b1bad117679c1e4a752e605 Mon Sep 17 00:00:00 2001 From: bernat Date: Wed, 11 Nov 2020 10:52:35 +0100 Subject: [PATCH 04/22] refactor back download --- back/methods/image/download.js | 34 +++++++++--------- back/methods/image/specs/download.spec.js | 4 +-- back/models/account.json | 3 ++ .../10250-curfew/00-imageCollection.sql | 2 +- db/changes/10250-curfew/00-user.sql | 2 ++ db/dump/fixtures.sql | 32 ++++++++--------- ...g => e7723f0b24ff05b32ed09d95196f2f29.png} | Bin 7 files changed, 42 insertions(+), 35 deletions(-) create mode 100644 db/changes/10250-curfew/00-user.sql rename storage/image/user/160x160/{9.png => e7723f0b24ff05b32ed09d95196f2f29.png} (100%) diff --git a/back/methods/image/download.js b/back/methods/image/download.js index 62be72496..6f1e0b8e7 100644 --- a/back/methods/image/download.js +++ b/back/methods/image/download.js @@ -50,25 +50,27 @@ module.exports = Self => { }); Self.download = async function(collection, size, id) { + const models = Self.app.models; const filter = { where: { - collectionFk: collection, - name: id}, + name: collection}, include: { - relation: 'collection', - scope: { - fields: ['name', 'readRoleFk'], - include: { - relation: 'readRole' - } - } + relation: 'readRole' } }; - const image = await Self.app.models.Image.findOne(filter); + const imageCollection = await models.ImageCollection.findOne(filter); + const entity = await models[imageCollection.model].findById(id, { + fields: ['id', imageCollection.property] + }); + const image = await models.Image.findOne({where: { + collectionFk: collection, + name: entity[imageCollection.property]} + }); + if (!image) return false; - const imageRole = image.collection().readRole().name; - const hasRole = await Self.app.models.Account.hasRole(id, imageRole); + const imageRole = imageCollection.readRole().name; + const hasRole = await models.Account.hasRole(id, imageRole); if (!hasRole) throw new UserError(`You don't have enough privileges`); @@ -76,15 +78,15 @@ module.exports = Self => { let env = process.env.NODE_ENV; if (env && env != 'development') { file = { - path: `/var/lib/salix/image/${collection}/${size}/${id}.png`, + path: `/var/lib/salix/image/${collection}/${size}/${image.name}.png`, contentType: 'image/png', - name: `${id}.png` + name: `${image.name}.png` }; } else { file = { - path: `${process.cwd()}/storage/image/${collection}/${size}/${id}.png`, + path: `${process.cwd()}/storage/image/${collection}/${size}/${image.name}.png`, contentType: 'image/png', - name: `${id}.png` + name: `${image.name}.png` }; } await fs.access(file.path); diff --git a/back/methods/image/specs/download.spec.js b/back/methods/image/specs/download.spec.js index 243871992..131e2c97a 100644 --- a/back/methods/image/specs/download.spec.js +++ b/back/methods/image/specs/download.spec.js @@ -1,6 +1,6 @@ const app = require('vn-loopback/server/server'); -describe('image download()', () => { +fdescribe('image download()', () => { const collection = 'user'; const size = '160x160'; @@ -12,7 +12,7 @@ describe('image download()', () => { }); it(`should return false if the user don't have image`, async() => { - const userId = 8; + const userId = 110; const image = await app.models.Image.download(collection, size, userId); expect(image).toBeFalse(); diff --git a/back/models/account.json b/back/models/account.json index 364fe77d2..9cac20bc0 100644 --- a/back/models/account.json +++ b/back/models/account.json @@ -45,6 +45,9 @@ }, "updated": { "type": "date" + }, + "image": { + "type": "String" } }, "relations": { diff --git a/db/changes/10250-curfew/00-imageCollection.sql b/db/changes/10250-curfew/00-imageCollection.sql index bb5b1262e..f08f2f670 100644 --- a/db/changes/10250-curfew/00-imageCollection.sql +++ b/db/changes/10250-curfew/00-imageCollection.sql @@ -2,4 +2,4 @@ ALTER TABLE `hedera`.`imageCollection` ADD COLUMN `readRoleFk` VARCHAR(45) NULL DEFAULT NULL AFTER `column`; -update `hedera`.`imageCollection` set `readRoleFk` = 1 where id = 6; \ No newline at end of file +update `hedera`.`imageCollection` set `readRoleFk` = 1; \ No newline at end of file diff --git a/db/changes/10250-curfew/00-user.sql b/db/changes/10250-curfew/00-user.sql new file mode 100644 index 000000000..90bcf8fc5 --- /dev/null +++ b/db/changes/10250-curfew/00-user.sql @@ -0,0 +1,2 @@ +ALTER TABLE `account`.`user` +ADD COLUMN `image` VARCHAR(255) NULL AFTER `password`; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 200ba8bb7..ccb017609 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -29,8 +29,8 @@ INSERT INTO `vn`.`packagingConfig`(`upperGap`) UPDATE `account`.`role` SET id = 100 WHERE id = 0; CALL `account`.`role_sync`; -INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `password`,`role`,`active`,`email`, `lang`) - SELECT id, name, CONCAT(name, 'Nick'),MD5('nightmare'), id, 1, CONCAT(name, '@mydomain.com'), 'en' +INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `password`,`role`,`active`,`email`, `lang`, `image`) + SELECT id, name, CONCAT(name, 'Nick'),MD5('nightmare'), id, 1, CONCAT(name, '@mydomain.com'), 'en', 'e7723f0b24ff05b32ed09d95196f2f29' FROM `account`.`role` WHERE id <> 20 ORDER BY id; @@ -51,20 +51,20 @@ INSERT INTO `hedera`.`tpvConfig`(`id`, `currency`, `terminal`, `transactionType` VALUES (1, 978, 1, 0, 2000, 9, 0); -INSERT INTO `account`.`user`(`id`,`name`,`nickname`, `password`,`role`,`active`,`email`,`lang`) +INSERT INTO `account`.`user`(`id`,`name`,`nickname`, `password`,`role`,`active`,`email`,`lang`, `image`) VALUES - (101, 'BruceWayne', 'Bruce Wayne', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'BruceWayne@mydomain.com', 'es'), - (102, 'PetterParker', 'Petter Parker', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'PetterParker@mydomain.com', 'en'), - (103, 'ClarkKent', 'Clark Kent', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'ClarkKent@mydomain.com', 'fr'), - (104, 'TonyStark', 'Tony Stark', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'TonyStark@mydomain.com', 'es'), - (105, 'MaxEisenhardt', 'Max Eisenhardt', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'MaxEisenhardt@mydomain.com', 'pt'), - (106, 'DavidCharlesHaller', 'David Charles Haller', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'DavidCharlesHaller@mydomain.com', 'en'), - (107, 'HankPym', 'Hank Pym', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'HankPym@mydomain.com', 'en'), - (108, 'CharlesXavier', 'Charles Xavier', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'CharlesXavier@mydomain.com', 'en'), - (109, 'BruceBanner', 'Bruce Banner', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'BruceBanner@mydomain.com', 'en'), - (110, 'JessicaJones', 'Jessica Jones', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'JessicaJones@mydomain.com', 'en'), - (111, 'Missing', 'Missing', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'en'), - (112, 'Trash', 'Trash', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'en'); + (101, 'BruceWayne', 'Bruce Wayne', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'BruceWayne@mydomain.com', 'es', 'e7723f0b24ff05b32ed09d95196f2f29'), + (102, 'PetterParker', 'Petter Parker', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'PetterParker@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'), + (103, 'ClarkKent', 'Clark Kent', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'ClarkKent@mydomain.com', 'fr', 'e7723f0b24ff05b32ed09d95196f2f29'), + (104, 'TonyStark', 'Tony Stark', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'TonyStark@mydomain.com', 'es', 'e7723f0b24ff05b32ed09d95196f2f29'), + (105, 'MaxEisenhardt', 'Max Eisenhardt', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'MaxEisenhardt@mydomain.com', 'pt', 'e7723f0b24ff05b32ed09d95196f2f29'), + (106, 'DavidCharlesHaller', 'David Charles Haller', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'DavidCharlesHaller@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'), + (107, 'HankPym', 'Hank Pym', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'HankPym@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'), + (108, 'CharlesXavier', 'Charles Xavier', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'CharlesXavier@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'), + (109, 'BruceBanner', 'Bruce Banner', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'BruceBanner@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'), + (110, 'JessicaJones', 'Jessica Jones', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'JessicaJones@mydomain.com', 'en', NULL), + (111, 'Missing', 'Missing', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'en', NULL), + (112, 'Trash', 'Trash', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'en', NULL); INSERT INTO `account`.`mailAlias`(`id`, `alias`, `description`, `isPublic`) VALUES @@ -2143,7 +2143,7 @@ INSERT INTO `vn`.`campaign`(`code`, `dated`) INSERT INTO `hedera`.`image`(`collectionFk`, `name`) VALUES - ('user', 9); + ('user', 'e7723f0b24ff05b32ed09d95196f2f29'); INSERT INTO `hedera`.`imageCollectionSize`(`id`, `collectionFk`,`width`, `height`) VALUES diff --git a/storage/image/user/160x160/9.png b/storage/image/user/160x160/e7723f0b24ff05b32ed09d95196f2f29.png similarity index 100% rename from storage/image/user/160x160/9.png rename to storage/image/user/160x160/e7723f0b24ff05b32ed09d95196f2f29.png From a060ac5c7039f514cad88d042363e7a20dc555ba Mon Sep 17 00:00:00 2001 From: bernat Date: Wed, 11 Nov 2020 11:58:14 +0100 Subject: [PATCH 05/22] fix pr changes --- back/methods/image/specs/download.spec.js | 7 ++++--- e2e/helpers/selectors.js | 2 +- e2e/paths/07-order/04_catalog.spec.js | 3 ++- front/salix/components/layout/index.html | 1 + 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/back/methods/image/specs/download.spec.js b/back/methods/image/specs/download.spec.js index 131e2c97a..e646d829d 100644 --- a/back/methods/image/specs/download.spec.js +++ b/back/methods/image/specs/download.spec.js @@ -1,17 +1,18 @@ const app = require('vn-loopback/server/server'); -fdescribe('image download()', () => { +describe('image download()', () => { const collection = 'user'; const size = '160x160'; it('should return the image content-type of the user', async() => { const userId = 9; const image = await app.models.Image.download(collection, size, userId); + const contentType = image[1]; - expect(image[1]).toEqual('image/png'); + expect(contentType).toEqual('image/png'); }); - it(`should return false if the user don't have image`, async() => { + it(`should return false if the user doesn't have image`, async() => { const userId = 110; const image = await app.models.Image.download(collection, size, userId); diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index e79477ef4..3df6eec8c 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -2,7 +2,7 @@ export default { globalItems: { applicationsMenuButton: '#apps', - userMenuButton: 'div.side.end img', + userMenuButton: '#user', logoutButton: '#logout', applicationsMenuVisible: '.modules-menu', clientsButton: '.modules-menu [ui-sref="client.index"]', diff --git a/e2e/paths/07-order/04_catalog.spec.js b/e2e/paths/07-order/04_catalog.spec.js index b8a20e938..054e52c60 100644 --- a/e2e/paths/07-order/04_catalog.spec.js +++ b/e2e/paths/07-order/04_catalog.spec.js @@ -1,7 +1,7 @@ import selectors from '../../helpers/selectors.js'; import getBrowser from '../../helpers/puppeteer'; -describe('Order catalog', () => { +fdescribe('Order catalog', () => { let browser; let page; @@ -40,6 +40,7 @@ describe('Order catalog', () => { it('should perfom an "OR" search for the item tag colors silver and brown', async() => { await page.waitToClick(selectors.orderCatalog.openTagSearch); await page.autocompleteSearch(selectors.orderCatalog.tag, 'Color'); + await page.wait(2999); await page.autocompleteSearch(selectors.orderCatalog.firstTagAutocomplete, 'silver'); await page.waitToClick(selectors.orderCatalog.addTagButton); await page.autocompleteSearch(selectors.orderCatalog.secondTagAutocomplete, 'brown'); diff --git a/front/salix/components/layout/index.html b/front/salix/components/layout/index.html index 84e9fe566..cb0fb93b4 100644 --- a/front/salix/components/layout/index.html +++ b/front/salix/components/layout/index.html @@ -33,6 +33,7 @@
+ diff --git a/modules/travel/front/descriptor/index.js b/modules/travel/front/descriptor/index.js index 6136d5347..744166580 100644 --- a/modules/travel/front/descriptor/index.js +++ b/modules/travel/front/descriptor/index.js @@ -10,6 +10,39 @@ class Controller extends Descriptor { this.entity = value; } + get travelFilter() { + let travelFilter; + const travel = this.travel; + + if (travel && travel.agencyFk) { + travelFilter = this.travel && JSON.stringify({ + agencyFk: this.travel.agencyFk + }); + } + return travelFilter; + } + + get entryFilter() { + let travel = this.travel; + + if (!travel || !travel.landed) return null; + + const date = new Date(travel.landed); + date.setHours(0, 0, 0, 0); + + const from = new Date(date.getTime()); + from.setDate(from.getDate() - 10); + + const to = new Date(date.getTime()); + to.setDate(to.getDate() + 10); + + return JSON.stringify({ + supplierFk: this.travel.cargoSupplierFk, + from, + to + }); + } + loadData() { const filter = { fields: [ @@ -19,7 +52,8 @@ class Controller extends Descriptor { 'landed', 'totalEntries', 'warehouseInFk', - 'warehouseOutFk' + 'warehouseOutFk', + 'cargoSupplierFk' ], include: [ { From dc7a6d4ce159be22e2c132a6c9f8c25e0fcb0843 Mon Sep 17 00:00:00 2001 From: carlosjr Date: Mon, 16 Nov 2020 19:04:37 +0100 Subject: [PATCH 09/22] dot menu along with clone option --- .../travel/front/descriptor-menu/index.html | 21 ++++++++++ modules/travel/front/descriptor-menu/index.js | 31 +++++++++++++++ .../front/descriptor-menu/index.spec.js | 39 +++++++++++++++++++ .../travel/front/descriptor-menu/style.scss | 24 ++++++++++++ modules/travel/front/descriptor/index.html | 3 ++ modules/travel/front/index.js | 1 + 6 files changed, 119 insertions(+) create mode 100644 modules/travel/front/descriptor-menu/index.html create mode 100644 modules/travel/front/descriptor-menu/index.js create mode 100644 modules/travel/front/descriptor-menu/index.spec.js create mode 100644 modules/travel/front/descriptor-menu/style.scss diff --git a/modules/travel/front/descriptor-menu/index.html b/modules/travel/front/descriptor-menu/index.html new file mode 100644 index 000000000..45c745688 --- /dev/null +++ b/modules/travel/front/descriptor-menu/index.html @@ -0,0 +1,21 @@ + + + + + + Clone travel + + + + + + + diff --git a/modules/travel/front/descriptor-menu/index.js b/modules/travel/front/descriptor-menu/index.js new file mode 100644 index 000000000..84a510e55 --- /dev/null +++ b/modules/travel/front/descriptor-menu/index.js @@ -0,0 +1,31 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; +import './style.scss'; + +class Controller extends Section { + constructor($element, $) { + super($element, $); + } + + onCloneAccept() { + const params = JSON.stringify({ + ref: this.travel.ref, + agencyModeFk: this.travel.agencyFk, + shipped: this.travel.shipped, + landed: this.travel.landed, + warehouseInFk: this.travel.warehouseInFk, + warehouseOutFk: this.travel.warehouseOutFk + }); + this.$state.go('travel.create', {q: params}); + } +} + +Controller.$inject = ['$element', '$scope']; + +ngModule.vnComponent('vnTravelDescriptorMenu', { + template: require('./index.html'), + controller: Controller, + bindings: { + travel: '<', + } +}); diff --git a/modules/travel/front/descriptor-menu/index.spec.js b/modules/travel/front/descriptor-menu/index.spec.js new file mode 100644 index 000000000..d66f3a435 --- /dev/null +++ b/modules/travel/front/descriptor-menu/index.spec.js @@ -0,0 +1,39 @@ +import './index.js'; + +describe('Travel Component vnTravelDescriptorMenu', () => { + let controller; + beforeEach(ngModule('travel')); + + beforeEach(inject(($componentController, $state,) => { + const $element = angular.element(''); + controller = $componentController('vnTravelDescriptorMenu', {$element}); + })); + + describe('onCloneAccept()', () => { + it('should call state.go with the travel data', () => { + jest.spyOn(controller.$state, 'go').mockReturnValue('ok'); + + controller.travel = { + ref: 'the ref', + agencyFk: 'the agency', + shipped: 'the shipped date', + landed: 'the landing date', + warehouseInFk: 'the receiver warehouse', + warehouseOutFk: 'the sender warehouse' + }; + + controller.onCloneAccept(); + + const params = JSON.stringify({ + ref: controller.travel.ref, + agencyModeFk: controller.travel.agencyFk, + shipped: controller.travel.shipped, + landed: controller.travel.landed, + warehouseInFk: controller.travel.warehouseInFk, + warehouseOutFk: controller.travel.warehouseOutFk + }); + + expect(controller.$state.go).toHaveBeenCalledWith('travel.create', {'q': params}); + }); + }); +}); diff --git a/modules/travel/front/descriptor-menu/style.scss b/modules/travel/front/descriptor-menu/style.scss new file mode 100644 index 000000000..beab9335e --- /dev/null +++ b/modules/travel/front/descriptor-menu/style.scss @@ -0,0 +1,24 @@ +@import "./effects"; +@import "variables"; + +vn-travel-descriptor-menu { + & > vn-icon-button[icon="more_vert"] { + display: flex; + min-width: 45px; + height: 45px; + box-sizing: border-box; + align-items: center; + justify-content: center; + } + & > vn-icon-button[icon="more_vert"] { + @extend %clickable; + color: inherit; + + & > vn-icon { + padding: 10px; + } + vn-icon { + font-size: 1.75rem; + } + } +} \ No newline at end of file diff --git a/modules/travel/front/descriptor/index.html b/modules/travel/front/descriptor/index.html index cd6143cae..1e5d0294f 100644 --- a/modules/travel/front/descriptor/index.html +++ b/modules/travel/front/descriptor/index.html @@ -1,6 +1,9 @@ + + +
Date: Tue, 17 Nov 2020 11:04:17 +0100 Subject: [PATCH 10/22] removed commented code --- modules/item/front/last-entries/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/item/front/last-entries/index.html b/modules/item/front/last-entries/index.html index 6d8add2cb..4d8dea570 100644 --- a/modules/item/front/last-entries/index.html +++ b/modules/item/front/last-entries/index.html @@ -50,7 +50,6 @@ {{::entry.warehouse| dashIfEmpty}} {{::entry.landed | date:'dd/MM/yyyy HH:mm'}} - Date: Tue, 17 Nov 2020 15:54:45 +0100 Subject: [PATCH 11/22] refactor --- modules/travel/front/descriptor-menu/index.js | 43 ++++++++++++++++++- .../front/descriptor-menu/locale/es.yml | 1 + modules/travel/front/descriptor/index.html | 7 +-- modules/travel/front/descriptor/index.js | 21 --------- 4 files changed, 44 insertions(+), 28 deletions(-) create mode 100644 modules/travel/front/descriptor-menu/locale/es.yml diff --git a/modules/travel/front/descriptor-menu/index.js b/modules/travel/front/descriptor-menu/index.js index 84a510e55..975cd9134 100644 --- a/modules/travel/front/descriptor-menu/index.js +++ b/modules/travel/front/descriptor-menu/index.js @@ -7,6 +7,47 @@ class Controller extends Section { super($element, $); } + get travelId() { + return this._travelId; + } + + set travelId(value) { + this._travelId = value; + + if (value) this.loadData(); + } + + loadData() { + const filter = { + fields: [ + 'id', + 'ref', + 'shipped', + 'landed', + 'totalEntries', + 'agencyFk', + 'warehouseInFk', + 'warehouseOutFk', + 'cargoSupplierFk' + ], + include: [ + { + relation: 'warehouseIn', + scope: { + fields: ['name'] + } + }, { + relation: 'warehouseOut', + scope: { + fields: ['name'] + } + } + ] + }; + return this.$http.get(`Travels/${this.travelId}`, {filter}) + .then(res => this.travel = res.data); + } + onCloneAccept() { const params = JSON.stringify({ ref: this.travel.ref, @@ -26,6 +67,6 @@ ngModule.vnComponent('vnTravelDescriptorMenu', { template: require('./index.html'), controller: Controller, bindings: { - travel: '<', + travelId: '<', } }); diff --git a/modules/travel/front/descriptor-menu/locale/es.yml b/modules/travel/front/descriptor-menu/locale/es.yml new file mode 100644 index 000000000..117611660 --- /dev/null +++ b/modules/travel/front/descriptor-menu/locale/es.yml @@ -0,0 +1 @@ +Clone travel: Clonar envío \ No newline at end of file diff --git a/modules/travel/front/descriptor/index.html b/modules/travel/front/descriptor/index.html index 1e5d0294f..28e908d18 100644 --- a/modules/travel/front/descriptor/index.html +++ b/modules/travel/front/descriptor/index.html @@ -2,7 +2,7 @@ module="travel" description="$ctrl.travel.ref"> - +
@@ -36,11 +36,6 @@
- -
diff --git a/modules/travel/front/descriptor/index.js b/modules/travel/front/descriptor/index.js index 744166580..dc19f68af 100644 --- a/modules/travel/front/descriptor/index.js +++ b/modules/travel/front/descriptor/index.js @@ -22,27 +22,6 @@ class Controller extends Descriptor { return travelFilter; } - get entryFilter() { - let travel = this.travel; - - if (!travel || !travel.landed) return null; - - const date = new Date(travel.landed); - date.setHours(0, 0, 0, 0); - - const from = new Date(date.getTime()); - from.setDate(from.getDate() - 10); - - const to = new Date(date.getTime()); - to.setDate(to.getDate() + 10); - - return JSON.stringify({ - supplierFk: this.travel.cargoSupplierFk, - from, - to - }); - } - loadData() { const filter = { fields: [ From 7dc8ae9dbcb81d6422baac4668fea527dbbdea7a Mon Sep 17 00:00:00 2001 From: carlosjr Date: Tue, 17 Nov 2020 16:52:19 +0100 Subject: [PATCH 12/22] e2e path for travel descriptor --- e2e/helpers/selectors.js | 9 ++++ e2e/paths/10-travel/03_descriptor.spec.js | 45 +++++++++++++++++++ .../travel/front/descriptor-menu/index.html | 1 + 3 files changed, 55 insertions(+) create mode 100644 e2e/paths/10-travel/03_descriptor.spec.js diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 95ffca91e..f5f2c4050 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -815,6 +815,10 @@ export default { ticketOne: 'vn-invoice-out-summary > vn-card > vn-horizontal > vn-auto > vn-table > div > vn-tbody > vn-tr:nth-child(1)', ticketTwo: 'vn-invoice-out-summary > vn-card > vn-horizontal > vn-auto > vn-table > div > vn-tbody > vn-tr:nth-child(2)' }, + travelIndex: { + anySearchResult: 'vn-travel-index vn-tbody > a', + firstSearchResult: 'vn-travel-index vn-tbody > a:nth-child(1)' + }, travelBasicDada: { reference: 'vn-travel-basic-data vn-textfield[ng-model="$ctrl.travel.ref"]', agency: 'vn-travel-basic-data vn-autocomplete[ng-model="$ctrl.travel.agencyModeFk"]', @@ -841,6 +845,11 @@ export default { createdThermograph: 'vn-travel-thermograph-index vn-tbody > vn-tr', upload: 'vn-travel-thermograph-create button[type=submit]' }, + travelDescriptor: { + filterByAgencyButton: 'vn-descriptor-content .quicklinks > div:nth-child(1) > vn-quick-link > a[vn-tooltip="All travels with current agency"]', + dotMenu: 'vn-travel-descriptor vn-icon-button[icon="more_vert"]', + dotMenuClone: '#clone' + }, zoneIndex: { searchResult: 'vn-zone-index a.vn-tr', }, diff --git a/e2e/paths/10-travel/03_descriptor.spec.js b/e2e/paths/10-travel/03_descriptor.spec.js new file mode 100644 index 000000000..13b2dfe7a --- /dev/null +++ b/e2e/paths/10-travel/03_descriptor.spec.js @@ -0,0 +1,45 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Travel descriptor path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'travel'); + await page.accessToSearchResult('3'); + await page.waitForState('travel.card.summary'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should click the descriptor button to navigate to the travel index showing all travels with current agency', async() => { + await page.waitToClick(selectors.travelDescriptor.filterByAgencyButton); + await page.waitForState('travel.index'); + const result = await page.countElement(selectors.travelIndex.anySearchResult); + + expect(result).toEqual(8); + }); + + it('should navigate to the first search result', async() => { + await page.waitToClick(selectors.travelIndex.firstSearchResult); + await page.waitForState('travel.card.summary'); + const state = await page.getState(); + + expect(state).toBe('travel.card.summary'); + }); + + it('should be redirected to the create travel when using the clone option of the dot menu', async() => { + await page.waitToClick(selectors.travelDescriptor.dotMenu); + await page.waitToClick(selectors.travelDescriptor.dotMenuClone); + await page.respondToDialog('accept'); + await page.waitForState('travel.create'); + const state = await page.getState(); + + expect(state).toBe('travel.create'); + }); +}); diff --git a/modules/travel/front/descriptor-menu/index.html b/modules/travel/front/descriptor-menu/index.html index 45c745688..1eb558008 100644 --- a/modules/travel/front/descriptor-menu/index.html +++ b/modules/travel/front/descriptor-menu/index.html @@ -5,6 +5,7 @@ Clone travel From 71f0a1932037cb33f3627d1ab63471f4199928d4 Mon Sep 17 00:00:00 2001 From: carlosjr Date: Tue, 17 Nov 2020 17:19:22 +0100 Subject: [PATCH 13/22] descriptor path fix --- e2e/paths/10-travel/03_descriptor.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/paths/10-travel/03_descriptor.spec.js b/e2e/paths/10-travel/03_descriptor.spec.js index 13b2dfe7a..8567f6a66 100644 --- a/e2e/paths/10-travel/03_descriptor.spec.js +++ b/e2e/paths/10-travel/03_descriptor.spec.js @@ -20,6 +20,7 @@ describe('Travel descriptor path', () => { it('should click the descriptor button to navigate to the travel index showing all travels with current agency', async() => { await page.waitToClick(selectors.travelDescriptor.filterByAgencyButton); await page.waitForState('travel.index'); + await page.waitForNumberOfElements(selectors.travelIndex.anySearchResult, 8); const result = await page.countElement(selectors.travelIndex.anySearchResult); expect(result).toEqual(8); From 7dff246e46b3bbeca75a6ef5e35397d93cccd95c Mon Sep 17 00:00:00 2001 From: carlosjr Date: Tue, 17 Nov 2020 18:07:04 +0100 Subject: [PATCH 14/22] adapted e2e path for full run --- e2e/paths/02-client/14_balance.spec.js | 1 - e2e/paths/10-travel/03_descriptor.spec.js | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/e2e/paths/02-client/14_balance.spec.js b/e2e/paths/02-client/14_balance.spec.js index a9d5e304a..bd00af58e 100644 --- a/e2e/paths/02-client/14_balance.spec.js +++ b/e2e/paths/02-client/14_balance.spec.js @@ -41,7 +41,6 @@ describe('Client balance path', () => { it('should click the new payment button', async() => { await page.closePopup(); await page.reloadSection('client.card.balance.index'); - await page.waitForState('client.card.balance.index'); }); it('should create a new payment that clears the debt', async() => { diff --git a/e2e/paths/10-travel/03_descriptor.spec.js b/e2e/paths/10-travel/03_descriptor.spec.js index 8567f6a66..3f79bea06 100644 --- a/e2e/paths/10-travel/03_descriptor.spec.js +++ b/e2e/paths/10-travel/03_descriptor.spec.js @@ -9,7 +9,7 @@ describe('Travel descriptor path', () => { browser = await getBrowser(); page = browser.page; await page.loginAndModule('buyer', 'travel'); - await page.accessToSearchResult('3'); + await page.accessToSearchResult('1'); await page.waitForState('travel.card.summary'); }); @@ -20,10 +20,9 @@ describe('Travel descriptor path', () => { it('should click the descriptor button to navigate to the travel index showing all travels with current agency', async() => { await page.waitToClick(selectors.travelDescriptor.filterByAgencyButton); await page.waitForState('travel.index'); - await page.waitForNumberOfElements(selectors.travelIndex.anySearchResult, 8); const result = await page.countElement(selectors.travelIndex.anySearchResult); - expect(result).toEqual(8); + expect(result).toBeGreaterThanOrEqual(7); }); it('should navigate to the first search result', async() => { From 0fc01b2316f78235e8df8e354b77a006f37d768d Mon Sep 17 00:00:00 2001 From: joan Date: Wed, 18 Nov 2020 08:16:37 +0100 Subject: [PATCH 15/22] 1419 - Claim update discount hotfix --- modules/claim/front/detail/index.html | 4 +-- modules/claim/front/detail/index.js | 25 +++++++++---------- .../back/methods/ticket/updateDiscount.js | 8 ++++-- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/modules/claim/front/detail/index.html b/modules/claim/front/detail/index.html index e364801bf..690fb6fad 100644 --- a/modules/claim/front/detail/index.html +++ b/modules/claim/front/detail/index.html @@ -55,8 +55,8 @@
{{::saleClaimed.sale.price | currency: 'EUR':2}} - {{saleClaimed.sale.discount}} % diff --git a/modules/claim/front/detail/index.js b/modules/claim/front/detail/index.js index 9f6767318..4e0017baa 100644 --- a/modules/claim/front/detail/index.js +++ b/modules/claim/front/detail/index.js @@ -54,6 +54,10 @@ class Controller extends Section { this.updateNewPrice(); } + get isClaimManager() { + return this.aclService.hasAny(['claimManager']); + } + openAddSalesDialog() { this.getClaimableFromTicket(); this.$.addSales.show(); @@ -131,17 +135,6 @@ class Controller extends Section { return total; } - showEditPopover(event, saleClaimed) { - if (this.isEditable) { - if (!this.aclService.hasAny(['claimManager'])) - return this.vnApp.showError(this.$t('Insuficient permisos')); - - this.saleClaimed = saleClaimed; - this.$.editPopover.parent = event.target; - this.$.editPopover.show(); - } - } - getSalespersonMana() { this.$http.get(`Tickets/${this.claim.ticketFk}/getSalesPersonMana`).then(res => { this.mana = res.data; @@ -164,6 +157,14 @@ class Controller extends Section { }); } + showEditPopover(event, saleClaimed) { + if (this.aclService.hasAny(['claimManager'])) { + this.saleClaimed = saleClaimed; + this.$.editPopover.parent = event.target; + this.$.editPopover.show(); + } + } + updateDiscount() { const claimedSale = this.saleClaimed.sale; if (this.newDiscount != claimedSale.discount) { @@ -176,8 +177,6 @@ class Controller extends Section { this.clearDiscount(); this.vnApp.showSuccess(this.$t('Data saved!')); - }).catch(err => { - this.vnApp.showError(err.message); }); } diff --git a/modules/ticket/back/methods/ticket/updateDiscount.js b/modules/ticket/back/methods/ticket/updateDiscount.js index 7c9f9ae48..1bebaae8d 100644 --- a/modules/ticket/back/methods/ticket/updateDiscount.js +++ b/modules/ticket/back/methods/ticket/updateDiscount.js @@ -70,13 +70,17 @@ module.exports = Self => { const userId = ctx.req.accessToken.userId; const isLocked = await models.Ticket.isLocked(id); - const isSalesPerson = await models.Account.hasRole(userId, 'salesPerson'); + const roles = await models.Account.getRoles(userId); + const hasAllowedRoles = roles.filter(role => + role == 'salesPerson' || role == 'claimManager' + ); + const state = await Self.app.models.TicketState.findOne({ where: {ticketFk: id} }); const alertLevel = state ? state.alertLevel : null; - if (isLocked || (!isSalesPerson && alertLevel > 0)) + if (isLocked || (!hasAllowedRoles && alertLevel > 0)) throw new UserError(`The sales of this ticket can't be modified`); const usesMana = await models.WorkerMana.findOne({ From d174a363d282076e6cbf232fcb8ebcb5c350fe52 Mon Sep 17 00:00:00 2001 From: joan Date: Wed, 18 Nov 2020 11:23:51 +0100 Subject: [PATCH 16/22] Removed wait --- e2e/paths/07-order/04_catalog.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/paths/07-order/04_catalog.spec.js b/e2e/paths/07-order/04_catalog.spec.js index 5b6365b28..b8a20e938 100644 --- a/e2e/paths/07-order/04_catalog.spec.js +++ b/e2e/paths/07-order/04_catalog.spec.js @@ -40,7 +40,6 @@ describe('Order catalog', () => { it('should perfom an "OR" search for the item tag colors silver and brown', async() => { await page.waitToClick(selectors.orderCatalog.openTagSearch); await page.autocompleteSearch(selectors.orderCatalog.tag, 'Color'); - await page.wait(2999); await page.autocompleteSearch(selectors.orderCatalog.firstTagAutocomplete, 'silver'); await page.waitToClick(selectors.orderCatalog.addTagButton); await page.autocompleteSearch(selectors.orderCatalog.secondTagAutocomplete, 'brown'); From 801fdb076b8258ce8fbe24d7c88d9808e4f03e12 Mon Sep 17 00:00:00 2001 From: bernat Date: Wed, 18 Nov 2020 11:47:23 +0100 Subject: [PATCH 17/22] supplier changes --- e2e/helpers/selectors.js | 2 +- e2e/paths/13-supplier/02_basic_data.spec.js | 6 +- .../back/methods/supplier/getSummary.js | 24 +++++- .../back/models/supplier-contact.json | 6 +- modules/supplier/back/models/supplier.json | 20 ++++- modules/supplier/front/basic-data/index.html | 4 +- .../supplier/front/basic-data/locale/es.yml | 2 +- modules/supplier/front/contact/index.html | 2 +- modules/supplier/front/descriptor/index.html | 6 +- modules/supplier/front/descriptor/index.js | 2 +- .../supplier/front/descriptor/locale/es.yml | 2 +- modules/supplier/front/fiscal-data/index.html | 9 +++ modules/supplier/front/index/index.html | 2 +- modules/supplier/front/index/index.js | 12 ++- modules/supplier/front/main/index.html | 3 +- modules/supplier/front/summary/index.html | 80 +++++++++++-------- modules/supplier/front/summary/locale/es.yml | 7 +- 17 files changed, 133 insertions(+), 56 deletions(-) diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 95ffca91e..c28743585 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -927,7 +927,7 @@ export default { }, supplierBasicData: { alias: 'vn-supplier-basic-data vn-textfield[ng-model="$ctrl.supplier.nickname"]', - isOfficial: 'vn-supplier-basic-data vn-check[ng-model="$ctrl.supplier.isOfficial"]', + isSerious: 'vn-supplier-basic-data vn-check[ng-model="$ctrl.supplier.isSerious"]', isActive: 'vn-supplier-basic-data vn-check[ng-model="$ctrl.supplier.isActive"]', notes: 'vn-supplier-basic-data vn-textarea[ng-model="$ctrl.supplier.note"]', saveButton: 'vn-supplier-basic-data button[type="submit"]', diff --git a/e2e/paths/13-supplier/02_basic_data.spec.js b/e2e/paths/13-supplier/02_basic_data.spec.js index 9620af98f..319130b98 100644 --- a/e2e/paths/13-supplier/02_basic_data.spec.js +++ b/e2e/paths/13-supplier/02_basic_data.spec.js @@ -20,7 +20,7 @@ describe('Supplier basic data path', () => { it('should edit the basic data', async() => { await page.clearInput(selectors.supplierBasicData.alias); await page.write(selectors.supplierBasicData.alias, 'Plants Nick SL'); - await page.waitToClick(selectors.supplierBasicData.isOfficial); + await page.waitToClick(selectors.supplierBasicData.isSerious); await page.waitToClick(selectors.supplierBasicData.isActive); await page.write(selectors.supplierBasicData.notes, 'Some notes'); @@ -40,8 +40,8 @@ describe('Supplier basic data path', () => { expect(result).toEqual('Plants Nick SL'); }); - it('should check the isOffical checkbox is now unchecked', async() => { - const result = await page.checkboxState(selectors.supplierBasicData.isOfficial); + it('should check the isSerious checkbox is now unchecked', async() => { + const result = await page.checkboxState(selectors.supplierBasicData.isSerious); expect(result).toBe('unchecked'); }); diff --git a/modules/supplier/back/methods/supplier/getSummary.js b/modules/supplier/back/methods/supplier/getSummary.js index 24a592acf..e6509eaa2 100644 --- a/modules/supplier/back/methods/supplier/getSummary.js +++ b/modules/supplier/back/methods/supplier/getSummary.js @@ -25,7 +25,7 @@ module.exports = Self => { 'id', 'name', 'nickname', - 'isOfficial', + 'isSerious', 'isActive', 'note', 'nif', @@ -39,6 +39,9 @@ module.exports = Self => { 'payDay', 'account', 'isFarmer', + 'sageTaxTypeFk', + 'sageTransactionTypeFk', + 'sageWithholdingFk', ], include: [ { @@ -64,10 +67,27 @@ module.exports = Self => { scope: { fields: ['id', 'payDem'] } + }, + { + relation: 'sageTaxType', + scope: { + fields: ['id', 'vat'] + } + }, + { + relation: 'sageTransactionType', + scope: { + fields: ['id', 'transaction'] + } + }, + { + relation: 'sageWithholding', + scope: { + fields: ['id', 'withholding'] + } } ] }; - let supplier = await Self.app.models.Supplier.findOne(filter); return supplier; }; diff --git a/modules/supplier/back/models/supplier-contact.json b/modules/supplier/back/models/supplier-contact.json index accb2acd6..104a3a404 100644 --- a/modules/supplier/back/models/supplier-contact.json +++ b/modules/supplier/back/models/supplier-contact.json @@ -1,6 +1,10 @@ { "name": "SupplierContact", - "base": "VnModel", + "base": "Loggable", + "log": { + "model":"SupplierLog", + "relation": "supplier" + }, "options": { "mysql": { "table": "supplierContact" diff --git a/modules/supplier/back/models/supplier.json b/modules/supplier/back/models/supplier.json index 596aad745..eb3c5989e 100644 --- a/modules/supplier/back/models/supplier.json +++ b/modules/supplier/back/models/supplier.json @@ -51,6 +51,9 @@ "isOfficial": { "type": "Boolean" }, + "isSerious": { + "type": "Boolean" + }, "note": { "type": "String" }, @@ -123,7 +126,22 @@ "model": "Client", "foreignKey": "nif", "primaryKey": "fi" - } + }, + "sageTaxType": { + "type": "belongsTo", + "model": "SageTaxType", + "foreignKey": "sageTaxTypeFk" + }, + "sageTransactionType": { + "type": "belongsTo", + "model": "SageTransactionType", + "foreignKey": "sageTransactionTypeFk" + }, + "sageWithholding": { + "type": "belongsTo", + "model": "SageWithholding", + "foreignKey": "sageWithholdingFk" + } }, "acls": [ { diff --git a/modules/supplier/front/basic-data/index.html b/modules/supplier/front/basic-data/index.html index 03622cf38..a39f07da2 100644 --- a/modules/supplier/front/basic-data/index.html +++ b/modules/supplier/front/basic-data/index.html @@ -18,8 +18,8 @@ + label="Verified" + ng-model="$ctrl.supplier.isSerious"> diff --git a/modules/supplier/front/descriptor/index.html b/modules/supplier/front/descriptor/index.html index 5e7225e1c..c98a8e001 100644 --- a/modules/supplier/front/descriptor/index.html +++ b/modules/supplier/front/descriptor/index.html @@ -29,9 +29,9 @@ ng-class="{bright: $ctrl.supplier.isActive == false}"> + vn-tooltip="Verified supplier" + icon="verified_user" + ng-class="{bright: $ctrl.supplier.isSerious == true}">
+ + +
+
-
+
Date: Thu, 19 Nov 2020 10:51:28 +0100 Subject: [PATCH 22/22] hasToInvoice gets enabled with isActive --- modules/client/front/fiscal-data/index.js | 8 ++++++++ modules/client/front/fiscal-data/index.spec.js | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/modules/client/front/fiscal-data/index.js b/modules/client/front/fiscal-data/index.js index 65129d3f8..397bb20e1 100644 --- a/modules/client/front/fiscal-data/index.js +++ b/modules/client/front/fiscal-data/index.js @@ -8,6 +8,14 @@ export default class Controller extends Section { const hasContactData = this.client.email || this.client.phone || this.client.mobile; const hasChangedTaxData = !orgData.isTaxDataChecked && this.client.isTaxDataChecked; + + const shouldInvoice = this.client.isActive && !this.client.hasToInvoice; + const clientActivation = this.client.isActive && (orgData.isActive != this.client.isActive); + if (shouldInvoice && clientActivation) { + this.client.hasToInvoice = true; + this.vnApp.showMessage(this.$t('Client invoices enabled')); + } + if (hasChangedTaxData && hasContactData) this.checkExistingClient(); else this.save(); diff --git a/modules/client/front/fiscal-data/index.spec.js b/modules/client/front/fiscal-data/index.spec.js index e028d01f3..17e730914 100644 --- a/modules/client/front/fiscal-data/index.spec.js +++ b/modules/client/front/fiscal-data/index.spec.js @@ -50,6 +50,16 @@ describe('Client', () => { expect(controller.save).not.toHaveBeenCalledWith(); expect(controller.checkExistingClient).toHaveBeenCalledWith(); }); + + it('should enable the hasToInvoice property any time the form activates the client with isActive', () => { + $scope.watcher.orgData.isActive = false; + controller.client.isActive = true; + controller.client.hasToInvoice = false; + + controller.onSubmit(); + + expect(controller.client.hasToInvoice).toBe(true); + }); }); describe('checkExistingClient()', () => {