test passed
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
This commit is contained in:
parent
e16d1761f9
commit
ea1cc68d29
|
@ -5,6 +5,7 @@ e2e/dms/*/
|
||||||
!e2e/dms/c4c
|
!e2e/dms/c4c
|
||||||
!e2e/dms/c81
|
!e2e/dms/c81
|
||||||
!e2e/dms/ecc
|
!e2e/dms/ecc
|
||||||
|
!e2e/dms/a87
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
.eslintcache
|
.eslintcache
|
||||||
datasources.*.json
|
datasources.*.json
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
const UserError = require('vn-loopback/util/user-error');
|
|
||||||
|
|
||||||
checkRole = async function(ctx, id) {
|
|
||||||
const models = Self.app.models;
|
|
||||||
const dms = await Self.findById(id);
|
|
||||||
|
|
||||||
const hasReadRole = await models.DmsType.hasReadRole(ctx, dms.dmsTypeFk);
|
|
||||||
if (!hasReadRole)
|
|
||||||
throw new UserError(`You don't have enough privileges`);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
|
@ -1,7 +1,4 @@
|
||||||
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
const checkRole = require('./checkRole');
|
|
||||||
const getfile = require('./getfile');
|
|
||||||
|
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
Self.remoteMethodCtx('downloadFile', {
|
Self.remoteMethodCtx('downloadFile', {
|
||||||
description: 'Download a document',
|
description: 'Download a document',
|
||||||
|
@ -36,7 +33,8 @@ module.exports = Self => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.downloadFile = async function(ctx, id) {
|
Self.downloadFile = async function(ctx, id) {
|
||||||
await checkRole(ctx, id);
|
if (!await Self.checkRole(ctx, id))
|
||||||
return await getfile(ctx, id);
|
throw new UserError(`You don't have enough privileges`);
|
||||||
|
return await Self.getFile(id);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
const UserError = require('vn-loopback/util/user-error');
|
|
||||||
|
|
||||||
getFile = async function(ctx, id) {
|
|
||||||
const storageConnector = Self.app.dataSources.storage.connector;
|
|
||||||
const models = Self.app.models;
|
|
||||||
const dms = await Self.findById(id);
|
|
||||||
|
|
||||||
const hasReadRole = await models.DmsType.hasReadRole(ctx, dms.dmsTypeFk);
|
|
||||||
if (!hasReadRole)
|
|
||||||
|
|
||||||
throw new UserError(`You don't have enough privileges`);
|
|
||||||
|
|
||||||
const pathHash = storageConnector.getPathHash(dms.id);
|
|
||||||
try {
|
|
||||||
await models.Container.getFile(pathHash, dms.file);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.code != 'ENOENT')
|
|
||||||
throw e;
|
|
||||||
|
|
||||||
const error = new UserError(`File doesn't exists`);
|
|
||||||
error.statusCode = 404;
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream = models.Container.downloadStream(pathHash, dms.file);
|
|
||||||
|
|
||||||
return [stream, dms.contentType, `filename="${dms.file}"`];
|
|
||||||
};
|
|
|
@ -1,6 +1,6 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const app = require('vn-loopback/server/server');
|
||||||
|
|
||||||
fdescribe('dms downloadFile()', () => {
|
describe('dms downloadFile()', () => {
|
||||||
let dmsId = 1;
|
let dmsId = 1;
|
||||||
|
|
||||||
it('should return a response for an employee with text content-type', async() => {
|
it('should return a response for an employee with text content-type', async() => {
|
||||||
|
@ -11,7 +11,7 @@ fdescribe('dms downloadFile()', () => {
|
||||||
expect(result[1]).toEqual('text/plain');
|
expect(result[1]).toEqual('text/plain');
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should return an error for a user without enough privileges`, async() => {
|
it('should return an error for a user without enough privileges', async() => {
|
||||||
let clientId = 101;
|
let clientId = 101;
|
||||||
let ctx = {req: {accessToken: {userId: clientId}}};
|
let ctx = {req: {accessToken: {userId: clientId}}};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,37 @@
|
||||||
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
require('../methods/dms/downloadFile')(Self);
|
require('../methods/dms/downloadFile')(Self);
|
||||||
require('../methods/dms/uploadFile')(Self);
|
require('../methods/dms/uploadFile')(Self);
|
||||||
require('../methods/dms/removeFile')(Self);
|
require('../methods/dms/removeFile')(Self);
|
||||||
require('../methods/dms/updateFile')(Self);
|
require('../methods/dms/updateFile')(Self);
|
||||||
|
|
||||||
|
Self.checkRole = async function(ctx, id) {
|
||||||
|
const models = Self.app.models;
|
||||||
|
const dms = await Self.findById(id);
|
||||||
|
|
||||||
|
return await models.DmsType.hasReadRole(ctx, dms.dmsTypeFk);
|
||||||
|
};
|
||||||
|
|
||||||
|
Self.getFile = async function(id) {
|
||||||
|
const storageConnector = Self.app.dataSources.storage.connector;
|
||||||
|
const models = Self.app.models;
|
||||||
|
const dms = await Self.findById(id);
|
||||||
|
const pathHash = storageConnector.getPathHash(dms.id);
|
||||||
|
try {
|
||||||
|
await models.Container.getFile(pathHash, dms.file);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code != 'ENOENT')
|
||||||
|
throw e;
|
||||||
|
|
||||||
|
const error = new UserError(`File doesn't exists`);
|
||||||
|
error.statusCode = 404;
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = models.Container.downloadStream(pathHash, dms.file);
|
||||||
|
|
||||||
|
return [stream, dms.contentType, `filename="${dms.file}"`];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
const app = require('vn-loopback/server/server');
|
||||||
|
describe('Dms', () => {
|
||||||
|
const Dms = app.models.Dms;
|
||||||
|
|
||||||
|
describe('getFile()', () => {
|
||||||
|
it('should return a response with text content-type', async() => {
|
||||||
|
const result = await Dms.getFile(1);
|
||||||
|
|
||||||
|
expect(result[1]).toEqual('text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error for a file does not exists', async() => {
|
||||||
|
let error = {};
|
||||||
|
try {
|
||||||
|
await Dms.getFile(6);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error.statusCode).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error for a record does not exists', async() => {
|
||||||
|
let error = {};
|
||||||
|
try {
|
||||||
|
await app.models.Dms.getFile('NotExistentId');
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error.statusCode).not.toBe(404);
|
||||||
|
expect(error).toEqual(jasmine.any(Error));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkRole()', () => {
|
||||||
|
const dmsId = 1;
|
||||||
|
it('should return a true for an employee with permission', async() => {
|
||||||
|
let ctx = {req: {accessToken: {userId: 107}}};
|
||||||
|
const result = await Dms.checkRole(ctx, dmsId);
|
||||||
|
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for an employee without permission', async() => {
|
||||||
|
let ctx = {req: {accessToken: {userId: 101}}};
|
||||||
|
const result = await Dms.checkRole(ctx, dmsId);
|
||||||
|
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
|
||||||
|
VALUES
|
||||||
|
('WorkerDms', 'filter', 'READ', 'ALLOW', 'ROLE', 'employee'),
|
||||||
|
('WorkerDms', 'downloadFile', 'READ', 'ALLOW', 'ROLE', 'employee');
|
||||||
|
DELETE FROM `salix`.`ACL` WHERE (`id` = '205');
|
|
@ -1953,11 +1953,13 @@ INSERT INTO `vn`.`dmsType`(`id`, `name`, `path`, `readRoleFk`, `writeRoleFk`, `c
|
||||||
|
|
||||||
INSERT INTO `vn`.`dms`(`id`, `dmsTypeFk`, `file`, `contentType`, `workerFk`, `warehouseFk`, `companyFk`, `hardCopyNumber`, `hasFile`, `reference`, `description`, `created`)
|
INSERT INTO `vn`.`dms`(`id`, `dmsTypeFk`, `file`, `contentType`, `workerFk`, `warehouseFk`, `companyFk`, `hardCopyNumber`, `hasFile`, `reference`, `description`, `created`)
|
||||||
VALUES
|
VALUES
|
||||||
(1, 14, '1.txt', 'text/plain', 5, 1, 442, NULL, FALSE, 'Ticket:11', 'Ticket:11 dms for the ticket', CURDATE()),
|
(1, 14, '1.txt', 'text/plain', 5, 1, 442, NULL, FALSE, 'Ticket:11', 'Ticket:11 dms for the ticket', CURDATE()),
|
||||||
(2, 5, '2.txt', 'text/plain', 5, 1, 442, 1, TRUE, 'Client:104', 'Client:104 dms for the client', CURDATE()),
|
(2, 5, '2.txt', 'text/plain', 5, 1, 442, 1, TRUE, 'Client:104', 'Client:104 dms for the client', CURDATE()),
|
||||||
(3, 5, '3.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Client: 104', 'Client:104 readme', CURDATE()),
|
(3, 5, '3.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Client: 104', 'Client:104 readme', CURDATE()),
|
||||||
(4, 3, '4.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Worker: 106', 'Worker:106 readme', CURDATE()),
|
(4, 3, '4.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Worker: 106', 'Worker:106 readme', CURDATE()),
|
||||||
(5, 5, '5.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'travel: 1', 'dmsForThermograph', CURDATE());
|
(5, 5, '5.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'travel: 1', 'dmsForThermograph', CURDATE()),
|
||||||
|
(6, 5, '6.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'NotExists', 'DoesNotExists', CURDATE());
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO `vn`.`ticketDms`(`ticketFk`, `dmsFk`)
|
INSERT INTO `vn`.`ticketDms`(`ticketFk`, `dmsFk`)
|
||||||
VALUES
|
VALUES
|
||||||
|
@ -1968,9 +1970,10 @@ INSERT INTO `vn`.`clientDms`(`clientFk`, `dmsFk`)
|
||||||
(104, 2),
|
(104, 2),
|
||||||
(104, 3);
|
(104, 3);
|
||||||
|
|
||||||
INSERT INTO `vn`.`workerDocument`(`id`, `worker`, `document`)
|
INSERT INTO `vn`.`workerDocument`(`id`, `worker`, `document`,`isReadableByWorker`)
|
||||||
VALUES
|
VALUES
|
||||||
(1, 106, 4);
|
(1, 106, 4, TRUE),
|
||||||
|
(2, 107, 3, FALSE);
|
||||||
|
|
||||||
INSERT INTO `vn`.`device` (`sn`, `model`, `userFk`)
|
INSERT INTO `vn`.`device` (`sn`, `model`, `userFk`)
|
||||||
VALUES
|
VALUES
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
File: 4.txt. It works!
|
|
@ -0,0 +1,40 @@
|
||||||
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('downloadFile', {
|
||||||
|
description: 'Download a worker document',
|
||||||
|
accessType: 'READ',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'id',
|
||||||
|
type: 'Number',
|
||||||
|
description: 'The document id',
|
||||||
|
http: {source: 'path'}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
returns: [
|
||||||
|
{
|
||||||
|
arg: 'body',
|
||||||
|
type: 'file',
|
||||||
|
root: true
|
||||||
|
}, {
|
||||||
|
arg: 'Content-Type',
|
||||||
|
type: 'String',
|
||||||
|
http: {target: 'header'}
|
||||||
|
}, {
|
||||||
|
arg: 'Content-Disposition',
|
||||||
|
type: 'String',
|
||||||
|
http: {target: 'header'}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
http: {
|
||||||
|
path: `/:id/downloadFile`,
|
||||||
|
verb: 'GET'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.downloadFile = async function(ctx, id) {
|
||||||
|
if (!await Self.app.models.Dms.checkRole(ctx, id) && !await Self.isMine(ctx, id))
|
||||||
|
throw new UserError(`You don't have enough privileges`);
|
||||||
|
return await Self.app.models.Dms.getFile(id);
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,53 @@
|
||||||
|
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('filter', {
|
||||||
|
description: 'Find all instances of the model matched by filter from the data source.',
|
||||||
|
accessType: 'READ',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'filter',
|
||||||
|
type: 'Object',
|
||||||
|
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
|
||||||
|
http: {source: 'query'}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
returns: {
|
||||||
|
type: ['Object'],
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/filter`,
|
||||||
|
verb: 'GET'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.filter = async(ctx, filter) => {
|
||||||
|
const conn = Self.dataSource.connector;
|
||||||
|
const userId = ctx.req.accessToken.userId;
|
||||||
|
|
||||||
|
const account = await Self.app.models.Account.findById(userId);
|
||||||
|
const stmt = new ParameterizedSQL(
|
||||||
|
`SELECT d.id dmsFk, d.reference, d.description, d.file, d.created
|
||||||
|
FROM workerDocument wd
|
||||||
|
JOIN dms d ON d.id = wd.document
|
||||||
|
JOIN dmsType dt ON dt.id = d.dmsTypeFk
|
||||||
|
LEFT JOIN account.roleRole rr ON rr.inheritsFrom = dt.readRoleFk AND rr.role = ?
|
||||||
|
`, [account.roleFk]
|
||||||
|
);
|
||||||
|
const oldWhere = filter.where;
|
||||||
|
const yourOwnDms = {and: [{isReadableByWorker: true}, {worker: userId}]};
|
||||||
|
|
||||||
|
filter.where = {
|
||||||
|
and: [{
|
||||||
|
or: [yourOwnDms, {
|
||||||
|
role: {
|
||||||
|
neq: null
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, oldWhere]};
|
||||||
|
stmt.merge(conn.makeSuffix(filter));
|
||||||
|
|
||||||
|
return await conn.executeStmt(stmt);
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,27 @@
|
||||||
|
const app = require('vn-loopback/server/server');
|
||||||
|
|
||||||
|
describe('worker-dms downloadFile()', () => {
|
||||||
|
let dmsId = 4;
|
||||||
|
|
||||||
|
it('should return a response for an employee with text content-type', async() => {
|
||||||
|
let workerId = 106;
|
||||||
|
let ctx = {req: {accessToken: {userId: workerId}}};
|
||||||
|
const result = await app.models.WorkerDms.downloadFile(ctx, dmsId);
|
||||||
|
|
||||||
|
expect(result[1]).toEqual('text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error for a user without enough privileges', async() => {
|
||||||
|
let clientId = 1;
|
||||||
|
let ctx = {req: {accessToken: {userId: clientId}}};
|
||||||
|
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await app.models.WorkerDms.downloadFile(ctx, dmsId);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
|
@ -125,7 +125,6 @@ module.exports = Self => {
|
||||||
LEFT JOIN account.emailUser mu ON mu.userFk = u.id`
|
LEFT JOIN account.emailUser mu ON mu.userFk = u.id`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
stmt.merge(conn.makeSuffix(filter));
|
stmt.merge(conn.makeSuffix(filter));
|
||||||
let itemsIndex = stmts.push(stmt) - 1;
|
let itemsIndex = stmts.push(stmt) - 1;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,17 @@
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
|
require('../methods/worker-dms/downloadFile')(Self);
|
||||||
require('../methods/worker-dms/removeFile')(Self);
|
require('../methods/worker-dms/removeFile')(Self);
|
||||||
require('../methods/worker-dms/allowedContentTypes')(Self);
|
require('../methods/worker-dms/allowedContentTypes')(Self);
|
||||||
// require('../methods/worker-dms/filter')(Self);
|
require('../methods/worker-dms/filter')(Self);
|
||||||
|
|
||||||
|
Self.isMine = async function(ctx, dmsId) {
|
||||||
|
const myUserId = ctx.req.accessToken.userId;
|
||||||
|
let workerDms = await Self.findOne({
|
||||||
|
where: {
|
||||||
|
workerFk: myUserId,
|
||||||
|
dmsFk: dmsId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return workerDms !== null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<vn-crud-model
|
<vn-crud-model
|
||||||
vn-id="model"
|
vn-id="model"
|
||||||
url="filter"
|
url="WorkerDms/filter"
|
||||||
link="{workerFk: $ctrl.$params.id}"
|
link="{worker: $ctrl.$params.id}"
|
||||||
filter="::$ctrl.filter"
|
|
||||||
limit="20"
|
limit="20"
|
||||||
data="$ctrl.workerDms"
|
data="$ctrl.workerDms"
|
||||||
order="dmsFk DESC"
|
order="dmsFk DESC"
|
||||||
|
@ -28,28 +27,28 @@
|
||||||
<vn-tbody>
|
<vn-tbody>
|
||||||
<vn-tr ng-repeat="document in $ctrl.workerDms">
|
<vn-tr ng-repeat="document in $ctrl.workerDms">
|
||||||
<vn-td number shrink>{{::document.dmsFk}}</vn-td>
|
<vn-td number shrink>{{::document.dmsFk}}</vn-td>
|
||||||
<vn-td shrink>
|
<vn-td expand>
|
||||||
<span title="{{::document.dms.reference}}">
|
<span title="{{::document.reference}}">
|
||||||
{{::document.dms.reference}}
|
{{::document.reference}}
|
||||||
</span>
|
</span>
|
||||||
</vn-td>
|
</vn-td>
|
||||||
<vn-td expand>
|
<vn-td expand>
|
||||||
<span title="{{::document.dms.description}}">
|
<span title="{{::document.description}}">
|
||||||
{{::document.dms.description}}
|
{{::document.description}}
|
||||||
</span>
|
</span>
|
||||||
</vn-td>
|
</vn-td >
|
||||||
<vn-td shrink>
|
<vn-td shrink>
|
||||||
<a target="_blank"
|
<a target="_blank"
|
||||||
title="{{'Download file' | translate}}"
|
title="{{'Download file' | translate}}"
|
||||||
href="api/dms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">{{::document.dms.file}}
|
href="api/workerDms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">{{::document.file}}
|
||||||
</a>
|
</a>
|
||||||
</vn-td>
|
</vn-td>
|
||||||
<vn-td>
|
<vn-td>
|
||||||
{{::document.dms.created | date:'dd/MM/yyyy HH:mm'}}
|
{{::document.created | date:'dd/MM/yyyy HH:mm'}}
|
||||||
</vn-td>
|
</vn-td>
|
||||||
<vn-td shrink>
|
<vn-td shrink>
|
||||||
<a target="_blank"
|
<a target="_blank"
|
||||||
href="api/dms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
|
href="api/workerDms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
|
||||||
<vn-icon-button
|
<vn-icon-button
|
||||||
icon="cloud_download"
|
icon="cloud_download"
|
||||||
title="{{'Download file' | translate}}">
|
title="{{'Download file' | translate}}">
|
||||||
|
@ -57,7 +56,7 @@
|
||||||
</a>
|
</a>
|
||||||
</vn-td>
|
</vn-td>
|
||||||
<vn-td shrink>
|
<vn-td shrink>
|
||||||
<vn-icon-button ui-sref="worker.card.dms.edit({dmsId: {{::document.dmsFk}}})"
|
<vn-icon-button ui-sref="worker.card.edit({dmsId: {{::document.dmsFk}}})"
|
||||||
icon="edit"
|
icon="edit"
|
||||||
title="{{'Edit file' | translate}}">
|
title="{{'Edit file' | translate}}">
|
||||||
</vn-icon-button>
|
</vn-icon-button>
|
||||||
|
@ -78,7 +77,7 @@
|
||||||
<vn-worker-descriptor-popover
|
<vn-worker-descriptor-popover
|
||||||
vn-id="workerDescriptor">
|
vn-id="workerDescriptor">
|
||||||
</vn-worker-descriptor-popover>
|
</vn-worker-descriptor-popover>
|
||||||
<a ui-sref="worker.card.dms.create"
|
<a ui-sref="worker.card.create"
|
||||||
vn-tooltip="Upload file"
|
vn-tooltip="Upload file"
|
||||||
vn-bind="+"
|
vn-bind="+"
|
||||||
fixed-bottom-right>
|
fixed-bottom-right>
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
"state": "worker.card.dms.index",
|
"state": "worker.card.dms.index",
|
||||||
"component": "vn-worker-dms-index",
|
"component": "vn-worker-dms-index",
|
||||||
"description": "My documentation",
|
"description": "My documentation",
|
||||||
"acl": ["hr"]
|
"acl": ["employee"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "/create",
|
"url": "/create",
|
||||||
|
|
Loading…
Reference in New Issue