Merge branch 'dev' into 2517-clientBalanceCompensaciones
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Javi Gallego 2020-12-22 07:42:28 +01:00
commit 36bace2a94
177 changed files with 1162 additions and 387 deletions

6
.gitignore vendored
View File

@ -1,11 +1,7 @@
coverage
node_modules
dist
e2e/dms/*/
!e2e/dms/c4c
!e2e/dms/c81
!e2e/dms/ecc
!e2e/dms/a87
storage
npm-debug.log
.eslintcache
datasources.*.json

View File

@ -1,5 +1,6 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const path = require('path');
module.exports = Self => {
Self.remoteMethodCtx('updateFile', {
@ -84,66 +85,46 @@ module.exports = Self => {
};
async function uploadNewFile(ctx, dms, myOptions) {
const storageConnector = Self.app.dataSources.storage.connector;
const models = Self.app.models;
const TempContainer = models.TempContainer;
const DmsContainer = models.DmsContainer;
const fileOptions = {};
const tempContainer = await getContainer('temp');
const makeUpload = await models.Container.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const tempContainer = await TempContainer.container('dms');
const makeUpload = await TempContainer.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const keys = Object.values(makeUpload.files);
const files = keys.map(file => file[0]);
const file = files[0];
const uploadedFile = files[0];
if (file) {
const oldExtension = storageConnector.getFileExtension(dms.file);
const newExtension = storageConnector.getFileExtension(file.name);
if (uploadedFile) {
const oldExtension = DmsContainer.getFileExtension(dms.file);
const newExtension = DmsContainer.getFileExtension(uploadedFile.name);
const fileName = `${dms.id}.${newExtension}`;
try {
if (oldExtension != newExtension) {
const pathHash = storageConnector.getPathHash(dms.id);
const pathHash = DmsContainer.getHash(dms.id);
await models.Container.removeFile(pathHash, dms.file);
await DmsContainer.removeFile(pathHash, dms.file);
}
} catch (err) {}
const updatedDms = await dms.updateAttributes({
contentType: file.type,
contentType: uploadedFile.type,
file: fileName
}, myOptions);
const pathHash = storageConnector.getPathHash(updatedDms.id);
const container = await getContainer(pathHash);
const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
const srcFile = path.join(file.client.root, file.container, file.name);
const originPath = `${tempContainer.client.root}/${tempContainer.name}/${file.name}`;
const destinationPath = `${container.client.root}/${pathHash}/${updatedDms.file}`;
const pathHash = DmsContainer.getHash(updatedDms.id);
const dmsContainer = await DmsContainer.container(pathHash);
const dstFile = path.join(dmsContainer.client.root, pathHash, updatedDms.file);
fs.rename(originPath, destinationPath);
await fs.move(srcFile, dstFile, {
overwrite: true
});
return updatedDms;
}
}
/**
* 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.Container.getContainer(name);
} catch (err) {
if (err.code === 'ENOENT') {
container = await models.Container.createContainer({
name: name
});
} else throw err;
}
return container;
}
};

View File

@ -1,5 +1,6 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const path = require('path');
module.exports = Self => {
Self.remoteMethodCtx('uploadFile', {
@ -46,8 +47,9 @@ module.exports = Self => {
});
Self.uploadFile = async(ctx, options) => {
const storageConnector = Self.app.dataSources.storage.connector;
const models = Self.app.models;
const TempContainer = models.TempContainer;
const DmsContainer = models.DmsContainer;
const fileOptions = {};
const args = ctx.args;
@ -62,28 +64,33 @@ module.exports = Self => {
myOptions.transaction = tx;
}
let srcFile;
try {
const hasWriteRole = await models.DmsType.hasWriteRole(ctx, args.dmsTypeId, myOptions);
if (!hasWriteRole)
throw new UserError(`You don't have enough privileges`);
// Upload file to temporary path
const tempContainer = await getContainer('temp');
const uploaded = await models.Container.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const tempContainer = await TempContainer.container('dms');
const uploaded = await TempContainer.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const files = Object.values(uploaded.files).map(file => {
return file[0];
});
const addedDms = [];
for (const file of files) {
const newDms = await createDms(ctx, file, myOptions);
const pathHash = storageConnector.getPathHash(newDms.id);
const container = await getContainer(pathHash);
for (const uploadedFile of files) {
const newDms = await createDms(ctx, uploadedFile, myOptions);
const pathHash = DmsContainer.getHash(newDms.id);
const originPath = `${tempContainer.client.root}/${tempContainer.name}/${file.name}`;
const destinationPath = `${container.client.root}/${pathHash}/${newDms.file}`;
const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
srcFile = path.join(file.client.root, file.container, file.name);
await fs.rename(originPath, destinationPath);
const dmsContainer = await DmsContainer.container(pathHash);
const dstFile = path.join(dmsContainer.client.root, pathHash, newDms.file);
await fs.move(srcFile, dstFile, {
overwrite: true
});
addedDms.push(newDms);
}
@ -92,13 +99,16 @@ module.exports = Self => {
return addedDms;
} catch (e) {
if (tx) await tx.rollback();
if (fs.existsSync(srcFile))
await fs.unlink(srcFile);
throw e;
}
};
async function createDms(ctx, file, myOptions) {
const models = Self.app.models;
const storageConnector = Self.app.dataSources.storage.connector;
const myUserId = ctx.req.accessToken.userId;
const myWorker = await models.Worker.findOne({where: {userFk: myUserId}}, myOptions);
const args = ctx.args;
@ -115,33 +125,9 @@ module.exports = Self => {
}, myOptions);
let fileName = file.name;
const extension = storageConnector.getFileExtension(fileName);
const extension = models.DmsContainer.getFileExtension(fileName);
fileName = `${newDms.id}.${extension}`;
return newDms.updateAttribute('file', fileName, myOptions);
}
/**
* 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.Container.getContainer(name);
} catch (err) {
if (err.code === 'ENOENT') {
container = await models.Container.createContainer({
name: name
});
} else throw err;
}
return container;
}
};

View File

@ -1,8 +1,9 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const path = require('path');
module.exports = Self => {
Self.remoteMethod('download', {
Self.remoteMethodCtx('download', {
description: 'Get the user image',
accessType: 'READ',
accepts: [
@ -49,15 +50,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 +64,23 @@ 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 fileSrc = path.join(rootPath, collection, size);
const file = {
path: `${fileSrc}/${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}"`];
};
};

View File

@ -3,10 +3,12 @@ const app = require('vn-loopback/server/server');
describe('image download()', () => {
const collection = 'user';
const size = '160x160';
const employeeId = 1;
const ctx = {req: {accessToken: {userId: employeeId}}};
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 image = await app.models.Image.download(ctx, collection, size, userId);
const contentType = image[1];
expect(contentType).toEqual('image/png');
@ -14,7 +16,7 @@ describe('image download()', () => {
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);
const image = await app.models.Image.download(ctx, collection, size, userId);
expect(image).toBeFalse();
});

View File

@ -0,0 +1,129 @@
const app = require('vn-loopback/server/server');
describe('image upload()', () => {
describe('as buyer', () => {
const buyerId = 35;
const workerId = 106;
const itemId = 4;
it('should try to upload a file for the collection "catalog" and throw a privileges error', async() => {
const ctx = {req: {accessToken: {userId: buyerId}},
args: {
id: workerId,
collection: 'user'
}
};
let error;
try {
await app.models.Image.upload(ctx);
} catch (err) {
error = err;
}
expect(error.message).toEqual(`You don't have enough privileges`);
});
it('should call to the TempContainer upload method for the collection "catalog"', async() => {
const containerModel = app.models.TempContainer;
spyOn(containerModel, 'upload');
const ctx = {req: {accessToken: {userId: buyerId}},
args: {
id: itemId,
collection: 'catalog'
}
};
try {
await app.models.Image.upload(ctx);
} catch (err) { }
expect(containerModel.upload).toHaveBeenCalled();
});
});
describe('as marketing', () => {
const marketingId = 51;
const workerId = 106;
const itemId = 4;
it('should be able to call to the TempContainer upload method for the collection "user"', async() => {
const containerModel = app.models.TempContainer;
spyOn(containerModel, 'upload');
const ctx = {req: {accessToken: {userId: marketingId}},
args: {
id: workerId,
collection: 'user'
}
};
try {
await app.models.Image.upload(ctx);
} catch (err) { }
expect(containerModel.upload).toHaveBeenCalled();
});
it('should be able to call to the TempContainer upload method for the collection "catalog"', async() => {
const containerModel = app.models.TempContainer;
spyOn(containerModel, 'upload');
const ctx = {req: {accessToken: {userId: marketingId}},
args: {
id: itemId,
collection: 'catalog'
}
};
try {
await app.models.Image.upload(ctx);
} catch (err) { }
expect(containerModel.upload).toHaveBeenCalled();
});
});
describe('as hhrr', () => {
const hhrrId = 37;
const workerId = 106;
const itemId = 4;
it('should upload a file for the collection "user" and call to the TempContainer upload method', async() => {
const containerModel = app.models.TempContainer;
spyOn(containerModel, 'upload');
const ctx = {req: {accessToken: {userId: hhrrId}},
args: {
id: itemId,
collection: 'user'
}
};
try {
await app.models.Image.upload(ctx);
} catch (err) { }
expect(containerModel.upload).toHaveBeenCalled();
});
it('should try to upload a file for the collection "catalog" and throw a privilege error', async() => {
const ctx = {req: {accessToken: {userId: hhrrId}},
args: {
id: workerId,
collection: 'catalog'
}
};
let error;
try {
await app.models.Image.upload(ctx);
} catch (err) {
error = err;
}
expect(error.message).toEqual(`You don't have enough privileges`);
});
});
});

View File

@ -0,0 +1,70 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const path = require('path');
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
},
{
arg: 'fileName',
type: 'string',
description: 'The file name',
required: true
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/upload`,
verb: 'POST'
}
});
Self.upload = async ctx => {
const models = Self.app.models;
const TempContainer = models.TempContainer;
const fileOptions = {};
const args = ctx.args;
let srcFile;
try {
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 tempContainer = await TempContainer.container(args.collection);
const uploaded = await TempContainer.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const [uploadedFile] = Object.values(uploaded.files).map(file => {
return file[0];
});
const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
srcFile = path.join(file.client.root, file.container, file.name);
await models.Image.registerImage(args.collection, srcFile, args.fileName, args.id);
} catch (e) {
if (fs.existsSync(srcFile))
await fs.unlink(srcFile);
throw e;
}
};
};

View File

@ -17,9 +17,6 @@
"Company": {
"dataSource": "vn"
},
"Container": {
"dataSource": "storage"
},
"Continent": {
"dataSource": "vn"
},
@ -35,6 +32,9 @@
"Delivery": {
"dataSource": "vn"
},
"DmsContainer": {
"dataSource": "dmsStorage"
},
"Image": {
"dataSource": "vn"
},
@ -44,12 +44,18 @@
"ImageCollectionSize": {
"dataSource": "vn"
},
"ImageContainer": {
"dataSource": "imageStorage"
},
"Language": {
"dataSource": "vn"
},
"Province": {
"dataSource": "vn"
},
"TempContainer": {
"dataSource": "tempStorage"
},
"UserConfig": {
"dataSource": "vn"
},

View File

@ -1,13 +0,0 @@
{
"name": "Container",
"base": "VnModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {},
"acls": [],
"methods": []
}

View File

@ -0,0 +1,10 @@
{
"name": "DmsContainer",
"base": "Container",
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -14,12 +14,12 @@ module.exports = Self => {
};
Self.getFile = async function(id) {
const storageConnector = Self.app.dataSources.storage.connector;
const models = Self.app.models;
const DmsContainer = models.DmsContainer;
const dms = await Self.findById(id);
const pathHash = storageConnector.getPathHash(dms.id);
const pathHash = DmsContainer.getHash(dms.id);
try {
await models.Container.getFile(pathHash, dms.file);
await DmsContainer.getFile(pathHash, dms.file);
} catch (e) {
if (e.code != 'ENOENT')
throw e;
@ -30,7 +30,7 @@ module.exports = Self => {
throw error;
}
const stream = models.Container.downloadStream(pathHash, dms.file);
const stream = DmsContainer.downloadStream(pathHash, dms.file);
return [stream, dms.contentType, `filename="${dms.file}"`];
};

View File

@ -0,0 +1,64 @@
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({
include: {
relation: 'writeRole'
},
where: {name}
}, 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;
}
};

View File

@ -48,7 +48,12 @@
"type": "belongsTo",
"model": "Role",
"foreignKey": "readRoleFk"
}
},
"writeRole": {
"type": "belongsTo",
"model": "Role",
"foreignKey": "writeRoleFk"
}
},
"acls": [
{

View File

@ -0,0 +1,10 @@
{
"name": "ImageContainer",
"base": "Container",
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -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, srcFilePath, fileName, entityId) => {
const models = Self.app.models;
const tx = await Self.beginTransaction({});
const myOptions = {transaction: tx};
@ -33,13 +30,10 @@ module.exports = Self => {
}
}, myOptions);
const fileName = file.split('.')[0];
const rootPath = Self.getPath();
const data = {
name: fileName,
collectionFk: collectionName
};
const newImage = await Self.upsertWithWhere(data, {
name: fileName,
collectionFk: collectionName,
@ -47,7 +41,10 @@ module.exports = Self => {
}, myOptions);
// Resizes and saves the image
const container = await models.ImageContainer.container(collectionName);
const rootPath = container.client.root;
const collectionDir = path.join(rootPath, collectionName);
const file = `${fileName}.png`;
const dstDir = path.join(collectionDir, 'full');
const dstFile = path.join(dstDir, file);
@ -83,7 +80,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,

View File

@ -0,0 +1,10 @@
{
"name": "TempContainer",
"base": "Container",
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -0,0 +1 @@
INSERT INTO salix.ACL (model, property, accessType, permission, principalType, principalId) VALUES ('Image', '*', 'WRITE', 'ALLOW', 'ROLE', 'employee')

View File

@ -0,0 +1,13 @@
INSERT INTO account.role (id, name, description)
VALUES
(74, 'userPhotos', 'Privilegios para subir fotos de usuario'),
(75, 'catalogPhotos', 'Privilegios para subir fotos del catálogo');
INSERT INTO account.roleInherit (role, inheritsFrom)
VALUES
(37, (SELECT id FROM account.role WHERE name = 'userPhotos')),
(51, (SELECT id FROM account.role WHERE name = 'userPhotos')),
(51, (SELECT id FROM account.role WHERE name = 'catalogPhotos')),
(35, (SELECT id FROM account.role WHERE name = 'catalogPhotos'));
CALL account.role_sync();

View File

@ -0,0 +1,27 @@
ALTER TABLE `hedera`.`imageCollection`
ADD writeRoleFk INT UNSIGNED NULL DEFAULT 1;
ALTER TABLE `hedera`.`imageCollection`
ADD CONSTRAINT role_id_writeRoleFk
FOREIGN KEY (writeRoleFk) REFERENCES account.role (id)
ON UPDATE CASCADE;
ALTER TABLE `hedera`.`imageCollection` modify readRoleFk INT UNSIGNED default 1 null;
ALTER TABLE `hedera`.`imageCollection`
ADD CONSTRAINT role_id_readRoleFk
FOREIGN KEY (readRoleFk) REFERENCES account.role (id)
ON UPDATE CASCADE;
UPDATE hedera.imageCollection t SET t.writeRoleFk = (
SELECT id FROM `account`.`role` WHERE name = 'catalogPhotos'
)
WHERE t.name = 'catalog';
UPDATE hedera.imageCollection t SET t.writeRoleFk = (
SELECT id FROM `account`.`role` WHERE name = 'userPhotos'
)
WHERE t.name = 'user';
UPDATE hedera.imageCollection t SET t.writeRoleFk = 9
WHERE t.name IN ('link', 'news');

View File

@ -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`
--

View File

@ -30,7 +30,7 @@ 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`, `image`)
SELECT id, name, CONCAT(name, 'Nick'),MD5('nightmare'), id, 1, CONCAT(name, '@mydomain.com'), 'en', 'e7723f0b24ff05b32ed09d95196f2f29'
SELECT id, name, CONCAT(name, 'Nick'),MD5('nightmare'), id, 1, CONCAT(name, '@mydomain.com'), 'en', '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd'
FROM `account`.`role` WHERE id <> 20
ORDER BY id;
@ -773,23 +773,23 @@ INSERT INTO `vn`.`intrastat`(`id`, `description`, `taxClassFk`, `taxCodeFk`)
INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `isOnOffer`, `expenceFk`, `isBargain`, `comment`, `relevancy`, `image`, `taxClassFk`, `subName`, `minPrice`)
VALUES
(1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 67, 1, NULL, 0),
(2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 66, 1, NULL, 0),
(3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, 65, 1, NULL, 0),
(4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 1, 4751000000, 0, NULL, 0, 69, 2, NULL, 0),
(5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, 74, 2, NULL, 0),
(6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 62, 2, NULL, 0),
(7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 64, 2, NULL, 0),
(8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 75, 1, NULL, 0),
(9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 76, 1, NULL, 0),
(10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, 77, 1, NULL, 0),
(11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 1, 4751000000, 0, NULL, 0, 78, 2, NULL, 0),
(12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, 82, 2, NULL, 0),
(13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 83, 2, NULL, 0),
(14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 84, 2, NULL, 0),
(15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 88, 2, NULL, 0),
(16, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 88, 2, NULL, 0),
(71, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 1, 4751000000, 0, NULL, 0, 88, 2, NULL, 0);
(1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '1', 1, NULL, 0),
(2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '2', 1, NULL, 0),
(3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, '3', 1, NULL, 0),
(4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 1, 4751000000, 0, NULL, 0, '4', 2, NULL, 0),
(5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, '5', 2, NULL, 0),
(6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '6', 2, NULL, 0),
(7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '7', 2, NULL, 0),
(8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '8', 1, NULL, 0),
(9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '9', 1, NULL, 0),
(10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, '10', 1, NULL, 0),
(11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 1, 4751000000, 0, NULL, 0, '11', 2, NULL, 0),
(12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, '12', 2, NULL, 0),
(13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '13', 2, NULL, 0),
(14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0),
(15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0),
(16, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0),
(71, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 1, 4751000000, 0, NULL, 0, '', 2, NULL, 0);
INSERT INTO `vn`.`priceFixed`(`id`, `itemFk`, `rate0`, `rate1`, `rate2`, `rate3`, `started`, `ended`, `bonus`, `warehouseFk`, `created`)
VALUES
@ -2156,7 +2156,7 @@ INSERT INTO `vn`.`campaign`(`code`, `dated`)
INSERT INTO `hedera`.`image`(`collectionFk`, `name`)
VALUES
('user', 'e7723f0b24ff05b32ed09d95196f2f29');
('user', '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd');
INSERT INTO `hedera`.`imageCollectionSize`(`id`, `collectionFk`,`width`, `height`)
VALUES

View File

@ -38,7 +38,7 @@ describe('Travel thermograph path', () => {
it('should select the file to upload', async() => {
let currentDir = process.cwd();
let filePath = `${currentDir}/e2e/dms/ecc/3.jpeg`;
let filePath = `${currentDir}/storage/dms/ecc/3.jpeg`;
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),

View File

@ -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

View File

@ -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;

View File

@ -13,3 +13,4 @@ import './section';
import './summary';
import './topbar/topbar';
import './user-popover';
import './upload-photo';

View File

@ -109,14 +109,15 @@ vn-layout {
}
}
}
img {
width: 40px;
border-radius: 50%;
}
.buttonAccount {
.buttonAccount {
background: none;
border: none;
}
img {
width: 40px;
border-radius: 50%;
}
}
@media screen and (max-width: $mobile-width) {
& > vn-topbar {
& > .start > .logo {

View File

@ -0,0 +1,39 @@
<vn-dialog class="edit"
vn-id="dialog"
on-accept="$ctrl.onUploadAccept()"
message="Upload new photo">
<tpl-body class="upload-photo">
<vn-horizontal ng-show="file.value" class="photo vn-mb-md">
<div><img vn-id="photo" ng-src=""/></div>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="File name"
ng-model="$ctrl.newPhoto.fileName"
required="true">
</vn-input-file>
</vn-horizontal>
<vn-horizontal>
<vn-input-file vn-id="file"
vn-one
label="File"
ng-model="$ctrl.newPhoto.files"
on-change="$ctrl.updatePhotoPreview(value)"
accept="{{$ctrl.allowedContentTypes}}"
required="true">
<append>
<vn-icon vn-none
color-marginal
title="{{$ctrl.contentTypesInfo}}"
icon="info">
</vn-icon>
</append>
</vn-input-file>
</vn-horizontal>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Upload</button>
</tpl-buttons>
</vn-dialog>

View File

@ -0,0 +1,105 @@
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: id,
collection: collection,
fileName: id
};
this.$.dialog.show();
this.getAllowedContentTypes();
}
getAllowedContentTypes() {
this.$http.get('ImageContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});
}
get contentTypesInfo() {
return this.$t('ContentTypesInfo', {
allowedContentTypes: this.allowedContentTypes
});
}
/**
* 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: '<'
}
});

View File

@ -0,0 +1,57 @@
import './index.js';
describe('Salix', () => {
describe('Component vnUploadPhoto', () => {
let controller;
let $scope;
let $httpBackend;
beforeEach(ngModule('salix'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-upload-photo></vn-upload-photo>');
controller = $componentController('vnUploadPhoto', {$element, $scope});
controller.newPhoto = {};
}));
afterEach(() => {
$scope.$destroy();
});
describe('onUploadAccept()', () => {
it('should throw an error message containing "Select an image"', () => {
jest.spyOn(controller.vnApp, 'showError');
controller.onUploadAccept();
expect(controller.vnApp.showError).toHaveBeenCalledWith('Select an image');
});
it('should call to the makeRequest() method', () => {
jest.spyOn(controller, 'makeRequest');
controller.newPhoto.files = [0];
controller.onUploadAccept();
expect(controller.makeRequest).toHaveBeenCalledWith();
});
});
describe('makeRequest()', () => {
it('should make an http query and then emit a response event', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller, 'emit');
controller.newPhoto.files = [{name: 'hola'}];
$httpBackend.expectRoute('POST', 'Images/upload').respond(200);
controller.makeRequest();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
expect(controller.emit).toHaveBeenCalledWith('response');
});
});
});
});

View File

@ -0,0 +1,3 @@
Upload new photo: Subir una nueva foto
Select an image: Selecciona una imagen
File name: Nombre del fichero

View File

@ -0,0 +1,39 @@
@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;
}
vn-input-file {
max-width: 256px;
div.control {
overflow: hidden
}
}
}

View File

@ -14,7 +14,7 @@
<vn-vertical class="user-popover vn-pa-md">
<div class="profile-card vn-pb-md">
<img
ng-src="{{$ctrl.getImageUrl($root.user.id)}}"
ng-src="{{::$root.imagePath('user', '160x160', $root.user.id)}}"
on-error-src/>
<div class="vn-pl-sm">
<div>

View File

@ -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'];

View File

@ -2,14 +2,16 @@ import './index.js';
describe('Salix', () => {
describe('Component vnUserPopover', () => {
const userId = 9;
let controller;
let $scope;
let $root;
beforeEach(ngModule('salix'));
beforeEach(inject(($componentController, $rootScope, $httpBackend) => {
$httpBackend.expectGET('UserConfigs/getUserConfig');
$root = $rootScope;
$scope = $rootScope.$new();
controller = $componentController('vnUserPopover', {$scope});
}));
@ -60,9 +62,10 @@ describe('Salix', () => {
describe('getImageUrl()', () => {
it('should return de url image', () => {
const url = controller.getImageUrl();
const url = $root.imagePath('user', '160x160', userId);
expect(url).toBeDefined();
expect(url).toEqual(`/api/Images/user/160x160/${userId}/download?access_token=null`);
});
});
});

View File

@ -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;

View File

@ -0,0 +1,57 @@
const md5 = require('md5');
module.exports = function(Self) {
Self.setup = function() {
Self.super_.setup.call(this);
this.remoteMethod('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
};
/**
* Returns a container instance
* If doesn't exists creates a new one
*
* @param {String} name Container name
* @return {Object} Container instance
*/
Self.container = async function(name) {
const models = Self.app.models;
let container;
try {
container = await models[this.modelName].getContainer(name);
} catch (err) {
if (err.code === 'ENOENT') {
container = await models[this.modelName].createContainer({
name: name
});
} else throw err;
}
return container;
};
Self.getHash = function(id) {
return md5(id.toString()).substring(0, 3);
};
Self.getFileExtension = function(fileName) {
return fileName.split('.').pop().toLowerCase();
};
Self.allowedContentTypes = async function() {
const connector = this.dataSource.connector;
const allowedContentTypes = connector.allowedContentTypes;
return allowedContentTypes;
};
};

View File

@ -0,0 +1,12 @@
{
"name": "Container",
"base": "VnModel",
"acls": [
{
"property": "status",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -163,5 +163,8 @@
"ASSIGN_ZONE_FIRST": "Asigna una zona primero",
"Amount cannot be zero": "El importe no puede ser cero",
"Company has to be official": "Empresa inválida",
"You can not select this payment method without a registered bankery account": "No se puede utilizar este método de pago si no has registrado una cuenta bancaria"
"You can not select this payment method without a registered bankery account": "No se puede utilizar este método de pago si no has registrado una cuenta bancaria",
"You can't upload images on the test environment": "No puedes subir imágenes en el entorno de pruebas",
"The selected ticket is not suitable for this route": "El ticket seleccionado no es apto para esta ruta",
"Sorts whole route": "Reordena ruta entera"
}

View File

@ -1,18 +0,0 @@
const uuid = require('uuid/v1');
const md5 = require('md5');
module.exports = app => {
const storageConnector = app.dataSources.storage.connector;
storageConnector.getFilename = function(file) {
return `${uuid()}.${storageConnector.getFileExtension(file.name)}`;
};
storageConnector.getFileExtension = function(fileName) {
return fileName.split('.').pop().toLowerCase();
};
storageConnector.getPathHash = function(id) {
return md5(id.toString()).substring(0, 3);
};
};

View File

@ -17,11 +17,11 @@
"connectTimeout": 40000,
"acquireTimeout": 20000
},
"storage": {
"name": "storage",
"tempStorage": {
"name": "tempStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./e2e/dms",
"root": "./storage/tmp",
"maxFileSize": "262144000",
"allowedContentTypes": [
"application/x-7z-compressed",
@ -36,5 +36,37 @@
"image/jpeg",
"image/jpg"
]
},
"dmsStorage": {
"name": "dmsStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/dms",
"maxFileSize": "262144000",
"allowedContentTypes": [
"application/x-7z-compressed",
"application/x-zip-compressed",
"application/x-rar-compressed",
"application/octet-stream",
"application/pdf",
"application/zip",
"application/rar",
"multipart/x-zip",
"image/png",
"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"
]
}
}

View File

@ -49,5 +49,8 @@
},
"Application": {
"dataSource": "vn"
},
"Container": {
"dataSource": "vn"
}
}

View File

@ -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;

View File

@ -1,23 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;
return modelAllowedContentTypes || allowedContentTypes;
};
};

View File

@ -1,4 +1,3 @@
module.exports = Self => {
require('../methods/client-dms/removeFile')(Self);
require('../methods/client-dms/allowedContentTypes')(Self);
};

View File

@ -26,7 +26,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('clientDms/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -62,7 +62,7 @@ describe('Client', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `clientDms/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -17,7 +17,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('clientDms/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -69,7 +69,7 @@ describe('Client', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `clientDms/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -70,8 +70,8 @@
</vn-td>
<vn-td shrink >
<img
ng-src="{{::$root.imagePath}}/catalog/50x50/{{::buy.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::buy.image}}"
ng-src="{{::$root.imagePath('catalog', '50x50', buy.itemFk)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', buy.itemFk)}}"
vn-click-stop
on-error-src/>
</vn-td>

View File

@ -33,8 +33,10 @@ module.exports = Self => {
// Exit loop
if (!image) return clearInterval(timer);
const fileName = `${image.itemFk}.png`;
const filePath = path.join(tempPath, fileName);
const srcFile = image.url.split('/').pop();
const fileName = srcFile.split('.')[0];
const file = `${fileName}.png`;
const filePath = path.join(tempPath, file);
const writeStream = fs.createWriteStream(filePath);
writeStream.on('open', () => {
@ -57,7 +59,7 @@ module.exports = Self => {
writeStream.on('finish', async function() {
try {
await models.Image.registerImage('catalog', fileName, filePath);
await models.Image.registerImage('catalog', filePath, fileName, image.itemFk);
await image.destroy();
} catch (error) {
await errorHandler(image.itemFk, error, filePath);

View File

@ -1,5 +1,5 @@
<vn-portal slot="menu">
<vn-item-descriptor item="$ctrl.item"></vn-item-descriptor>
<vn-item-descriptor item="$ctrl.item" card-reload="$ctrl.reload()"></vn-item-descriptor>
<vn-left-menu source="card"></vn-left-menu>
</vn-portal>
<ui-view></ui-view>

View File

@ -16,19 +16,15 @@
</vn-item>
</slot-menu>
<slot-before>
<div style="position: relative" text-center>
<img
ng-src="{{::$root.imagePath}}/catalog/200x200/{{$ctrl.item.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{$ctrl.item.image}}"
on-error-src
/>
<a href="//verdnatura.es/#!form=admin/items&filter={{$ctrl.item.id}}" target="_blank">
<vn-float-button
icon="edit"
style="position: absolute; margin: 1em; bottom: 0; right: 0;"
vn-visible-by="marketing, buyer">
</vn-float-button>
</a>
<div class="photo" text-center>
<img vn-id="photo"
ng-src="{{$root.imagePath('catalog', '200x200', $ctrl.item.id)}}"
zoom-image="{{$root.imagePath('catalog', '1600x900', $ctrl.item.id)}}"
on-error-src/>
<vn-float-button ng-click="uploadPhoto.show('catalog', $ctrl.item.id)"
icon="edit"
vn-visible-by="catalogPhotos">
</vn-float-button>
</div>
<vn-horizontal class="item-state">
<vn-one>
@ -102,4 +98,10 @@
</vn-confirm>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
</vn-worker-descriptor-popover>
<!-- Upload photo dialog -->
<vn-upload-photo
vn-id="uploadPhoto"
on-response="$ctrl.onUploadResponse()">
</vn-upload-photo>

View File

@ -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: '&'
}
});

View File

@ -35,8 +35,8 @@
ui-sref="item.card.summary({id: item.id})">
<vn-td shrink>
<img
ng-src="{{::$root.imagePath}}/catalog/50x50/{{::item.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::item.image}}"
ng-src="{{::$root.imagePath('catalog', '50x50', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
vn-click-stop
on-error-src/>
</vn-td>
@ -44,7 +44,7 @@
<span
vn-click-stop="itemDescriptor.show($event, item.id)"
class="link">
{{::item.id | zeroFill:6}}
{{::item.id}}
</span>
</vn-td>
<vn-td shrink>{{::item.grouping | dashIfEmpty}}</vn-td>

View File

@ -11,8 +11,8 @@
<vn-horizontal>
<vn-one>
<img style="width: 100%; display: block;"
ng-src="{{::$root.imagePath}}/catalog/200x200/{{$ctrl.item.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{$ctrl.item.image}}" on-error-src/>
ng-src="{{$root.imagePath('catalog', '200x200', $ctrl.item.id)}}"
zoom-image="{{$root.imagePath('catalog', '1600x900', $ctrl.item.id)}}" on-error-src/>
<vn-horizontal class="item-state">
<vn-one>
<p translate>Visible</p>

View File

@ -5,11 +5,11 @@
<vn-card>
<div class="image">
<div ng-if="::item.hex != null" class="item-color-background">
<div class="item-color" style="background-color: #{{::item.hex}}"></div>
<div class="item-color" ng-style="{'background-color': '#' + item.hex}"></div>
</div>
<img
ng-src="{{::$root.imagePath}}/catalog/200x200/{{::item.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::item.image}}"
ng-src="{{::$root.imagePath('catalog', '200x200', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
on-error-src/>
</div>
<div class="description">

View File

@ -32,8 +32,8 @@
<vn-tr ng-repeat="row in $ctrl.rows">
<vn-td shrink>
<img
ng-src="{{::$root.imagePath}}/catalog/50x50/{{::row.item.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::row.item.image}}"
ng-src="{{::$root.imagePath('catalog', '50x50', row.item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', row.item.id)}}"
on-error-src/>
</vn-td>
<vn-td number>

View File

@ -1,5 +1,5 @@
module.exports = Self => {
Self.remoteMethod('guessPriority', {
Self.remoteMethodCtx('guessPriority', {
description: 'Changes automatically the priority of the tickets in a route',
accessType: 'READ',
accepts: [{
@ -19,10 +19,32 @@ module.exports = Self => {
}
});
Self.guessPriority = async id => {
let query = `CALL vn.routeGuessPriority(?)`;
Self.guessPriority = async(ctx, id) => {
const userId = ctx.req.accessToken.userId;
const $t = ctx.req.__; // $translate
const query = `CALL vn.routeGuessPriority(?)`;
const tx = await Self.beginTransaction({});
let options = [id];
return await Self.rawSql(query, options);
try {
let options = {transaction: tx};
const priority = await Self.rawSql(query, [id], options);
let logRecord = {
originFk: id,
userFk: userId,
action: 'update',
changedModel: 'Route',
description: $t('Sorts whole route')
};
await Self.app.models.RouteLog.create(logRecord, options);
await tx.commit();
return priority;
} catch (e) {
await tx.rollback();
throw e;
}
};
};

View File

@ -1,18 +1,18 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('ticketToRoute', {
Self.remoteMethod('insertTicket', {
description: 'Check if the ticket can be insert into the route and insert it',
accessType: 'READ',
accepts: [{
arg: 'ticketId',
arg: 'routeId',
type: 'number',
required: true,
description: 'ticketId ',
description: 'The route id',
http: {source: 'path'}
},
{
arg: 'routeId',
arg: 'ticketId',
type: 'number',
required: true
}],
@ -21,12 +21,12 @@ module.exports = Self => {
root: true
},
http: {
path: `/:ticketId/ticketToRoute`,
path: `/:routeId/insertTicket`,
verb: 'PATCH'
}
});
Self.ticketToRoute = async(ticketId, routeId) => {
Self.insertTicket = async(routeId, ticketId) => {
const models = Self.app.models;
const route = await models.Route.findById(routeId);
@ -43,9 +43,10 @@ module.exports = Self => {
landed: {between: [minDate, maxDate]},
}
});
if (!ticket)
throw new UserError('The selected ticket is not suitable for this route');
return await ticket.updateAttribute('routeFk', route.id);
return ticket.updateAttribute('routeFk', route.id);
};
};

View File

@ -15,9 +15,15 @@ describe('route guessPriority()', () => {
});
it('should call guessPriority() and then check the tickets in the target route now have their priorities defined', async() => {
const ctx = {
req: {
accessToken: {userId: 9},
__: () => {}
},
};
routeTicketsToRestore = await app.models.Ticket.find({where: {routeFk: targetRouteId}});
await app.models.Route.guessPriority(targetRouteId);
await app.models.Route.guessPriority(ctx, targetRouteId);
let routeTickets = await app.models.Ticket.find({where: {routeFk: targetRouteId}, fields: ['id', 'priority']});
expect(routeTickets.length).toEqual(2);

View File

@ -1,7 +1,7 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('route ticketToRoute()', () => {
describe('route insertTicket()', () => {
const deliveryId = 56;
let originalTicket;
const routeId = 2;
@ -30,7 +30,7 @@ describe('route ticketToRoute()', () => {
originalTicket = await app.models.Ticket.findById(14);
const ticketId = 14;
const result = await app.models.Route.ticketToRoute(ticketId, routeId);
const result = await app.models.Route.insertTicket(routeId, ticketId);
expect(result.routeFk).toEqual(2);
});
@ -40,7 +40,7 @@ describe('route ticketToRoute()', () => {
let error;
try {
await app.models.Route.ticketToRoute(ticketId, routeId);
await app.models.Route.insertTicket(routeId, ticketId);
} catch (e) {
error = e.message;
}

View File

@ -5,7 +5,7 @@ module.exports = Self => {
require('../methods/route/guessPriority')(Self);
require('../methods/route/updateVolume')(Self);
require('../methods/route/getDeliveryPoint')(Self);
require('../methods/route/ticketToRoute')(Self);
require('../methods/route/insertTicket')(Self);
Self.validate('kmStart', validateDistance, {
message: 'Distance must be lesser than 1000'

View File

@ -26,7 +26,8 @@
<vn-tbody>
<a ng-repeat="route in model.data"
class="clickable vn-tr search-result"
ui-sref="route.card.summary({id: {{::route.id}}})">
ui-sref="route.card.summary({id: {{::route.id}}})"
ng-attr-id="{{::route.id}}" vn-droppable="$ctrl.onDrop($event)">
<vn-td shrink>
<vn-check
ng-model="route.checked"

View File

@ -5,6 +5,7 @@ export default class Controller extends Section {
constructor($element, $, vnReport) {
super($element, $);
this.vnReport = vnReport;
this.droppableElement = 'a.vn-tr';
}
preview(route) {
@ -38,6 +39,41 @@ export default class Controller extends Section {
routeId: routesId
});
}
onDrop($event) {
const target = $event.target;
const droppable = target.closest(this.droppableElement);
const ticketId = $event.dataTransfer.getData('Text');
const routeId = droppable.id;
if (isNaN(ticketId)) {
const regexp = new RegExp(/\/ticket\/([0-9]+)\//i);
const matches = ticketId.match(regexp);
if (matches && matches.length)
this.insert(routeId, matches[1]);
else
this.vnApp.showError(this.$t('Ticket not found'));
}
if (!isNaN(ticketId))
this.insert(routeId, ticketId);
}
insert(routeId, ticketId) {
routeId = parseInt(routeId);
ticketId = parseInt(ticketId);
const query = `Routes/${routeId}/insertTicket`;
return this.$http.patch(query, {ticketId}).then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.$.model.refresh();
}).catch(error => {
if (error.status == 404)
return this.vnApp.showError(this.$t('Ticket not found'));
throw error;
});
}
}
Controller.$inject = ['$element', '$scope', 'vnReport'];

View File

@ -3,10 +3,12 @@ import crudModel from 'core/mocks/crud-model';
describe('Component vnRouteIndex', () => {
let controller;
let $httpBackend;
beforeEach(ngModule('route'));
beforeEach(inject($componentController => {
beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-route-index></vn-route-index>');
controller = $componentController('vnRouteIndex', {$element});
controller.$.model = crudModel;
@ -57,4 +59,83 @@ describe('Component vnRouteIndex', () => {
expect(controller.vnReport.show).toHaveBeenCalledWith('driver-route', expectedParams);
});
});
describe('onDrop()', () => {
it('should call the insert method when dragging a ticket number', () => {
jest.spyOn(controller, 'insert');
const routeId = '1';
const expectedTicketId = '16';
const draggedElement = '16';
const droppable = document.createElement('a');
droppable.setAttribute('id', 1);
droppable.classList.add('vn-tr');
const $event = {
dataTransfer: {
getData: () => draggedElement
},
target: droppable
};
controller.onDrop($event);
expect(controller.insert).toHaveBeenCalledWith(routeId, expectedTicketId);
});
it('should call the insert method when dragging a ticket link', () => {
jest.spyOn(controller, 'insert');
const routeId = '1';
const expectedTicketId = '11';
const draggedElement = 'http://arkamcity.com/#!/ticket/11/summary';
const droppable = document.createElement('a');
droppable.setAttribute('id', 1);
droppable.classList.add('vn-tr');
const $event = {
dataTransfer: {
getData: () => draggedElement
},
target: droppable
};
controller.onDrop($event);
expect(controller.insert).toHaveBeenCalledWith(routeId, expectedTicketId);
});
it('should throw an error when dragging an invalid ticket link', () => {
jest.spyOn(controller.vnApp, 'showError');
const draggedElement = 'http://arkamcity.com/#!/item/11/summary';
const droppable = document.createElement('a');
droppable.setAttribute('id', 1);
droppable.classList.add('vn-tr');
const $event = {
dataTransfer: {
getData: () => draggedElement
},
target: droppable
};
controller.onDrop($event);
expect(controller.vnApp.showError).toHaveBeenCalledWith('Ticket not found');
});
});
describe('insert()', () => {
it('should make a HTTP patch query and then call both refresh and showSuccess methods', () => {
jest.spyOn(controller.$.model, 'refresh').mockReturnThis();
jest.spyOn(controller.vnApp, 'showSuccess');
const routeId = 1;
const ticketId = 11;
const data = {ticketId};
$httpBackend.expect('PATCH', `Routes/1/insertTicket`, data).respond();
controller.insert(routeId, ticketId);
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.$.model.refresh).toHaveBeenCalledWith();
});
});
});

View File

@ -31,13 +31,13 @@
model="model">
</vn-multi-check>
</vn-th>
<vn-th shrink>Order</vn-th>
<vn-th class="order-field">Order</vn-th>
<vn-th number>Ticket</vn-th>
<vn-th expand>Client</vn-th>
<vn-th shrink>Packages</vn-th>
<vn-th number></vn-th>
<vn-th shrink></vn-th>
<vn-th>Warehouse</vn-th>
<vn-th>Postcode</vn-th>
<vn-th translate-attr="{title: 'Postcode'}" shrink>PC</vn-th>
<vn-th expand>Street</vn-th>
<vn-th shrink></vn-th>
<vn-th shrink></vn-th>
@ -50,7 +50,7 @@
ng-model="ticket.checked">
</vn-check>
</vn-td>
<vn-td shrink>
<vn-td class="order-field">
<vn-input-number
on-change="$ctrl.setPriority(ticket.id, ticket.priority)"
ng-model="ticket.priority"
@ -73,9 +73,9 @@
</span>
</vn-td>
<vn-td shrink>{{ticket.packages}}</vn-td>
<vn-td number>{{::ticket.volume | number:1}}</vn-td>
<vn-td shrink>{{::ticket.volume | number:1}}</vn-td>
<vn-td expand>{{ticket.warehouse.name}}</vn-td>
<vn-td>{{ticket.address.postalCode}}</vn-td>
<vn-td shrink>{{ticket.address.postalCode}}</vn-td>
<vn-td expand title="{{ticket.address.street}}">{{ticket.address.street}}</vn-td>
<vn-td shrink>
<vn-icon

View File

@ -161,11 +161,11 @@ class Controller extends Section {
this.insert(ticketId);
}
insert(id) {
const params = {routeId: this.route.id};
const query = `Routes/${id}/ticketToRoute`;
insert(ticketId) {
ticketId = parseInt(ticketId);
return this.$http.patch(query, params).then(() => {
const query = `Routes/${this.route.id}/insertTicket`;
return this.$http.patch(query, {ticketId}).then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.$.model.refresh();
this.card.reload();

View File

@ -309,8 +309,8 @@ describe('Route', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
const ticketId = 11;
const data = {routeId: 1};
$httpBackend.expect('PATCH', `Routes/11/ticketToRoute`, data).respond();
const data = {ticketId};
$httpBackend.expect('PATCH', `Routes/1/insertTicket`, data).respond();
controller.insert(ticketId);
$httpBackend.flush();

View File

@ -7,4 +7,5 @@ Sort routes: Ordenar rutas
Add ticket: Añadir ticket
Tickets to add: Tickets a añadir
Ticket not found: No se ha encontrado el ticket
The selected ticket is not suitable for this route: El ticket seleccionado no es apto para esta ruta
The selected ticket is not suitable for this route: El ticket seleccionado no es apto para esta ruta
PC: CP

View File

@ -3,4 +3,8 @@
vn-route-tickets form{
margin: 0 auto;
max-width: $width-lg;
.order-field {
max-width: 30px;
}
}

View File

@ -1,23 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;
return modelAllowedContentTypes || allowedContentTypes;
};
};

View File

@ -1,4 +1,3 @@
module.exports = Self => {
require('../methods/ticket-dms/removeFile')(Self);
require('../methods/ticket-dms/allowedContentTypes')(Self);
};

View File

@ -25,7 +25,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('ticketDms/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -67,7 +67,7 @@ describe('Ticket', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `ticketDms/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -16,7 +16,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('ticketDms/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -69,7 +69,7 @@ describe('Ticket', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `ticketDms/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -72,7 +72,7 @@ Notes: Notas
Volume: Volumen
Expedition: Expedición
New state: Nuevo estado
Packages: Embalajes
Packages: Bultos
Tracking: Estados
Sale checked: Control clientes
Components: Componentes

View File

@ -19,8 +19,8 @@
<vn-card>
<div class="image">
<img
ng-src="{{::$root.imagePath}}/catalog/200x200/{{::sale.item.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::sale.item.image}}"
ng-src="{{::$root.imagePath('catalog', '200x200', sale.itemFk)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', sale.itemFk)}}"
on-error-src/>
</div>
<div class="description">

View File

@ -93,8 +93,8 @@
</vn-td>
<vn-td shrink>
<img
ng-src="{{::$root.imagePath}}/catalog/50x50/{{sale.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{sale.image}}"
ng-src="{{::$root.imagePath('catalog', '50x50', sale.itemFk)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', sale.itemFk)}}"
on-error-src/>
</vn-td>
<vn-td number>

View File

@ -1,23 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;
return modelAllowedContentTypes || allowedContentTypes;
};
};

View File

@ -1,5 +1,4 @@
module.exports = Self => {
require('../methods/travel-thermograph/allowedContentTypes')(Self);
require('../methods/travel-thermograph/getThermographTemperatures')(Self);
};

View File

@ -21,7 +21,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('TravelThermographs/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});
@ -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()
};

View File

@ -53,7 +53,7 @@ describe('Ticket', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['application/pdf', 'image/png', 'image/jpg'];
$httpBackend.expect('GET', `TravelThermographs/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -17,7 +17,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('TravelThermographs/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -79,7 +79,7 @@ describe('Worker', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `TravelThermographs/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -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

View File

@ -146,7 +146,7 @@ module.exports = Self => {
});
if (currentContract) {
const maxHolidays = currentContract.holidays().days;
const maxHolidays = currentContract.holidays() && currentContract.holidays().days;
calendar.totalHolidays = maxHolidays;
workedDays -= entitlementRate;

View File

@ -1,23 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;
return modelAllowedContentTypes || allowedContentTypes;
};
};

View File

@ -1,7 +1,6 @@
module.exports = Self => {
require('../methods/worker-dms/downloadFile')(Self);
require('../methods/worker-dms/removeFile')(Self);
require('../methods/worker-dms/allowedContentTypes')(Self);
require('../methods/worker-dms/filter')(Self);
Self.isMine = async function(ctx, dmsId) {

View File

@ -26,7 +26,7 @@
<h6 translate>Holidays</h6>
<div>
{{'Used' | translate}} {{$ctrl.calendar.holidaysEnjoyed}}
{{'of' | translate}} {{$ctrl.calendar.totalHolidays}} {{'days' | translate}}
{{'of' | translate}} {{$ctrl.calendar.totalHolidays || 0}} {{'days' | translate}}
</div>
</div>
<div class="vn-pt-md">

View File

@ -1,6 +1,18 @@
<vn-descriptor-content
module="worker"
description="$ctrl.worker.firstName +' '+ $ctrl.worker.lastName">
<slot-before>
<div class="photo" text-center>
<img vn-id="photo"
ng-src="{{$root.imagePath('user', '520x520', $ctrl.worker.id)}}"
zoom-image="{{$root.imagePath('user', '1600x1600', $ctrl.worker.id)}}"
on-error-src/>
<vn-float-button ng-click="uploadPhoto.show('user', $ctrl.worker.id)"
icon="edit"
vn-visible-by="userPhotos">
</vn-float-button>
</div>
</slot-before>
<slot-body>
<div class="attributes">
<vn-label-value
@ -42,4 +54,10 @@
<div ng-transclude="btnThree"></div>
</div>
</slot-body>
</vn-descriptor-content>
</vn-descriptor-content>
<!-- Upload photo dialog -->
<vn-upload-photo
vn-id="uploadPhoto"
on-response="$ctrl.onUploadResponse()">
</vn-upload-photo>

View File

@ -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,

View File

@ -26,7 +26,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('workerDms/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -64,7 +64,7 @@ describe('Client', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `workerDms/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -17,7 +17,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('WorkerDms/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -70,7 +70,7 @@ describe('Worker', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `WorkerDms/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -60,7 +60,7 @@
</vn-icon-button>
</vn-td>
<vn-td shrink>
<vn-icon-button ui-sref="worker.card.edit({dmsId: {{::document.dmsFk}}})"
<vn-icon-button ui-sref="worker.card.dms.edit({dmsId: {{::document.dmsFk}}})"
icon="edit"
title="{{'Edit file' | translate}}">
</vn-icon-button>

View File

@ -43,6 +43,6 @@
"pool": true
},
"storage": {
"root": "./e2e/dms"
"root": "./storage/dms"
}
}

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