From 1ba1d27ba0c6c58b976cc98430d962023f9b77d7 Mon Sep 17 00:00:00 2001 From: joan Date: Fri, 27 Nov 2020 13:10:39 +0100 Subject: [PATCH] 2576 - Photo upload component --- .gitignore | 10 +- back/methods/dms/updateFile.js | 2 +- back/methods/dms/uploadFile.js | 5 +- back/methods/image/download.js | 44 ++++----- back/methods/image/upload.js | 77 ++++++++++++++++ back/model-config.json | 5 +- back/models/dms.js | 2 +- back/models/image-collection.js | 63 +++++++++++++ back/models/image-collection.json | 7 +- back/models/image-container.json | 13 +++ back/models/image.js | 21 ++--- db/changes/10260-holidays/00-ACL.sql | 1 + .../10260-holidays/00-imageCollection.sql | 8 ++ db/dump/dumpedFixtures.sql | 19 ++++ e2e/dms/a87/4.txt | 1 - e2e/dms/c4c/1.txt | 1 - e2e/dms/c81/2.txt | 1 - e2e/dms/ecc/3.jpeg | Bin 13518 -> 0 bytes e2e/dms/ecc/3.txt | 1 - front/core/directives/zoom-image.js | 2 +- front/salix/components/descriptor/style.scss | 18 ++++ front/salix/components/index.js | 1 + .../salix/components/upload-photo/index.html | 23 +++++ front/salix/components/upload-photo/index.js | 87 ++++++++++++++++++ .../components/upload-photo/locale/es.yml | 2 + .../salix/components/upload-photo/style.scss | 31 +++++++ .../salix/components/user-popover/index.html | 2 +- front/salix/components/user-popover/index.js | 4 - front/salix/module.js | 11 ++- loopback/locale/es.json | 3 +- loopback/server/boot/storage.js | 15 ++- loopback/server/datasources.json | 18 +++- .../methods/claim-dms/allowedContentTypes.js | 2 +- .../methods/client-dms/allowedContentTypes.js | 2 +- modules/entry/front/latest-buys/index.html | 4 +- .../item-image-queue/downloadImages.js | 2 +- modules/item/front/card/index.html | 2 +- modules/item/front/descriptor/index.html | 30 +++--- modules/item/front/descriptor/index.js | 21 ++++- modules/item/front/index/index.html | 6 +- modules/item/front/summary/index.html | 4 +- modules/order/front/catalog-view/index.html | 6 +- modules/order/front/line/index.html | 4 +- .../methods/ticket-dms/allowedContentTypes.js | 2 +- modules/ticket/front/picture/index.html | 4 +- modules/ticket/front/sale/index.html | 4 +- .../travel-thermograph/allowedContentTypes.js | 2 +- .../travel/front/thermograph/create/index.js | 2 +- .../travel/front/thermograph/locale/es.yml | 2 +- .../methods/worker-dms/allowedContentTypes.js | 2 +- modules/worker/front/descriptor/index.html | 20 +++- modules/worker/front/descriptor/index.js | 18 ++++ 52 files changed, 527 insertions(+), 110 deletions(-) create mode 100644 back/methods/image/upload.js create mode 100644 back/models/image-collection.js create mode 100644 back/models/image-container.json create mode 100644 db/changes/10260-holidays/00-ACL.sql create mode 100644 db/changes/10260-holidays/00-imageCollection.sql delete mode 100644 e2e/dms/a87/4.txt delete mode 100644 e2e/dms/c4c/1.txt delete mode 100644 e2e/dms/c81/2.txt delete mode 100644 e2e/dms/ecc/3.jpeg delete mode 100644 e2e/dms/ecc/3.txt create mode 100644 front/salix/components/upload-photo/index.html create mode 100644 front/salix/components/upload-photo/index.js create mode 100644 front/salix/components/upload-photo/locale/es.yml create mode 100644 front/salix/components/upload-photo/style.scss diff --git a/.gitignore b/.gitignore index 35172e5d2..b7064cdbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ coverage node_modules dist -e2e/dms/*/ -!e2e/dms/c4c -!e2e/dms/c81 -!e2e/dms/ecc -!e2e/dms/a87 +storage +!storage/dms/c4c +!storage/dms/c81 +!storage/dms/ecc +!storage/dms/a87 npm-debug.log .eslintcache datasources.*.json diff --git a/back/methods/dms/updateFile.js b/back/methods/dms/updateFile.js index 7585dd1b0..9f8f4f293 100644 --- a/back/methods/dms/updateFile.js +++ b/back/methods/dms/updateFile.js @@ -84,7 +84,7 @@ module.exports = Self => { }; async function uploadNewFile(ctx, dms, myOptions) { - const storageConnector = Self.app.dataSources.storage.connector; + const storageConnector = Self.app.dataSources.dmsStorage.connector; const models = Self.app.models; const fileOptions = {}; diff --git a/back/methods/dms/uploadFile.js b/back/methods/dms/uploadFile.js index 27e5169c9..8e5c4eb63 100644 --- a/back/methods/dms/uploadFile.js +++ b/back/methods/dms/uploadFile.js @@ -46,7 +46,7 @@ module.exports = Self => { }); Self.uploadFile = async(ctx, options) => { - const storageConnector = Self.app.dataSources.storage.connector; + const storageConnector = Self.app.dataSources.dmsStorage.connector; const models = Self.app.models; const fileOptions = {}; const args = ctx.args; @@ -98,7 +98,7 @@ module.exports = Self => { async function createDms(ctx, file, myOptions) { const models = Self.app.models; - const storageConnector = Self.app.dataSources.storage.connector; + const storageConnector = Self.app.dataSources.dmsStorage.connector; const myUserId = ctx.req.accessToken.userId; const myWorker = await models.Worker.findOne({where: {userFk: myUserId}}, myOptions); const args = ctx.args; @@ -121,7 +121,6 @@ module.exports = Self => { return newDms.updateAttribute('file', fileName, myOptions); } - /** * Returns a container instance * If doesn't exists creates a new one diff --git a/back/methods/image/download.js b/back/methods/image/download.js index 6f1e0b8e7..ba709aff8 100644 --- a/back/methods/image/download.js +++ b/back/methods/image/download.js @@ -2,7 +2,7 @@ const UserError = require('vn-loopback/util/user-error'); const fs = require('fs-extra'); module.exports = Self => { - Self.remoteMethod('download', { + Self.remoteMethodCtx('download', { description: 'Get the user image', accessType: 'READ', accepts: [ @@ -49,15 +49,9 @@ module.exports = Self => { } }); - Self.download = async function(collection, size, id) { + Self.download = async function(ctx, collection, size, id) { const models = Self.app.models; - const filter = { - where: { - name: collection}, - include: { - relation: 'readRole' - } - }; + const filter = {where: {name: collection}}; const imageCollection = await models.ImageCollection.findOne(filter); const entity = await models[imageCollection.model].findById(id, { fields: ['id', imageCollection.property] @@ -69,28 +63,22 @@ module.exports = Self => { if (!image) return false; - const imageRole = imageCollection.readRole().name; - const hasRole = await models.Account.hasRole(id, imageRole); - if (!hasRole) + const hasReadRole = models.ImageCollection.hasReadRole(ctx, collection); + if (!hasReadRole) 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}/${image.name}.png`, - contentType: 'image/png', - name: `${image.name}.png` - }; - } else { - file = { - path: `${process.cwd()}/storage/image/${collection}/${size}/${image.name}.png`, - contentType: 'image/png', - name: `${image.name}.png` - }; - } + const container = await models.ImageContainer.getContainer(collection); + const rootPath = container.client.root; + const file = { + path: `${rootPath}/${collection}/${size}/${image.name}.png`, + contentType: 'image/png', + name: `${image.name}.png` + }; + + if (!fs.existsSync(file.path)) return []; + await fs.access(file.path); - let stream = fs.createReadStream(file.path); + const stream = fs.createReadStream(file.path); return [stream, file.contentType, `filename="${file.name}"`]; }; }; diff --git a/back/methods/image/upload.js b/back/methods/image/upload.js new file mode 100644 index 000000000..b50a55b34 --- /dev/null +++ b/back/methods/image/upload.js @@ -0,0 +1,77 @@ +const UserError = require('vn-loopback/util/user-error'); +const fs = require('fs-extra'); + +module.exports = Self => { + Self.remoteMethodCtx('upload', { + description: 'Uploads a file and inserts into dms model', + accessType: 'WRITE', + accepts: [ + { + arg: 'id', + type: 'Number', + description: 'The entity id', + required: true + }, + { + arg: 'collection', + type: 'string', + description: 'The collection name', + required: true + }], + returns: { + type: 'Object', + root: true + }, + http: { + path: `/upload`, + verb: 'POST' + } + }); + + Self.upload = async ctx => { + const models = Self.app.models; + const fileOptions = {}; + const args = ctx.args; + + const hasWriteRole = await models.ImageCollection.hasWriteRole(ctx, args.collection); + if (!hasWriteRole) + throw new UserError(`You don't have enough privileges`); + + if (process.env.NODE_ENV == 'test') + throw new UserError(`You can't upload images on the test instance`); + + // Upload file to temporary path + const container = await getContainer(args.collection); + const uploaded = await models.ImageContainer.upload(container.name, ctx.req, ctx.result, fileOptions); + const [uploadedFile] = Object.values(uploaded.files).map(file => { + return file[0]; + }); + + const file = await models.ImageContainer.getFile(container.name, uploadedFile.name); + const srcFile = `${file.client.root}/${file.container}/${file.name}`; + await models.Image.registerImage(container.name, srcFile, args.id); + }; + + /** + * Returns a container instance + * If doesn't exists creates a new one + * + * @param {String} name Container name + * @return {Object} Container instance + */ + async function getContainer(name) { + const models = Self.app.models; + let container; + try { + container = await models.ImageContainer.getContainer(name); + } catch (err) { + if (err.code === 'ENOENT') { + container = await models.ImageContainer.createContainer({ + name: name + }); + } else throw err; + } + + return container; + } +}; diff --git a/back/model-config.json b/back/model-config.json index adb67eaed..3db249f0f 100644 --- a/back/model-config.json +++ b/back/model-config.json @@ -18,7 +18,7 @@ "dataSource": "vn" }, "Container": { - "dataSource": "storage" + "dataSource": "dmsStorage" }, "Continent": { "dataSource": "vn" @@ -44,6 +44,9 @@ "ImageCollectionSize": { "dataSource": "vn" }, + "ImageContainer": { + "dataSource": "imageStorage" + }, "Language": { "dataSource": "vn" }, diff --git a/back/models/dms.js b/back/models/dms.js index 8be539a0f..9e767904e 100644 --- a/back/models/dms.js +++ b/back/models/dms.js @@ -14,7 +14,7 @@ module.exports = Self => { }; Self.getFile = async function(id) { - const storageConnector = Self.app.dataSources.storage.connector; + const storageConnector = Self.app.dataSources.dmsStorage.connector; const models = Self.app.models; const dms = await Self.findById(id); const pathHash = storageConnector.getPathHash(dms.id); diff --git a/back/models/image-collection.js b/back/models/image-collection.js new file mode 100644 index 000000000..2b541dcd6 --- /dev/null +++ b/back/models/image-collection.js @@ -0,0 +1,63 @@ +module.exports = Self => { + /** + * Checks if current user has + * read privileges over a collection + * + * @param {object} ctx - Request context + * @param {interger} name - Collection name + * @param {object} options - Query options + * @return {boolean} True for user with read privileges + */ + Self.hasReadRole = async(ctx, name, options) => { + const collection = await Self.findOne({where: {name}}, { + include: { + relation: 'readRole' + } + }, options); + + return await hasRole(ctx, collection, options); + }; + + /** + * Checks if current user has + * write privileges over a collection + * + * @param {object} ctx - Request context + * @param {string} name - Collection name + * @param {object} options - Query options + * @return {boolean} True for user with write privileges + */ + Self.hasWriteRole = async(ctx, name, options) => { + const collection = await Self.findOne({where: {name}}, { + include: { + relation: 'writeRole' + } + }, options); + + return await hasRole(ctx, collection, options); + }; + + /** + * Checks if current user has + * read or write privileges + * @param {Object} ctx - Context + * @param {Object} collection - Collection [read/write] + * @param {Object} options - Query options + */ + async function hasRole(ctx, collection, options) { + const models = Self.app.models; + const myUserId = ctx.req.accessToken.userId; + + const readRole = collection.readRole() && collection.readRole().name; + const writeRole = collection.writeRole() && collection.writeRole().name; + const requiredRole = readRole || writeRole; + + const hasRequiredRole = await models.Account.hasRole(myUserId, requiredRole, options); + const isRoot = await models.Account.hasRole(myUserId, 'root', options); + + if (isRoot || hasRequiredRole) + return true; + + return false; + } +}; diff --git a/back/models/image-collection.json b/back/models/image-collection.json index 75faaf722..fd019ecc3 100644 --- a/back/models/image-collection.json +++ b/back/models/image-collection.json @@ -48,7 +48,12 @@ "type": "belongsTo", "model": "Role", "foreignKey": "readRoleFk" - } + }, + "writeRole": { + "type": "belongsTo", + "model": "Role", + "foreignKey": "writeRoleFk" + } }, "acls": [ { diff --git a/back/models/image-container.json b/back/models/image-container.json new file mode 100644 index 000000000..22cea05f5 --- /dev/null +++ b/back/models/image-container.json @@ -0,0 +1,13 @@ +{ + "name": "ImageContainer", + "base": "VnModel", + "idInjection": true, + "options": { + "validateUpsert": true + }, + "properties": {}, + "validations": [], + "relations": {}, + "acls": [], + "methods": [] + } \ No newline at end of file diff --git a/back/models/image.js b/back/models/image.js index f6f4cf5db..cbe2cfdac 100644 --- a/back/models/image.js +++ b/back/models/image.js @@ -4,12 +4,9 @@ const path = require('path'); module.exports = Self => { require('../methods/image/download')(Self); + require('../methods/image/upload')(Self); - Self.getPath = function() { - return '/var/lib/salix/image'; - }; - - Self.registerImage = async(collectionName, file, srcFilePath) => { + Self.registerImage = async(collectionName, srcFile, entityId) => { const models = Self.app.models; const tx = await Self.beginTransaction({}); const myOptions = {transaction: tx}; @@ -33,8 +30,8 @@ module.exports = Self => { } }, myOptions); + const file = srcFile.split('/').pop(); const fileName = file.split('.')[0]; - const rootPath = Self.getPath(); const data = { name: fileName, collectionFk: collectionName @@ -47,6 +44,8 @@ module.exports = Self => { }, myOptions); // Resizes and saves the image + const container = await models.ImageContainer.getContainer(collectionName); + const rootPath = container.client.root; const collectionDir = path.join(rootPath, collectionName); const dstDir = path.join(collectionDir, 'full'); const dstFile = path.join(dstDir, file); @@ -57,7 +56,7 @@ module.exports = Self => { }; await fs.mkdir(dstDir, {recursive: true}); - await sharp(srcFilePath, {failOnError: false}) + await sharp(srcFile, {failOnError: false}) .resize(collection.maxWidth, collection.maxHeight, resizeOpts) .png() .toFile(dstFile); @@ -72,7 +71,7 @@ module.exports = Self => { }; await fs.mkdir(dstDir, {recursive: true}); - await sharp(srcFilePath, {failOnError: false}) + await sharp(srcFile, {failOnError: false}) .resize(size.width, size.height, resizeOpts) .png() .toFile(dstFile); @@ -83,7 +82,7 @@ module.exports = Self => { if (!model) throw new Error('Matching model not found'); - const item = await model.findById(fileName, null, myOptions); + const item = await model.findById(entityId, null, myOptions); if (item) { await item.updateAttribute( collection.property, @@ -92,8 +91,8 @@ module.exports = Self => { ); } - if (fs.existsSync(srcFilePath)) - await fs.unlink(srcFilePath); + if (fs.existsSync(srcFile)) + await fs.unlink(srcFile); await tx.commit(); return newImage; diff --git a/db/changes/10260-holidays/00-ACL.sql b/db/changes/10260-holidays/00-ACL.sql new file mode 100644 index 000000000..1f8045bc7 --- /dev/null +++ b/db/changes/10260-holidays/00-ACL.sql @@ -0,0 +1 @@ +INSERT INTO salix.ACL (model, property, accessType, permission, principalType, principalId) VALUES ('Image', '*', 'WRITE', 'ALLOW', 'ROLE', 'employee') \ No newline at end of file diff --git a/db/changes/10260-holidays/00-imageCollection.sql b/db/changes/10260-holidays/00-imageCollection.sql new file mode 100644 index 000000000..28ffdff9c --- /dev/null +++ b/db/changes/10260-holidays/00-imageCollection.sql @@ -0,0 +1,8 @@ +ALTER TABLE `hedera`.`imageCollection` + ADD writeRoleFk INT NULL DEFAULT 1; + +ALTER TABLE `hedera`.`imageCollection` + ADD CONSTRAINT role_id___fk + FOREIGN KEY (writeRoleFk) REFERENCES account.role (id) + ON UPDATE CASCADE; + diff --git a/db/dump/dumpedFixtures.sql b/db/dump/dumpedFixtures.sql index 879b1eb42..abc6bb8c1 100644 --- a/db/dump/dumpedFixtures.sql +++ b/db/dump/dumpedFixtures.sql @@ -447,6 +447,25 @@ INSERT INTO `imageCollection` VALUES (1,'catalog','Artículo',3840,2160,'Item',' /*!40000 ALTER TABLE `imageCollection` ENABLE KEYS */; UNLOCK TABLES; +-- +-- Dumping data for table `imageCollectionSize` +-- +LOCK TABLES `imageCollectionSize` WRITE; +/*!40000 ALTER TABLE `imageCollectionSize` DISABLE KEYS */; +INSERT INTO `imageCollectionSize` (`id`, `collectionFk`, `width`, `height`, `crop`) + VALUES + (2, 1, 50, 50, 1), + (3, 1, 200, 200, 1), + (5, 5, 200, 200, 1), + (6, 1, 70, 70, 1), + (8, 5, 50, 50, 1), + (9, 1, 1600, 900, 0), + (13, 6, 160, 160, 1), + (14, 6, 520, 520, 1), + (15, 6, 1600, 1600, 1); +/*!40000 ALTER TABLE `imageCollectionSize` ENABLE KEYS */; +UNLOCK TABLES; + -- -- Dumping data for table `tpvError` -- diff --git a/e2e/dms/a87/4.txt b/e2e/dms/a87/4.txt deleted file mode 100644 index a46bfbeda..000000000 --- a/e2e/dms/a87/4.txt +++ /dev/null @@ -1 +0,0 @@ -File: 4.txt. It works! \ No newline at end of file diff --git a/e2e/dms/c4c/1.txt b/e2e/dms/c4c/1.txt deleted file mode 100644 index 5ef648a88..000000000 --- a/e2e/dms/c4c/1.txt +++ /dev/null @@ -1 +0,0 @@ -It works! \ No newline at end of file diff --git a/e2e/dms/c81/2.txt b/e2e/dms/c81/2.txt deleted file mode 100644 index 5ef648a88..000000000 --- a/e2e/dms/c81/2.txt +++ /dev/null @@ -1 +0,0 @@ -It works! \ No newline at end of file diff --git a/e2e/dms/ecc/3.jpeg b/e2e/dms/ecc/3.jpeg deleted file mode 100644 index fb2483f69669a8f3ac8238a0ec0e135b074609c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13518 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 diff --git a/e2e/dms/ecc/3.txt b/e2e/dms/ecc/3.txt deleted file mode 100644 index 5ef648a88..000000000 --- a/e2e/dms/ecc/3.txt +++ /dev/null @@ -1 +0,0 @@ -It works! \ No newline at end of file diff --git a/front/core/directives/zoom-image.js b/front/core/directives/zoom-image.js index 607dbe337..a5d57bd14 100644 --- a/front/core/directives/zoom-image.js +++ b/front/core/directives/zoom-image.js @@ -46,7 +46,7 @@ export function directive($timeout) { $element.on('click', function(event) { if (event.defaultPrevented) return; - let src = $attrs.zoomImage || $attrs.src; + let src = $element[0].getAttribute('zoom-image') || $element[0].src; if (src) createContainers(src); else diff --git a/front/salix/components/descriptor/style.scss b/front/salix/components/descriptor/style.scss index 814df2ca5..bad94a934 100644 --- a/front/salix/components/descriptor/style.scss +++ b/front/salix/components/descriptor/style.scss @@ -5,6 +5,24 @@ vn-descriptor-content { display: block; + .photo { + position: relative; + + & > img[ng-src] { + min-height: 16em; + display: block; + height: 100%; + width: 100%; + } + + vn-float-button { + position: absolute; + margin: 1em; + bottom: 0; + right: 0 + } + } + & > vn-spinner { display: block; height: 40px; diff --git a/front/salix/components/index.js b/front/salix/components/index.js index 1586272c0..13f8366cd 100644 --- a/front/salix/components/index.js +++ b/front/salix/components/index.js @@ -13,3 +13,4 @@ import './section'; import './summary'; import './topbar/topbar'; import './user-popover'; +import './upload-photo'; diff --git a/front/salix/components/upload-photo/index.html b/front/salix/components/upload-photo/index.html new file mode 100644 index 000000000..fc5b6c735 --- /dev/null +++ b/front/salix/components/upload-photo/index.html @@ -0,0 +1,23 @@ + + + +
+
+ + + + +
+ + + + +
\ No newline at end of file diff --git a/front/salix/components/upload-photo/index.js b/front/salix/components/upload-photo/index.js new file mode 100644 index 000000000..aa3c1a22a --- /dev/null +++ b/front/salix/components/upload-photo/index.js @@ -0,0 +1,87 @@ +import ngModule from '../../module'; +import Component from 'core/lib/component'; +import './style.scss'; + +/** + * Small card with basing entity information and actions. + */ +export default class UploadPhoto extends Component { + /** + * Opens the dialog and sets the default data + * @param {*} collection - Collection name + * @param {*} id - Entity id + */ + show(collection, id) { + this.newPhoto = {id, collection}; + this.$.dialog.show(); + } + + /** + * Updates the image preview + * + * @param {string} value + */ + updatePhotoPreview(value) { + if (value && value[0]) { + const reader = new FileReader(); + reader.onload = e => this.$.photo.src = e.target.result; + reader.readAsDataURL(value[0]); + } + } + + /** + * Dialog response handler + * + * @return {boolean} Response + */ + onUploadAccept() { + try { + if (!this.newPhoto.files) + throw new Error(`Select an image`); + + this.makeRequest(); + } catch (e) { + this.vnApp.showError(this.$t(e.message)); + return false; + } + return true; + } + + /** + * Performs a cancellable request. + * + */ + makeRequest() { + if (this.canceler) this.canceler.resolve(); + this.canceler = this.$q.defer(); + + const options = { + method: 'POST', + url: `Images/upload`, + params: this.newPhoto, + headers: {'Content-Type': undefined}, + timeout: this.canceler.promise, + transformRequest: files => { + const formData = new FormData(); + for (let i = 0; i < files.length; i++) + formData.append(files[i].name, files[i]); + + return formData; + }, + data: this.newPhoto.files + }; + + this.$http(options) + .then(() => this.vnApp.showSuccess(this.$t('Data saved!'))) + .then(() => this.emit('response')) + .finally(() => this.canceler = null); + } +} + +ngModule.vnComponent('vnUploadPhoto', { + controller: UploadPhoto, + template: require('./index.html'), + bindings: { + data: '<' + } +}); diff --git a/front/salix/components/upload-photo/locale/es.yml b/front/salix/components/upload-photo/locale/es.yml new file mode 100644 index 000000000..10271cf92 --- /dev/null +++ b/front/salix/components/upload-photo/locale/es.yml @@ -0,0 +1,2 @@ +Upload new photo: Subir una nueva foto +Select an image: Selecciona una imagen \ No newline at end of file diff --git a/front/salix/components/upload-photo/style.scss b/front/salix/components/upload-photo/style.scss new file mode 100644 index 000000000..cdf35341f --- /dev/null +++ b/front/salix/components/upload-photo/style.scss @@ -0,0 +1,31 @@ +@import "./variables"; + +.upload-photo { + .photo { + position: relative; + margin: 0 auto; + text-align: center; + + & > div { + border: 3px solid $color-primary; + max-width: 256px; + max-height: 256px; + border-radius: 50%; + overflow: hidden + } + + & > div > img[ng-src] { + width: 256px; + height: 256px; + display: block; + height: 100%; + width: 100%; + } + } + + & > vn-spinner { + display: block; + height: 40px; + padding: $spacing-md; + } +} diff --git a/front/salix/components/user-popover/index.html b/front/salix/components/user-popover/index.html index 9bd0f1411..22d86f1aa 100644 --- a/front/salix/components/user-popover/index.html +++ b/front/salix/components/user-popover/index.html @@ -14,7 +14,7 @@
diff --git a/front/salix/components/user-popover/index.js b/front/salix/components/user-popover/index.js index e4d7b4466..0d35d8995 100644 --- a/front/salix/components/user-popover/index.js +++ b/front/salix/components/user-popover/index.js @@ -78,10 +78,6 @@ 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', 'vnToken']; diff --git a/front/salix/module.js b/front/salix/module.js index 2c61af4d1..a8de61ae0 100644 --- a/front/salix/module.js +++ b/front/salix/module.js @@ -7,9 +7,14 @@ export const appName = 'salix'; const ngModule = ng.module('salix', ['vnCore']); export default ngModule; -run.$inject = ['$window', '$rootScope', 'vnAuth', 'vnApp', '$state']; -export function run($window, $rootScope, vnAuth, vnApp, $state) { - $rootScope.imagePath = appConfig.imagePath; +run.$inject = ['$window', '$rootScope', 'vnAuth', 'vnApp', 'vnToken', '$state']; +export function run($window, $rootScope, vnAuth, vnApp, vnToken, $state) { + $rootScope.imagePath = (collection, size, id) => { + if (!collection || !size || !id) return; + + const basePath = `/api/Images/${collection}/${size}/${id}`; + return `${basePath}/download?access_token=${vnToken.token}`; + }; $window.validations = {}; vnApp.name = appName; diff --git a/loopback/locale/es.json b/loopback/locale/es.json index f75b1778e..0080228ae 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -160,5 +160,6 @@ "The social name cannot be empty": "La razón social no puede quedar en blanco", "The nif cannot be empty": "El NIF no puede quedar en blanco", "You need to fill sage information before you check verified data": "Debes rellenar la información de sage antes de marcar datos comprobados", - "ASSIGN_ZONE_FIRST": "Asigna una zona primero" + "ASSIGN_ZONE_FIRST": "Asigna una zona primero", + "You can't upload images on the test environment": "No puedes subir imágenes en el entorno de pruebas" } \ No newline at end of file diff --git a/loopback/server/boot/storage.js b/loopback/server/boot/storage.js index 12662ab73..7559a7dec 100644 --- a/loopback/server/boot/storage.js +++ b/loopback/server/boot/storage.js @@ -2,7 +2,7 @@ const uuid = require('uuid/v1'); const md5 = require('md5'); module.exports = app => { - const storageConnector = app.dataSources.storage.connector; + const storageConnector = app.dataSources.dmsStorage.connector; storageConnector.getFilename = function(file) { return `${uuid()}.${storageConnector.getFileExtension(file.name)}`; @@ -15,4 +15,17 @@ module.exports = app => { storageConnector.getPathHash = function(id) { return md5(id.toString()).substring(0, 3); }; + + const imageStorageConnector = app.dataSources.imageStorage.connector; + imageStorageConnector.getFilename = function(file) { + return `${uuid()}.png`; + }; + + /* imageStorageConnector.getFileExtension = function(fileName) { + return fileName.split('.').pop().toLowerCase(); + }; + + imageStorageConnector.getPathHash = function(id) { + return md5(id.toString()).substring(0, 3); + }; */ }; diff --git a/loopback/server/datasources.json b/loopback/server/datasources.json index 0ea634484..793dcaea8 100644 --- a/loopback/server/datasources.json +++ b/loopback/server/datasources.json @@ -17,11 +17,11 @@ "connectTimeout": 40000, "acquireTimeout": 20000 }, - "storage": { - "name": "storage", + "dmsStorage": { + "name": "dmsStorage", "connector": "loopback-component-storage", "provider": "filesystem", - "root": "./e2e/dms", + "root": "./storage/dms", "maxFileSize": "262144000", "allowedContentTypes": [ "application/x-7z-compressed", @@ -36,5 +36,17 @@ "image/jpeg", "image/jpg" ] + }, + "imageStorage": { + "name": "imageStorage", + "connector": "loopback-component-storage", + "provider": "filesystem", + "root": "./storage/image", + "maxFileSize": "52428800", + "allowedContentTypes": [ + "image/png", + "image/jpeg", + "image/jpg" + ] } } diff --git a/modules/claim/back/methods/claim-dms/allowedContentTypes.js b/modules/claim/back/methods/claim-dms/allowedContentTypes.js index 2f5183f92..3d4b90876 100644 --- a/modules/claim/back/methods/claim-dms/allowedContentTypes.js +++ b/modules/claim/back/methods/claim-dms/allowedContentTypes.js @@ -13,7 +13,7 @@ module.exports = Self => { }); Self.allowedContentTypes = async() => { - const storageConnector = Self.app.dataSources.storage.connector; + const storageConnector = Self.app.dataSources.dmsStorage.connector; const allowedContentTypes = storageConnector.allowedContentTypes; const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes; diff --git a/modules/client/back/methods/client-dms/allowedContentTypes.js b/modules/client/back/methods/client-dms/allowedContentTypes.js index 2f5183f92..3d4b90876 100644 --- a/modules/client/back/methods/client-dms/allowedContentTypes.js +++ b/modules/client/back/methods/client-dms/allowedContentTypes.js @@ -13,7 +13,7 @@ module.exports = Self => { }); Self.allowedContentTypes = async() => { - const storageConnector = Self.app.dataSources.storage.connector; + const storageConnector = Self.app.dataSources.dmsStorage.connector; const allowedContentTypes = storageConnector.allowedContentTypes; const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes; diff --git a/modules/entry/front/latest-buys/index.html b/modules/entry/front/latest-buys/index.html index 4e0c6ded8..6ab675d76 100644 --- a/modules/entry/front/latest-buys/index.html +++ b/modules/entry/front/latest-buys/index.html @@ -70,8 +70,8 @@ diff --git a/modules/item/back/methods/item-image-queue/downloadImages.js b/modules/item/back/methods/item-image-queue/downloadImages.js index d953d1938..ec9177505 100644 --- a/modules/item/back/methods/item-image-queue/downloadImages.js +++ b/modules/item/back/methods/item-image-queue/downloadImages.js @@ -57,7 +57,7 @@ module.exports = Self => { writeStream.on('finish', async function() { try { - await models.Image.registerImage('catalog', fileName, filePath); + await models.Image.registerImage('catalog', filePath, image.itemFk); await image.destroy(); } catch (error) { await errorHandler(image.itemFk, error, filePath); diff --git a/modules/item/front/card/index.html b/modules/item/front/card/index.html index f547a9e7a..b7513a42a 100644 --- a/modules/item/front/card/index.html +++ b/modules/item/front/card/index.html @@ -1,5 +1,5 @@ - + diff --git a/modules/item/front/descriptor/index.html b/modules/item/front/descriptor/index.html index 8363a652f..168325a10 100644 --- a/modules/item/front/descriptor/index.html +++ b/modules/item/front/descriptor/index.html @@ -16,19 +16,15 @@ -
- - - - - +
+ + +
@@ -102,4 +98,10 @@ - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/modules/item/front/descriptor/index.js b/modules/item/front/descriptor/index.js index 2791e960a..195a97d13 100644 --- a/modules/item/front/descriptor/index.js +++ b/modules/item/front/descriptor/index.js @@ -3,6 +3,11 @@ import Descriptor from 'salix/components/descriptor'; import './style.scss'; class Controller extends Descriptor { + constructor($element, $, $rootScope) { + super($element, $); + this.$rootScope = $rootScope; + } + get item() { return this.entity; } @@ -65,13 +70,27 @@ class Controller extends Descriptor { this.$http.post(`Items/${this.item.id}/clone`) .then(res => this.$state.go('item.card.tags', {id: res.data.id})); } + + onUploadResponse() { + const timestamp = new Date().getTime(); + const src = this.$rootScope.imagePath('catalog', '200x200', this.item.id); + const zoomSrc = this.$rootScope.imagePath('catalog', '1600x900', this.item.id); + const newSrc = `${src}&t=${timestamp}`; + const newZoomSrc = `${zoomSrc}&t=${timestamp}`; + + this.$.photo.setAttribute('src', newSrc); + this.$.photo.setAttribute('zoom-image', newZoomSrc); + } } +Controller.$inject = ['$element', '$scope', '$rootScope']; + ngModule.vnComponent('vnItemDescriptor', { template: require('./index.html'), controller: Controller, bindings: { item: '<', - dated: '<' + dated: '<', + cardReload: '&' } }); diff --git a/modules/item/front/index/index.html b/modules/item/front/index/index.html index 0d4ae61f3..8b03bc0d6 100644 --- a/modules/item/front/index/index.html +++ b/modules/item/front/index/index.html @@ -35,8 +35,8 @@ ui-sref="item.card.summary({id: item.id})"> @@ -44,7 +44,7 @@ - {{::item.id | zeroFill:6}} + {{::item.id}} {{::item.grouping | dashIfEmpty}} diff --git a/modules/item/front/summary/index.html b/modules/item/front/summary/index.html index e9c835dac..5fb556bd4 100644 --- a/modules/item/front/summary/index.html +++ b/modules/item/front/summary/index.html @@ -11,8 +11,8 @@ + ng-src="{{$root.imagePath('catalog', '200x200', $ctrl.item.id)}}" + zoom-image="{{$root.imagePath('catalog', '1600x900', $ctrl.item.id)}}" on-error-src/>

Visible

diff --git a/modules/order/front/catalog-view/index.html b/modules/order/front/catalog-view/index.html index 2d7492d87..25d84db75 100644 --- a/modules/order/front/catalog-view/index.html +++ b/modules/order/front/catalog-view/index.html @@ -5,11 +5,11 @@
-
+
diff --git a/modules/order/front/line/index.html b/modules/order/front/line/index.html index 1ba6e3f9c..51702e16e 100644 --- a/modules/order/front/line/index.html +++ b/modules/order/front/line/index.html @@ -32,8 +32,8 @@ diff --git a/modules/ticket/back/methods/ticket-dms/allowedContentTypes.js b/modules/ticket/back/methods/ticket-dms/allowedContentTypes.js index 2f5183f92..3d4b90876 100644 --- a/modules/ticket/back/methods/ticket-dms/allowedContentTypes.js +++ b/modules/ticket/back/methods/ticket-dms/allowedContentTypes.js @@ -13,7 +13,7 @@ module.exports = Self => { }); Self.allowedContentTypes = async() => { - const storageConnector = Self.app.dataSources.storage.connector; + const storageConnector = Self.app.dataSources.dmsStorage.connector; const allowedContentTypes = storageConnector.allowedContentTypes; const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes; diff --git a/modules/ticket/front/picture/index.html b/modules/ticket/front/picture/index.html index 52cab5b31..c95e604dd 100644 --- a/modules/ticket/front/picture/index.html +++ b/modules/ticket/front/picture/index.html @@ -19,8 +19,8 @@
diff --git a/modules/ticket/front/sale/index.html b/modules/ticket/front/sale/index.html index 90cfa39d1..ed3cbc02b 100644 --- a/modules/ticket/front/sale/index.html +++ b/modules/ticket/front/sale/index.html @@ -93,8 +93,8 @@ diff --git a/modules/travel/back/methods/travel-thermograph/allowedContentTypes.js b/modules/travel/back/methods/travel-thermograph/allowedContentTypes.js index 2f5183f92..3d4b90876 100644 --- a/modules/travel/back/methods/travel-thermograph/allowedContentTypes.js +++ b/modules/travel/back/methods/travel-thermograph/allowedContentTypes.js @@ -13,7 +13,7 @@ module.exports = Self => { }); Self.allowedContentTypes = async() => { - const storageConnector = Self.app.dataSources.storage.connector; + const storageConnector = Self.app.dataSources.dmsStorage.connector; const allowedContentTypes = storageConnector.allowedContentTypes; const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes; diff --git a/modules/travel/front/thermograph/create/index.js b/modules/travel/front/thermograph/create/index.js index 4b4cebb9f..df8d0fe68 100644 --- a/modules/travel/front/thermograph/create/index.js +++ b/modules/travel/front/thermograph/create/index.js @@ -46,7 +46,7 @@ class Controller extends Section { warehouseId: warehouseId, companyId: companyId, dmsTypeId: dmsTypeId, - description: this.$t('FileDescription', { + description: this.$t('TravelFileDescription', { travelId: this.travel.id }).toUpperCase() }; diff --git a/modules/travel/front/thermograph/locale/es.yml b/modules/travel/front/thermograph/locale/es.yml index 0e3bc99fc..1fdb98c8e 100644 --- a/modules/travel/front/thermograph/locale/es.yml +++ b/modules/travel/front/thermograph/locale/es.yml @@ -8,7 +8,7 @@ Upload file: Subir fichero Edit file: Editar fichero Upload: Subir File: Fichero -FileDescription: Travel id {{travelId}} +TravelFileDescription: Travel id {{travelId}} ContentTypesInfo: 'Tipos de archivo permitidos: {{allowedContentTypes}}' Are you sure you want to continue?: ¿Seguro que quieres continuar? Add thermograph: Añadir termógrafo diff --git a/modules/worker/back/methods/worker-dms/allowedContentTypes.js b/modules/worker/back/methods/worker-dms/allowedContentTypes.js index 2f5183f92..3d4b90876 100644 --- a/modules/worker/back/methods/worker-dms/allowedContentTypes.js +++ b/modules/worker/back/methods/worker-dms/allowedContentTypes.js @@ -13,7 +13,7 @@ module.exports = Self => { }); Self.allowedContentTypes = async() => { - const storageConnector = Self.app.dataSources.storage.connector; + const storageConnector = Self.app.dataSources.dmsStorage.connector; const allowedContentTypes = storageConnector.allowedContentTypes; const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes; diff --git a/modules/worker/front/descriptor/index.html b/modules/worker/front/descriptor/index.html index fb2264494..bceadff33 100644 --- a/modules/worker/front/descriptor/index.html +++ b/modules/worker/front/descriptor/index.html @@ -1,6 +1,18 @@ + +
+ + + +
+
- \ No newline at end of file + + + + + \ No newline at end of file diff --git a/modules/worker/front/descriptor/index.js b/modules/worker/front/descriptor/index.js index 98f8f2f72..c5dc1ea2c 100644 --- a/modules/worker/front/descriptor/index.js +++ b/modules/worker/front/descriptor/index.js @@ -2,6 +2,11 @@ import ngModule from '../module'; import Descriptor from 'salix/components/descriptor'; class Controller extends Descriptor { + constructor($element, $, $rootScope) { + super($element, $); + this.$rootScope = $rootScope; + } + get worker() { return this.entity; } @@ -48,8 +53,21 @@ class Controller extends Descriptor { return this.getData(`Workers/${this.id}`, {filter}) .then(res => this.entity = res.data); } + + onUploadResponse() { + const timestamp = new Date().getTime(); + const src = this.$rootScope.imagePath('user', '520x520', this.worker.id); + const zoomSrc = this.$rootScope.imagePath('user', '1600x900', this.worker.id); + const newSrc = `${src}&t=${timestamp}`; + const newZoomSrc = `${zoomSrc}&t=${timestamp}`; + + this.$.photo.setAttribute('src', newSrc); + this.$.photo.setAttribute('zoom-image', newZoomSrc); + } } +Controller.$inject = ['$element', '$scope', '$rootScope']; + ngModule.vnComponent('vnWorkerDescriptor', { template: require('./index.html'), controller: Controller,