3900-client_credit #946
|
@ -2271,8 +2271,14 @@ INSERT INTO `vn`.`dms`(`id`, `dmsTypeFk`, `file`, `contentType`, `workerFk`, `wa
|
|||
(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()),
|
||||
(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());
|
||||
(6, 5, '6.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'NotExists', 'DoesNotExists', CURDATE()),
|
||||
(7, 20, '7.jpg', 'image/jpeg', 9, 1, 442, NULL, FALSE, '1', 'TICKET ID DEL CLIENTE BRUCE WAYNE ID 1101', CURDATE()),
|
||||
(8, 20, '8.mp4', 'video/mp4', 9, 1, 442, NULL, FALSE, '1', 'TICKET ID DEL CLIENTE BRUCE WAYNE ID 1101', CURDATE());
|
||||
|
||||
INSERT INTO `vn`.`claimDms`(`claimFk`, `dmsFk`)
|
||||
VALUES
|
||||
(1, 7),
|
||||
(1, 8);
|
||||
|
||||
INSERT INTO `vn`.`ticketDms`(`ticketFk`, `dmsFk`)
|
||||
VALUES
|
||||
|
|
|
@ -39,7 +39,8 @@
|
|||
"multipart/x-zip",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/jpg"
|
||||
"image/jpg",
|
||||
"video/mp4"
|
||||
]
|
||||
},
|
||||
"dmsStorage": {
|
||||
|
@ -84,5 +85,18 @@
|
|||
"application/octet-stream",
|
||||
"application/pdf"
|
||||
]
|
||||
},
|
||||
"claimStorage": {
|
||||
"name": "claimStorage",
|
||||
"connector": "loopback-component-storage",
|
||||
"provider": "filesystem",
|
||||
"root": "./storage/dms",
|
||||
"maxFileSize": "31457280",
|
||||
"allowedContentTypes": [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"video/mp4"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('downloadFile', {
|
||||
description: 'Get the claim file',
|
||||
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) {
|
||||
const models = Self.app.models;
|
||||
const ClaimContainer = models.ClaimContainer;
|
||||
const dms = await models.Dms.findById(id);
|
||||
const pathHash = ClaimContainer.getHash(dms.id);
|
||||
try {
|
||||
await ClaimContainer.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 = ClaimContainer.downloadStream(pathHash, dms.file);
|
||||
|
||||
return [stream, dms.contentType, `filename="${dms.file}"`];
|
||||
};
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
|
||||
describe('claim downloadFile()', () => {
|
||||
const dmsId = 7;
|
||||
|
||||
it('should return a response for an employee with image content-type', async() => {
|
||||
const workerId = 1107;
|
||||
const ctx = {req: {accessToken: {userId: workerId}}};
|
||||
const result = await app.models.Claim.downloadFile(ctx, dmsId);
|
||||
|
||||
expect(result[1]).toEqual('image/jpeg');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
|
||||
describe('claim uploadFile()', () => {
|
||||
it(`should return an error for a user without enough privileges`, async() => {
|
||||
const clientId = 1101;
|
||||
const ticketDmsTypeId = 14;
|
||||
const ctx = {req: {accessToken: {userId: clientId}}, args: {dmsTypeId: ticketDmsTypeId}};
|
||||
|
||||
let error;
|
||||
await app.models.Claim.uploadFile(ctx).catch(e => {
|
||||
error = e;
|
||||
}).finally(() => {
|
||||
expect(error.message).toEqual(`You don't have enough privileges`);
|
||||
});
|
||||
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -1,6 +1,10 @@
|
|||
const UserError = require('vn-loopback/util/user-error');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('uploadFile', {
|
||||
description: 'Upload and attach a document',
|
||||
description: 'Upload and attach a file',
|
||||
accessType: 'WRITE',
|
||||
accepts: [{
|
||||
arg: 'id',
|
||||
|
@ -53,22 +57,54 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
Self.uploadFile = async(ctx, id, options) => {
|
||||
let tx;
|
||||
const tx = await Self.beginTransaction({});
|
||||
const myOptions = {};
|
||||
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
if (!myOptions.transaction) {
|
||||
tx = await Self.beginTransaction({});
|
||||
if (!myOptions.transaction)
|
||||
myOptions.transaction = tx;
|
||||
}
|
||||
|
||||
const models = Self.app.models;
|
||||
const promises = [];
|
||||
const TempContainer = models.TempContainer;
|
||||
const ClaimContainer = models.ClaimContainer;
|
||||
const fileOptions = {};
|
||||
const args = ctx.args;
|
||||
|
||||
let srcFile;
|
||||
try {
|
||||
const uploadedFiles = await models.Dms.uploadFile(ctx, myOptions);
|
||||
uploadedFiles.forEach(dms => {
|
||||
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 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 uploadedFile of files) {
|
||||
const newDms = await createDms(ctx, uploadedFile, myOptions);
|
||||
const pathHash = ClaimContainer.getHash(newDms.id);
|
||||
|
||||
const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
|
||||
srcFile = path.join(file.client.root, file.container, file.name);
|
||||
|
||||
const claimContainer = await ClaimContainer.container(pathHash);
|
||||
const dstFile = path.join(claimContainer.client.root, pathHash, newDms.file);
|
||||
|
||||
await fs.move(srcFile, dstFile, {
|
||||
overwrite: true
|
||||
});
|
||||
|
||||
addedDms.push(newDms);
|
||||
}
|
||||
|
||||
addedDms.forEach(dms => {
|
||||
const newClaimDms = models.ClaimDms.create({
|
||||
claimFk: id,
|
||||
dmsFk: dms.id
|
||||
|
@ -83,7 +119,34 @@ module.exports = Self => {
|
|||
return resolvedPromises;
|
||||
} 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 myUserId = ctx.req.accessToken.userId;
|
||||
const args = ctx.args;
|
||||
|
||||
const newDms = await models.Dms.create({
|
||||
workerFk: myUserId,
|
||||
dmsTypeFk: args.dmsTypeId,
|
||||
companyFk: args.companyId,
|
||||
warehouseFk: args.warehouseId,
|
||||
reference: args.reference,
|
||||
description: args.description,
|
||||
contentType: file.type,
|
||||
hasFile: args.hasFile
|
||||
}, myOptions);
|
||||
|
||||
let fileName = file.name;
|
||||
const extension = models.DmsContainer.getFileExtension(fileName);
|
||||
fileName = `${newDms.id}.${extension}`;
|
||||
|
||||
return newDms.updateAttribute('file', fileName, myOptions);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -37,5 +37,8 @@
|
|||
},
|
||||
"ClaimLog": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"ClaimContainer": {
|
||||
"dataSource": "claimStorage"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "ClaimContainer",
|
||||
"base": "Container",
|
||||
"acls": [{
|
||||
"accessType": "READ",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
}]
|
||||
}
|
|
@ -7,4 +7,5 @@ module.exports = Self => {
|
|||
require('../methods/claim/uploadFile')(Self);
|
||||
require('../methods/claim/updateClaimAction')(Self);
|
||||
require('../methods/claim/isEditable')(Self);
|
||||
require('../methods/claim/downloadFile')(Self);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<vn-crud-model
|
||||
vn-id="model"
|
||||
auto-load="true"
|
||||
filter="::$ctrl.filter"
|
||||
url="ClaimDms"
|
||||
link="{claimFk: $ctrl.$params.id}"
|
||||
limit="20"
|
||||
|
@ -14,8 +15,13 @@
|
|||
<section class="photo" ng-repeat="photo in $ctrl.photos">
|
||||
<section class="image vn-shadow" on-error-src
|
||||
ng-style="{'background': 'url(' + $ctrl.getImagePath(photo.dmsFk) + ')'}"
|
||||
zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}">
|
||||
zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}"
|
||||
ng-if="photo.dms.contentType != 'video/mp4'">
|
||||
</section>
|
||||
<video id="videobcg" muted="muted" controls ng-if="photo.dms.contentType == 'video/mp4'"
|
||||
class="video">
|
||||
<source src="{{$ctrl.getImagePath(photo.dmsFk)}}" type="video/mp4">
|
||||
</video>
|
||||
<section class="actions">
|
||||
<vn-button
|
||||
class="round"
|
||||
|
@ -35,7 +41,7 @@
|
|||
</vn-confirm>
|
||||
<vn-float-button
|
||||
icon="add"
|
||||
vn-tooltip="Select photo"
|
||||
vn-tooltip="Select file"
|
||||
vn-bind="+"
|
||||
ng-click="$ctrl.openUploadDialog()"
|
||||
fixed-bottom-right>
|
||||
|
|
|
@ -6,6 +6,13 @@ class Controller extends Section {
|
|||
constructor($element, $, vnFile) {
|
||||
super($element, $);
|
||||
this.vnFile = vnFile;
|
||||
this.filter = {
|
||||
include: [
|
||||
{
|
||||
relation: 'dms'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
deleteDms(index) {
|
||||
|
@ -13,7 +20,7 @@ class Controller extends Section {
|
|||
return this.$http.post(`ClaimDms/${dmsFk}/removeFile`)
|
||||
.then(() => {
|
||||
this.$.model.remove(index);
|
||||
this.vnApp.showSuccess(this.$t('Photo deleted'));
|
||||
this.vnApp.showSuccess(this.$t('File deleted'));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -81,13 +88,13 @@ class Controller extends Section {
|
|||
data: this.dms.files
|
||||
};
|
||||
this.$http(options).then(() => {
|
||||
this.vnApp.showSuccess(this.$t('Photo uploaded!'));
|
||||
this.vnApp.showSuccess(this.$t('File uploaded!'));
|
||||
this.$.model.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
getImagePath(dmsId) {
|
||||
return this.vnFile.getPath(`/api/dms/${dmsId}/downloadFile`);
|
||||
return this.vnFile.getPath(`/api/Claims/${dmsId}/downloadFile`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Are you sure you want to continue?: ¿Seguro que quieres continuar?
|
||||
Drag & Drop photos here...: Arrastra y suelta fotos aquí...
|
||||
Photo deleted: Foto eliminada
|
||||
Photo uploaded!: Foto subida!
|
||||
Select photo: Seleccionar foto
|
||||
File deleted: Archivo eliminado
|
||||
File uploaded!: Archivo subido!
|
||||
Select file: Seleccionar fichero
|
|
@ -29,4 +29,19 @@ vn-claim-photos {
|
|||
height: 288px;
|
||||
}
|
||||
}
|
||||
|
||||
.video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),
|
||||
0 3px 1px -2px rgba(0,0,0,.2),
|
||||
0 1px 5px 0 rgba(0,0,0,.12);
|
||||
border: 2px solid transparent;
|
||||
|
||||
}
|
||||
.video:hover {
|
||||
border: 2px solid $color-primary
|
||||
}
|
||||
}
|
|
@ -33,7 +33,6 @@ describe('Address updateAddress', () => {
|
|||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toEqual('Incoterms is required for a non UEE member');
|
||||
});
|
||||
|
||||
|
@ -57,7 +56,6 @@ describe('Address updateAddress', () => {
|
|||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toEqual('Customs agent is required for a non UEE member');
|
||||
});
|
||||
|
||||
|
@ -91,6 +89,8 @@ describe('Address updateAddress', () => {
|
|||
it('should return an error for a user without enough privileges', async() => {
|
||||
const tx = await models.Client.beginTransaction({});
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
ctx.args = {
|
||||
|
@ -124,8 +124,10 @@ describe('Address updateAddress', () => {
|
|||
expect(address.isLogifloraAllowed).toEqual(true);
|
||||
|
||||
await tx.rollback();
|
||||
ctx.req.accessToken.userId = employeeId;
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
ctx.req.accessToken.userId = employeeId;
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -89,11 +89,12 @@ module.exports = function(Self) {
|
|||
const args = ctx.args;
|
||||
const userId = ctx.req.accessToken.userId;
|
||||
const myOptions = {};
|
||||
const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', myOptions);
|
||||
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', myOptions);
|
||||
|
||||
if (args.isLogifloraAllowed && !isSalesAssistant)
|
||||
throw new UserError(`You don't have enough privileges`);
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
|
||||
const {models} = require('vn-loopback/server/server');
|
||||
describe('item lastEntriesFilter()', () => {
|
||||
const minDate = new Date(value);
|
||||
minHour.setHours(0, 0, 0, 0);
|
||||
const maxDate = new Date(value);
|
||||
maxHour.setHours(23, 59, 59, 59);
|
||||
it('should return one entry for the given item', async() => {
|
||||
const minDate = new Date();
|
||||
minDate.setHours(0, 0, 0, 0);
|
||||
const maxDate = new Date();
|
||||
maxDate.setHours(23, 59, 59, 59);
|
||||
|
||||
it('should return one entry for a given item', async() => {
|
||||
const tx = await models.Item.beginTransaction({});
|
||||
const options = {transaction: tx};
|
||||
|
||||
|
@ -23,13 +22,18 @@ describe('item lastEntriesFilter()', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('should return five entries for a given item', async() => {
|
||||
it('should return five entries for the given item', async() => {
|
||||
const minDate = new Date();
|
||||
minDate.setHours(0, 0, 0, 0);
|
||||
minDate.setMonth(minDate.getMonth() - 2, 1);
|
||||
|
||||
const maxDate = new Date();
|
||||
maxDate.setHours(23, 59, 59, 59);
|
||||
|
||||
const tx = await models.Item.beginTransaction({});
|
||||
const options = {transaction: tx};
|
||||
|
||||
try {
|
||||
minDate.setMonth(minDate.getMonth() - 2, 1);
|
||||
|
||||
const filter = {where: {itemFk: 1, landed: {between: [minDate, maxDate]}}};
|
||||
const result = await models.Item.lastEntriesFilter(filter, options);
|
||||
|
||||
|
|
|
@ -304,7 +304,7 @@ module.exports = Self => {
|
|||
{'tp.hasTicketRequest': true},
|
||||
{'tp.hasComponentLack': true},
|
||||
{'tp.isTaxDataChecked': false},
|
||||
{'tp.isAvailable': false}
|
||||
{'tp.itemShortage': {neq: null}}
|
||||
]};
|
||||
} else if (hasProblems === false) {
|
||||
whereProblems = {and: [
|
||||
|
@ -313,7 +313,7 @@ module.exports = Self => {
|
|||
{'tp.hasTicketRequest': false},
|
||||
{'tp.hasComponentLack': false},
|
||||
{'tp.isTaxDataChecked': true},
|
||||
{'tp.isAvailable': true}
|
||||
{'tp.itemShortage': null}
|
||||
]};
|
||||
}
|
||||
|
||||
|
|
|
@ -98,8 +98,8 @@
|
|||
icon="icon-buyrequest">
|
||||
</vn-icon>
|
||||
<vn-icon
|
||||
ng-show="::ticket.isAvailable === 0"
|
||||
translate-attr="{title: 'Not available'}"
|
||||
ng-show="::ticket.itemShortage"
|
||||
translate-attr="{title: 'Not visible'}"
|
||||
class="bright"
|
||||
icon="icon-unavailable">
|
||||
</vn-icon>
|
||||
|
|
|
@ -324,7 +324,7 @@ module.exports = Self => {
|
|||
case true:
|
||||
condition = `or`;
|
||||
hasProblem = true;
|
||||
range = 0;
|
||||
range = {neq: null};
|
||||
hasWhere = true;
|
||||
break;
|
||||
|
||||
|
@ -340,7 +340,7 @@ module.exports = Self => {
|
|||
{'tp.isFreezed': hasProblem},
|
||||
{'tp.risk': hasProblem},
|
||||
{'tp.hasTicketRequest': hasProblem},
|
||||
{'tp.isAvailable': range}
|
||||
{'tp.itemShortage': range}
|
||||
]};
|
||||
|
||||
if (hasWhere)
|
||||
|
|
|
@ -94,7 +94,7 @@ module.exports = Self => {
|
|||
sale.visible = itemStock.visible;
|
||||
sale.claim = claimedSales.get(sale.id);
|
||||
if (problems) {
|
||||
sale.isAvailable = problems.isAvailable;
|
||||
sale.itemShortage = problems.itemShortage;
|
||||
sale.hasTicketRequest = problems.hasTicketRequest;
|
||||
sale.hasComponentLack = problems.hasComponentLack;
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ describe('ticket filter()', () => {
|
|||
const filter = {};
|
||||
const result = await models.Ticket.filter(ctx, filter, options);
|
||||
|
||||
expect(result.length).toEqual(4);
|
||||
expect(result.length).toEqual(6);
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
|
|
|
@ -52,8 +52,8 @@
|
|||
icon="icon-buyrequest">
|
||||
</vn-icon>
|
||||
<vn-icon
|
||||
ng-show="::ticket.isAvailable === 0"
|
||||
translate-attr="{title: 'Not available'}"
|
||||
ng-show="::ticket.itemShortage"
|
||||
translate-attr="{title: 'Not visible'}"
|
||||
class="bright"
|
||||
icon="icon-unavailable">
|
||||
</vn-icon>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
Weekly tickets: Tickets programados
|
||||
Go to lines: Ir a lineas
|
||||
Not available: No disponible
|
||||
Not visible: No visible
|
||||
Payment on account...: Pago a cuenta...
|
||||
Set as delivered and open delivery note(s): Marcar como servido/s y abrir albarán/es
|
||||
Closure: Cierre
|
||||
|
|
|
@ -95,8 +95,8 @@
|
|||
translate-attr="{title: 'Reserved'}">
|
||||
</vn-icon>
|
||||
<vn-icon
|
||||
ng-show="::sale.isAvailable === 0"
|
||||
translate-attr="{title: 'Not available'}"
|
||||
ng-show="::sale.itemShortage"
|
||||
translate-attr="{title: 'Not visible'}"
|
||||
class="bright"
|
||||
icon="icon-unavailable">
|
||||
</vn-icon>
|
||||
|
|
|
@ -157,8 +157,8 @@
|
|||
translate-attr="{title: 'Reserved'}">
|
||||
</vn-icon>
|
||||
<vn-icon
|
||||
ng-show="::sale.isAvailable === 0"
|
||||
translate-attr="{title: 'Not available'}"
|
||||
ng-show="::sale.itemShortage"
|
||||
translate-attr="{title: 'Not visible'}"
|
||||
class="bright"
|
||||
icon="icon-unavailable">
|
||||
</vn-icon>
|
||||
|
|
|
@ -20,7 +20,7 @@ class Controller extends Section {
|
|||
set sales(value) {
|
||||
this._sales = value;
|
||||
|
||||
if (value) this.applyVolumes();
|
||||
if (value && value.length) this.applyVolumes();
|
||||
}
|
||||
|
||||
applyVolumes() {
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 429 KiB After Width: | Height: | Size: 429 KiB |
Binary file not shown.
Loading…
Reference in New Issue