Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2661-travel_cloneWithEntries
This commit is contained in:
commit
a3f17ae302
|
@ -69,13 +69,13 @@ pipeline {
|
|||
}
|
||||
}
|
||||
}
|
||||
/* stage('Backend') {
|
||||
steps {
|
||||
nodejs('node-lts') {
|
||||
sh 'gulp launchBackTest --ci'
|
||||
}
|
||||
}
|
||||
} */
|
||||
// stage('Backend') {
|
||||
// steps {
|
||||
// nodejs('node-lts') {
|
||||
// sh 'gulp launchBackTest --ci'
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
stage('Build') {
|
||||
|
|
|
@ -300,9 +300,14 @@ let actions = {
|
|||
},
|
||||
|
||||
waitForNumberOfElements: async function(selector, count) {
|
||||
return await this.waitForFunction((selector, count) => {
|
||||
try {
|
||||
await this.waitForFunction((selector, count) => {
|
||||
return document.querySelectorAll(selector).length == count;
|
||||
}, {}, selector, count);
|
||||
} catch (error) {
|
||||
const amount = await this.countElement(selector);
|
||||
throw new Error(`actual amount of elements was: ${amount} instead of ${count}, ${error}`);
|
||||
}
|
||||
},
|
||||
|
||||
waitForClassNotPresent: async function(selector, className) {
|
||||
|
|
|
@ -674,7 +674,14 @@ export default {
|
|||
confirmButton: '.vn-confirm.shown button[response="accept"]',
|
||||
},
|
||||
routeIndex: {
|
||||
addNewRouteButton: 'vn-route-index a[ui-sref="route.create"]'
|
||||
anyResult: 'vn-table a',
|
||||
firstRouteCheckbox: 'a:nth-child(1) vn-td:nth-child(1) > vn-check',
|
||||
addNewRouteButton: 'vn-route-index a[ui-sref="route.create"]',
|
||||
cloneButton: 'vn-route-index button > vn-icon[icon="icon-clone"]',
|
||||
submitClonationButton: 'tpl-buttons > button[response="accept"]',
|
||||
openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
|
||||
searchAgencyAutocomlete: 'vn-route-search-panel vn-autocomplete[ng-model="filter.agencyModeFk"]',
|
||||
advancedSearchButton: 'vn-route-search-panel button[type=submit]',
|
||||
},
|
||||
createRouteView: {
|
||||
worker: 'vn-route-create vn-autocomplete[ng-model="$ctrl.route.workerFk"]',
|
||||
|
|
|
@ -57,5 +57,36 @@ describe('Route create path', () => {
|
|||
it(`should confirm the redirection to the created route summary`, async() => {
|
||||
await page.waitForState('route.card.summary');
|
||||
});
|
||||
|
||||
it(`should navigate back to the route index`, async() => {
|
||||
await page.waitToClick(selectors.globalItems.returnToModuleIndexButton);
|
||||
await page.waitForState('route.index');
|
||||
});
|
||||
|
||||
let count;
|
||||
it(`should count the amount of routes before clonation`, async() => {
|
||||
await page.waitForFunction(selector => {
|
||||
return document.querySelectorAll(selector).length > 6;
|
||||
}, {}, selectors.routeIndex.anyResult);
|
||||
count = await page.countElement(selectors.routeIndex.anyResult);
|
||||
|
||||
expect(count).toBeGreaterThanOrEqual(7);
|
||||
});
|
||||
|
||||
it(`should clone the first route`, async() => {
|
||||
await page.waitToClick(selectors.routeIndex.firstRouteCheckbox);
|
||||
await page.waitToClick(selectors.routeIndex.cloneButton);
|
||||
await page.waitToClick(selectors.routeIndex.submitClonationButton);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('Data saved!');
|
||||
});
|
||||
|
||||
it(`should reload the section and count the amount of routes after clonation`, async() => {
|
||||
await page.waitForNumberOfElements(selectors.routeIndex.anyResult, count + 1);
|
||||
const result = await page.countElement(selectors.routeIndex.anyResult);
|
||||
|
||||
expect(result).toEqual(count + 1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -145,9 +145,15 @@ module.exports = function(Self) {
|
|||
rewriteDbError(replaceErrFunc) {
|
||||
function replaceErr(err, replaceErrFunc) {
|
||||
if (Array.isArray(err)) {
|
||||
const errors = err.filter(error => {
|
||||
return error != undefined && error != null;
|
||||
});
|
||||
let errs = [];
|
||||
for (let e of err)
|
||||
for (let e of errors) {
|
||||
if (!(e instanceof UserError))
|
||||
errs.push(replaceErrFunc(e));
|
||||
else errs.push(e);
|
||||
}
|
||||
return errs;
|
||||
}
|
||||
return replaceErrFunc(err);
|
||||
|
|
|
@ -246,3 +246,98 @@ exports.initialize = function initialize(dataSource, callback) {
|
|||
dataSource.connector.connect(callback);
|
||||
}
|
||||
};
|
||||
|
||||
MySQL.prototype.connect = function(callback) {
|
||||
const self = this;
|
||||
const options = generateOptions(this.settings);
|
||||
|
||||
if (this.client) {
|
||||
if (callback) {
|
||||
process.nextTick(function() {
|
||||
callback(null, self.client);
|
||||
});
|
||||
}
|
||||
} else
|
||||
this.client = connectionHandler(options, callback);
|
||||
|
||||
function connectionHandler(options, callback) {
|
||||
const client = mysql.createPool(options);
|
||||
client.getConnection(function(err, connection) {
|
||||
const conn = connection;
|
||||
if (!err) {
|
||||
if (self.debug)
|
||||
debug('MySQL connection is established: ' + self.settings || {});
|
||||
|
||||
connection.release();
|
||||
} else {
|
||||
if (err.code == 'ECONNREFUSED' || err.code == 'PROTOCOL_CONNECTION_LOST') { // PROTOCOL_CONNECTION_LOST
|
||||
console.error(`MySQL connection lost (${err.code}). Retrying..`);
|
||||
|
||||
return setTimeout(() =>
|
||||
connectionHandler(options, callback), 5000);
|
||||
}
|
||||
if (self.debug || !callback)
|
||||
console.error('MySQL connection is failed: ' + self.settings || {}, err);
|
||||
}
|
||||
callback && callback(err, conn);
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
};
|
||||
|
||||
function generateOptions(settings) {
|
||||
const s = settings || {};
|
||||
if (s.collation) {
|
||||
// Charset should be first 'chunk' of collation.
|
||||
s.charset = s.collation.substr(0, s.collation.indexOf('_'));
|
||||
} else {
|
||||
s.collation = 'utf8_general_ci';
|
||||
s.charset = 'utf8';
|
||||
}
|
||||
|
||||
s.supportBigNumbers = (s.supportBigNumbers || false);
|
||||
s.timezone = (s.timezone || 'local');
|
||||
|
||||
if (isNaN(s.connectionLimit))
|
||||
s.connectionLimit = 10;
|
||||
|
||||
let options;
|
||||
if (s.url) {
|
||||
// use url to override other settings if url provided
|
||||
options = s.url;
|
||||
} else {
|
||||
options = {
|
||||
host: s.host || s.hostname || 'localhost',
|
||||
port: s.port || 3306,
|
||||
user: s.username || s.user,
|
||||
password: s.password,
|
||||
timezone: s.timezone,
|
||||
socketPath: s.socketPath,
|
||||
charset: s.collation.toUpperCase(), // Correct by docs despite seeming odd.
|
||||
supportBigNumbers: s.supportBigNumbers,
|
||||
connectionLimit: s.connectionLimit,
|
||||
};
|
||||
|
||||
// Don't configure the DB if the pool can be used for multiple DBs
|
||||
if (!s.createDatabase)
|
||||
options.database = s.database;
|
||||
|
||||
// Take other options for mysql driver
|
||||
// See https://github.com/strongloop/loopback-connector-mysql/issues/46
|
||||
for (const p in s) {
|
||||
if (p === 'database' && s.createDatabase)
|
||||
continue;
|
||||
|
||||
if (options[p] === undefined)
|
||||
options[p] = s[p];
|
||||
}
|
||||
// Legacy UTC Date Processing fallback - SHOULD BE TRANSITIONAL
|
||||
if (s.legacyUtcDateProcessing === undefined)
|
||||
s.legacyUtcDateProcessing = true;
|
||||
|
||||
if (s.legacyUtcDateProcessing)
|
||||
options.timezone = 'Z';
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
module.exports = Self => {
|
||||
Self.remoteMethod('clone', {
|
||||
description: 'Clones the selected routes',
|
||||
accessType: 'WRITE',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'ids',
|
||||
type: ['number'],
|
||||
required: true,
|
||||
description: 'The routes ids to clone'
|
||||
},
|
||||
{
|
||||
arg: 'started',
|
||||
type: 'date',
|
||||
required: true,
|
||||
description: 'The started date for all routes'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: ['Object'],
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/clone`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.clone = async(ids, started) => {
|
||||
const tx = await Self.beginTransaction({});
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
const originalRoutes = await Self.find({
|
||||
where: {id: {inq: ids}},
|
||||
fields: ['workerFk', 'agencyModeFk', 'vehicleFk', 'description']
|
||||
}, options);
|
||||
|
||||
if (ids.length != originalRoutes.length)
|
||||
throw new Error(`The amount of routes found don't match`);
|
||||
|
||||
const routes = originalRoutes.map(route => {
|
||||
route.started = started;
|
||||
route.created = new Date();
|
||||
return route;
|
||||
});
|
||||
|
||||
const clones = await Self.create(routes, options);
|
||||
|
||||
await tx.commit();
|
||||
return clones;
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
|
||||
describe('route clone()', () => {
|
||||
const startDate = new Date();
|
||||
it('should throw an error if the amount of ids pased to the clone function do no match the database', async() => {
|
||||
const ids = [996, 997, 998, 999];
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await app.models.Route.clone(ids, startDate);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toEqual(`The amount of routes found don't match`);
|
||||
});
|
||||
|
||||
it('should clone two routes', async() => {
|
||||
const ids = [1, 2];
|
||||
|
||||
const clones = await app.models.Route.clone(ids, startDate);
|
||||
|
||||
expect(clones.length).toEqual(2);
|
||||
|
||||
// restores
|
||||
for (const clone of clones)
|
||||
await app.models.Route.destroyById(clone.id);
|
||||
});
|
||||
});
|
|
@ -6,6 +6,7 @@ module.exports = Self => {
|
|||
require('../methods/route/updateVolume')(Self);
|
||||
require('../methods/route/getDeliveryPoint')(Self);
|
||||
require('../methods/route/insertTicket')(Self);
|
||||
require('../methods/route/clone')(Self);
|
||||
|
||||
Self.validate('kmStart', validateDistance, {
|
||||
message: 'Distance must be lesser than 1000'
|
||||
|
|
|
@ -3,3 +3,5 @@ Date started: Fecha inicio
|
|||
Km start: Km de inicio
|
||||
Km end: Km de fin
|
||||
Description: Descripción
|
||||
Hour started: Hora inicio
|
||||
Hour finished: Hora fin
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
</vn-td>
|
||||
<vn-td>{{::route.agencyName | dashIfEmpty}}</vn-td>
|
||||
<vn-td>{{::route.vehiclePlateNumber | dashIfEmpty}}</vn-td>
|
||||
<vn-td expand>{{::route.created | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
|
||||
<vn-td expand>{{::route.created | dashIfEmpty | date:'dd/MM/yyyy'}}</vn-td>
|
||||
<vn-td number>{{::route.m3 | dashIfEmpty}}</vn-td>
|
||||
<vn-td>{{::route.description | dashIfEmpty}}</vn-td>
|
||||
<vn-td>
|
||||
|
@ -59,19 +59,26 @@
|
|||
</vn-table>
|
||||
</vn-card>
|
||||
</vn-data-viewer>
|
||||
|
||||
<vn-popup vn-id="summary">
|
||||
<vn-route-summary
|
||||
route="$ctrl.routeSelected">
|
||||
</vn-route-summary>
|
||||
</vn-popup>
|
||||
|
||||
<vn-worker-descriptor-popover
|
||||
vn-id="workerDescriptor">
|
||||
</vn-worker-descriptor-popover>
|
||||
|
||||
|
||||
</vn-data-viewer>
|
||||
<div fixed-bottom-right>
|
||||
<vn-vertical style="align-items: center;">
|
||||
<vn-button class="round sm vn-mb-sm"
|
||||
icon="icon-clone"
|
||||
ng-show="$ctrl.totalChecked > 0"
|
||||
ng-click="$ctrl.openClonationDialog()"
|
||||
vn-tooltip="Clone selected routes"
|
||||
tooltip-position="left">
|
||||
</vn-button>
|
||||
<vn-button class="round sm vn-mb-sm"
|
||||
icon="cloud_download"
|
||||
ng-show="$ctrl.totalChecked > 0"
|
||||
|
@ -79,7 +86,6 @@
|
|||
vn-tooltip="Download selected routes as PDF"
|
||||
tooltip-position="left">
|
||||
</vn-button>
|
||||
|
||||
<a ui-sref="route.create" vn-bind="+">
|
||||
<vn-button class="round md vn-mb-sm"
|
||||
icon="add"
|
||||
|
@ -89,3 +95,23 @@
|
|||
</a>
|
||||
</vn-vertical>
|
||||
</div>
|
||||
|
||||
<!-- Clonation dialog -->
|
||||
<vn-dialog class="edit"
|
||||
vn-id="clonationDialog"
|
||||
on-accept="$ctrl.cloneSelectedRoutes()"
|
||||
message="Select the starting date">
|
||||
<tpl-body>
|
||||
<vn-horizontal>
|
||||
<vn-date-picker
|
||||
label="Starting date"
|
||||
ng-model="$ctrl.startedDate"
|
||||
required="true">
|
||||
</vn-date-picker>
|
||||
</vn-horizontal>
|
||||
</tpl-body>
|
||||
<tpl-buttons>
|
||||
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
|
||||
<button response="accept" translate>Clone</button>
|
||||
</tpl-buttons>
|
||||
</vn-dialog>
|
|
@ -40,6 +40,22 @@ export default class Controller extends Section {
|
|||
});
|
||||
}
|
||||
|
||||
openClonationDialog() {
|
||||
this.startedDate = new Date();
|
||||
this.$.clonationDialog.show();
|
||||
}
|
||||
|
||||
cloneSelectedRoutes() {
|
||||
const routesIds = [];
|
||||
for (let route of this.checked)
|
||||
routesIds.push(route.id);
|
||||
|
||||
return this.$http.post('Routes/clone', {ids: routesIds, started: this.startedDate}).then(() => {
|
||||
this.$.model.refresh();
|
||||
this.vnApp.showSuccess(this.$t('Data saved!'));
|
||||
});
|
||||
}
|
||||
|
||||
onDrop($event) {
|
||||
const target = $event.target;
|
||||
const droppable = target.closest(this.droppableElement);
|
||||
|
|
|
@ -60,6 +60,16 @@ describe('Component vnRouteIndex', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('cloneSelectedRoutes()', () => {
|
||||
it('should perform an http request to Routes/clone', () => {
|
||||
controller.startedDate = new Date();
|
||||
|
||||
$httpBackend.expect('POST', 'Routes/clone').respond();
|
||||
controller.cloneSelectedRoutes();
|
||||
$httpBackend.flush();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onDrop()', () => {
|
||||
it('should call the insert method when dragging a ticket number', () => {
|
||||
jest.spyOn(controller, 'insert');
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
Vehicle: Vehículo
|
||||
Download selected routes as PDF: Descargar rutas seleccionadas como PDF
|
||||
Clone selected routes: Clonar rutas seleccionadas
|
||||
Select the starting date: Seleccione fecha de inicio
|
||||
Starting date: Fecha de inicio
|
Loading…
Reference in New Issue