merge
gitea/salix/dev This commit looks good Details

This commit is contained in:
Bernat 2019-10-14 07:01:29 +02:00
commit e267bf93a4
261 changed files with 4687 additions and 4490 deletions

View File

@ -94,7 +94,7 @@ module.exports = Self => {
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}});
const myWorker = await models.Worker.findOne({where: {userFk: myUserId}}, myOptions);
const args = ctx.args;
const newDms = await Self.create({

View File

@ -0,0 +1,83 @@
ALTER TABLE `vn2008`.`department`
ADD COLUMN `parentFk` INT UNSIGNED NULL AFTER `sons`,
ADD COLUMN `path` VARCHAR(255) NULL AFTER `parentFk`,
CHANGE COLUMN `sons` `sons` DECIMAL(10,0) NOT NULL DEFAULT '0' ;
USE `vn`;
CREATE
OR REPLACE ALGORITHM = UNDEFINED
DEFINER = `root`@`%`
SQL SECURITY DEFINER
VIEW `department` AS
SELECT
`b`.`department_id` AS `id`,
`b`.`name` AS `name`,
`b`.`production` AS `isProduction`,
`b`.`parentFk` AS `parentFk`,
`b`.`path` AS `path`,
`b`.`lft` AS `lft`,
`b`.`rgt` AS `rgt`,
`b`.`isSelected` AS `isSelected`,
`b`.`depth` AS `depth`,
`b`.`sons` AS `sons`
FROM
`vn2008`.`department` `b`;
DROP TRIGGER IF EXISTS `vn2008`.`department_AFTER_DELETE`;
DELIMITER $$
USE `vn2008`$$
CREATE DEFINER = CURRENT_USER TRIGGER `vn2008`.`department_AFTER_DELETE`
AFTER DELETE ON `department` FOR EACH ROW
BEGIN
UPDATE vn.department_recalc SET isChanged = TRUE;
END$$
DELIMITER ;
DROP TRIGGER IF EXISTS `vn2008`.`department_BEFORE_INSERT`;
DELIMITER $$
USE `vn2008`$$
CREATE DEFINER = CURRENT_USER TRIGGER `vn2008`.`department_BEFORE_INSERT`
BEFORE INSERT ON `department` FOR EACH ROW
BEGIN
UPDATE vn.department_recalc SET isChanged = TRUE;
END$$
DELIMITER ;
DROP TRIGGER IF EXISTS `vn2008`.`department_AFTER_UPDATE`;
DELIMITER $$
USE `vn2008`$$
CREATE DEFINER = CURRENT_USER TRIGGER `vn2008`.`department_AFTER_UPDATE`
AFTER UPDATE ON `department` FOR EACH ROW
BEGIN
IF !(OLD.parentFk <=> NEW.parentFk) THEN
UPDATE vn.department_recalc SET isChanged = TRUE;
END IF;
END$$
DELIMITER ;
CREATE TABLE `vn`.`department_recalc` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`isChanged` TINYINT(4) NOT NULL,
PRIMARY KEY (`id`));
INSERT INTO `vn`.`department_recalc` (`id`, `isChanged`) VALUES ('1', '0');
ALTER TABLE `vn2008`.`department`
CHANGE COLUMN `lft` `lft` INT(11) NULL ,
CHANGE COLUMN `rgt` `rgt` INT(11) NULL ;
ALTER TABLE `vn2008`.`department`
DROP INDEX `rgt_UNIQUE` ,
DROP INDEX `lft_UNIQUE` ;
;
ALTER TABLE `vn2008`.`department`
ADD INDEX `lft_rgt_depth_idx` (`lft` ASC, `rgt` ASC, `depth` ASC);
;
UPDATE vn.department SET lft = NULL, rgt = NULL;

View File

@ -0,0 +1,37 @@
USE `vn`;
DROP procedure IF EXISTS `department_calcTree`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `department_calcTree`()
BEGIN
/**
* Calculates the #path, #lft, #rgt, #sons and #depth columns of
* the #department table. To build the tree, it uses the #parentFk
* column.
*/
DECLARE vIndex INT DEFAULT 0;
DECLARE vSons INT;
DROP TEMPORARY TABLE IF EXISTS tNestedTree;
CREATE TEMPORARY TABLE tNestedTree
SELECT id, path, lft, rgt, depth, sons
FROM department LIMIT 0;
SET max_sp_recursion_depth = 5;
CALL department_calcTreeRec(NULL, '/', 0, vIndex, vSons);
SET max_sp_recursion_depth = 0;
UPDATE department z
JOIN tNestedTree t ON t.id = z.id
SET z.path = t.path,
z.lft = t.lft,
z.rgt = t.rgt,
z.depth = t.depth,
z.sons = t.sons;
DROP TEMPORARY TABLE tNestedTree;
END$$
DELIMITER ;

View File

@ -0,0 +1,75 @@
USE `vn`;
DROP procedure IF EXISTS `department_calcTreeRec`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `department_calcTreeRec`(
vSelf INT,
vPath VARCHAR(255),
vDepth INT,
INOUT vIndex INT,
OUT vSons INT
)
BEGIN
/**
* Calculates and sets the #path, #lft, #rgt, #sons and #depth
* columns for all children of the passed node. Once calculated
* the last node rgt index and the number of sons are returned.
* To update it's children, this procedure calls itself recursively
* for each one.
*
* @vSelf The node identifier
* @vPath The initial path
* @vDepth The initial depth
* @vIndex The initial lft index
* @vSons The number of direct sons
*/
DECLARE vChildFk INT;
DECLARE vLft INT;
DECLARE vMySons INT;
DECLARE vDone BOOL;
DECLARE vChildren CURSOR FOR
SELECT id FROM department
WHERE (vSelf IS NULL AND parentFk IS NULL)
OR (vSelf IS NOT NULL AND parentFk = vSelf);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
SET vSons = 0;
OPEN vChildren;
myLoop: LOOP
SET vDone = FALSE;
FETCH vChildren INTO vChildFk;
IF vDone THEN
LEAVE myLoop;
END IF;
SET vIndex = vIndex + 1;
SET vLft = vIndex;
SET vSons = vSons + 1;
CALL department_calcTreeRec(
vChildFk,
CONCAT(vPath, vChildFk, '/'),
vDepth + 1,
vIndex,
vMySons
);
SET vIndex = vIndex + 1;
INSERT INTO tNestedTree
SET id = vChildFk,
path = vPath,
lft = vLft,
rgt = vIndex,
depth = vDepth,
sons = vMySons;
END LOOP;
CLOSE vChildren;
END$$
DELIMITER ;

View File

@ -0,0 +1,35 @@
USE `vn`;
DROP procedure IF EXISTS `department_doCalc`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `department_doCalc`()
proc: BEGIN
/**
* Recalculates the department tree.
*/
DECLARE vIsChanged BOOL;
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
DO RELEASE_LOCK('vn.department_doCalc');
RESIGNAL;
END;
IF !GET_LOCK('vn.department_doCalc', 0) THEN
LEAVE proc;
END IF;
SELECT isChanged INTO vIsChanged
FROM department_recalc;
IF vIsChanged THEN
UPDATE department_recalc SET isChanged = FALSE;
CALL vn.department_calcTree;
END IF;
DO RELEASE_LOCK('vn.department_doCalc');
END$$
DELIMITER ;

View File

@ -0,0 +1,84 @@
USE `vn`;
DROP procedure IF EXISTS `department_getLeaves`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `department_getLeaves`(
vParentFk INT,
vSearch VARCHAR(255)
)
BEGIN
DECLARE vIsNumber BOOL;
DECLARE vIsSearch BOOL DEFAULT vSearch IS NOT NULL AND vSearch != '';
DROP TEMPORARY TABLE IF EXISTS tNodes;
CREATE TEMPORARY TABLE tNodes
(UNIQUE (id))
ENGINE = MEMORY
SELECT id FROM department LIMIT 0;
IF vIsSearch THEN
SET vIsNumber = vSearch REGEXP '^[0-9]+$';
INSERT INTO tNodes
SELECT id FROM department
WHERE (vIsNumber AND `name` = vSearch)
OR (!vIsNumber AND `name` LIKE CONCAT('%', vSearch, '%'))
LIMIT 1000;
END IF;
IF vParentFk IS NULL THEN
DROP TEMPORARY TABLE IF EXISTS tChilds;
CREATE TEMPORARY TABLE tChilds
ENGINE = MEMORY
SELECT id FROM tNodes;
DROP TEMPORARY TABLE IF EXISTS tParents;
CREATE TEMPORARY TABLE tParents
ENGINE = MEMORY
SELECT id FROM department LIMIT 0;
myLoop: LOOP
DELETE FROM tParents;
INSERT INTO tParents
SELECT parentFk id
FROM department g
JOIN tChilds c ON c.id = g.id
WHERE g.parentFk IS NOT NULL;
INSERT IGNORE INTO tNodes
SELECT id FROM tParents;
IF ROW_COUNT() = 0 THEN
LEAVE myLoop;
END IF;
DELETE FROM tChilds;
INSERT INTO tChilds
SELECT id FROM tParents;
END LOOP;
DROP TEMPORARY TABLE
tChilds,
tParents;
END IF;
IF !vIsSearch THEN
INSERT IGNORE INTO tNodes
SELECT id FROM department
WHERE parentFk <=> vParentFk;
END IF;
SELECT d.id,
d.`name`,
d.parentFk,
d.sons
FROM department d
JOIN tNodes n ON n.id = d.id
ORDER BY depth, `name`;
DROP TEMPORARY TABLE tNodes;
END$$
DELIMITER ;

View File

@ -471,7 +471,7 @@ INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeF
(14, 1, 2, 1, 3, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 104, 'Malibu Point', 4, NULL, 0, 9, CURDATE()),
(15, 1, 7, 1, 3, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 105, 'Plastic Cell', 125, NULL, 0, 3, CURDATE()),
(16, 1, 7, 1, 3, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 106, 'Many Places', 126, NULL, 0, 3, CURDATE()),
(17, 1, 7, 1, 3, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 106, 'Many Places', 126, NULL, 0, 3, CURDATE()),
(17, 1, 7, 2, 3, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 106, 'Many Places', 126, NULL, 0, 3, CURDATE()),
(18, 1, 4, 4, 3, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 108, 'Cerebro', 128, NULL, 0, 12, CURDATE()),
(19, 1, 5, 5, 3, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 109, 'Somewhere in Thailand', 129, NULL, 1, 13, CURDATE()),
(20, 1, 5, 5, 3, DATE_ADD(CURDATE(), INTERVAL +1 MONTH), DATE_ADD(DATE_ADD(CURDATE(),INTERVAL +1 MONTH), INTERVAL +1 DAY), 109, 'Somewhere in Thailand', 129, NULL, 0, 13, DATE_ADD(CURDATE(), INTERVAL +1 MONTH)),

View File

@ -1,6 +1,6 @@
export default {
vnTextfield: 'vn-textfield > div > div > div > input',
vnInputNumber: 'vn-input-number > div > div > div > input',
vnTextfield: 'vn-textfield input',
vnInputNumber: 'vn-input-number input',
vnSubmit: 'vn-submit > input',
vnFloatButton: 'vn-float-button > button'
};

View File

@ -18,8 +18,11 @@ let actions = {
clearInput: function(selector, done) {
this.wait(selector)
.evaluate(inputSelector => {
return document.querySelector(inputSelector).closest('*[model], *[field], *[value]').$ctrl.value = '';
.evaluate(selector => {
let $ctrl = document.querySelector(selector).closest('.vn-field').$ctrl;
$ctrl.field = null;
$ctrl.$.$apply();
$ctrl.input.dispatchEvent(new Event('change'));
}, selector)
.then(done)
.catch(done);
@ -31,6 +34,7 @@ let actions = {
let doLogin = () => {
this.wait(`vn-login input[name=user]`)
.clearInput(`vn-login input[name=user]`)
.write(`vn-login input[name=user]`, userName)
.write(`vn-login input[name=password]`, 'nightmare')
.click(`vn-login input[type=submit]`)
@ -75,7 +79,7 @@ let actions = {
},
changeLanguageToEnglish: function(done) {
let langSelector = '.user-configuration vn-autocomplete[field="$ctrl.lang"]';
let langSelector = '.user-popover vn-autocomplete[ng-model="$ctrl.lang"]';
this.waitToClick('#user')
.wait(langSelector)
@ -167,8 +171,8 @@ let actions = {
focusElement: function(selector, done) {
this.wait(selector)
.evaluate_now(elemenetSelector => {
let element = document.querySelector(elemenetSelector);
.evaluate_now(selector => {
let element = document.querySelector(selector);
element.focus();
}, done, selector)
.then(done)
@ -401,8 +405,7 @@ let actions = {
},
autocompleteSearch: function(autocompleteSelector, searchValue, done) {
this.wait(`${autocompleteSelector} input`)
.waitToClick(`${autocompleteSelector} input`)
this.waitToClick(`${autocompleteSelector} input`)
.write(`.vn-popover.shown .vn-drop-down input`, searchValue)
.waitToClick(`.vn-popover.shown .vn-drop-down li.active`)
.wait((autocompleteSelector, searchValue) => {
@ -412,7 +415,7 @@ let actions = {
}, autocompleteSelector, searchValue)
.then(done)
.catch(() => {
done(new Error(`.autocompleteSearch() for ${autocompleteSelector}, timed out`));
done(new Error(`.autocompleteSearch() for value ${searchValue} in ${autocompleteSelector} timed out`));
});
},
@ -427,25 +430,22 @@ let actions = {
.catch(done);
},
datePicker: function(datePickerSelector, changeMonth, day, done) {
this.wait(datePickerSelector)
.mousedown(datePickerSelector)
.wait('div.flatpickr-calendar.open');
datePicker: function(selector, changeMonth, day, done) {
this.wait(selector)
.mousedown(`${selector} input`)
.wait('.flatpickr-calendar.open');
if (changeMonth > 0)
this.mousedown('body > div.flatpickr-calendar.open > div.flatpickr-months > span.flatpickr-next-month > svg');
this.mousedown(`.flatpickr-calendar.open .flatpickr-next-month`);
if (changeMonth < 0)
this.mousedown('body > div.flatpickr-calendar.open > div.flatpickr-months > span.flatpickr-prev-month > svg');
this.mousedown(`.flatpickr-calendar.open .flatpickr-prev-month`);
let daySelector;
if (!day)
daySelector = 'div.flatpickr-calendar.open span.flatpickr-day:nth-child(16)';
daySelector = `.flatpickr-calendar.open .flatpickr-day:nth-child(16)`;
if (day)
daySelector = `span.flatpickr-day[aria-label~="${day},"]:not(.prevMonthDay):not(.nextMonthDay)`;
daySelector = `.flatpickr-calendar.open .flatpickr-day[aria-label~="${day},"]:not(.prevMonthDay):not(.nextMonthDay)`;
this.wait(selector => {
return document.querySelector(selector);

View File

@ -11,14 +11,14 @@ export default {
claimsButton: '.modules-menu > li[ui-sref="claim.index"]',
returnToModuleIndexButton: 'a[ui-sref="order.index"]',
userMenuButton: 'vn-topbar #user',
userLocalWarehouse: '.user-configuration vn-autocomplete[field="$ctrl.localWarehouseFk"]',
userLocalBank: '.user-configuration vn-autocomplete[field="$ctrl.localBankFk"]',
userLocalCompany: '.user-configuration vn-autocomplete[field="$ctrl.localCompanyFk"]',
userWarehouse: '.user-configuration vn-autocomplete[field="$ctrl.warehouseFk"]',
userCompany: '.user-configuration vn-autocomplete[field="$ctrl.companyFk"]',
userConfigFirstAutocompleteClear: '#localWarehouse > div > div > div > vn-icon.clear',
userConfigSecondAutocompleteClear: '#localBank > div > div > div > vn-icon.clear',
userConfigThirdAutocompleteClear: '#localCompany > div > div > div > vn-icon.clear',
userLocalWarehouse: '.user-popover vn-autocomplete[ng-model="$ctrl.localWarehouseFk"]',
userLocalBank: '.user-popover vn-autocomplete[ng-model="$ctrl.localBankFk"]',
userLocalCompany: '.user-popover vn-autocomplete[ng-model="$ctrl.localCompanyFk"]',
userWarehouse: '.user-popover vn-autocomplete[ng-model="$ctrl.warehouseFk"]',
userCompany: '.user-popover vn-autocomplete[ng-model="$ctrl.companyFk"]',
userConfigFirstAutocompleteClear: '#localWarehouse .icons > vn-icon[icon=clear]',
userConfigSecondAutocompleteClear: '#localBank .icons > vn-icon[icon=clear]',
userConfigThirdAutocompleteClear: '#localCompany .icons > vn-icon[icon=clear]',
acceptButton: 'vn-confirm button[response=ACCEPT]'
},
clientsIndex: {
@ -35,11 +35,11 @@ export default {
street: `${components.vnTextfield}[name="street"]`,
postcode: `${components.vnTextfield}[name="postcode"]`,
city: `${components.vnTextfield}[name="city"]`,
province: `vn-autocomplete[field="$ctrl.client.provinceFk"]`,
country: `vn-autocomplete[field="$ctrl.client.countryFk"]`,
province: `vn-autocomplete[ng-model="$ctrl.client.provinceFk"]`,
country: `vn-autocomplete[ng-model="$ctrl.client.countryFk"]`,
userName: `${components.vnTextfield}[name="userName"]`,
email: `${components.vnTextfield}[name="email"]`,
salesPersonAutocomplete: `vn-autocomplete[field="$ctrl.client.salesPersonFk"]`,
salesPersonAutocomplete: `vn-autocomplete[ng-model="$ctrl.client.salesPersonFk"]`,
createButton: `${components.vnSubmit}`,
cancelButton: 'vn-button[href="#!/client/index"]'
},
@ -49,13 +49,13 @@ export default {
},
clientBasicData: {
basicDataButton: 'vn-left-menu a[ui-sref="client.card.basicData"]',
nameInput: 'vn-textfield[field="$ctrl.client.name"] input',
contactInput: 'vn-textfield[field="$ctrl.client.contact"] input',
phoneInput: 'vn-textfield[field="$ctrl.client.phone"] input',
mobileInput: 'vn-textfield[field="$ctrl.client.mobile"] input',
emailInput: 'vn-textfield[field="$ctrl.client.email"] input',
salesPersonAutocomplete: 'vn-autocomplete[field="$ctrl.client.salesPersonFk"]',
channelAutocomplete: 'vn-autocomplete[field="$ctrl.client.contactChannelFk"]',
nameInput: 'vn-textfield[ng-model="$ctrl.client.name"] input',
contactInput: 'vn-textfield[ng-model="$ctrl.client.contact"] input',
phoneInput: 'vn-textfield[ng-model="$ctrl.client.phone"] input',
mobileInput: 'vn-textfield[ng-model="$ctrl.client.mobile"] input',
emailInput: 'vn-textfield[ng-model="$ctrl.client.email"] input',
salesPersonAutocomplete: 'vn-autocomplete[ng-model="$ctrl.client.salesPersonFk"]',
channelAutocomplete: 'vn-autocomplete[ng-model="$ctrl.client.contactChannelFk"]',
saveButton: `${components.vnSubmit}`
},
clientFiscalData: {
@ -67,8 +67,8 @@ export default {
addressInput: `${components.vnTextfield}[name="street"]`,
postcodeInput: `${components.vnTextfield}[name="postcode"]`,
cityInput: `${components.vnTextfield}[name="city"]`,
provinceAutocomplete: 'vn-autocomplete[field="$ctrl.client.provinceFk"]',
countryAutocomplete: 'vn-autocomplete[field="$ctrl.client.countryFk"]',
provinceAutocomplete: 'vn-autocomplete[ng-model="$ctrl.client.provinceFk"]',
countryAutocomplete: 'vn-autocomplete[ng-model="$ctrl.client.countryFk"]',
activeCheckbox: 'vn-check[label="Active"]',
frozenCheckbox: 'vn-check[label="Frozen"]',
invoiceByAddressCheckbox: 'vn-check[label="Invoice by address"]',
@ -79,14 +79,14 @@ export default {
saveButton: `${components.vnSubmit}`
},
clientBillingData: {
payMethodAutocomplete: 'vn-autocomplete[field="$ctrl.client.payMethodFk"]',
IBANInput: `${components.vnTextfield}[name="iban"]`,
dueDayInput: `${components.vnInputNumber}[name="dueDay"]`,
receivedCoreLCRCheckbox: 'vn-check[label="Received LCR"]',
receivedCoreVNLCheckbox: 'vn-check[label="Received core VNL"]',
receivedB2BVNLCheckbox: 'vn-check[label="Received B2B VNL"]',
swiftBicAutocomplete: 'vn-client-billing-data vn-autocomplete[field="$ctrl.client.bankEntityFk"]',
clearswiftBicButton: 'vn-client-billing-data vn-autocomplete[field="$ctrl.client.bankEntityFk"] > div > div > div > vn-icon > i',
payMethodAutocomplete: 'vn-client-billing-data vn-autocomplete[ng-model="$ctrl.client.payMethodFk"]',
IBANInput: `vn-client-billing-data ${components.vnTextfield}[name="iban"]`,
dueDayInput: `vn-client-billing-data ${components.vnInputNumber}[name="dueDay"]`,
receivedCoreLCRCheckbox: 'vn-client-billing-data vn-check[label="Received LCR"]',
receivedCoreVNLCheckbox: 'vn-client-billing-data vn-check[label="Received core VNL"]',
receivedB2BVNLCheckbox: 'vn-client-billing-data vn-check[label="Received B2B VNL"]',
swiftBicAutocomplete: 'vn-client-billing-data vn-autocomplete[ng-model="$ctrl.client.bankEntityFk"]',
clearswiftBicButton: 'vn-client-billing-data vn-autocomplete[ng-model="$ctrl.client.bankEntityFk"] .icons > vn-icon[icon=clear]',
newBankEntityButton: 'vn-client-billing-data vn-icon-button[vn-tooltip="New bank entity"] > button',
newBankEntityName: 'vn-client-billing-data > vn-dialog vn-textfield[label="Name"] input',
newBankEntityBIC: 'vn-client-billing-data > vn-dialog vn-textfield[label="Swift / BIC"] input',
@ -102,8 +102,8 @@ export default {
streetAddressInput: `${components.vnTextfield}[name="street"]`,
postcodeInput: `${components.vnTextfield}[name="postalCode"]`,
cityInput: `${components.vnTextfield}[name="city"]`,
provinceAutocomplete: 'vn-autocomplete[field="$ctrl.address.provinceFk"]',
agencyAutocomplete: 'vn-autocomplete[field="$ctrl.address.agencyModeFk"]',
provinceAutocomplete: 'vn-autocomplete[ng-model="$ctrl.address.provinceFk"]',
agencyAutocomplete: 'vn-autocomplete[ng-model="$ctrl.address.agencyModeFk"]',
phoneInput: `${components.vnTextfield}[name="phone"]`,
mobileInput: `${components.vnTextfield}[name="mobile"]`,
defaultAddress: 'vn-client-address-index div:nth-child(1) div[name="street"]',
@ -112,10 +112,10 @@ export default {
secondEditAddress: 'vn-client-address-index div:nth-child(2) > a',
activeCheckbox: 'vn-check[label="Enabled"]',
equalizationTaxCheckbox: 'vn-client-address-edit vn-check[label="Is equalizated"]',
firstObservationTypeAutocomplete: 'vn-client-address-edit [name=observations] :nth-child(1) [field="observation.observationTypeFk"]',
firstObservationDescriptionInput: 'vn-client-address-edit [name=observations] :nth-child(1) [model="observation.description"] input',
secondObservationTypeAutocomplete: 'vn-client-address-edit [name=observations] :nth-child(2) [field="observation.observationTypeFk"]',
secondObservationDescriptionInput: 'vn-client-address-edit [name=observations] :nth-child(2) [model="observation.description"] input',
firstObservationTypeAutocomplete: 'vn-client-address-edit [name=observations] :nth-child(1) [ng-model="observation.observationTypeFk"]',
firstObservationDescriptionInput: 'vn-client-address-edit [name=observations] :nth-child(1) [ng-model="observation.description"] input',
secondObservationTypeAutocomplete: 'vn-client-address-edit [name=observations] :nth-child(2) [ng-model="observation.observationTypeFk"]',
secondObservationDescriptionInput: 'vn-client-address-edit [name=observations] :nth-child(2) [ng-model="observation.description"] input',
addObservationButton: 'vn-client-address-edit div[name="observations"] vn-icon-button[icon="add_circle"]',
saveButton: `${components.vnSubmit}`,
cancelCreateAddressButton: 'button[ui-sref="client.card.address.index"]',
@ -143,7 +143,7 @@ export default {
addGreugeFloatButton: `${components.vnFloatButton}`,
amountInput: `${components.vnInputNumber}[name="amount"]`,
descriptionInput: `${components.vnTextfield}[name="description"]`,
typeAutocomplete: 'vn-autocomplete[field="$ctrl.greuge.greugeTypeFk"]',
typeAutocomplete: 'vn-autocomplete[ng-model="$ctrl.greuge.greugeTypeFk"]',
saveButton: `${components.vnSubmit}`,
firstGreugeText: 'vn-client-greuge-index vn-card > div vn-table vn-tbody > vn-tr'
},
@ -162,10 +162,10 @@ export default {
},
clientBalance: {
balanceButton: 'vn-left-menu a[ui-sref="client.card.balance.index"]',
companyAutocomplete: 'vn-client-balance-index vn-autocomplete[field="$ctrl.companyFk"]',
companyAutocomplete: 'vn-client-balance-index vn-autocomplete[ng-model="$ctrl.companyFk"]',
newPaymentButton: `${components.vnFloatButton}`,
newPaymentBank: 'vn-client-balance-create vn-autocomplete[field="$ctrl.receipt.bankFk"]',
newPaymentAmountInput: 'vn-client-balance-create vn-input-number[field="$ctrl.receipt.amountPaid"] input',
newPaymentBank: 'vn-client-balance-create vn-autocomplete[ng-model="$ctrl.receipt.bankFk"]',
newPaymentAmountInput: 'vn-client-balance-create vn-input-number[ng-model="$ctrl.receipt.amountPaid"] input',
saveButton: 'vn-client-balance-create vn-button[label="Save"]',
firstBalanceLine: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(8)'
@ -209,9 +209,9 @@ export default {
},
itemCreateView: {
temporalName: `${components.vnTextfield}[name="provisionalName"]`,
typeAutocomplete: 'vn-autocomplete[field="$ctrl.item.typeFk"]',
intrastatAutocomplete: 'vn-autocomplete[field="$ctrl.item.intrastatFk"]',
originAutocomplete: 'vn-autocomplete[field="$ctrl.item.originFk"]',
typeAutocomplete: 'vn-autocomplete[ng-model="$ctrl.item.typeFk"]',
intrastatAutocomplete: 'vn-autocomplete[ng-model="$ctrl.item.intrastatFk"]',
originAutocomplete: 'vn-autocomplete[ng-model="$ctrl.item.originFk"]',
createButton: `${components.vnSubmit}`,
cancelButton: 'button[ui-sref="item.index"]'
},
@ -219,8 +219,8 @@ export default {
goBackToModuleIndexButton: 'vn-item-descriptor a[href="#!/item/index"]',
moreMenu: 'vn-item-descriptor vn-icon-menu > div > vn-icon',
moreMenuRegularizeButton: '.vn-popover.shown .vn-drop-down li[name="Regularize stock"]',
regularizeQuantityInput: 'vn-item-descriptor > vn-dialog > div > form > div.body > tpl-body > div > vn-textfield > div > div > div.infix > input',
regularizeWarehouseAutocomplete: 'vn-item-descriptor vn-dialog vn-autocomplete[field="$ctrl.warehouseFk"]',
regularizeQuantityInput: 'vn-item-descriptor vn-dialog tpl-body > div > vn-textfield input',
regularizeWarehouseAutocomplete: 'vn-item-descriptor vn-dialog vn-autocomplete[ng-model="$ctrl.warehouseFk"]',
editButton: 'vn-item-card vn-item-descriptor vn-float-button[icon="edit"]',
regularizeSaveButton: 'vn-item-descriptor > vn-dialog > div > form > div.buttons > tpl-buttons > button',
inactiveIcon: 'vn-item-descriptor vn-icon[icon="icon-unavailable"]',
@ -229,13 +229,13 @@ export default {
itemBasicData: {
basicDataButton: 'vn-left-menu a[ui-sref="item.card.basicData"]',
goToItemIndexButton: 'vn-item-descriptor [ui-sref="item.index"]',
typeAutocomplete: 'vn-autocomplete[field="$ctrl.item.typeFk"]',
intrastatAutocomplete: 'vn-autocomplete[field="$ctrl.item.intrastatFk"]',
typeAutocomplete: 'vn-autocomplete[ng-model="$ctrl.item.typeFk"]',
intrastatAutocomplete: 'vn-autocomplete[ng-model="$ctrl.item.intrastatFk"]',
nameInput: 'vn-textfield[label="Name"] input',
relevancyInput: 'vn-input-number[label="Relevancy"] input',
originAutocomplete: 'vn-autocomplete[field="$ctrl.item.originFk"]',
expenceAutocomplete: 'vn-autocomplete[field="$ctrl.item.expenceFk"]',
longNameInput: 'vn-textfield[field="$ctrl.item.longName"] input',
relevancyInput: 'vn-input-number[ng-model="$ctrl.item.relevancy"] input',
originAutocomplete: 'vn-autocomplete[ng-model="$ctrl.item.originFk"]',
expenceAutocomplete: 'vn-autocomplete[ng-model="$ctrl.item.expenceFk"]',
longNameInput: 'vn-textfield[ng-model="$ctrl.item.longName"] input',
isActiveCheckbox: 'vn-check[label="Active"]',
priceInKgCheckbox: 'vn-check[label="Price in kg"]',
submitBasicDataButton: `${components.vnSubmit}`
@ -243,26 +243,27 @@ export default {
itemTags: {
goToItemIndexButton: 'vn-item-descriptor [ui-sref="item.index"]',
tagsButton: 'vn-left-menu a[ui-sref="item.card.tags"]',
fourthTagAutocomplete: 'vn-item-tags vn-horizontal:nth-child(4) > vn-autocomplete[field="itemTag.tagFk"]',
fourthTagAutocomplete: 'vn-item-tags vn-horizontal:nth-child(4) > vn-autocomplete[ng-model="itemTag.tagFk"]',
fourthValueInput: 'vn-item-tags vn-horizontal:nth-child(4) > vn-textfield[label="Value"] input',
fourthRelevancyInput: 'vn-item-tags vn-horizontal:nth-child(4) > vn-textfield[label="Relevancy"] input',
fourthRemoveTagButton: 'vn-item-tags vn-horizontal:nth-child(4) vn-icon-button[icon="delete"]',
fifthTagAutocomplete: 'vn-item-tags vn-horizontal:nth-child(5) > vn-autocomplete[field="itemTag.tagFk"]',
fifthTagAutocomplete: 'vn-item-tags vn-horizontal:nth-child(5) > vn-autocomplete[ng-model="itemTag.tagFk"]',
fifthValueInput: 'vn-item-tags vn-horizontal:nth-child(5) > vn-textfield[label="Value"] input',
fifthRelevancyInput: 'vn-item-tags vn-horizontal:nth-child(5) > vn-textfield[label="Relevancy"] input',
sixthTagAutocomplete: 'vn-item-tags vn-horizontal:nth-child(6) > vn-autocomplete[field="itemTag.tagFk"]',
sixthTagAutocomplete: 'vn-item-tags vn-horizontal:nth-child(6) > vn-autocomplete[ng-model="itemTag.tagFk"]',
sixthValueInput: 'vn-item-tags vn-horizontal:nth-child(6) > vn-textfield[label="Value"] input',
sixthRelevancyInput: 'vn-item-tags vn-horizontal:nth-child(6) > vn-textfield[label="Relevancy"] input',
seventhTagAutocomplete: 'vn-item-tags vn-horizontal:nth-child(7) > vn-autocomplete[field="itemTag.tagFk"]',
seventhTagAutocomplete: 'vn-item-tags vn-horizontal:nth-child(7) > vn-autocomplete[ng-model="itemTag.tagFk"]',
seventhValueInput: 'vn-item-tags vn-horizontal:nth-child(7) > vn-textfield[label="Value"] input',
seventhRelevancyInput: 'vn-item-tags vn-horizontal:nth-child(7) > vn-textfield[label="Relevancy"] input',
addItemTagButton: 'vn-item-tags vn-icon-button[icon="add_circle"]',
submitItemTagsButton: `vn-item-tags ${components.vnSubmit}`
},
itemTax: {
firstClassAutocomplete: 'vn-item-tax vn-horizontal:nth-child(1) > vn-autocomplete[field="tax.taxClassFk"]',
secondClassAutocomplete: 'vn-item-tax vn-horizontal:nth-child(2) > vn-autocomplete[field="tax.taxClassFk"]',
thirdClassAutocomplete: 'vn-item-tax vn-horizontal:nth-child(3) > vn-autocomplete[field="tax.taxClassFk"]',
undoChangesButton: 'vn-item-tax vn-button-bar > vn-button[label="Undo changes"]',
firstClassAutocomplete: 'vn-item-tax vn-horizontal:nth-child(1) > vn-autocomplete[ng-model="tax.taxClassFk"]',
secondClassAutocomplete: 'vn-item-tax vn-horizontal:nth-child(2) > vn-autocomplete[ng-model="tax.taxClassFk"]',
thirdClassAutocomplete: 'vn-item-tax vn-horizontal:nth-child(3) > vn-autocomplete[ng-model="tax.taxClassFk"]',
submitTaxButton: `vn-item-tax ${components.vnSubmit}`
},
itemBarcodes: {
@ -273,19 +274,19 @@ export default {
},
itemNiches: {
addNicheButton: 'vn-item-niche vn-icon[icon="add_circle"]',
firstWarehouseAutocomplete: 'vn-item-niche vn-autocomplete[field="niche.warehouseFk"]',
firstWarehouseAutocomplete: 'vn-item-niche vn-autocomplete[ng-model="niche.warehouseFk"]',
firstCodeInput: 'vn-item-niche vn-horizontal:nth-child(1) > vn-textfield[label="Code"] input',
secondWarehouseAutocomplete: 'vn-item-niche vn-horizontal:nth-child(2) > vn-autocomplete[field="niche.warehouseFk"]',
secondWarehouseAutocomplete: 'vn-item-niche vn-horizontal:nth-child(2) > vn-autocomplete[ng-model="niche.warehouseFk"]',
secondCodeInput: 'vn-item-niche vn-horizontal:nth-child(2) > vn-textfield[label="Code"] input',
secondNicheRemoveButton: 'vn-item-niche vn-horizontal:nth-child(2) > vn-none > vn-icon-button[icon="delete"]',
thirdWarehouseAutocomplete: 'vn-item-niche vn-horizontal:nth-child(3) > vn-autocomplete[field="niche.warehouseFk"]',
thirdWarehouseAutocomplete: 'vn-item-niche vn-horizontal:nth-child(3) > vn-autocomplete[ng-model="niche.warehouseFk"]',
thirdCodeInput: 'vn-item-niche vn-horizontal:nth-child(3) > vn-textfield[label="Code"] input',
submitNichesButton: `vn-item-niche ${components.vnSubmit}`
},
itemBotanical: {
botanicalInput: `vn-item-botanical vn-horizontal:nth-child(1) > ${components.vnTextfield}`,
genusAutocomplete: 'vn-item-botanical vn-autocomplete[field="$ctrl.botanical.genusFk"]',
speciesAutocomplete: 'vn-item-botanical vn-autocomplete[field="$ctrl.botanical.specieFk"]',
genusAutocomplete: 'vn-item-botanical vn-autocomplete[ng-model="$ctrl.botanical.genusFk"]',
speciesAutocomplete: 'vn-item-botanical vn-autocomplete[ng-model="$ctrl.botanical.specieFk"]',
submitBotanicalButton: `vn-item-botanical ${components.vnSubmit}`
},
itemSummary: {
@ -300,7 +301,7 @@ export default {
secondTicketId: 'vn-item-diary vn-tbody > vn-tr:nth-child(2) > vn-td:nth-child(2) > span',
firstBalance: 'vn-item-diary vn-tbody > vn-tr:nth-child(1) > vn-td.balance',
fourthBalance: 'vn-item-diary vn-tbody > vn-tr:nth-child(4) > vn-td.balance',
warehouseAutocomplete: 'vn-item-diary vn-autocomplete[field="$ctrl.warehouseFk"]',
warehouseAutocomplete: 'vn-item-diary vn-autocomplete[ng-model="$ctrl.warehouseFk"]',
},
itemLog: {
anyLineCreated: 'vn-item-log > vn-log vn-tbody > vn-tr',
@ -323,31 +324,31 @@ export default {
setOk: 'vn-ticket-summary vn-button[label="SET OK"] > button'
},
ticketsIndex: {
openAdvancedSearchButton: 'vn-ticket-index vn-searchbar t-right-icons > vn-icon[icon="keyboard_arrow_down"]',
advancedSearchInvoiceOut: 'vn-ticket-search-panel vn-textfield[model="filter.refFk"] input',
openAdvancedSearchButton: 'vn-ticket-index vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
advancedSearchInvoiceOut: 'vn-ticket-search-panel vn-textfield[ng-model="filter.refFk"] input',
newTicketButton: 'vn-ticket-index > a',
searchResult: 'vn-ticket-index vn-card > div > vn-table > div > vn-tbody > a.vn-tr',
searchWeeklyResult: 'vn-ticket-weekly-index vn-table vn-tbody > vn-tr',
searchResultDate: 'vn-ticket-index vn-table vn-tbody > a:nth-child(1) > vn-td:nth-child(5)',
searchTicketInput: `vn-ticket-index ${components.vnTextfield}`,
searchWeeklyTicketInput: `vn-ticket-weekly-index ${components.vnTextfield}`,
searchWeeklyClearInput: 'vn-ticket-weekly-index vn-searchbar i[class="material-icons clear"]',
searchWeeklyClearInput: 'vn-ticket-weekly-index vn-searchbar vn-icon[icon=clear]',
advancedSearchButton: 'vn-ticket-search-panel vn-submit[label="Search"] input',
searchButton: 'vn-ticket-index vn-searchbar vn-icon[icon="search"]',
searchWeeklyButton: 'vn-ticket-weekly-index vn-searchbar vn-icon[icon="search"]',
moreMenu: 'vn-ticket-index vn-icon-menu[vn-id="more-button"] > div > vn-icon',
moreMenuWeeklyTickets: '.vn-popover.shown .vn-drop-down li:nth-child(2)',
sixthWeeklyTicket: 'vn-ticket-weekly-index vn-table vn-tr:nth-child(6) vn-autocomplete[field="weekly.weekDay"] input',
sixthWeeklyTicket: 'vn-ticket-weekly-index vn-table vn-tr:nth-child(6) vn-autocomplete[ng-model="weekly.weekDay"] input',
weeklyTicket: 'vn-ticket-weekly-index vn-table > div > vn-tbody > vn-tr',
firstWeeklyTicketDeleteIcon: 'vn-ticket-weekly-index vn-tr:nth-child(1) vn-icon-button[icon="delete"]',
acceptDeleteTurn: 'vn-ticket-weekly-index > vn-confirm[vn-id="deleteWeekly"] button[response="ACCEPT"]'
},
createTicketView: {
clientAutocomplete: 'vn-ticket-create vn-autocomplete[field="$ctrl.clientFk"]',
addressAutocomplete: 'vn-ticket-create vn-autocomplete[field="$ctrl.addressFk"]',
deliveryDateInput: 'vn-ticket-create > div > div > vn-card > div > vn-ticket-create-card > vn-date-picker > div > input',
warehouseAutocomplete: 'vn-ticket-create vn-autocomplete[field="$ctrl.warehouseFk"]',
agencyAutocomplete: 'vn-ticket-create vn-autocomplete[field="$ctrl.ticket.agencyModeFk"]',
clientAutocomplete: 'vn-ticket-create vn-autocomplete[ng-model="$ctrl.clientFk"]',
addressAutocomplete: 'vn-ticket-create vn-autocomplete[ng-model="$ctrl.addressFk"]',
deliveryDateInput: 'vn-ticket-create vn-date-picker[ng-model="$ctrl.landed"]',
warehouseAutocomplete: 'vn-ticket-create vn-autocomplete[ng-model="$ctrl.warehouseFk"]',
agencyAutocomplete: 'vn-ticket-create vn-autocomplete[ng-model="$ctrl.ticket.agencyModeFk"]',
createButton: `${components.vnSubmit}`
},
ticketDescriptor: {
@ -376,7 +377,7 @@ export default {
ticketNotes: {
firstNoteRemoveButton: 'vn-icon[icon="delete"]',
addNoteButton: 'vn-icon[icon="add_circle"]',
firstNoteTypeAutocomplete: 'vn-autocomplete[field="observation.observationTypeFk"]',
firstNoteTypeAutocomplete: 'vn-autocomplete[ng-model="observation.observationTypeFk"]',
firstDescriptionInput: 'vn-textfield[label="Description"] input',
submitNotesButton: `${components.vnSubmit}`
},
@ -389,10 +390,10 @@ export default {
ticketPackages: {
packagesButton: 'vn-left-menu a[ui-sref="ticket.card.package"]',
firstPackageAutocomplete: 'vn-autocomplete[label="Package"]',
firstQuantityInput: 'vn-input-number[label="Quantity"] input',
firstQuantityInput: 'vn-input-number[ng-model="package.quantity"] input',
firstRemovePackageButton: 'vn-icon-button[vn-tooltip="Remove package"]',
addPackageButton: 'vn-icon-button[vn-tooltip="Add package"]',
clearPackageAutocompleteButton: 'vn-autocomplete[label="Package"] > div > div > div > vn-icon > i',
clearPackageAutocompleteButton: 'vn-autocomplete[label="Package"] .icons > vn-icon[icon=clear]',
savePackagesButton: `${components.vnSubmit}`
},
ticketSales: {
@ -408,7 +409,7 @@ export default {
moreMenuReserve: '.vn-popover.shown .vn-drop-down li[name="Mark as reserved"]',
moreMenuUnmarkReseved: '.vn-popover.shown .vn-drop-down li[name="Unmark as reserved"]',
moreMenuUpdateDiscount: '.vn-popover.shown .vn-drop-down li[name="Update discount"]',
moreMenuUpdateDiscountInput: 'vn-ticket-sale vn-dialog form vn-ticket-sale-edit-discount vn-input-number[model="$ctrl.newDiscount"] input',
moreMenuUpdateDiscountInput: 'vn-ticket-sale vn-dialog form vn-ticket-sale-edit-discount vn-input-number[ng-model="$ctrl.newDiscount"] input',
transferQuantityInput: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable > span > text',
transferQuantityCell: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable',
firstSaleClaimIcon: 'vn-ticket-sale vn-table vn-tbody > vn-tr:nth-child(1) vn-icon[icon="icon-claims"]',
@ -416,11 +417,11 @@ export default {
firstSaleText: 'vn-table div > vn-tbody > vn-tr:nth-child(1)',
firstSaleThumbnailImage: 'vn-ticket-sale:nth-child(1) vn-tr:nth-child(1) vn-td:nth-child(3) > img',
firstSaleZoomedImage: 'body > div > div > img',
firstSaleQuantity: 'vn-input-number[model="sale.quantity"]:nth-child(1) input',
firstSaleQuantity: 'vn-input-number[ng-model="sale.quantity"]:nth-child(1) input',
firstSaleQuantityCell: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td-editable:nth-child(5)',
firstSaleQuantityClearInput: 'vn-textfield[model="sale.quantity"] div.suffix > i',
firstSaleIdInput: 'body > vn-app > div > ui-view > vn-ticket-card > vn-main-block > div > vn-ticket-sale > vn-vertical > vn-card > div > vn-vertical > vn-table > div > vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(4) > vn-autocomplete > div > div > input',
firstSaleIdAutocomplete: 'vn-ticket-sale > vn-vertical > vn-card > div > vn-vertical > vn-table > div > vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(4) > vn-autocomplete',
firstSaleQuantityClearInput: 'vn-textfield[ng-model="sale.quantity"] div.suffix > i',
firstSaleIdInput: 'vn-ticket-sale vn-table vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(4) > vn-autocomplete input',
firstSaleIdAutocomplete: 'vn-ticket-sale vn-table vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(4) > vn-autocomplete',
idAutocompleteFirstResult: '.vn-popover.shown .vn-drop-down li',
firstSalePrice: 'vn-ticket-sale vn-table vn-tr:nth-child(1) > vn-td:nth-child(7) > span',
firstSalePriceInput: '.vn-popover.shown vn-input-number input',
@ -430,7 +431,7 @@ export default {
firstSaleReservedIcon: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td:nth-child(2) > vn-icon:nth-child(3)',
firstSaleColour: 'vn-tr:nth-child(1) vn-fetched-tags section',
firstSaleLength: 'vn-ticket-sale vn-tr:nth-child(1) vn-td-editable:nth-child(6) section:nth-child(3)',
firstSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(1) vn-check[field="sale.checked"]',
firstSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(1) vn-check[ng-model="sale.checked"]',
secondSaleColour: 'vn-tr:nth-child(2) vn-fetched-tags section',
secondSalePrice: 'vn-ticket-sale vn-tr:nth-child(2) vn-td:nth-child(7) > span',
secondSaleDiscount: 'vn-ticket-sale vn-tr:nth-child(2) vn-td:nth-child(8)',
@ -438,18 +439,18 @@ export default {
secondSaleText: 'vn-table div > vn-tbody > vn-tr:nth-child(2)',
secondSaleId: 'vn-ticket-sale:nth-child(2) vn-td-editable:nth-child(4) text > span',
secondSaleIdCell: 'vn-ticket-sale vn-tr:nth-child(2) > vn-td-editable:nth-child(4)',
secondSaleIdInput: 'body > vn-app > div > ui-view > vn-ticket-card > vn-main-block > div > vn-ticket-sale > vn-vertical > vn-card > div > vn-vertical > vn-table > div > vn-tbody > vn-tr:nth-child(2) > vn-td:nth-child(4) > vn-autocomplete > div > div > input',
secondSaleIdAutocomplete: 'vn-ticket-sale > vn-vertical > vn-card > div > vn-vertical > vn-table > div > vn-tbody > vn-tr:nth-child(2) > vn-td:nth-child(4) > vn-autocomplete',
secondSaleIdInput: 'vn-ticket-sale vn-table vn-tbody > vn-tr:nth-child(2) > vn-td:nth-child(4) > vn-autocomplete input',
secondSaleIdAutocomplete: 'vn-ticket-sale vn-table vn-tbody > vn-tr:nth-child(2) > vn-td:nth-child(4) > vn-autocomplete',
secondSaleQuantity: 'vn-ticket-sale vn-table vn-tr:nth-child(2) vn-input-number input',
secondSaleConceptCell: 'vn-ticket-sale vn-table > div > vn-tbody > vn-tr:nth-child(2) > vn-td-editable:nth-child(6)',
secondSaleConceptCell: 'vn-ticket-sale vn-table vn-tbody > vn-tr:nth-child(2) > vn-td-editable:nth-child(6)',
secondSaleConceptInput: 'vn-ticket-sale vn-table vn-tr:nth-child(2) > vn-td-editable.ng-isolate-scope.selected vn-textfield input',
totalImport: 'vn-ticket-sale > vn-vertical > vn-card > div > vn-vertical > vn-horizontal > vn-one > p:nth-child(3) > strong',
selectAllSalesCheckbox: 'vn-ticket-sale vn-thead vn-check',
secondSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(2) vn-check[field="sale.checked"]',
thirdSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(3) vn-check[field="sale.checked"]',
secondSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(2) vn-check[ng-model="sale.checked"]',
thirdSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(3) vn-check[ng-model="sale.checked"]',
deleteSaleButton: 'vn-ticket-sale vn-tool-bar > vn-button[icon="delete"]',
transferSaleButton: 'vn-ticket-sale vn-tool-bar > vn-button[icon="call_split"]',
moveToTicketInput: '.vn-popover.shown vn-textfield[model="$ctrl.transfer.ticketId"] input',
moveToTicketInput: '.vn-popover.shown vn-textfield[ng-model="$ctrl.transfer.ticketId"] input',
moveToTicketInputClearButton: '.vn-popover.shown i[title="Clear"]',
moveToTicketButton: '.vn-popover.shown vn-icon[icon="arrow_forward_ios"]',
moveToNewTicketButton: '.vn-popover.shown vn-button[label="New ticket"]',
@ -460,36 +461,34 @@ export default {
ticketTracking: {
trackingButton: 'vn-left-menu a[ui-sref="ticket.card.tracking.index"]',
createStateButton: `${components.vnFloatButton}`,
stateAutocomplete: 'vn-ticket-tracking-edit vn-autocomplete[field="$ctrl.stateFk"]',
stateAutocomplete: 'vn-ticket-tracking-edit vn-autocomplete[ng-model="$ctrl.stateFk"]',
saveButton: `${components.vnSubmit}`,
cancelButton: 'vn-ticket-tracking-edit vn-button[ui-sref="ticket.card.tracking.index"]'
},
ticketBasicData: {
basicDataButton: 'vn-left-menu a[ui-sref="ticket.card.basicData.stepOne"]',
clientAutocomplete: 'vn-autocomplete[field="$ctrl.clientFk"]',
addressAutocomplete: 'vn-autocomplete[field="$ctrl.ticket.addressFk"]',
agencyAutocomplete: 'vn-autocomplete[field="$ctrl.agencyModeId"]',
zoneAutocomplete: 'vn-autocomplete[field="$ctrl.zoneId"]',
clientAutocomplete: 'vn-autocomplete[ng-model="$ctrl.clientFk"]',
addressAutocomplete: 'vn-autocomplete[ng-model="$ctrl.ticket.addressFk"]',
agencyAutocomplete: 'vn-autocomplete[ng-model="$ctrl.agencyModeId"]',
zoneAutocomplete: 'vn-autocomplete[ng-model="$ctrl.zoneId"]',
nextStepButton: 'vn-step-control > section > section.buttons > section:nth-child(2) > vn-button',
finalizeButton: 'vn-step-control > section > section.buttons > section:nth-child(2) > vn-submit',
stepTwoTotalPriceDif: 'vn-ticket-basic-data-step-two > form > vn-card > div > vn-horizontal > table > tfoot > tr > td:nth-child(4)',
chargesReasonAutocomplete: 'vn-autocomplete[field="$ctrl.ticket.option"]',
chargesReasonAutocomplete: 'vn-autocomplete[ng-model="$ctrl.ticket.option"]',
},
ticketComponents: {
base: 'vn-ticket-components tfoot > tr:nth-child(1) > td',
margin: 'vn-ticket-components tfoot > tr:nth-child(2) > td',
total: 'vn-ticket-components tfoot > tr:nth-child(3) > td'
base: 'vn-ticket-components [name="base-sum"]'
},
ticketRequests: {
addRequestButton: 'vn-ticket-request-index > a > vn-float-button > button',
request: 'vn-ticket-request-index > form > vn-card > div > vn-horizontal > vn-table > div > vn-tbody > vn-tr',
descriptionInput: 'vn-ticket-request-create > form > div > vn-card > div > vn-horizontal:nth-child(1) > vn-textfield > div > div > div.infix > input',
atenderAutocomplete: 'vn-ticket-request-create vn-autocomplete[field="$ctrl.ticketRequest.atenderFk"]',
quantityInput: 'vn-ticket-request-create > form > div > vn-card > div > vn-horizontal:nth-child(2) > vn-input-number:nth-child(1) > div > div > div.infix > input',
priceInput: 'vn-ticket-request-create > form > div > vn-card > div > vn-horizontal:nth-child(2) > vn-input-number:nth-child(2) > div > div > div.infix > input',
request: 'vn-ticket-request-index vn-table vn-tr',
descriptionInput: 'vn-ticket-request-create > form > div > vn-card > div > vn-horizontal:nth-child(1) > vn-textfield input',
atenderAutocomplete: 'vn-ticket-request-create vn-autocomplete[ng-model="$ctrl.ticketRequest.atenderFk"]',
quantityInput: 'vn-ticket-request-create vn-input-number input[name=quantity]',
priceInput: 'vn-ticket-request-create vn-input-number input[name=price]',
firstRemoveRequestButton: 'vn-ticket-request-index vn-icon[icon="delete"]:nth-child(1)',
saveButton: 'vn-ticket-request-create > form > div > vn-button-bar > vn-submit[label="Create"] input',
firstDescription: 'vn-ticket-request-index > form > vn-card > div > vn-horizontal > vn-table > div > vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(2)',
firstDescription: 'vn-ticket-request-index vn-table vn-tr:nth-child(1) > vn-td:nth-child(2)',
},
ticketLog: {
@ -501,20 +500,20 @@ export default {
ticketService: {
addServiceButton: 'vn-ticket-service vn-icon-button[vn-tooltip="Add service"] > button',
firstAddDescriptionButton: 'vn-ticket-service vn-icon-button[vn-tooltip="New service type"] > button',
firstDescriptionAutocomplete: 'vn-ticket-service vn-autocomplete[field="service.description"]',
firstDescriptionAutocomplete: 'vn-ticket-service vn-autocomplete[ng-model="service.description"]',
firstQuantityInput: 'vn-ticket-service vn-input-number[label="Quantity"] input',
firstPriceInput: 'vn-ticket-service vn-input-number[label="Price"] input',
firstVatTypeAutocomplete: 'vn-ticket-service vn-autocomplete[label="Tax class"]',
fistDeleteServiceButton: 'vn-ticket-card > vn-main-block > div.content-block.ng-scope > vn-ticket-service > form > vn-card > div > vn-one:nth-child(1) > vn-horizontal:nth-child(1) > vn-auto > vn-icon-button[icon="delete"]',
newDescriptionInput: 'vn-ticket-service > vn-dialog vn-textfield[model="$ctrl.newServiceType.name"] input',
fistDeleteServiceButton: 'vn-ticket-service form vn-horizontal:nth-child(1) vn-icon-button[icon="delete"]',
newDescriptionInput: 'vn-ticket-service > vn-dialog vn-textfield[ng-model="$ctrl.newServiceType.name"] input',
serviceLine: 'vn-ticket-service > form > vn-card > div > vn-one:nth-child(2) > vn-horizontal',
saveServiceButton: `${components.vnSubmit}`,
saveDescriptionButton: 'vn-ticket-service > vn-dialog[vn-id="createServiceTypeDialog"] > div > form > div.buttons > tpl-buttons > button'
},
createStateView: {
stateAutocomplete: 'vn-autocomplete[field="$ctrl.stateFk"]',
workerAutocomplete: 'vn-autocomplete[field="$ctrl.workerFk"]',
clearStateInputButton: 'vn-autocomplete[field="$ctrl.stateFk"] > div > div > div > vn-icon > i',
stateAutocomplete: 'vn-autocomplete[ng-model="$ctrl.stateFk"]',
workerAutocomplete: 'vn-autocomplete[ng-model="$ctrl.workerFk"]',
clearStateInputButton: 'vn-autocomplete[ng-model="$ctrl.stateFk"] .icons > vn-icon[icon=clear]',
saveStateButton: `${components.vnSubmit}`
},
claimsIndex: {
@ -530,7 +529,7 @@ export default {
claimSummary: {
header: 'vn-claim-summary > vn-card > div > h5',
state: 'vn-claim-summary vn-label-value[label="State"] > section > span',
observation: 'vn-claim-summary vn-textarea[model="$ctrl.summary.claim.observation"] > div > textarea',
observation: 'vn-claim-summary vn-textarea[ng-model="$ctrl.summary.claim.observation"] textarea',
firstSaleItemId: 'vn-claim-summary vn-horizontal > vn-auto:nth-child(4) > vn-table > div > vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(1) > span',
firstSaleDescriptorImage: '.vn-popover.shown vn-item-descriptor img',
itemDescriptorPopover: '.vn-popover.shown vn-item-descriptor',
@ -541,45 +540,45 @@ export default {
firstActionTicketDescriptor: '.vn-popover.shown vn-ticket-descriptor'
},
claimBasicData: {
claimStateAutocomplete: 'vn-claim-basic-data vn-autocomplete[field="$ctrl.claim.claimStateFk"]',
claimStateAutocomplete: 'vn-claim-basic-data vn-autocomplete[ng-model="$ctrl.claim.claimStateFk"]',
responsabilityInputRange: 'vn-input-range',
observationInput: 'vn-textarea[field="$ctrl.claim.observation"] textarea',
observationInput: 'vn-textarea[ng-model="$ctrl.claim.observation"] textarea',
saveButton: `${components.vnSubmit}`
},
claimDetail: {
secondItemDiscount: 'vn-claim-detail > vn-vertical > vn-card > div > vn-vertical > vn-table > div > vn-tbody > vn-tr:nth-child(2) > vn-td:nth-child(6) > span',
discountInput: '.vn-popover.shown vn-input-number[model="$ctrl.newDiscount"] > div > div > div.infix > input',
discountInput: '.vn-popover.shown vn-input-number[ng-model="$ctrl.newDiscount"] input',
discoutPopoverMana: '.vn-popover.shown .content > div > vn-horizontal > h5',
addItemButton: 'vn-claim-detail a vn-float-button',
firstClaimableSaleFromTicket: 'vn-claim-detail > vn-dialog vn-tbody > vn-tr',
claimDetailLine: 'vn-claim-detail > vn-vertical > vn-card > div > vn-vertical > vn-table > div > vn-tbody > vn-tr',
firstItemQuantityInput: 'vn-claim-detail vn-tr:nth-child(1) vn-input-number[model="saleClaimed.quantity"] input',
firstItemQuantityInput: 'vn-claim-detail vn-tr:nth-child(1) vn-input-number[ng-model="saleClaimed.quantity"] input',
totalClaimed: 'vn-claim-detail > vn-vertical > vn-card > div > vn-vertical > vn-horizontal > div > vn-label-value:nth-child(2) > section > span',
secondItemDeleteButton: 'vn-claim-detail > vn-vertical > vn-card > div > vn-vertical > vn-table > div > vn-tbody > vn-tr:nth-child(2) > vn-td:nth-child(8) > vn-icon-button > button > vn-icon > i'
},
claimDevelopment: {
addDevelopmentButton: 'vn-claim-development > vn-vertical > vn-card > div > vn-vertical > vn-one > vn-icon-button > button > vn-icon',
firstDeleteDevelopmentButton: 'vn-claim-development > vn-vertical > vn-card > div > vn-vertical > form > vn-horizontal:nth-child(2) > vn-icon-button > button > vn-icon',
firstClaimReasonAutocomplete: 'vn-claim-development vn-horizontal:nth-child(1) vn-autocomplete[field="claimDevelopment.claimReasonFk"]',
firstClaimResultAutocomplete: 'vn-claim-development vn-horizontal:nth-child(1) vn-autocomplete[field="claimDevelopment.claimResultFk"]',
firstClaimResponsibleAutocomplete: 'vn-claim-development vn-horizontal:nth-child(1) vn-autocomplete[field="claimDevelopment.claimResponsibleFk"]',
firstClaimWorkerAutocomplete: 'vn-claim-development vn-horizontal:nth-child(1) vn-autocomplete[field="claimDevelopment.workerFk"]',
firstClaimRedeliveryAutocomplete: 'vn-claim-development vn-horizontal:nth-child(1) vn-autocomplete[field="claimDevelopment.claimRedeliveryFk"]',
secondClaimReasonAutocomplete: 'vn-claim-development vn-horizontal:nth-child(2) vn-autocomplete[field="claimDevelopment.claimReasonFk"]',
secondClaimResultAutocomplete: 'vn-claim-development vn-horizontal:nth-child(2) vn-autocomplete[field="claimDevelopment.claimResultFk"]',
secondClaimResponsibleAutocomplete: 'vn-claim-development vn-horizontal:nth-child(2) vn-autocomplete[field="claimDevelopment.claimResponsibleFk"]',
secondClaimWorkerAutocomplete: 'vn-claim-development vn-horizontal:nth-child(2) vn-autocomplete[field="claimDevelopment.workerFk"]',
secondClaimRedeliveryAutocomplete: 'vn-claim-development vn-horizontal:nth-child(2) vn-autocomplete[field="claimDevelopment.claimRedeliveryFk"]',
firstClaimReasonAutocomplete: 'vn-claim-development vn-horizontal:nth-child(1) vn-autocomplete[ng-model="claimDevelopment.claimReasonFk"]',
firstClaimResultAutocomplete: 'vn-claim-development vn-horizontal:nth-child(1) vn-autocomplete[ng-model="claimDevelopment.claimResultFk"]',
firstClaimResponsibleAutocomplete: 'vn-claim-development vn-horizontal:nth-child(1) vn-autocomplete[ng-model="claimDevelopment.claimResponsibleFk"]',
firstClaimWorkerAutocomplete: 'vn-claim-development vn-horizontal:nth-child(1) vn-autocomplete[ng-model="claimDevelopment.workerFk"]',
firstClaimRedeliveryAutocomplete: 'vn-claim-development vn-horizontal:nth-child(1) vn-autocomplete[ng-model="claimDevelopment.claimRedeliveryFk"]',
secondClaimReasonAutocomplete: 'vn-claim-development vn-horizontal:nth-child(2) vn-autocomplete[ng-model="claimDevelopment.claimReasonFk"]',
secondClaimResultAutocomplete: 'vn-claim-development vn-horizontal:nth-child(2) vn-autocomplete[ng-model="claimDevelopment.claimResultFk"]',
secondClaimResponsibleAutocomplete: 'vn-claim-development vn-horizontal:nth-child(2) vn-autocomplete[ng-model="claimDevelopment.claimResponsibleFk"]',
secondClaimWorkerAutocomplete: 'vn-claim-development vn-horizontal:nth-child(2) vn-autocomplete[ng-model="claimDevelopment.workerFk"]',
secondClaimRedeliveryAutocomplete: 'vn-claim-development vn-horizontal:nth-child(2) vn-autocomplete[ng-model="claimDevelopment.claimRedeliveryFk"]',
saveDevelopmentButton: `${components.vnSubmit}`
},
claimAction: {
importClaimButton: 'vn-claim-action vn-button[label="Import claim"]',
importTicketButton: 'vn-claim-action vn-button[label="Import ticket"]',
secondImportableTicket: '.vn-popover.shown .content > div > vn-table > div > vn-tbody > vn-tr:nth-child(2)',
firstLineDestination: 'vn-claim-action vn-tr:nth-child(1) vn-autocomplete[field="saleClaimed.claimDestinationFk"]',
secondLineDestination: 'vn-claim-action vn-tr:nth-child(2) vn-autocomplete[field="saleClaimed.claimDestinationFk"]',
firstLineDestination: 'vn-claim-action vn-tr:nth-child(1) vn-autocomplete[ng-model="saleClaimed.claimDestinationFk"]',
secondLineDestination: 'vn-claim-action vn-tr:nth-child(2) vn-autocomplete[ng-model="saleClaimed.claimDestinationFk"]',
firstDeleteLine: 'vn-claim-action vn-tr:nth-child(1) vn-icon-button[icon="delete"]',
isPaidWithManaCheckbox: 'vn-check[field="$ctrl.claim.isChargedToMana"]'
isPaidWithManaCheckbox: 'vn-check[ng-model="$ctrl.claim.isChargedToMana"]'
},
ordersIndex: {
searchResult: 'vn-order-index vn-card > div > vn-table > div > vn-tbody > a.vn-tr',
@ -597,7 +596,7 @@ export default {
clientAutocomplete: 'vn-autocomplete[label="Client"]',
addressAutocomplete: 'vn-autocomplete[label="Address"]',
agencyAutocomplete: 'vn-autocomplete[label="Agency"]',
landedDatePicker: 'vn-date-picker[label="Landed"] input',
landedDatePicker: 'vn-date-picker[label="Landed"]',
createButton: `${components.vnSubmit}`,
cancelButton: 'vn-button[href="#!/client/index"]'
},
@ -605,14 +604,14 @@ export default {
orderByAutocomplete: 'vn-autocomplete[label="Order by"]',
plantRealmButton: 'vn-order-catalog > vn-side-menu vn-icon[icon="icon-plant"]',
typeAutocomplete: 'vn-autocomplete[data="$ctrl.itemTypes"]',
itemIdInput: 'vn-order-catalog > vn-side-menu vn-catalog-filter vn-textfield[model="$ctrl.itemFk"] input',
itemTagValueInput: 'vn-order-catalog > vn-side-menu vn-catalog-filter vn-textfield[model="$ctrl.value"] input',
openTagSearch: 'vn-order-catalog > vn-side-menu > div > vn-catalog-filter > div > vn-vertical > vn-textfield[model="$ctrl.value"] > div > div > div.rightIcons > t-right-icons > i',
tagAutocomplete: 'vn-order-catalog-search-panel vn-autocomplete[field="filter.tagFk"]',
tagValueInput: 'vn-order-catalog-search-panel vn-textfield[model="filter.value"] input',
itemIdInput: 'vn-catalog-filter vn-textfield[ng-model="$ctrl.itemFk"] input',
itemTagValueInput: 'vn-catalog-filter vn-textfield[ng-model="$ctrl.value"] input',
openTagSearch: 'vn-catalog-filter > div > vn-vertical > vn-textfield[ng-model="$ctrl.value"] .append i',
tagAutocomplete: 'vn-order-catalog-search-panel vn-autocomplete[ng-model="filter.tagFk"]',
tagValueInput: 'vn-order-catalog-search-panel vn-textfield[ng-model="filter.value"] input',
searchTagButton: 'vn-order-catalog-search-panel > div > form > vn-horizontal:nth-child(3) > vn-submit > input',
thirdFilterRemoveButton: 'vn-order-catalog > vn-side-menu vn-catalog-filter > div > vn-horizontal.chips > vn-chip:nth-child(3) button',
fourthFilterRemoveButton: 'vn-order-catalog > vn-side-menu vn-catalog-filter > div > vn-horizontal.chips > vn-chip:nth-child(4) button',
thirdFilterRemoveButton: 'vn-catalog-filter > div > vn-horizontal.chips > vn-chip:nth-child(3) button',
fourthFilterRemoveButton: 'vn-catalog-filter > div > vn-horizontal.chips > vn-chip:nth-child(4) button',
},
orderBasicData: {
clientAutocomplete: 'vn-autocomplete[label="Client"]',
@ -632,11 +631,11 @@ export default {
addNewRouteButton: 'vn-route-index > a[ui-sref="route.create"]'
},
createRouteView: {
workerAutocomplete: 'vn-route-create vn-autocomplete[field="$ctrl.route.workerFk"]',
createdDatePicker: 'vn-route-create vn-date-picker[model="$ctrl.route.created"] > div > input',
vehicleAutoComplete: 'vn-route-create vn-autocomplete[field="$ctrl.route.vehicleFk"]',
agencyAutoComplete: 'vn-route-create vn-autocomplete[field="$ctrl.route.agencyModeFk"]',
descriptionInput: 'vn-route-create vn-textfield[field="$ctrl.route.description"] input',
workerAutocomplete: 'vn-route-create vn-autocomplete[ng-model="$ctrl.route.workerFk"]',
createdDatePicker: 'vn-route-create vn-date-picker[ng-model="$ctrl.route.created"]',
vehicleAutoComplete: 'vn-route-create vn-autocomplete[ng-model="$ctrl.route.vehicleFk"]',
agencyAutoComplete: 'vn-route-create vn-autocomplete[ng-model="$ctrl.route.agencyModeFk"]',
descriptionInput: 'vn-route-create vn-textfield[ng-model="$ctrl.route.description"] input',
submitButton: 'vn-route-create vn-submit > input[type="submit"]'
},
routeDescriptor: {
@ -646,29 +645,29 @@ export default {
routeId: 'vn-route-summary > vn-card > div > vn-horizontal > vn-one:nth-child(1) > vn-label-value:nth-child(1) > section > span'
},
routeBasicData: {
workerAutoComplete: 'vn-route-basic-data vn-autocomplete[field="$ctrl.route.workerFk"]',
vehicleAutoComplete: 'vn-route-basic-data vn-autocomplete[field="$ctrl.route.vehicleFk"]',
agencyAutoComplete: 'vn-route-basic-data vn-autocomplete[field="$ctrl.route.agencyModeFk"]',
kmStartInput: 'vn-route-basic-data vn-input-number[field="$ctrl.route.kmStart"] input',
kmEndInput: 'vn-route-basic-data vn-input-number[model="$ctrl.route.kmEnd"] input',
createdDateInput: 'vn-route-basic-data vn-date-picker[model="$ctrl.route.created"] > div > input',
startedHourInput: 'vn-route-basic-data vn-input-time[model="$ctrl.route.started"] input',
finishedHourInput: 'vn-route-basic-data vn-input-time[model="$ctrl.route.finished"] input',
workerAutoComplete: 'vn-route-basic-data vn-autocomplete[ng-model="$ctrl.route.workerFk"]',
vehicleAutoComplete: 'vn-route-basic-data vn-autocomplete[ng-model="$ctrl.route.vehicleFk"]',
agencyAutoComplete: 'vn-route-basic-data vn-autocomplete[ng-model="$ctrl.route.agencyModeFk"]',
kmStartInput: 'vn-route-basic-data vn-input-number[ng-model="$ctrl.route.kmStart"] input',
kmEndInput: 'vn-route-basic-data vn-input-number[ng-model="$ctrl.route.kmEnd"] input',
createdDateInput: 'vn-route-basic-data vn-date-picker[ng-model="$ctrl.route.created"]',
startedHourInput: 'vn-route-basic-data vn-input-time[ng-model="$ctrl.route.started"] input',
finishedHourInput: 'vn-route-basic-data vn-input-time[ng-model="$ctrl.route.finished"] input',
saveButton: 'vn-route-basic-data vn-submit[label="Save"] input'
},
routeTickets: {
firstTicketPriority: 'vn-route-tickets vn-tr:nth-child(1) vn-textfield[model="ticket.priority"] input',
secondTicketPriority: 'vn-route-tickets vn-tr:nth-child(2) vn-textfield[model="ticket.priority"] input',
thirdTicketPriority: 'vn-route-tickets vn-tr:nth-child(3) vn-textfield[model="ticket.priority"] input',
fourthTicketPriority: 'vn-route-tickets vn-tr:nth-child(4) vn-textfield[model="ticket.priority"] input',
eleventhTicketPriority: 'vn-route-tickets vn-tr:nth-child(11) vn-textfield[model="ticket.priority"] input',
firstTicketPriority: 'vn-route-tickets vn-tr:nth-child(1) vn-textfield[ng-model="ticket.priority"] input',
secondTicketPriority: 'vn-route-tickets vn-tr:nth-child(2) vn-textfield[ng-model="ticket.priority"] input',
thirdTicketPriority: 'vn-route-tickets vn-tr:nth-child(3) vn-textfield[ng-model="ticket.priority"] input',
fourthTicketPriority: 'vn-route-tickets vn-tr:nth-child(4) vn-textfield[ng-model="ticket.priority"] input',
eleventhTicketPriority: 'vn-route-tickets vn-tr:nth-child(11) vn-textfield[ng-model="ticket.priority"] input',
firstTicketCheckbox: 'vn-route-tickets vn-tr:nth-child(1) vn-check',
buscamanButton: 'vn-route-tickets vn-button[icon="icon-buscaman"]',
firstTicketDeleteButton: 'vn-route-tickets vn-tr:nth-child(1) vn-icon[icon="delete"]',
confirmButton: 'vn-route-tickets > vn-confirm button[response="ACCEPT"]'
},
workerPbx: {
extensionInput: 'vn-worker-pbx vn-textfield[model="$ctrl.worker.sip.extension"] input',
extensionInput: 'vn-worker-pbx vn-textfield[ng-model="$ctrl.worker.sip.extension"] input',
saveButton: 'vn-worker-pbx vn-submit[label="Save"] input'
},
workerTimeControl: {

View File

@ -176,7 +176,7 @@ describe('Client lock verified data path', () => {
.wait(selectors.clientFiscalData.socialNameInput)
.evaluate(selector => {
return document.querySelector(selector).disabled;
}, 'vn-textfield[model="$ctrl.client.socialName"] > div');
}, 'vn-textfield[ng-model="$ctrl.client.socialName"] > div');
expect(result).toBeFalsy();
});

View File

@ -1,8 +1,7 @@
import selectors from '../../helpers/selectors.js';
import createNightmare from '../../helpers/nightmare';
// #1702 Autocomplete no siempre refresca al cancelar formulario
xdescribe('Item edit tax path', () => {
describe('Item edit tax path', () => {
const nightmare = createNightmare();
beforeAll(() => {
@ -14,9 +13,9 @@ xdescribe('Item edit tax path', () => {
it(`should add the item tax to all countries`, async() => {
const result = await nightmare
.autocompleteSearch(selectors.itemTax.firstClassAutocomplete, 'Reduced VAT')
.autocompleteSearch(selectors.itemTax.firstClassAutocomplete, 'General VAT')
.autocompleteSearch(selectors.itemTax.secondClassAutocomplete, 'General VAT')
.autocompleteSearch(selectors.itemTax.thirdClassAutocomplete, 'Reduced VAT')
.autocompleteSearch(selectors.itemTax.thirdClassAutocomplete, 'General VAT')
.waitToClick(selectors.itemTax.submitTaxButton)
.waitForLastSnackbar();
@ -28,7 +27,7 @@ xdescribe('Item edit tax path', () => {
.reloadSection('item.card.tax')
.waitToGetProperty(`${selectors.itemTax.firstClassAutocomplete} input`, 'value');
expect(firstVatType).toEqual('Reduced VAT');
expect(firstVatType).toEqual('General VAT');
});
it(`should confirm the second item tax class was edited`, async() => {
@ -42,6 +41,22 @@ xdescribe('Item edit tax path', () => {
const thirdVatType = await nightmare
.waitToGetProperty(`${selectors.itemTax.thirdClassAutocomplete} input`, 'value');
expect(thirdVatType).toEqual('Reduced VAT');
expect(thirdVatType).toEqual('General VAT');
});
it(`should edit the first class without saving the form`, async() => {
const firstVatType = await nightmare
.autocompleteSearch(selectors.itemTax.firstClassAutocomplete, 'Reduced VAT')
.waitToGetProperty(`${selectors.itemTax.firstClassAutocomplete} input`, 'value');
expect(firstVatType).toEqual('Reduced VAT');
});
it(`should now click the undo changes button and see the changes works`, async() => {
const firstVatType = await nightmare
.waitToClick(selectors.itemTax.undoChangesButton)
.waitToGetProperty(`${selectors.itemTax.firstClassAutocomplete} input`, 'value');
expect(firstVatType).toEqual('General VAT');
});
});

View File

@ -160,7 +160,7 @@ describe('Ticket descriptor path', () => {
it('should confirm the sixth weekly ticket was deleted', async() => {
const result = await nightmare
.waitToClick('vn-ticket-weekly-index vn-searchbar i[class="material-icons clear"]')
.waitToClick('vn-ticket-weekly-index vn-searchbar vn-icon[icon=clear]')
.waitToClick(selectors.ticketsIndex.searchWeeklyButton)
.waitForNumberOfElements(selectors.ticketsIndex.searchWeeklyResult, 5)
.countElement(selectors.ticketsIndex.searchWeeklyResult);

View File

@ -78,7 +78,7 @@ describe('Ticket services path', () => {
.waitToClick(selectors.ticketService.saveDescriptionButton)
.waitForLastSnackbar();
expect(result).toEqual(`Name can't be empty`);
expect(result).toEqual(`can't be blank`);
});
it('should create a new description then add price then create the service', async() => {

View File

@ -49,7 +49,7 @@ xdescribe('Route basic Data path', () => {
it('should count how many tickets are in route', async() => {
const result = await nightmare
.countElement('vn-route-tickets vn-textfield[model="ticket.priority"]');
.countElement('vn-route-tickets vn-textfield[ng-model="ticket.priority"]');
expect(result).toEqual(11);
});
@ -74,7 +74,7 @@ xdescribe('Route basic Data path', () => {
it('should now count how many tickets are in route to find one less', async() => {
const result = await nightmare
.countElement('vn-route-tickets vn-textfield[model="ticket.priority"]');
.countElement('vn-route-tickets vn-textfield[ng-model="ticket.priority"]');
expect(result).toEqual(9);
});

View File

@ -1,27 +0,0 @@
<div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input
type="button"
class="mdl-textfield__input"
ng-click="$ctrl.onMouseDown($event)"
ng-keydown="$ctrl.onKeyDown($event)"/>
<div class="icons">
<vn-icon
ng-show="!$ctrl.disabled"
icon="clear"
class="clear"
ng-click="$ctrl.onClearClick($event)"
translate-attr="{title: 'Clear'}">
</vn-icon>
</div>
<label class="mdl-textfield__label">
<span translate>{{::$ctrl.label}}</span>
<span translate ng-show="::$ctrl.required">*</span>
</label>
</div>
</div>
<vn-drop-down
vn-id="drop-down"
on-select="$ctrl.onDropDownSelect(item)"
on-data-ready="$ctrl.onDataReady()">
</vn-drop-down>

View File

@ -0,0 +1,52 @@
<div
class="container"
ng-click="$ctrl.onContainerMouseDown($event)"
ng-keydown="$ctrl.onContainerKeyDown($event)">
<div
ng-transclude="prepend"
class="prepend">
</div>
<div class="infix">
<div class="fix prefix"></div>
<div class="control">
<input
type="button">
</input>
</div>
<div class="fix suffix"></div>
<label>
<span translate>{{::$ctrl.label}}</span>
<span class="required">*</span>
</label>
</div>
<div class="icons pre">
<vn-icon
icon="clear"
translate-attr="{title: 'Clear'}"
ng-click="$ctrl.onClear($event)">
</vn-icon>
<vn-icon
ng-if="::$ctrl.info"
icon="info_outline"
vn-tooltip="{{::$ctrl.info}}">
</vn-icon>
</div>
<div
ng-transclude="append"
class="append">
</div>
<div class="icons post">
<vn-icon
icon="arrow_drop_down">
</vn-icon>
</div>
<div class="underline blur"></div>
<div class="underline focus"></div>
</div>
<div class="hint"></div>
<vn-drop-down
vn-id="drop-down"
on-select="$ctrl.onDropDownSelect(item)"
on-data-ready="$ctrl.onDataReady()"
on-close="$ctrl.focus()">
</vn-drop-down>

View File

@ -1,5 +1,5 @@
import ngModule from '../../module';
import Input from '../../lib/input';
import Field from '../field';
import assignProps from '../../lib/assign-props';
import {mergeWhere} from 'vn-loopback/util/filter';
import './style.scss';
@ -16,23 +16,18 @@ import './style.scss';
*
* @event change Thrown when value is changed
*/
export default class Autocomplete extends Input {
constructor($element, $scope, $http, $transclude, $translate, $interpolate) {
super($element, $scope);
this.$http = $http;
this.$interpolate = $interpolate;
this.$transclude = $transclude;
this.$translate = $translate;
this._field = undefined;
export default class Autocomplete extends Field {
constructor($element, $scope, $compile, $http, $transclude, $translate, $interpolate) {
super($element, $scope, $compile);
Object.assign(this, {
$http,
$interpolate,
$transclude,
$translate
});
this._selection = null;
this.readonly = true;
this.form = null;
this.input = this.element.querySelector('.mdl-textfield__input');
componentHandler.upgradeElement(
this.element.querySelector('.mdl-textfield'));
this.registerEvents();
this.input = this.element.querySelector('input');
}
$postLink() {
@ -44,12 +39,16 @@ export default class Autocomplete extends Input {
}
/**
* Registers all event emitters
* @type {any} The autocomplete value.
*/
registerEvents() {
this.input.addEventListener('focus', event => {
this.emit('focus', {event});
});
get field() {
return super.field;
}
set field(value) {
super.field = value;
this.refreshSelection();
this.emit('change', {value});
}
get model() {
@ -83,20 +82,6 @@ export default class Autocomplete extends Input {
Object.assign(this.$.dropDown, props);
}
/**
* @type {any} The autocomplete value.
*/
get field() {
return this._field;
}
set field(value) {
this._field = value;
this.refreshSelection();
this.emit('change', {value});
}
/**
* @type {Object} The selected data object, you can use this property
* to prevent requests to display the initial value.
@ -136,7 +121,7 @@ export default class Autocomplete extends Input {
return;
const selection = this.fetchSelection();
if (!this.selection)
this.selection = selection;
}
@ -216,39 +201,21 @@ export default class Autocomplete extends Input {
if (this.translateFields.indexOf(this.showField) > -1)
this.input.value = this.$translate.instant(display);
}
this.mdlUpdate();
}
mdlUpdate() {
let field = this.element.querySelector('.mdl-textfield');
let mdlField = field.MaterialTextfield;
if (mdlField) mdlField.updateClasses_();
}
setValue(value) {
this.field = value;
if (this.form) this.form.$setDirty();
}
onDropDownSelect(item) {
const value = item[this.valueField];
this.selection = item;
this.setValue(value);
this.field = value;
}
onClearClick(event) {
event.preventDefault();
this.setValue(null);
}
onContainerKeyDown(event) {
if (event.defaultPrevented) return;
onKeyDown(event) {
// if (event.defaultPrevented) return;
switch (event.keyCode) {
case 38: // Up
case 40: // Down
case 13: // Enter
switch (event.code) {
case 'ArrowUp':
case 'ArrowDown':
case 'Enter':
this.showDropDown();
break;
default:
@ -261,7 +228,8 @@ export default class Autocomplete extends Input {
event.preventDefault();
}
onMouseDown(event) {
onContainerMouseDown(event) {
if (event.defaultPrevented) return;
event.preventDefault();
this.showDropDown();
}
@ -293,7 +261,7 @@ export default class Autocomplete extends Input {
showDropDown(search) {
this.assignDropdownProps();
this.$.dropDown.show(this.input, search);
this.$.dropDown.show(this.container, search);
}
get fetchFunction() {
@ -307,16 +275,12 @@ export default class Autocomplete extends Input {
this.refreshSelection();
}
}
Autocomplete.$inject = ['$element', '$scope', '$http', '$transclude', '$translate', '$interpolate'];
Autocomplete.$inject = ['$element', '$scope', '$compile', '$http', '$transclude', '$translate', '$interpolate'];
ngModule.component('vnAutocomplete', {
template: require('./autocomplete.html'),
ngModule.vnComponent('vnAutocomplete', {
template: require('./index.html'),
controller: Autocomplete,
bindings: {
label: '@',
field: '=?',
disabled: '<?',
required: '@?',
showField: '@?',
valueField: '@?',
initialData: '<?',
@ -336,8 +300,5 @@ ngModule.component('vnAutocomplete', {
},
transclude: {
tplItem: '?tplItem'
},
require: {
form: '?^form'
}
});

View File

@ -1,66 +1,23 @@
@import "effects";
vn-autocomplete {
vn-autocomplete.vn-field {
overflow: hidden;
& > div > .mdl-textfield {
position: relative;
width: 100%;
& > .container {
cursor: pointer;
& > .infix > .control {
overflow: hidden;
& > input {
cursor: pointer;
height: 30px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
& > .icons {
display: none;
position: absolute;
right: 0;
top: 1.3em;
height: 1em;
color: $color-font-secondary;
border-radius: .2em;
& > vn-icon {
cursor: pointer;
font-size: 18px;
&:hover {
color: $color-font;
text-align: left;
padding-left: 0;
padding-right: 0;
}
}
}
&:hover > .icons,
& > input:focus + .icons {
display: block;
}
}
label span:nth-child(2) {
color: $color-alert
}
}
ul.vn-autocomplete {
list-style-type: none;
padding: 1em;
margin: 0;
padding: 0;
overflow: auto;
max-height: 300px;
li {
@extend %clickable;
display: block;
padding: .8em;
margin: 0;
&.load-more {
color: $color-main;
font-family: vn-font-bold;
padding: .4em .8em;
}
}
}

View File

@ -12,13 +12,13 @@ import './style.scss';
*/
export default class Check extends Toggle {
set field(value) {
this._field = value;
super.field = value;
this.element.classList.toggle('checked', Boolean(value));
this.indeterminate = Boolean(value == null && this.tripleState);
}
get field() {
return this._field;
return super.field;
}
set checked(value) {
@ -64,14 +64,10 @@ export default class Check extends Toggle {
}
}
ngModule.component('vnCheck', {
ngModule.vnComponent('vnCheck', {
template: require('./index.html'),
controller: Check,
bindings: {
label: '@?',
field: '=?',
disabled: '<?',
checked: '<?',
tripleState: '<?',
indeterminate: '<?',
info: '@?'

View File

@ -8,7 +8,7 @@ describe('Component vnCheck', () => {
}));
beforeEach(inject(($compile, $rootScope) => {
$element = $compile(`<vn-check></vn-check`)($rootScope);
$element = $compile(`<vn-check></vn-check>`)($rootScope);
$ctrl = $element.controller('vnCheck');
element = $element[0];
}));

View File

@ -1,21 +0,0 @@
<div
class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label"
ng-focus="$ctrl.hasFocus = true"
ng-blur="$ctrl.hasFocus = false"
ng-mouseenter="$ctrl.hasMouseIn = true"
ng-mouseleave="$ctrl.hasMouseIn = false">
<input type="text"
class="mdl-textfield__input"
name="{{::$ctrl.name}}"
ng-disabled="$ctrl.disabled"
rule="{{::$ctrl.rule}}"/>
<div class="mdl-chip__action">
<i
class="material-icons pointer"
ng-show="!$ctrl.disabled && $ctrl.model && ($ctrl.hasFocus || $ctrl.hasMouseIn)"
ng-click="$ctrl.onClear()">
clear
</i>
</div>
<label class="mdl-textfield__label" translate>{{$ctrl.label}}</label>
</div>

View File

@ -1,107 +0,0 @@
import ngModule from '../../module';
import Component from '../../lib/component';
import {Flatpickr} from '../../vendor';
import './style.scss';
class DatePicker extends Component {
constructor($element, $scope, $translate, $attrs) {
super($element, $scope);
this.input = $element[0].querySelector('input');
this.$translate = $translate;
this.$attrs = $attrs;
this._model = undefined;
this.dateValue = undefined;
this.hasMouseIn = false;
let locale = this.$translate.use();
this.defaultOptions = {
locale: locale,
dateFormat: locale == 'es' ? 'd-m-Y' : 'Y-m-d',
enableTime: false,
disableMobile: true,
onValueUpdate: () => this.onValueUpdate()
};
this.userOptions = {};
this._iniOptions = this.defaultOptions;
componentHandler.upgradeElement($element[0].firstChild);
this.vp = new Flatpickr(this.input, this._iniOptions);
}
onValueUpdate() {
if (this.vp.selectedDates.length) {
let date = this.vp.selectedDates[0];
let offset = date.getTimezoneOffset() * 60000;
date.setTime(date.getTime() - offset);
this._model = date;
} else
this.model = null;
this.$.$apply();
}
set iniOptions(value) {
this.userOptions = value;
let options = Object.assign({}, this.defaultOptions, value);
this._iniOptions = options;
// TODO: When some properties change Flatpickr doesn't refresh the view
// for (let option in options)
// this.vp.set(option, options[option]);
if (this.vp) this.vp.destroy();
this.vp = new Flatpickr(this.input, this._iniOptions);
this.vp.setDate(this.dateValue);
this.mdlUpdate();
}
get iniOptions() {
return this.userOptions;
}
get model() {
return this._model;
}
set model(value) {
this._model = value;
this.dateValue = value;
let date;
if (value && this.iniOptions.enableTime) {
date = new Date(value);
let offset = date.getTimezoneOffset() * 60000;
date.setTime(date.getTime() + offset);
} else
date = value;
this.vp.setDate(date);
this.mdlUpdate();
}
onClear() {
this.model = null;
}
mdlUpdate() {
let mdlField = this.element.firstChild.MaterialTextfield;
if (mdlField)
mdlField.updateClasses_();
}
$onDestroy() {
this.vp.destroy();
this.dateValue = undefined;
}
}
DatePicker.$inject = ['$element', '$scope', '$translate', '$attrs'];
ngModule.component('vnDatePicker', {
template: require('./date-picker.html'),
bindings: {
iniOptions: '<?',
model: '=',
label: '@?',
name: '@?',
disabled: '<?',
rule: '<?',
isLocale: '<?'
},
controller: DatePicker
});

View File

@ -1,37 +0,0 @@
describe('Component vnDatePicker', () => {
let controller;
let $attrs;
let $element;
let today = new Date();
today.setHours(0, 0, 0, 0);
beforeEach(angular.mock.module('vnCore', $translateProvider => {
$translateProvider.translations('en', {});
}));
beforeEach(angular.mock.inject(($componentController, $translate) => {
$attrs = {};
$element = angular.element(`<vn-date-picker><div><input type="text" class="mdl-textfield__input" name="MyName" ng-disabled="$ctrl.disabled" rule=""></input></div></vn-date-picker>`);
controller = $componentController('vnDatePicker', {$element, $attrs, $translate});
}));
describe('onValueUpdate() while date is selected', () => {
it(`should store the selected date in the controller`, () => {
controller.vp = {selectedDates: [today]};
controller.isLocale = true;
controller.onValueUpdate();
expect(controller._model).toEqual(today);
});
it(`should format the date`, () => {
controller.vp = {selectedDates: [today], destroy: () => {}};
controller.isLocale = undefined;
controller._iniOptions.enableTime = undefined;
controller.onValueUpdate();
expect(controller._model).toEqual(today);
});
});
});

View File

@ -0,0 +1,99 @@
import ngModule from '../../module';
import Field from '../field';
import {Flatpickr} from '../../vendor';
import './style.scss';
class DatePicker extends Field {
constructor($element, $scope, $compile, $translate) {
super($element, $scope, $compile);
this.$translate = $translate;
this.input = $compile(`<input type="text"></input>`)($scope)[0];
this.initPicker();
}
get field() {
return super.field;
}
set field(value) {
super.field = value;
let date = value;
if (date && !(date instanceof Date))
date = new Date(date);
this.picker.setDate(fixDate(date));
}
set options(value) {
let selectedDates = this.picker.selectedDates || [];
this._options = value;
this.initPicker();
this.picker.setDate(selectedDates[0]);
}
get options() {
return this._options;
}
initPicker() {
let locale = this.$translate.use();
let format = locale == 'es' ? 'd-m-Y' : 'Y-m-d';
let options = this.options || {};
let defaultOptions = {
locale: locale,
dateFormat: format,
enableTime: false,
disableMobile: true,
onValueUpdate: () => this.onValueUpdate()
};
if (options.enableTime) {
Object.assign(defaultOptions, {
dateFormat: `${format} h:i`,
time_24hr: true
});
}
let mergedOptions = Object.assign({},
defaultOptions,
options
);
if (this.picker) this.picker.destroy();
this.picker = new Flatpickr(this.input, mergedOptions);
}
onValueUpdate() {
let date = null;
if (this.picker.selectedDates.length)
date = this.picker.selectedDates[0];
super.field = fixDate(date, -1);
this.$.$applyAsync();
}
$onDestroy() {
this.picker.destroy();
}
}
DatePicker.$inject = ['$element', '$scope', '$compile', '$translate'];
ngModule.vnComponent('vnDatePicker', {
controller: DatePicker,
bindings: {
options: '<?'
}
});
function fixDate(date, mult = 1) {
if (date) {
let offset = date.getTimezoneOffset() * 60000;
date.setTime(date.getTime() + (offset * mult));
}
return date;
}

View File

@ -0,0 +1,45 @@
describe('Component vnDatePicker', () => {
let $filter;
let $element;
let $ctrl;
let today;
beforeEach(angular.mock.module('vnCore', $translateProvider => {
$translateProvider.translations('en', {});
}));
beforeEach(angular.mock.inject(($compile, $rootScope, _$filter_) => {
$filter = _$filter_;
$element = $compile(`<vn-date-picker></vn-date-picker>`)($rootScope);
$ctrl = $element.controller('vnDatePicker');
today = new Date();
today.setUTCHours(0, 0, 0, 0);
}));
afterEach(() => {
$element.remove();
});
describe('field() setter', () => {
it(`should display the formated the date`, () => {
$ctrl.field = today;
let displayed = $filter('dateTime')(today, 'yyyy-MM-dd');
expect($ctrl.value).toEqual(displayed);
});
});
describe('options() setter', () => {
it(`should display the date with the new format`, () => {
$ctrl.options = {dateFormat: 'Y-m'};
$ctrl.field = today;
let displayed = $filter('dateTime')(today, 'yyyy-MM');
expect($ctrl.value).toEqual(displayed);
});
});
});

View File

@ -1,24 +1,5 @@
@import "variables";
vn-date-picker {
.mdl-chip__action {
position: absolute;
width: auto;
top: 0px;
right: -6px;
margin: 22px 0px;
background-color: white;
}
.mdl-textfield {
width: 100%;
}
.material-icons {
font-size: 18px;
float: right;
margin-right: 5px;
}
}
.flatpickr-months .flatpickr-month,
.flatpickr-weekdays,
span.flatpickr-weekday {

View File

@ -31,7 +31,7 @@
tpl-body {
display: block;
width: 20em;
min-width: 20em;
}
& > button.close {
@extend %clickable;

View File

@ -6,10 +6,10 @@
<div ng-show="$ctrl.showFilter" class="filter">
<vn-textfield
vn-id="input"
model="$ctrl.search"
class="search"
ng-model="$ctrl.search"
class="dense search"
ng-blur="$ctrl.onFocusOut()"
label="Search">
placeholder="{{::'Search' | translate}}">
</vn-textfield>
</div>
<div vn-id="list" class="list" tabindex="-1">

View File

@ -106,8 +106,8 @@ export default class DropDown extends Component {
*/
show(parent, search) {
this._activeOption = -1;
this.search = search;
this.$.popover.show(parent || this.parent);
this.search = search;
this.buildList();
}
@ -194,10 +194,12 @@ export default class DropDown extends Component {
this.document.addEventListener('keydown', this.docKeyDownHandler);
this.$.list.scrollTop = 0;
this.$.input.focus();
this.emit('open');
}
onClose() {
this.document.removeEventListener('keydown', this.docKeyDownHandler);
this.emit('close');
}
onClearClick() {

View File

@ -13,18 +13,7 @@
display: block;
width: 100%;
box-sizing: border-box;
border: none;
font-size: inherit;
padding: .6em;
margin: 0!important;
&.not-empty label{
display: none;
}
& .selected label {
font-size: inherit;
bottom: 2px;
color: $color-font-secondary;
}
padding: $spacing-sm;
}
& > vn-icon[icon=clear] {
display: none;

View File

@ -5,20 +5,18 @@
</div>
<div class="infix">
<div class="fix prefix"></div>
<div class="control">
<input type="text" ng-model="$ctrl.field"/>
</div>
<div class="control"></div>
<div class="fix suffix"></div>
<label>
<span translate>{{::$ctrl.label}}</span>
<span class="required">*</span>
</label>
</div>
<div class="icons">
<div class="icons pre">
<vn-icon
icon="clear"
translate-attr="{title: 'Clear'}"
ng-click="$ctrl.onClear()">
ng-click="$ctrl.onClear($event)">
</vn-icon>
<vn-icon
ng-if="::$ctrl.info"
@ -30,6 +28,8 @@
ng-transclude="append"
class="append">
</div>
<div class="icons post">
</div>
<div class="underline blur"></div>
<div class="underline focus"></div>
</div>

View File

@ -1,37 +1,63 @@
import ngModule from '../../module';
import Component from '../../lib/component';
import FormInput from '../form-input';
import './style.scss';
export default class Field extends Component {
constructor($element, $scope) {
export default class Field extends FormInput {
constructor($element, $scope, $compile) {
super($element, $scope);
this._value = undefined;
this.$compile = $compile;
this.prefix = null;
this.suffix = null;
this.input = this.element.querySelector('input');
this.control = this.element.querySelector('.control');
this.classList = this.element.classList;
this.classList.add('vn-field');
this.element.addEventListener('click', e => this.onClick(e));
this.element.addEventListener('focusin',
() => this.onFocus(true));
this.element.addEventListener('focusout',
() => this.onFocus(false));
this.element.addEventListener('click',
() => this.onClick());
this.container = this.element.querySelector('.container');
this.container.addEventListener('mousedown', e => this.onMouseDown(e));
}
$onInit() {
super.$onInit();
if (this.info) this.classList.add('has-icons');
this.input.addEventListener('focus', () => this.onFocus(true));
this.input.addEventListener('blur', () => this.onFocus(false));
this.input.addEventListener('change', e => {
this.emit('change', {event: e});
});
}
set field(value) {
this._field = value;
if (value === this.field) return;
super.field = value;
this.classList.toggle('not-empty', value != null && value !== '');
this.validateValue();
}
get field() {
return this._field;
return super.field;
}
set input(value) {
if (this.input) this.control.removeChild(this.input);
this._input = value;
this.control.appendChild(value);
}
get input() {
return this._input;
}
set value(value) {
this.field = value;
}
get value() {
return this.input.value;
}
set type(value) {
@ -42,6 +68,30 @@ export default class Field extends Component {
return this.input.type;
}
set name(value) {
this.input.name = value;
}
get name() {
return this.input.name;
}
set placeholder(value) {
this.input.placeholder = value;
}
get placeholder() {
return this.input.placeholder;
}
set tabIndex(value) {
this.input.tabIndex = value;
}
get tabIndex() {
return this.input.tabIndex;
}
set disabled(value) {
this._disabled = boolTag(value);
this.input.disabled = this._disabled;
@ -100,20 +150,28 @@ export default class Field extends Component {
}
set error(value) {
if (value === this.error) return;
this._error = value;
this.refreshHint();
this.classList.toggle('invalid', Boolean(value));
}
get error() {
return this._error;
}
get shownError() {
return this.error || this.inputError || null;
}
refreshHint() {
let hint = this.error || this.hint || '';
let error = this.shownError;
let hint = error || this.hint;
let hintEl = this.element.querySelector('.hint');
hintEl.innerText = hint;
hintEl.innerText = hint || '';
hintEl.classList.toggle('filled', Boolean(hint));
this.classList.toggle('invalid', Boolean(error));
}
refreshFix(selector, text) {
@ -123,16 +181,27 @@ export default class Field extends Component {
}
onClick() {
if (event.defaultPrevented) return;
event.preventDefault();
if (this.input !== document.activeElement)
this.input.focus();
this.focus();
}
onMouseDown(event) {
if (event.target == this.input) return;
event.preventDefault();
this.focus();
}
onFocus(hasFocus) {
this.classList.toggle('focused', hasFocus);
}
onClear() {
this.input.value = '';
onClear(event) {
if (event.defaultPrevented) return;
event.preventDefault();
this.field = null;
this.input.dispatchEvent(new Event('change'));
}
@ -143,10 +212,28 @@ export default class Field extends Component {
select() {
this.input.select();
}
}
Field.$inject = ['$element', '$scope'];
ngModule.component('vnField', {
buildInput(type) {
let template = `<input type="${type}" ng-model="$ctrl.field"></input>`;
this.input = this.$compile(template)(this.$)[0];
}
/**
* If input value is invalid, sets the error message as hint.
*/
validateValue() {
let error = this.input.checkValidity()
? null
: this.input.validationMessage;
if (error === this.inputError) return;
this.inputError = error;
this.refreshHint();
}
}
Field.$inject = ['$element', '$scope', '$compile'];
ngModule.vnComponent('vnField', {
template: require('./index.html'),
transclude: {
prepend: '?prepend',
@ -154,18 +241,17 @@ ngModule.component('vnField', {
},
controller: Field,
bindings: {
field: '=?',
label: '@?',
name: '@?',
type: '@?',
placeholder: '@?',
value: '=?',
info: '@?',
disabled: '@?',
readonly: '@?',
required: '@?',
required: '<?',
prefix: '@?',
suffix: '@?',
hint: '@?',
error: '<?'
error: '<?',
tabIndex: '<?',
rule: '@?'
}
});

View File

@ -8,7 +8,6 @@
display: flex;
align-items: stretch;
position: relative;
height: 56px;
& > .infix {
position: relative;
@ -50,8 +49,8 @@
& > .control {
height: 100%;
flex: auto;
}
& > .control > input {
& > * {
padding-top: 24px;
padding-bottom: 8px;
height: inherit;
@ -64,10 +63,13 @@
background: 0;
color: inherit;
box-sizing: border-box;
min-height: 56px;
&[type=time],
&[type=date] {
clip-path: inset(0 20px 0 0);
opacity: 0;
transition: opacity 200ms ease-in-out;
}
&[type=number] {
-moz-appearance: textfield;
@ -83,6 +85,7 @@
}
}
}
}
& > .prepend,
& > .append,
& > .icons {
@ -102,13 +105,17 @@
& > .prepend > prepend {
padding-right: 12px;
}
& > .append > append {
& > .icons {
&.pre {
padding-left: 12px;
}
& > .icons > vn-icon[icon=clear] {
display: none;
& > vn-icon {
cursor: pointer;
}
& > vn-icon[icon=clear] {
display: none;
}
}
& > .underline {
position: absolute;
bottom: 0;
@ -131,30 +138,58 @@
}
}
}
&.not-empty > .container,
&.focused > .container {
& > .infix {
& > .fix {
opacity: 1;
&.dense {
& > .hint {
display: none;
}
& > .container > .infix {
& > label {
top: 8px;
}
& > .control > * {
padding-top: 8px;
min-height: 40px;
}
}
&.not-empty,
&.focused,
&.invalid {
& > .container > .infix > label {
top: 0;
line-height: 8px;
}
}
}
&.not-empty,
&.focused,
&.invalid {
& > .container > .infix {
& > label {
top: 5px;
color: $color-main;
padding: 0;
font-size: 12px;
}
& > .control > * {
&[type=time],
&[type=date] {
opacity: 1;
}
}
}
}
&.has-icons,
&.not-empty:hover,
&.not-empty.focused {
& > .container > .append > append {
& > .container {
& > .append > append {
padding-left: 0;
}
& > .container > .icons {
& > .icons.pre {
padding-left: 12px;
}
}
}
&:not(.disabled):not(.readonly) {
&.focused > .container > .underline.focus {
left: 0;
@ -170,14 +205,8 @@
}
}
}
&:not(.not-empty):not(.focused) > .container > .infix > .control > input {
&[type=time],
&[type=date] {
opacity: 0;
}
}
&.readonly > .container {
& > .infix > .control > input {
& > .infix > .control > * {
caret-color: transparent;
}
& > .underline.blur {
@ -186,11 +215,11 @@
}
& > .hint {
z-index: -1;
padding-top: 8px;
height: 20px;
padding: 4px 0;
height: 12px;
color: rgba(0, 0, 0, .4);
font-size: 12px;
transform: translateY(-28px);
transform: translateY(-12px);
transition-property: opacity, transform, color;
transition-duration: 200ms;
transition-timing-function: ease-in-out;
@ -220,8 +249,3 @@
}
}
}
vn-table {
.vn-field {
margin: 0;
}
}

View File

@ -0,0 +1,58 @@
import ngModule from '../../module';
import Component from '../../lib/component';
/**
* Base component for form inputs.
*
* @property {String} label Label to display along the component
* @property {any} field The value with which the element is linked
* @property {Boolean} disabled Put component in disabled mode
*/
export default class FormInput extends Component {
$onInit() {
// XXX: Compatibility with old inputs
let attrs = this.$element[0].attributes;
if (!this.name && attrs['ng-model']) {
let split = attrs['ng-model'].nodeValue.split('.');
this.name = split[split.length - 1];
}
if (!this.ngModel) return;
this.ngModel.$render = () => {
this.field = this.ngModel.$viewValue;
};
}
set field(value) {
this._field = value;
if (this.ngModel)
this.ngModel.$setViewValue(value);
}
get field() {
return this._field;
}
set name(value) {
this.element.setAttribute('name', value);
}
get name() {
return this.element.getAttribute('name');
}
}
ngModule.vnComponent('vnFormInput', {
controller: FormInput,
bindings: {
label: '@?',
field: '=?',
name: '@?',
disabled: '<?',
readonly: '<?'
},
require: {
ngModel: '?ngModel'
}
});

View File

@ -3,23 +3,28 @@ import './style.scss';
export default class IconButton {
constructor($element) {
if ($element[0].getAttribute('tabindex') == null)
$element[0].tabIndex = 0;
this.element = $element[0];
$element.on('keyup', event => this.onKeyDown(event, $element));
let button = $element[0].querySelector('button');
$element[0].addEventListener('click', event => {
if (this.element.getAttribute('tabindex') == null)
this.element.tabIndex = 0;
this.element.addEventListener('keyup', e => this.onKeyup(e));
this.element.addEventListener('click', e => this.onClick(e));
}
onKeyup(event) {
if (event.code == 'Space')
this.onClick(event);
}
onClick(event) {
if (event.defaultPrevented) return;
event.preventDefault();
// FIXME: Don't use Event.stopPropagation()
let button = this.element.querySelector('button');
if (this.disabled || button.disabled)
event.stopImmediatePropagation();
});
}
onKeyDown(event, $element) {
if (event.defaultPrevented) return;
if (event.keyCode == 13) {
event.preventDefault();
$element.triggerHandler('click');
}
}
}

View File

@ -1,9 +1,11 @@
@import "variables";
@import "effects";
vn-icon-button {
@extend %clickable-light;
outline: 0;
color: $color-main;
display: inline-block;
display: inline-flex;
align-items: center;
font-size: 18pt;
padding: .25em;

View File

@ -13,13 +13,10 @@ import './tooltip/tooltip';
import './icon-menu/icon-menu';
import './button-menu/button-menu';
import './popover/popover';
import './autocomplete/autocomplete';
import './drop-down/drop-down';
import './menu/menu';
import './multi-check/multi-check';
import './date-picker/date-picker';
import './button/button';
import './textarea/textarea';
import './icon-button/icon-button';
import './submit/submit';
import './card/card';
@ -29,20 +26,22 @@ import './label-value/label-value';
import './pagination/pagination';
import './searchbar/searchbar';
import './scroll-up/scroll-up';
import './input-range';
import './autocomplete';
import './calendar';
import './check';
import './chip';
import './color-legend';
import './data-viewer';
import './date-picker';
import './field';
import './input-number';
import './input-range';
import './input-time';
import './input-file';
import './list';
import './radio';
import './table';
import './td-editable';
import './textarea';
import './th';
import './treeview';
import './treeview/child';

View File

@ -3,15 +3,13 @@ import Input from '../../lib/input';
import './style.scss';
export default class InputFile extends Input {
constructor($element, $scope, $attrs, vnTemplate) {
constructor($element, $scope) {
super($element, $scope);
this.element = $element[0];
this.hasFocus = false;
this._multiple = false;
this._value = 'Select a file';
vnTemplate.normalizeInputAttrs($attrs);
this.registerEvents();
}
@ -108,7 +106,7 @@ export default class InputFile extends Input {
}
}
InputFile.$inject = ['$element', '$scope', '$attrs', 'vnTemplate'];
InputFile.$inject = ['$element', '$scope'];
ngModule.component('vnInputFile', {
template: require('./index.html'),

View File

@ -14,7 +14,7 @@ describe('Component vnInputFile', () => {
beforeEach(angular.mock.inject(($componentController, $rootScope) => {
$scope = $rootScope.$new();
$attrs = {field: '$ctrl.dms.file'};
$element = angular.element('<vn-input-file label="File" field="$ctrl.dms.file"><input type="file"><div class="infix"><div class="rightIcons"></div></vn-input-file>');
$element = angular.element('<vn-input-file label="File" ng-model="$ctrl.dms.file"><input type="file"><div class="infix"><div class="rightIcons"></div></vn-input-file>');
controller = $componentController('vnInputFile', {$element, $scope, $attrs, $timeout, $transclude: () => {}});
controller.input = $element[0].querySelector('input');
controller.validate = () => {};

View File

@ -1,8 +1,6 @@
@import "variables";
@import '../textfield/style.scss';
vn-input-file {
@extend vn-textfield;
.value {
color: $color-font-secondary;
cursor: pointer;
@ -12,4 +10,156 @@ vn-input-file {
input {
display: none !important
}
margin: 20px 0;
display: inline-block;
width: 100%;
& > .container {
width: 100%;
position: relative;
padding-bottom: 2px;
display: flex;
& > .textField {
width: 100%;
display: flex;
align-items: center;
position: relative;
padding-top: 4px;
}
}
.leftIcons, .rightIcons, .suffix {
display: flex;
color: $color-font-secondary;
.material-icons {
font-size: 20px !important
}
}
.suffix vn-icon-button {
padding: 0
}
t-left-icons {
padding-right: 0.5em
}
t-right-icons {
padding-left: 0.5em
}
.infix {
position: relative;
display: block;
flex: auto;
width: 100%;
min-width: 0;
}
i.clear {
visibility: hidden;
cursor: pointer;
outline: 0;
&:hover {
color: #222;
}
}
&:hover i.clear {
visibility: visible;
}
i.visible {
visibility: visible;
}
label {
position: absolute;
bottom: 0;
left: 0;
padding: 4px 0!important;
pointer-events: none;
color: $color-font-secondary;
transition-duration: .2s;
transition-timing-function: cubic-bezier(.4,0,.2,1);
}
&.not-empty label{
bottom: 24px;
color: $color-main;
padding: 0;
font-size: 12px;
}
input {
outline: none;
border: none;
font-family: "Helvetica", "Arial", sans-serif;
display: block;
font-size: 16px;
width: 100%;
background: 0 0;
color: inherit;
padding: 4px;
box-sizing: border-box;
border-bottom: 0!important;
&[type=number] {
-moz-appearance: textfield;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
&:invalid {
box-shadow: none;
}
}
.underline {
position: absolute;
bottom: 0;
height: 1px;
content: ' ';
pointer-events: none;
width: 100%;
background-color: $color-input-underline;
}
.selected.underline {
background-color: $color-main;
height: 2px;
left: 50%;
width: 0px !important;
transition-duration: 0.2s;
transition-timing-function: cubic-bezier(.4,0,.2,1);
}
div.selected {
&.container{
border-bottom: 0px;
}
label {
bottom: 24px;
color: $color-main;
font-size: 12px;
}
.selected.underline{
left: 0;
width: 100%!important;
}
}
& > div.container > div.textField > div.infix.invalid {
@extend div.selected;
& > span.mdl-textfield__error {
visibility: visible;
}
& > label {
color: #d50000;
}
}
.infix.invalid + .underline {
background-color: #d50000;
}
label span:nth-child(2) {
color: $color-alert
}
}

View File

@ -1,48 +1,48 @@
<div class="container"
ng-class="{selected: $ctrl.hasFocus}">
<div class="textField">
<div class="leftIcons">
<div class="container">
<div
ng-transclude="prepend"
class="prepend">
</div>
<div class="infix">
<input
class="mdl-textfield__input"
type="number"
name="{{::$ctrl.name}}"
ng-model="$ctrl.value"
vn-validation="{{$ctrl.rule}}"
ng-disabled="$ctrl.disabled"
ng-readonly="$ctrl.readonly"
ng-focus="$ctrl.hasFocus = true"
ng-blur="$ctrl.hasFocus = false"
tabindex="{{$ctrl.input.tabindex}}"/>
<label class="label">
<div class="fix prefix"></div>
<div class="control"></div>
<div class="fix suffix"></div>
<label>
<span translate>{{::$ctrl.label}}</span>
<span translate ng-show="::$ctrl.required">*</span>
<span class="required">*</span>
</label>
</div>
<div class="underline"></div>
<div class="selected underline"></div>
<div class="suffix">
<vn-icon-button
<div class="icons pre">
<vn-icon
icon="clear"
translate-attr="{title: 'Clear'}"
ng-click="$ctrl.onClear($event)">
</vn-icon>
<vn-icon
ng-if="::$ctrl.info"
icon="info_outline"
vn-tooltip="{{::$ctrl.info}}">
</vn-icon>
</div>
<div
ng-transclude="append"
class="append">
</div>
<div class="icons post">
<vn-icon
ng-if="$ctrl.displayControls"
icon="remove"
ng-click="$ctrl.stepDown()"
tabindex="-1"
ng-click="$ctrl.onStep($event, 'down')"
translate-attr="{title: 'Remove'}">
</vn-icon-button>
<vn-icon-button
</vn-icon>
<vn-icon
ng-if="$ctrl.displayControls"
icon="add"
ng-click="$ctrl.stepUp()"
tabindex="-1"
ng-click="$ctrl.onStep($event, 'up')"
translate-attr="{title: 'Add'}">
</vn-icon-button>
<i class="material-icons"
ng-if="::$ctrl.hasInfo"
vn-tooltip="{{::$ctrl.info}}">
info_outline
</i>
</div>
<div class="rightIcons"></div>
</vn-icon>
</div>
<div class="underline blur"></div>
<div class="underline focus"></div>
</div>
<div class="hint"></div>

View File

@ -1,48 +1,10 @@
import ngModule from '../../module';
import Input from '../../lib/input';
import './style.scss';
import Field from '../field';
export default class InputNumber extends Input {
constructor($element, $scope, $attrs, vnTemplate) {
super($element, $scope);
this.displayControls = false;
this.hasFocus = false;
vnTemplate.normalizeInputAttrs($attrs);
this.registerEvents();
}
/**
* Registers all event emitters
*/
registerEvents() {
this.input.addEventListener('change', event => {
this.validateValue();
this.emit('change', {event});
});
}
/**
* Gets current value
*/
get value() {
return this._value;
}
/**
* Sets input value
*
* @param {Number} value - Value
*/
set value(value) {
this.hasValue = !(value === null || value === undefined || value === '');
if (!this.hasOwnProperty('_value') && this.hasValue || value === '')
this.input.value = value;
this._value = value;
this.element.classList.toggle('not-empty', this.hasValue);
this.validateValue();
export default class InputNumber extends Field {
constructor($element, $scope, $compile) {
super($element, $scope, $compile);
this.buildInput('number');
}
/**
@ -58,7 +20,8 @@ export default class InputNumber extends Input {
* @param {Number} value - Value
*/
set max(value) {
if (value) this.input.max = value;
this.input.max = value;
this.validateValue();
}
/**
@ -74,7 +37,8 @@ export default class InputNumber extends Input {
* @param {Number} value - Value
*/
set min(value) {
if (value) this.input.min = value;
this.input.min = value;
this.validateValue();
}
/**
@ -90,47 +54,38 @@ export default class InputNumber extends Input {
* @param {Number} value - Value
*/
set step(value) {
if (value) this.input.step = value;
this.input.step = value;
this.validateValue();
}
/**
* Increases the input value
*/
stepUp() {
this.input.stepUp();
this.input.dispatchEvent(new Event('change'));
}
/**
* Decreases the input value
*/
stepDown() {
this.input.stepDown();
}
onStep(event, way) {
if (event.defaultPrevented) return;
event.preventDefault();
if (way == 'up')
this.stepUp();
else
this.stepDown();
this.input.dispatchEvent(new Event('change'));
}
}
InputNumber.$inject = ['$element', '$scope', '$attrs', 'vnTemplate'];
ngModule.component('vnInputNumber', {
ngModule.vnComponent('vnInputNumber', {
template: require('./index.html'),
controller: InputNumber,
transclude: {
leftIcons: '?tLeftIcons',
rightIcons: '?tRightIcons'
},
bindings: {
label: '@?',
name: '@?',
disabled: '<?',
required: '@?',
min: '<?',
max: '<?',
step: '<?',
displayControls: '<?',
rule: '@?',
value: '=model',
validate: '&',
onChange: '&',
onClear: '&'
displayControls: '<?'
}
});

View File

@ -1,68 +1,69 @@
import './index.js';
describe('Component vnInputNumber', () => {
let $scope;
let $attrs;
let $timeout;
let $element;
let controller;
let $ctrl;
beforeEach(angular.mock.module('vnCore', $translateProvider => {
$translateProvider.translations('en', {});
}));
beforeEach(angular.mock.inject(($componentController, $rootScope) => {
$scope = $rootScope.$new();
$attrs = {field: '$ctrl.client.socialName'};
$element = angular.element('<vn-input-number label="SocialName" field="$ctrl.client.socialName"><input type="number"><div class="infix"><div class="rightIcons"></div></vn-input-number>');
controller = $componentController('vnInputNumber', {$element, $scope, $attrs, $timeout, $transclude: () => {}});
controller.input = $element[0].querySelector('input');
controller.validate = () => {};
beforeEach(angular.mock.inject(($compile, $rootScope) => {
$element = $compile(`<vn-input-number></vn-input-number>`)($rootScope);
$ctrl = $element.controller('vnInputNumber');
}));
describe('value() setter', () => {
it(`should set a value, add the class 'not-empty' and then call validateValue() method`, () => {
spyOn(controller, 'validateValue');
controller.value = 10;
let classes = controller.element.classList.toString();
expect(classes).toContain('not-empty');
expect(controller.validateValue).toHaveBeenCalledWith();
afterEach(() => {
$element.remove();
});
it(`should set an empty value, remove the class 'not-empty' and then call validateValue() method`, () => {
spyOn(controller, 'validateValue');
describe('min() setter', () => {
it(`should set error property when value is lower than min`, () => {
$ctrl.field = -1;
$ctrl.min = 0;
controller.value = null;
// FIXME: Input validation doesn't work with Jest?
// expect($ctrl.shownError).toContain('Please select a value that is no less than 0');
expect($ctrl.shownError).toBeNull();
});
let classes = controller.element.classList.toString();
it(`should unset error property when value is upper than min`, () => {
$ctrl.field = 1;
$ctrl.min = 0;
expect(classes).not.toContain('not-empty');
expect(controller.validateValue).toHaveBeenCalledWith();
expect($ctrl.shownError).toBeNull();
});
});
describe('validateValue()', () => {
it(`should call hasValidValue() and not add the class invalid and validated`, () => {
controller.input.min = 0;
controller.input.value = 10;
describe('max() setter', () => {
it(`should set error property when value is upper than max`, () => {
$ctrl.field = 1;
$ctrl.max = 0;
controller.validateValue();
let classes = controller.element.querySelector('.infix').classList.toString();
expect(classes).not.toContain('validated invalid');
// FIXME: Input validation doesn't work with Jest?
// expect($ctrl.shownError).toContain('Please select a value that is no more than 0');
expect($ctrl.shownError).toBeNull();
});
it(`should call hasValidValue() and add the class invalid and validated`, () => {
controller.input.min = 0;
controller.input.value = -10;
// FIXME: Input validation doesn't work with Jest?
it(`should unset error property when value is lower than max`, () => {
$ctrl.field = -1;
$ctrl.min = 0;
controller.validateValue();
let classes = controller.element.querySelector('.infix').classList.toString();
expect($ctrl.shownError).toBeNull();
});
});
expect(classes).toContain('validated invalid');
describe('step() setter', () => {
it(`should increase value when add icon is clicked`, () => {
$ctrl.step = 1;
$ctrl.field = 1;
// FIXME: Doesn't work with Jest?
// $ctrl.stepUp();
// $element[0].querySelector('vn-icon[icon=add]').click();
expect($ctrl.field).toBe(1);
});
});
});

View File

@ -1,16 +0,0 @@
@import "variables";
@import '../textfield/style.scss';
vn-input-number {
@extend vn-textfield;
vn-icon[icon=add],
vn-icon[icon=remove] {
&:not(:hover){
color: $color-font-secondary;
}
i {
user-select: none;
}
}
}

View File

@ -1,26 +0,0 @@
<div class="container"
ng-class="{selected: $ctrl.hasFocus}">
<div class="textField">
<div class="leftIcons" ng-transclude="leftIcons"></div>
<div class="infix">
<input
class="mdl-textfield__input"
type="time"
ng-disabled="$ctrl.disabled"
ng-readonly="$ctrl.readonly"
ng-focus="$ctrl.hasFocus = true"
ng-blur="$ctrl.hasFocus = false"/>
<label class="label" translate>{{::$ctrl.label}}</label>
</div>
<div class="underline"></div>
<div class="selected underline"></div>
<div class="suffix">
<i class="material-icons"
ng-if="$ctrl.hasInfo"
vn-tooltip="{{$ctrl.info}}">
info_outline
</i>
</div>
<div class="rightIcons" ng-transclude="rightIcons"></div>
</div>
</div>

View File

@ -1,80 +1,41 @@
import ngModule from '../../module';
import Input from '../../lib/input';
import './style.scss';
import Field from '../field';
export default class InputTime extends Input {
constructor($element, $scope, $filter) {
super($element, $scope);
export default class InputTime extends Field {
constructor($element, $scope, $compile, $filter) {
super($element, $scope, $compile);
this.$filter = $filter;
this.registerEvents();
this.input = $compile(`<input type="time"></input>`)($scope)[0];
this.input.addEventListener('change', () => this.onValueUpdate());
}
registerEvents() {
this.input.addEventListener('change', event => {
this.onTimeChange();
this.emit('change', {event});
});
this.input.addEventListener('focus', event => {
this.emit('focus', {event});
});
get field() {
return super.field;
}
/**
* Gets current value
*/
get value() {
return this._value;
}
/**
* Sets input value
*
* @param {Number} value - Value
*/
set value(value) {
this.updateValue(value);
set field(value) {
this.input.value = this.$filter('dateTime')(value, 'HH:mm');
super.field = value;
}
updateValue(value) {
this._value = value;
this.element.classList.toggle('not-empty', value != null);
this.validateValue();
}
onTimeChange() {
onValueUpdate() {
let date = null;
let value = this.input.value;
if (value) {
let split = value.split(':').map(i => parseInt(i) || null);
date = new Date(this.value || null);
date = new Date(this.field || null);
date.setHours(split[0], split[1], 0, 0);
}
this.updateValue(date);
super.field = date;
this.$.$applyAsync();
}
}
InputTime.$inject = ['$element', '$scope', '$filter'];
InputTime.$inject = ['$element', '$scope', '$compile', '$filter'];
ngModule.component('vnInputTime', {
template: require('./index.html'),
controller: InputTime,
transclude: {
leftIcons: '?tLeftIcons',
rightIcons: '?tRightIcons'
},
bindings: {
label: '@?',
disabled: '<?',
readonly: '<?',
rule: '@?',
value: '=model',
vnTabIndex: '@?',
onChange: '&',
onClear: '&'
}
ngModule.vnComponent('vnInputTime', {
controller: InputTime
});

View File

@ -1,43 +1,32 @@
import './index.js';
describe('Component vnInputTime', () => {
let $scope;
let $attrs;
let $timeout;
let $filter;
let $element;
let controller;
let $ctrl;
beforeEach(angular.mock.module('vnCore', $translateProvider => {
$translateProvider.translations('en', {});
}));
beforeEach(angular.mock.inject(($componentController, $rootScope) => {
$scope = $rootScope.$new();
$attrs = {field: '$ctrl.zone.hour'};
$element = angular.element('<vn-input-time label="Hour" field="$ctrl.zone.hour"><input><div class="infix invalid validated"><div class="rightIcons"></div></vn-input-time>');
controller = $componentController('vnInputTime', {$element, $scope, $attrs, $timeout, $transclude: () => {}});
beforeEach(angular.mock.inject(($compile, $rootScope, _$filter_) => {
$filter = _$filter_;
$element = $compile(`<vn-input-time></vn-input-time>`)($rootScope);
$ctrl = $element.controller('vnInputTime');
}));
describe('value() setter', () => {
it(`should set _value to a given value, add the class not-empty and remove invalid and validated`, () => {
const today = new Date();
controller.value = today;
let classes = controller.element.classList.toString();
expect(classes).toContain('not-empty');
expect(controller._value).toEqual(today);
classes = controller.element.querySelector('.infix').classList.toString();
expect(classes).not.toContain('invalid validated');
afterEach(() => {
$element.remove();
});
it(`should set _value to a given value and not add the class not-empty if the given value is null`, () => {
controller.value = null;
let classes = controller.element.classList.toString();
describe('field() setter', () => {
it(`should display the formated the date`, () => {
let date = new Date();
$ctrl.field = date;
let displayed = $filter('dateTime')(date, 'HH:mm');
expect(classes).not.toContain('not-empty');
expect(controller._value).toEqual(null);
expect($ctrl.value).toEqual(displayed);
});
});
});

View File

@ -1,12 +0,0 @@
@import "variables";
@import '../textfield/style.scss';
vn-input-time {
@extend vn-textfield;
input[type="time"] {
clip-path: inset(0 17px 0 0);
outline: none;
outline: 0;
}
}

View File

@ -1,5 +1,5 @@
<vn-check
field="$ctrl.checked"
ng-model="$ctrl.checked"
intermediate="$ctrl.isIntermediate"
translate-attr="{title: 'Check all'}">
</vn-check>

View File

@ -109,7 +109,6 @@ export default class Popover extends Component {
this.showTimeout = null;
this.element.style.display = 'none';
this.document.body.removeChild(this.element);
this.emit('close');
}, 250);
this.document.removeEventListener('keydown', this.docKeyDownHandler);
@ -118,8 +117,8 @@ export default class Popover extends Component {
this.element.removeEventListener('mousedown', this.bgMouseDownHandler);
this.bgMouseDownHandler = null;
if (this.deregisterCallback)
this.deregisterCallback();
if (this.deregisterCallback) this.deregisterCallback();
this.emit('close');
}
/**
@ -189,7 +188,8 @@ export default class Popover extends Component {
}
onBgMouseDown(event) {
if (event != this.lastMouseEvent)
if (event == this.lastMouseEvent || event.defaultPrevented) return;
event.preventDefault();
this.hide();
}
}

View File

@ -10,13 +10,13 @@ import './style.scss';
*/
export default class Radio extends Toggle {
set field(value) {
this._field = value;
super.field = value;
this.element.classList.toggle('checked',
Boolean(value) && value == this.val);
}
get field() {
return this._field;
return super.field;
}
set checked(value) {
@ -43,14 +43,10 @@ export default class Radio extends Toggle {
}
}
ngModule.component('vnRadio', {
ngModule.vnComponent('vnRadio', {
template: require('../toggle/index.html'),
controller: Radio,
bindings: {
label: '@?',
field: '=?',
disabled: '<?',
checked: '<?',
val: '@?'
}
});

View File

@ -1,29 +1,30 @@
<form ng-submit="$ctrl.onSubmit()">
<vn-horizontal>
<vn-textfield vn-one label="Search" model="$ctrl.searchString">
<t-left-icons>
<vn-textfield
class="dense vn-py-md"
placeholder="{{::'Search' | translate}}"
ng-model="$ctrl.searchString">
<prepend>
<vn-icon
icon="search"
ng-click="$ctrl.clearFilter(); $ctrl.onSubmit()"
pointer>
</vn-icon>
</t-left-icons>
<t-right-icons vn-horizontal>
<vn-icon vn-one
</prepend>
<append>
<vn-icon
ng-if="$ctrl.info"
icon="info_outline"
vn-tooltip="{{$ctrl.info}}"
pointer>
</vn-icon>
<vn-icon vn-one
<vn-icon
ng-if="$ctrl.panel"
ng-click="$ctrl.openPanel($event)"
icon="keyboard_arrow_down"
icon="arrow_drop_down"
pointer>
</vn-icon>
</t-right-icons>
</append>
</vn-textfield>
</vn-horizontal>
</form>
<vn-popover
vn-id="popover"

View File

@ -1,12 +1,7 @@
@import "variables";
vn-searchbar {
padding-top: 6px;
display: block;
& > form > vn-horizontal > vn-icon-button {
color: black;
}
}
.search-panel {

View File

@ -1,2 +1,2 @@
<div class="table" ng-transclude>
<div class="vn-table" ng-transclude>
</div>

View File

@ -5,16 +5,20 @@ vn-table {
display: block;
overflow: auto;
width: 100%;
& > div {
width: inherit;
}
.vn-table {
width: 100%;
display: table;
border-collapse: collapse;
& > vn-thead {
& > vn-thead,
& > thead {
display: table-header-group;
border-bottom: .15em solid $color-spacer;
& > * > th {
font-weight: normal;
}
& > * > vn-th[field] {
position: relative;
overflow: visible;
@ -42,34 +46,43 @@ vn-table {
}
}
}
& > vn-tbody {
& > vn-tbody,
& > tbody {
display: table-row-group;
}
& > vn-tfoot {
& > vn-tfoot,
& > tfoot {
border-top: .15em solid $color-spacer;
display: table-footer-group
}
& > * > vn-tr,
& > * > a.vn-tr {
& > * > a.vn-tr,
& > * > tr {
display: table-row;
height: 3em;
}
vn-thead, vn-tbody, vn-tfoot {
vn-thead, vn-tbody, vn-tfoot,
thead, tbody, tfoot {
& > * {
display: table-row;
& > vn-th {
& > vn-th,
& > th {
color: $color-font-light;
padding-top: 1em;
padding-bottom: .8em;
}
& > vn-th,
& > vn-td {
& > vn-td,
& > th,
& > td {
overflow: hidden;
}
& > vn-th,
& > vn-td,
& > vn-td-editable {
& > vn-td-editable,
& > th,
& > td {
vertical-align: middle;
display: table-cell;
text-align: left;
@ -108,7 +121,8 @@ vn-table {
color: inherit;
}
}
vn-tbody > * {
vn-tbody > *,
tbody > * {
border-bottom: .1em solid $color-spacer-light;
&:last-child {
@ -117,42 +131,36 @@ vn-table {
&.clickable {
@extend %clickable;
}
& > vn-td .chip {
& > vn-td,
& > td {
.chip {
padding: .3em;
border-radius: .3em
}
& > vn-td .chip.notice {
border-radius: .3em;
color: $color-font-bg;
&.notice {
background-color: $color-notice-medium
}
& > vn-td .chip.success {
color: $color-font-bg;
background-color: $color-success-medium
&.success {
background-color: $color-success-medium;
}
& > vn-td .chip.warning {
color: $color-font-bg;
&.warning {
background-color: $color-main-medium;
}
& > vn-td .chip.alert {
color: $color-font-bg;
&.alert {
background-color: $color-alert-medium;
}
& > vn-td .chip.message {
&.message {
color: $color-font-dark;
background-color: $color-bg-dark
}
& > vn-td vn-icon-menu {
}
vn-icon-menu {
display: inline-block;
color: $color-main;
padding: .25em
}
}
& > [actions] {
width: 1px;
@ -161,26 +169,6 @@ vn-table {
}
}
}
& > vn-empty-rows {
display: table-caption;
caption-side: bottom;
text-align: center;
padding: 1.5em;
width: 100%;
box-sizing: border-box;
}
}
vn-autocomplete {
div.mdl-textfield {
padding: 0 !important;
}
label.mdl-textfield__label:after {
bottom: 0;
}
div.icons {
display: none !important;
}
}
vn-textfield {
float: right;
margin: 0!important;

View File

@ -0,0 +1,26 @@
import ngModule from '../../module';
import Field from '../field';
export default class Textarea extends Field {
constructor($element, $scope, $compile) {
super($element, $scope, $compile);
let html = `<textarea ng-model="$ctrl.field"></textarea>`;
this.input = $compile(html)($scope)[0];
}
set rows(value) {
this.input.rows = typeof value == 'number' && value > 0 ? value : 3;
}
get rows() {
return this.input.rows;
}
}
ngModule.vnComponent('vnTextarea', {
controller: Textarea,
bindings: {
rows: '<?'
}
});

View File

@ -0,0 +1,39 @@
import './index.js';
describe('Component vnTextarea', () => {
let $element;
let $ctrl;
beforeEach(angular.mock.module('vnCore', $translateProvider => {
$translateProvider.translations('en', {});
}));
beforeEach(angular.mock.inject(($compile, $rootScope) => {
$element = $compile(`<vn-textarea></vn-textarea>`)($rootScope);
$ctrl = $element.controller('vnTextarea');
}));
afterEach(() => {
$element.remove();
});
describe('rows() setter', () => {
it(`should set rows property of the element to the given value if it's a valid number`, () => {
$ctrl.rows = 27;
expect($ctrl.rows).toEqual(27);
});
it(`should set rows property of the element to 3 if the given value if it's null`, () => {
$ctrl.rows = null;
expect($ctrl.rows).toEqual(3);
});
it(`should set rows property of the element to 3 if the given value if it's not a valid number`, () => {
$ctrl.rows = 'a';
expect($ctrl.rows).toEqual(3);
});
});
});

View File

@ -1,24 +0,0 @@
@import "variables";
vn-textarea {
& > .mdl-textfield {
width: initial;
display: block;
}
label {
position: absolute;
bottom: 0;
pointer-events: none;
color: $color-font-secondary;
transition-duration: .2s;
transition-timing-function: cubic-bezier(.4,0,.2,1);
}
& textarea.ng-not-empty+label.placeholder{
top: 5px;
color: $color-main;
padding: 0;
font-size: 12px;
visibility: visible!important;
content: normal!important;
}
}

View File

@ -1,9 +0,0 @@
<div class="mdl-textfield mdl-js-textfield">
<textarea
class="mdl-textfield__input"
type="text"
ng-disabled="$ctrl.disabled"
ng-model="$ctrl.model">
</textarea>
<label class="mdl-textfield__label placeholder" translate>{{$ctrl.label}}</label>
</div>

View File

@ -1,43 +0,0 @@
import ngModule from '../../module';
import './style.scss';
export default class Textarea {
constructor($element, $scope, $attrs, vnTemplate) {
this.$ = $scope;
this.$attrs = $attrs;
this.element = $element;
vnTemplate.normalizeInputAttrs($attrs);
this.textarea = this.element[0].querySelector('textarea');
this.rows = null;
}
set model(value) {
this._model = value;
this.mdlUpdate();
}
set rows(value) {
this.textarea.rows = value ? value : 3;
}
get model() {
return this._model;
}
mdlUpdate() {
let mdlField = this.element[0].firstChild.MaterialTextfield;
if (mdlField)
mdlField.updateClasses_();
componentHandler.upgradeElement(this.element[0].firstChild);
}
}
Textarea.$inject = ['$element', '$scope', '$attrs', 'vnTemplate'];
ngModule.component('vnTextarea', {
template: require('./textarea.html'),
controller: Textarea,
bindings: {
model: '=model',
label: '@?',
rows: '@?',
name: '@?',
disabled: '<?',
rule: '@?'
}
});

View File

@ -1,56 +0,0 @@
import './textarea.js';
describe('Component vnTextarea', () => {
let $scope;
let $attrs;
let $element;
let controller;
beforeEach(angular.mock.module('vnCore', $translateProvider => {
$translateProvider.translations('en', {});
}));
beforeEach(angular.mock.inject(($componentController, $rootScope) => {
$scope = $rootScope.$new();
$attrs = {field: '$ctrl.claim.observation'};
$element = angular.element('<vn-textarea vn-three label="Observation" field="$ctrl.claim.observation" rows="9"><textarea></vn-textarea>');
$element[0].firstChild.MaterialTextfield = {updateClasses_: () => {}};
controller = $componentController('vnTextarea', {$scope, $element, $attrs});
}));
describe('model() setter', () => {
it(`should set model and call mdlUpdate`, () => {
spyOn(controller, 'mdlUpdate');
let model = 'model';
controller.model = model;
expect(controller._model).toEqual(model);
expect(controller.mdlUpdate).toHaveBeenCalledWith();
});
});
describe('rows() setter', () => {
it(`should set rows property of the element to the given value if its not null`, () => {
controller.rows = 27;
expect(controller.textarea.rows).toEqual(27);
});
it(`should set rows property of the element to 3 if the given value if its null`, () => {
controller.rows = null;
expect(controller.textarea.rows).toEqual(3);
});
});
describe('mdlUpdate()', () => {
it(`should should call mdlField.updateClasses if theres an mdlField defined and call upgradeElement with the textarea element`, () => {
spyOn($element[0].firstChild.MaterialTextfield, 'updateClasses_');
spyOn(componentHandler, 'upgradeElement');
controller.mdlUpdate();
expect($element[0].firstChild.MaterialTextfield.updateClasses_).toHaveBeenCalledWith();
expect(componentHandler.upgradeElement).toHaveBeenCalledWith(controller.element[0].firstChild);
});
});
});

View File

@ -1,162 +0,0 @@
@import "variables";
vn-textfield {
margin: 20px 0;
display: inline-block;
width: 100%;
& > .container {
width: 100%;
position: relative;
padding-bottom: 2px;
display: flex;
& > .textField {
width: 100%;
display: flex;
align-items: center;
position: relative;
padding-top: 4px;
}
}
.leftIcons, .rightIcons, .suffix {
display: flex;
color: $color-font-secondary;
.material-icons {
font-size: 20px !important
}
}
.suffix vn-icon-button {
padding: 0
}
t-left-icons {
padding-right: 0.5em
}
t-right-icons {
padding-left: 0.5em
}
.infix {
position: relative;
display: block;
flex: auto;
width: 100%;
min-width: 0;
}
i.clear {
visibility: hidden;
cursor: pointer;
outline: 0;
&:hover {
color: #222;
}
}
&:hover i.clear {
visibility: visible;
}
i.visible {
visibility: visible;
}
label {
position: absolute;
bottom: 0;
left: 0;
padding: 4px 0!important;
pointer-events: none;
color: $color-font-secondary;
transition-duration: .2s;
transition-timing-function: cubic-bezier(.4,0,.2,1);
}
&.not-empty label{
bottom: 24px;
color: $color-main;
padding: 0;
font-size: 12px;
}
input {
outline: none;
border: none;
font-family: "Helvetica", "Arial", sans-serif;
display: block;
font-size: 16px;
width: 100%;
background: 0 0;
color: inherit;
padding: 4px;
box-sizing: border-box;
border-bottom: 0!important;
&[type=number] {
-moz-appearance: textfield;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
&:invalid {
box-shadow: none;
}
}
.underline {
position: absolute;
bottom: 0;
height: 1px;
content: ' ';
pointer-events: none;
width: 100%;
background-color: $color-input-underline;
}
.selected.underline {
background-color: $color-main;
height: 2px;
left: 50%;
width: 0px !important;
transition-duration: 0.2s;
transition-timing-function: cubic-bezier(.4,0,.2,1);
}
div.selected {
&.container{
border-bottom: 0px;
}
label {
bottom: 24px;
color: $color-main;
font-size: 12px;
}
.selected.underline{
left: 0;
width: 100%!important;
}
}
& > div.container > div.textField > div.infix.invalid {
@extend div.selected;
& > span.mdl-textfield__error {
visibility: visible;
}
& > label {
color: #d50000;
}
}
.infix.invalid + .underline {
background-color: #d50000;
}
label span:nth-child(2) {
color: $color-alert
}
}
vn-table {
vn-textfield {
margin: 0;
}
}

View File

@ -1,41 +0,0 @@
<div class="container"
ng-class="{selected: $ctrl.hasFocus}">
<div class="textField">
<div class="leftIcons" ng-transclude="leftIcons"></div>
<div class="infix">
<input
class="mdl-textfield__input"
type="{{$ctrl.type}}"
name="{{$ctrl.name}}"
ng-model="$ctrl.value"
vn-validation="{{$ctrl.rule}}"
ng-disabled="$ctrl.disabled"
ng-readonly="$ctrl.readonly"
ng-focus="$ctrl.hasFocus = true"
ng-blur="$ctrl.hasFocus = false"
tabindex="{{$ctrl.input.tabindex}}"/>
<label class="label">
<span translate>{{::$ctrl.label}}</span>
<span translate ng-show="::$ctrl.required">*</span>
</label>
</div>
<div class="underline"></div>
<div class="selected underline"></div>
<div class="suffix">
<i class="material-icons clear"
translate-attr="{title: 'Clear'}"
ng-show="!$ctrl.disabled
&& $ctrl.hasValue
&& !$ctrl.unclearable"
ng-click="$ctrl.clear()">
clear
</i>
<i class="material-icons"
ng-if="$ctrl.hasInfo"
vn-tooltip="{{$ctrl.info}}">
info_outline
</i>
</div>
<div class="rightIcons" ng-transclude="rightIcons"></div>
</div>
</div>

View File

@ -1,94 +1,13 @@
import ngModule from '../../module';
import Input from '../../lib/input';
import './style.scss';
import Field from '../field';
export default class Textfield extends Input {
constructor($element, $scope, $attrs, vnTemplate) {
super($element, $scope);
vnTemplate.normalizeInputAttrs($attrs);
this._value = null;
this.type = $attrs.type;
this.showActions = false;
this.hasInfo = Boolean($attrs.info);
this.hasFocus = false;
this.hasMouseIn = false;
export default class Textfield extends Field {
constructor($element, $scope, $compile) {
super($element, $scope, $compile);
this.buildInput('text');
}
}
this.input.addEventListener('keyup', e => {
if (e.defaultPrevented || e.key != 'Escape')
return;
this.value = this.oldValue;
this.cancelled = true;
e.preventDefault();
});
this.input.addEventListener('change', e => {
if (this.onChange && !this.cancelled && (this.oldValue != this.value)) {
this.onChange();
this.saveOldValue();
} else
this.cancelled = false;
});
}
$postLink() {
this.saveOldValue();
}
saveOldValue() {
this.oldValue = this.value;
}
set value(value) {
this._value = (value === undefined || value === '') ? null : value;
this.input.value = this._value;
this.hasValue = this._value != null;
this.element.classList.toggle('not-empty', this.hasValue);
}
get value() {
return this._value;
}
set type(value) {
this._type = value || 'text';
}
get type() {
return this._type;
}
set vnTabIndex(value) {
this.input.tabindex = value;
}
clear() {
this.saveOldValue();
this.value = null;
if (this.onClear) this.onClear();
this.input.focus();
}
}
Textfield.$inject = ['$element', '$scope', '$attrs', 'vnTemplate'];
ngModule.component('vnTextfield', {
template: require('./textfield.html'),
transclude: {
leftIcons: '?tLeftIcons',
rightIcons: '?tRightIcons'
},
controller: Textfield,
bindings: {
value: '=model',
label: '@?',
name: '@?',
disabled: '<?',
required: '@?',
readonly: '<?',
rule: '@?',
type: '@?',
vnTabIndex: '@?',
onChange: '&',
onClear: '&',
info: '@?'
}
ngModule.vnComponent('vnTextfield', {
controller: Textfield
});

View File

@ -1,84 +0,0 @@
import './textfield.js';
describe('Component vnTextfield', () => {
let $scope;
let $attrs;
let $timeout;
let $element;
let controller;
beforeEach(angular.mock.module('vnCore', $translateProvider => {
$translateProvider.translations('en', {});
}));
beforeEach(angular.mock.inject(($componentController, $rootScope) => {
$scope = $rootScope.$new();
$attrs = {field: '$ctrl.client.phone'};
$element = angular.element('<vn-textfield label="Phone" field="$ctrl.client.phone"><input><div class="leftIcons"><div class="rightIcons"></div></vn-textfield>');
controller = $componentController('vnTextfield', {$scope, $element, $attrs, $timeout, $transclude: () => {}});
}));
describe('saveOldValue()', () => {
it(`should equal oldValue property to the actual input value`, () => {
controller.value = 'pepino';
controller.saveOldValue();
expect(controller.oldValue).toEqual('pepino');
});
});
describe('value() setter', () => {
it(`should set _value, input.value and hasValue properties to null, '' and false`, () => {
let testValue = '';
controller.value = testValue;
expect(controller._value).toEqual(null);
expect(controller.input.value).toEqual(testValue);
expect(controller.hasValue).toEqual(Boolean(testValue));
});
it(`should set _value, input.value and hasValue propertiest to test, test and true`, () => {
let testValue = 'test';
controller.value = testValue;
expect(controller._value).toEqual(testValue);
expect(controller.input.value).toEqual(testValue);
expect(controller.hasValue).toEqual(Boolean(testValue));
});
it(`should add the class not-empty to the div`, () => {
let testValue = 'test';
controller.value = testValue;
expect(controller.element.classList).toContain('not-empty');
});
});
describe('type() setter', () => {
it(`should set _type to 'text' if theres not given value`, () => {
controller.type = null;
expect(controller._type).toEqual('text');
});
});
describe('vnTabIndex() setter', () => {
it(`should set input.tabindex to a given value`, () => {
controller.vnTabIndex = 9;
expect(controller.input.tabindex).toEqual(9);
});
});
describe('clear()', () => {
it(`should set value property to null, call focus and saveOldValue`, () => {
spyOn(controller.input, 'focus');
spyOn(controller, 'saveOldValue');
controller.clear();
expect(controller.value).toEqual(null);
expect(controller.saveOldValue).toHaveBeenCalledWith();
expect(controller.input.focus).toHaveBeenCalledWith();
});
});
});

View File

@ -1,16 +1,13 @@
import ngModule from '../../module';
import Component from '../../lib/component';
import FormInput from '../form-input';
import './style.scss';
/**
* Base component with common logic and styles for checkbox and radio button.
*
* @property {String} label Label to display along the component
* @property {any} field The value with which the element is linked
* @property {Boolean} checked Whether the checkbox is checked
* @property {Boolean} disabled Put component in disabled mode
*/
export default class Toggle extends Component {
export default class Toggle extends FormInput {
constructor($element, $) {
super($element, $);
@ -49,14 +46,10 @@ export default class Toggle extends Component {
this.emit('change', {value: this.field});
}
}
Toggle.$inject = ['$element', '$scope'];
ngModule.component('vnToggle', {
ngModule.vnComponent('vnToggle', {
controller: Toggle,
bindings: {
label: '@?',
field: '=?',
disabled: '<?',
checked: '<?'
}
});

View File

@ -3,7 +3,8 @@
.vn-toggle {
position: relative;
cursor: pointer;
display: inline-block;
display: inline-flex;
align-items: center;
outline: none;
&.disabled {
@ -19,8 +20,9 @@
vertical-align: middle;
width: 20px;
height: 20px;
min-width: 20px;
margin: 6px 0;
margin-right: .4em;
margin-right: .6em;
border: 2px solid #666;
}
&.checked > .btn {

View File

@ -1,61 +0,0 @@
<ul ng-if="::$ctrl.items">
<li
ng-repeat="item in $ctrl.items"
on-drop="$ctrl.onDrop(item, dragged, dropped)"
vn-draggable="{{::$ctrl.draggable}}"
vn-droppable="{{::$ctrl.droppable}}"
ng-class="{expanded: item.active}">
<div
ng-click="$ctrl.toggle($event, item)"
class="node clickable">
<vn-icon
class="arrow"
ng-class="{invisible: item.sons == 0}"
icon="keyboard_arrow_down"
translate-attr="{title: 'Toggle'}">
</vn-icon>
<vn-check
vn-acl="{{$ctrl.aclRole}}"
ng-if="$ctrl.selectable"
field="item.selected"
disabled="$ctrl.disabled"
on-change="$ctrl.select(item, value)"
triple-state="true"
label="{{::item.name}}">
</vn-check>
<vn-icon-button
icon="{{icon.icon}}"
ng-repeat="icon in $ctrl.icons"
ng-click="$ctrl.onIconClick(icon, item, $ctrl.parent, $parent.$index)"
vn-acl="{{$ctrl.aclRole}}"
vn-acl-action="remove">
</vn-icon-button>
</div>
<vn-treeview-child
items="item.childs"
parent="item"
selectable="$ctrl.selectable"
disabled="$ctrl.disabled"
editable="$ctrl.editable"
draggable="::$ctrl.draggable"
droppable="::$ctrl.droppable"
icons="::$ctrl.icons"
parent-scope="::$ctrl.parentScope"
acl-role="$ctrl.aclRole">
</vn-treeview-child>
</li>
<li
ng-if="$ctrl.isInsertable && $ctrl.editable"
ng-click="$ctrl.onCreate($ctrl.parent)"
vn-acl="{{$ctrl.aclRole}}"
vn-acl-action="remove">
<div class="node">
<vn-icon-button
icon="add_circle">
</vn-icon-button>
<div class="description" translate>
Create new one
</div>
</div>
</li>
</ul>

View File

@ -1,56 +0,0 @@
import ngModule from '../../module';
import Component from '../../lib/component';
class Controller extends Component {
constructor($element, $scope) {
super($element, $scope);
this.$scope = $scope;
}
toggle(event, item) {
if (event.defaultPrevented || !item.sons) return;
event.preventDefault();
this.treeview.onToggle(item);
}
select(item, value) {
this.treeview.onSelection(item, value);
}
onIconClick(icon, item, parent, index) {
let parentController = this.parentScope.$ctrl;
icon.callback.call(parentController, item, parent, index);
}
onCreate(parent) {
this.treeview.onCreate(parent);
}
onDrop(item, dragged, dropped) {
this.treeview.onDrop(item, dragged, dropped);
}
get isInsertable() {
return Array.isArray(this.parent) || this.parent.childs;
}
}
ngModule.component('vnTreeviewChild', {
template: require('./child.html'),
controller: Controller,
bindings: {
items: '<',
parent: '<',
icons: '<?',
disabled: '<?',
selectable: '<?',
editable: '<?',
draggable: '<?',
droppable: '<?',
aclRole: '<?',
parentScope: '<'
},
require: {
treeview: '^vnTreeview'
}
});

View File

@ -0,0 +1,34 @@
<ul ng-if="$ctrl.items">
<li ng-repeat="item in $ctrl.items" >
<div
ng-class="{expanded: item.active}"
ng-click="$ctrl.onClick($event, item)"
class="node clickable">
<vn-icon
class="arrow"
ng-class="{invisible: !item.sons}"
icon="keyboard_arrow_down"
translate-attr="::{title: 'Toggle'}">
</vn-icon>
<vn-treeview-content
item="::item">
</vn-treeview-content>
<section class="buttons" ng-if="::!$ctrl.treeview.readOnly">
<vn-icon-button translate-attr="::{title: 'Remove'}"
icon="delete"
ng-click="$ctrl.treeview.onRemove(item)"
ng-if="item.parent">
</vn-icon-button>
<vn-icon-button translate-attr="::{title: 'Create'}"
icon="add_circle"
ng-click="$ctrl.treeview.onCreate(item)">
</vn-icon-button>
</section>
</div>
<vn-treeview-childs
items="item.childs"
parent="::item">
</vn-treeview-childs>
</li>
</ul>

View File

@ -0,0 +1,26 @@
import ngModule from '../../module';
import Component from '../../lib/component';
class Controller extends Component {
onClick(event, item) {
if (event.defaultPrevented || !item.sons) return;
event.preventDefault();
this.treeview.onToggle(item);
}
onDrop(item, dragged, dropped) {
this.treeview.onDrop(item, dragged, dropped);
}
}
ngModule.component('vnTreeviewChilds', {
template: require('./childs.html'),
controller: Controller,
bindings: {
items: '<',
parent: '<?'
},
require: {
treeview: '^vnTreeview'
}
});

View File

@ -0,0 +1,39 @@
import ngModule from '../../module';
class Controller {
constructor($element, $scope, $compile) {
this.$element = $element;
this.$scope = $scope;
this.$compile = $compile;
}
$onInit() {
if (this.item.parent) {
this.treeview.$transclude(($clone, $scope) => {
this.$contentScope = $scope;
$scope.item = this.item;
this.$element.append($clone);
});
} else {
let template = `<span translate>{{$ctrl.treeview.rootLabel}}</span>`;
let $clone = this.$compile(template)(this.$scope);
this.$element.append($clone);
}
}
$onDestroy() {
if (this.$contentScope)
this.$contentScope.$destroy();
}
}
Controller.$inject = ['$element', '$scope', '$compile'];
ngModule.component('vnTreeviewContent', {
controller: Controller,
bindings: {
item: '<'
},
require: {
treeview: '^vnTreeview'
}
});

View File

@ -1,13 +1,3 @@
<vn-treeview-child
acl-role="$ctrl.aclRole"
items="$ctrl.data"
parent="$ctrl.data"
selectable="$ctrl.selectable"
editable="$ctrl.editable"
disabled="$ctrl.disabled"
icons="$ctrl.icons"
parent-scope="$ctrl.$scope.$parent"
draggable="$ctrl.draggable"
droppable="$ctrl.droppable"
vn-droppable="{{$ctrl.droppable}}">
</vn-treeview-child>
<vn-treeview-childs
items="$ctrl.items">
</vn-treeview-childs>

View File

@ -2,50 +2,71 @@ import ngModule from '../../module';
import Component from '../../lib/component';
import './style.scss';
import './childs';
import './content';
/**
* Treeview
*
* @property {String} position The relative position to the parent
*/
export default class Treeview extends Component {
constructor($element, $scope) {
constructor($element, $scope, $transclude) {
super($element, $scope);
this.$scope = $scope;
this.data = [];
this.$transclude = $transclude;
this.readOnly = true;
}
$onInit() {
this.refresh();
get data() {
return this._data;
}
refresh() {
this.model.refresh().then(() => {
this.data = this.model.data;
set data(value) {
this._data = value;
const sons = value.length;
const rootElement = [{
childs: value,
active: true,
sons: sons
}];
this.setParent(rootElement[0], value);
this.items = rootElement;
}
fetch() {
return this.fetchFunc().then(res =>
this.data = res
);
}
setParent(parent, childs) {
childs.forEach(child => {
child.parent = parent;
if (child.childs)
this.setParent(parent, child.childs);
});
}
/**
* Emits selection event
* @param {Object} item - Selected item
* @param {Boolean} value - Changed value
*/
onSelection(item, value) {
this.emit('selection', {item, value});
}
onCreate(parent) {
this.emit('create', {parent});
}
onToggle(item) {
if (item.active)
item.childs = undefined;
else {
this.model.applyFilter({}, {parentFk: item.id}).then(() => {
const newData = this.model.data;
this.fold(item);
else
this.unfold(item);
}
if (item.childs) {
let childs = item.childs;
fold(item) {
item.childs = undefined;
item.active = false;
}
unfold(item) {
return this.fetchFunc({$item: item}).then(newData => {
this.setParent(item, newData);
const childs = item.childs;
if (childs) {
childs.forEach(child => {
let index = newData.findIndex(newChild => {
return newChild.id == child.id;
@ -54,21 +75,49 @@ export default class Treeview extends Component {
});
}
item.childs = newData.sort((a, b) => {
if (b.selected !== a.selected) {
if (a.selected == null)
return 1;
if (b.selected == null)
return -1;
return b.selected - a.selected;
if (this.sortFunc) {
item.childs = newData.sort((a, b) =>
this.sortFunc({$a: a, $b: b})
);
}
}).then(() => item.active = true);
}
return a.name.localeCompare(b.name);
});
});
onRemove(item) {
if (this.removeFunc)
this.removeFunc({$item: item});
}
item.active = !item.active;
remove(item) {
const parent = item.parent;
let childs = parent.childs;
if (!childs) childs = [];
let index = childs.indexOf(item);
childs.splice(index, 1);
if (parent) parent.sons--;
}
onCreate(parent) {
if (this.createFunc)
this.createFunc({$parent: parent});
}
create(item) {
const parent = item.parent;
let childs = parent.childs;
if (!childs) childs = [];
childs.push(item);
if (this.sortFunc) {
childs = childs.sort((a, b) =>
this.sortFunc({$a: a, $b: b})
);
}
if (parent) parent.sons++;
}
onDrop(item, dragged, dropped) {
@ -76,19 +125,21 @@ export default class Treeview extends Component {
}
}
Treeview.$inject = ['$element', '$scope'];
Treeview.$inject = ['$element', '$scope', '$transclude'];
ngModule.component('vnTreeview', {
template: require('./index.html'),
controller: Treeview,
bindings: {
model: '<',
icons: '<?',
disabled: '<?',
selectable: '<?',
editable: '<?',
rootLabel: '@',
data: '<?',
draggable: '<?',
droppable: '<?',
aclRole: '@?'
}
fetchFunc: '&',
removeFunc: '&?',
createFunc: '&?',
sortFunc: '&?',
readOnly: '<?'
},
transclude: true
});

View File

@ -1,9 +1,8 @@
@import "effects";
vn-treeview {
vn-treeview-child {
display: block
}
vn-treeview-childs {
display: block;
ul {
padding: 0;
margin: 0;
@ -11,37 +10,43 @@ vn-treeview {
li {
list-style: none;
& > div > .arrow {
min-width: 24px;
margin-right: 10px;
transition: transform 200ms;
}
&.expanded > div > .arrow {
transform: rotate(180deg);
}
& > .node {
@extend %clickable;
display: flex;
padding: 5px;
align-items: center;
}
& > vn-check:not(.indeterminate) {
color: $color-main;
& > div > .arrow {
min-width: 24px;
margin-right: 10px;
transition: transform 200ms;
}
& > .btn {
border-color: $color-main;
}
}
& > vn-check.checked {
color: $color-main;
}
& > div.expanded > .arrow {
transform: rotate(180deg);
}
ul {
padding-left: 2.2em;
}
}
}
vn-icon-button {
padding: 0
display: table-cell;
vertical-align: middle;
padding: 0;
}
.node > .buttons {
display: none
}
.node:hover > .buttons {
display: block
}
}
vn-treeview-content {
flex-grow: 1
}

View File

@ -223,11 +223,8 @@ export default class Watcher extends Component {
}
loadOriginalData() {
Object.keys(this.data).forEach(key => {
delete this.data[key];
});
this.data = Object.assign(this.data, this.orgData);
const orgData = JSON.parse(JSON.stringify(this.orgData));
this.data = Object.assign(this.data, orgData);
this.setPristine();
}

View File

@ -2,7 +2,7 @@ import './id';
import './focus';
import './dialog';
import './popover';
import './validation';
import './rule';
import './acl';
import './on-error-src';
import './zoom-image';

View File

@ -0,0 +1,72 @@
import ngModule from '../module';
import {validateAll} from '../lib/validator';
import {firstUpper} from '../lib/string';
directive.$inject = ['$translate', '$window'];
export function directive($translate, $window) {
return {
restrict: 'A',
link: link,
require: {
ngModel: 'ngModel',
form: '^^?form'
}
};
function link($scope, $element, $attrs, $ctrl) {
let vnValidations = $window.validations;
if (!vnValidations) return;
if (!/^([A-Z]\w+(\.[a-z]\w*)?)?$/.test($attrs.rule))
throw new Error(`rule: Attribute must have this syntax: [ModelName[.fieldName]]`);
let rule = $attrs.rule.split('.');
let modelName = rule.shift();
let fieldName = rule.shift();
let split = $attrs.ngModel.split('.');
if (!fieldName) fieldName = split.pop() || null;
if (!modelName) modelName = firstUpper(split.pop() || '');
if (!modelName || !fieldName)
throw new Error(`rule: Cannot retrieve model or field attribute`);
let modelValidations = vnValidations[modelName];
if (!modelValidations)
throw new Error(`rule: Model '${modelName}' doesn't exist`);
let validations = modelValidations.validations[fieldName];
if (!validations || validations.length == 0)
return;
let ngModel = $ctrl.ngModel;
let form = $ctrl.form;
let field = $element[0].$ctrl;
let error;
function refreshError() {
if (!field) return;
let canShow = ngModel.$dirty || (form && form.$submitted);
field.error = error && canShow ? error.message : null;
}
ngModel.$options.$$options.allowInvalid = true;
ngModel.$validators.entity = value => {
try {
error = null;
validateAll($translate, value, validations);
} catch (e) {
error = e;
}
refreshError();
return error == null;
};
if (form)
$scope.$watch(form.$submitted, refreshError);
}
}
ngModule.directive('rule', directive);

View File

@ -0,0 +1,115 @@
describe('Directive rule', () => {
let $scope;
let $element;
let element;
beforeEach(angular.mock.module('vnCore', $translateProvider => {
$translateProvider.translations('en', {});
}));
function compile(html, value) {
inject(($compile, $rootScope, $window) => {
$window.validations = {Model:
{validations: {field: [{validation: 'absence'}]}}
};
$scope = $rootScope.$new();
$element = angular.element(html);
$compile($element)($scope);
element = $element[0];
$scope.model = {field: value};
$scope.$digest();
});
}
afterEach(() => {
$scope.$destroy();
$element.remove();
});
describe('Errors', () => {
it(`should throw an error if the rule doesn't have the right syntax`, () => {
expect(() => {
compile(`
<form>
<input
type="text"
ng-model="model.field"
rule="invalidLowerCamelCaseModel">
</input>
</form>
`);
}).toThrow(new Error(`rule: Attribute must have this syntax: [ModelName[.fieldName]]`));
});
it('should throw an error if cannot retrieve model or field', () => {
expect(() => {
compile(`
<form>
<input
type="text"
ng-model="model"
rule>
</input>
</form>
`);
}).toThrow(new Error(`rule: Cannot retrieve model or field attribute`));
});
it('should throw an error if the model is not defined', () => {
expect(() => {
compile(`
<form>
<input
type="text"
ng-model="model.field"
rule="NonExistentModel.field">
</input>
</form>
`);
}).toThrow(new Error(`rule: Model 'NonExistentModel' doesn't exist`));
});
});
describe('Validator extended', () => {
let html = `
<form>
<input
type="text"
ng-model="model.field"
rule="Model.field">
</input>
</form>
`;
it('should not validate the entity as it has a wrong value', () => {
compile(html, 'invalidValue');
expect(element.classList).toContain('ng-invalid');
});
it('should validate the entity as it has a valid value', () => {
compile(html, '');
expect(element.classList).toContain('ng-valid');
});
});
describe('Validator minimal', () => {
let html = `
<input
type="text"
ng-model="model.field"
rule>
</input>
`;
it('should validate with empty rule and without specifying a parent form', () => {
compile(html, 'invalidValue');
expect(element.classList).toContain('ng-invalid');
});
});
});

View File

@ -1,186 +0,0 @@
describe('Directive validation', () => {
let scope;
let element;
let compile;
beforeEach(angular.mock.module('vnCore', $translateProvider => {
$translateProvider.translations('en', {});
}));
compile = (_element, validations, value) => {
inject(($compile, $rootScope, $window) => {
$window.validations = validations;
scope = $rootScope.$new();
scope.user = {name: value};
element = angular.element(_element);
$compile(element)(scope);
scope.$digest();
});
};
it(`should throw an error if the vnValidation doesn't have the right syntax`, () => {
let html = `<input type="name" ng-model="user.name" vn-validation="user"/>`;
expect(() => {
compile(html, {});
}).toThrow(new Error(`vnValidation: Attribute must have this syntax: [entity].[field]`));
});
it('should throw an error if the window.validations aint defined', () => {
let html = `<input type="name" ng-model="user.name" vn-validation="user.name"/>`;
expect(() => {
compile(html, {});
}).toThrow(new Error(`vnValidation: Entity 'User' doesn't exist`));
});
describe('Validator presence()', () => {
it('should not validate the user name as it is an empty string', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'presence'}]}}};
compile(html, validations, 'Spiderman');
scope.user.name = '';
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
});
describe('Validator absence()', () => {
it('should not validate the entity as it should be an empty string', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'absence'}]}}};
compile(html, validations, 'Spiderman');
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
it('should validate the entity as it is an empty string', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'absence'}]}}};
compile(html, validations, '');
scope.$digest();
expect(element[0].classList).toContain('ng-valid');
expect(element[0].classList).not.toContain('ng-invalid');
});
});
describe('Validator length()', () => {
it('should not validate the user name as it should have min length of 15', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'length', min: 10, max: 50, is: 15}]}}};
compile(html, validations, 'fifteen!');
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
it('should validate the user name as it has length of 15', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'length', min: 10, max: 50, is: 15}]}}};
compile(html, validations, 'fifteen length!');
scope.$digest();
expect(element[0].classList).toContain('ng-valid');
expect(element[0].classList).not.toContain('ng-invalid');
});
it('should not validate the user name as it should have min length of 10', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'length', min: 10}]}}};
compile(html, validations, 'shortname');
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
it('should validate the user name as its length is greater then the minimum', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'length', min: 10}]}}};
compile(html, validations, 'verylongname');
scope.$digest();
expect(element[0].classList).toContain('ng-valid');
expect(element[0].classList).not.toContain('ng-invalid');
});
it('should not validate the user name as its length is greater then the maximum', () => {
let html = `<form><input type="name" ng-model="user.name" vn-validation="user.name"/></form>`;
let validations = {User: {validations: {name: [{validation: 'length', max: 10}]}}};
compile(html, validations, 'toolongname');
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
});
describe('Validator numericality()', () => {
it('should not validate the phone number as it should a integer', () => {
let html = `<form><input type="text" ng-model="user.phone" vn-validation="user.phone"/></form>`;
let validations = {User: {validations: {phone: [{validation: 'numericality', is: 'what is this?'}]}}};
compile(html, validations, 'spiderman');
scope.user.phone = 'this is not a phone number!';
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
it('should validate the phone number as it an integer', () => {
let html = `<form><input type="text" ng-model="user.phone" vn-validation="user.phone"/></form>`;
let validations = {User: {validations: {phone: [{validation: 'numericality', is: 'what is this?'}]}}};
compile(html, validations, 'spiderman');
scope.user.phone = '555555555';
scope.$digest();
expect(element[0].classList).toContain('ng-valid');
expect(element[0].classList).not.toContain('ng-invalid');
});
});
describe('Validator inclusion()', () => {
it('should not validate the phone number as it is not an integer', () => {
let html = `<form><input type="text" ng-model="user.phone" vn-validation="user.phone"/></form>`;
let validations = {User: {validations: {phone: [{validation: 'inclusion'}]}}};
compile(html, validations, 'spiderman');
scope.user.phone = 'this is not a phone number!';
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
});
describe('Validator exclusion()', () => {
it('should validate the phone number as it is an integer', () => {
let html = `<form><input type="text" ng-model="user.phone" vn-validation="user.phone"/></form>`;
let validations = {User: {validations: {phone: [{validation: 'exclusion'}]}}};
compile(html, validations, 'spiderman');
scope.user.phone = '555555555';
scope.$digest();
expect(element[0].classList).toContain('ng-valid');
expect(element[0].classList).not.toContain('ng-invalid');
});
});
describe('Validator format()', () => {
it('should not validate the email number as it doesnt contain @', () => {
let html = `<form><input type="text" ng-model="user.email" vn-validation="user.email"/></form>`;
let validations = {User: {validations: {email: [{validation: 'format', with: '@'}]}}};
compile(html, validations, 'spiderman');
scope.user.email = 'userverdnatura.es';
scope.$digest();
expect(element[0].classList).toContain('ng-invalid');
expect(element[0].classList).not.toContain('ng-valid');
});
});
});

View File

@ -15,7 +15,7 @@
<vn-horizontal ng-repeat="field in fields">
<vn-check
vn-one label="{{titles[field]}}"
field="tableConfiguration.configuration[field]">
ng-model="tableConfiguration.configuration[field]">
</vn-check>
</vn-horizontal>
<vn-horizontal>

View File

@ -1,80 +0,0 @@
import ngModule from '../module';
import {validateAll} from '../lib/validator';
import {firstUpper} from '../lib/string';
directive.$inject = ['$interpolate', '$compile', '$translate', '$window'];
export function directive(interpolate, compile, $translate, $window) {
return {
restrict: 'A',
require: ['ngModel', '^^?form'],
link: link
};
function link(scope, element, attrs, ctrl) {
let vnValidations = $window.validations;
if (!attrs.vnValidation || !vnValidations)
return;
let split = attrs.vnValidation.split('.');
if (split.length !== 2)
throw new Error(`vnValidation: Attribute must have this syntax: [entity].[field]`);
let entityName = firstUpper(split[0]);
let fieldName = split[1];
let entity = vnValidations[entityName];
if (!entity)
throw new Error(`vnValidation: Entity '${entityName}' doesn't exist`);
let validations = entity.validations[fieldName];
if (!validations || validations.length == 0)
return;
let ngModel = ctrl[0];
let form = ctrl[1];
let errorSpan = angular.element('<span class="mdl-textfield__error"></span>');
let errorMsg;
let errorShown = false;
ngModel.$options.$$options.allowInvalid = true;
ngModel.$validators.entity = value => {
try {
validateAll($translate, value, validations);
return true;
} catch (e) {
errorMsg = e.message;
if (errorShown) changeError();
return false;
}
};
scope.$watch(() => {
return (form.$submitted || ngModel.$dirty) && ngModel.$invalid;
}, value => {
let parent = element.parent();
if (value) {
changeError();
parent.addClass('invalid');
element.after(errorSpan);
} else if (errorShown) {
parent.removeClass('invalid');
parent.removeAttr('title');
errorSpan.remove();
errorSpan.empty();
}
errorShown = value;
});
function changeError() {
let parent = element.parent();
errorSpan.text(errorMsg);
parent.attr('title', errorMsg);
}
}
}
ngModule.directive('vnValidation', directive);

View File

@ -1,7 +1,5 @@
import './module-loader';
import './crud';
import './acl-service';
import './template';
import './copy';
import './equals';
import './modified';

View File

@ -30,79 +30,13 @@ export default class Input extends Component {
if (this.mdlElement)
this.mdlElement.updateClasses_();
}
get validationError() {
return this.input.validationMessage;
}
/**
* Validates a valid input value
*
* @return {Boolean} - True if has a valid value
*/
hasValidValue() {
return this.input.checkValidity();
}
/**
* Changes the input element
* if has a validation error
*/
validateValue() {
if (!this.hasValidValue()) {
this.hideError();
this.showError();
} else
this.hideError();
}
/**
* Shows the input validation error
*/
showError() {
const infixElement = this.element.querySelector('.infix');
const infixClassList = infixElement.classList;
const errorSpan = document.createElement('span');
errorSpan.className = 'mdl-textfield__error';
const errorText = document.createTextNode(this.validationError);
errorSpan.append(errorText);
infixElement.append(errorSpan);
infixClassList.add('validated', 'invalid');
}
/**
* Hides the input validation error
*/
hideError() {
const infixElement = this.element.querySelector('.infix');
const infixClassList = infixElement.classList;
const errorElement = this.element.querySelector('.infix span.mdl-textfield__error');
if (errorElement)
errorElement.remove();
infixClassList.remove('validated', 'invalid');
}
}
Input.$inject = ['$element', '$scope'];
export const component = {
transclude: {
leftIcons: '?tLeftIcons',
rightIcons: '?tRightIcons'
},
export const $options = {
bindings: {
label: '@?',
disabled: '<?',
readonly: '<?',
rule: '@?',
value: '=model',
vnTabIndex: '@?',
onChange: '&',
onClear: '&'
readonly: '<?'
}
};

View File

@ -0,0 +1,186 @@
import {validate} from '../validator.js';
describe('Validator', () => {
let $translate;
beforeEach(angular.mock.module('vnCore', $translateProvider => {
$translateProvider.translations('en', {});
}));
beforeEach(inject(_$translate_ => {
$translate = _$translate_;
}));
describe('presence', () => {
let conf = {validation: 'presence'};
it('should not validate the value as it should be defined', () => {
expect(() => {
validate($translate, '', conf);
}).toThrowError();
});
it('should validate the value as it should be defined', () => {
expect(() => {
validate($translate, 'aDefinedValue', conf);
}).not.toThrowError();
});
});
describe('absence', () => {
let conf = {validation: 'absence'};
it('should not validate the value as it should be undefined', () => {
expect(() => {
validate($translate, 'aDefinedValue', conf);
}).toThrowError();
});
it('should validate the value as it should be undefined', () => {
expect(() => {
validate($translate, '', conf);
}).not.toThrowError();
});
});
describe('length', () => {
it('should not validate the value as it should have an specific valid length', () => {
let conf = {
validation: 'length',
is: 25
};
expect(() => {
validate($translate, 'invalidSpecificLengthString', conf);
}).toThrowError();
});
it('should validate the value as it should have an specific valid length', () => {
let conf = {
validation: 'length',
is: 25
};
expect(() => {
validate($translate, 'validSpecificLengthString', conf);
}).not.toThrowError();
});
it('should not validate the value as it should have a min length', () => {
let conf = {
validation: 'length',
min: 10
};
expect(() => {
validate($translate, 'shortName', conf);
}).toThrowError();
});
it('should validate the value as it should have a min length', () => {
let conf = {
validation: 'length',
min: 10
};
expect(() => {
validate($translate, 'veryLongName', conf);
}).not.toThrowError();
});
it('should not validate the value as it should be smaller than the maximum', () => {
let conf = {
validation: 'length',
max: 20
};
expect(() => {
validate($translate, 'chainThatExceedsMaxLength', conf);
}).toThrowError();
});
it('should validate the value as it should be smaller than the maximum', () => {
let conf = {
validation: 'length',
max: 20
};
expect(() => {
validate($translate, 'shortString', conf);
}).not.toThrowError();
});
});
describe('numericality', () => {
let conf = {validation: 'numericality'};
it('should not validate the value as it should be an integer', () => {
expect(() => {
validate($translate, 'notANumber', conf);
}).toThrowError();
});
it('should validate the value as it should be an integer', () => {
expect(() => {
validate($translate, '123456789', conf);
}).not.toThrowError();
});
});
describe('inclusion', () => {
let conf = {
validation: 'inclusion',
in: ['firstValue', 'seekValue', 'lastValue']
};
it('should not validate the value as it should be in array', () => {
expect(() => {
validate($translate, 'notIncludedValue', conf);
}).toThrowError();
});
it('should validate the value as it should be in array', () => {
expect(() => {
validate($translate, 'seekValue', conf);
}).not.toThrowError();
});
});
describe('exclusion', () => {
let conf = {
validation: 'exclusion',
in: ['firstValue', 'seekValue', 'lastValue']
};
it('should not validate the value as it should not be in array', () => {
expect(() => {
validate($translate, 'seekValue', conf);
}).toThrowError();
});
it('should validate the value as it should not be in array', () => {
expect(() => {
validate($translate, 'notIncludedValue', conf);
}).not.toThrowError();
});
});
describe('format', () => {
let conf = {
validation: 'format',
with: /[a-z-]+@[a-z-]+\.[a-z]+/
};
it('should not validate the value as it should match regexp', () => {
expect(() => {
validate($translate, 'wrongValue', conf);
}).toThrowError();
});
it('should validate the value as it should match regexp', () => {
expect(() => {
validate($translate, 'valid-value@domain.com', conf);
}).not.toThrowError();
});
});
});

View File

@ -6,7 +6,7 @@
* @return {String} The camelized string
*/
export function kebabToCamel(str) {
var camelCased = str.replace(/-([a-z])/g, function(g) {
let camelCased = str.replace(/-([a-z])/g, function(g) {
return g[1].toUpperCase();
});
return camelCased;

View File

@ -1,40 +0,0 @@
import ngModule from '../module';
export default class Template {
getNormalized(template, $attrs, defaults) {
this.normalizeInputAttrs($attrs);
return this.get(template, $attrs, defaults);
}
normalizeInputAttrs($attrs) {
const field = $attrs.field || $attrs.model;
const split = field.split('.');
const len = split.length;
let i = len - 1;
const fieldName = split[i--];
const entity = i >= 0 ? split[i--] : 'model';
const ctrl = i >= 0 ? split[i--] : '$ctrl';
if ($attrs.field) {
if (len == 0)
throw new Error(`Attribute 'field' can not be empty`);
if (len > 3)
throw new Error(`Attribute 'field' must have this syntax: [ctrl].[entity].[field]`);
if ($attrs.model === undefined)
$attrs.model = `${ctrl}.${entity}.${fieldName}`;
if ($attrs.rule === undefined && len >= 2)
$attrs.rule = `${entity}.${fieldName}`;
if ($attrs.label === undefined && len >= 2)
$attrs.label = `${entity}.${fieldName}`;
}
if ($attrs.name === undefined)
$attrs.name = fieldName;
if ($attrs.focus !== undefined)
$attrs.focus = 'vn-focus';
}
}
ngModule.service('vnTemplate', Template);

View File

@ -4,6 +4,43 @@ const ngModule = ng.module('vnCore', ngDeps);
ngModule.constant('moment', require('moment-timezone'));
export default ngModule;
/**
* Acts like native Module.component() function but merging component options
* with parent component options. This method establishes the $options property
* to the component controller class with the merged component options. To
* retrieve parent options, it reads the same property of the parent class, so
* for the parent options to be copied, it must have been declared using this
* same function. If any of the options (template, transclude, bindings ...) is
* redeclared in the child component, it has preference.
*
* @param {String} name Coponent name in camelCase
* @param {Object} options The component options
* @return {angularModule} The same angular module
*/
ngModule.vnComponent = function(name, options) {
let controller = options.controller;
let parent = Object.getPrototypeOf(controller);
let parentOptions = parent.$options || {};
let mergedOptions = Object.assign({},
parentOptions,
options,
{
transclude: Object.assign({},
parentOptions.transclude,
options.transclude
),
bindings: Object.assign({},
parentOptions.bindings,
options.bindings
)
}
);
controller.$options = mergedOptions;
return this.component(name, mergedOptions);
};
config.$inject = ['$translateProvider', '$translatePartialLoaderProvider'];
export function config($translateProvider, $translatePartialLoaderProvider) {
$translatePartialLoaderProvider.addPart('core');

View File

@ -0,0 +1,79 @@
import ngModule from '../module';
/**
* Saves and loads the application configuration.
*/
export default class Config {
constructor($http, vnApp, $translate, $window) {
Object.assign(this, {
$http,
vnApp,
$translate,
storage: $window.localStorage,
user: {},
local: {}
});
this.params = [
'warehouseFk',
'companyFk',
'bankFk'
];
}
initialize() {
for (let param of this.params)
this.local[param] = this.getItem(param);
return this.$http.get('api/UserConfigs/getUserConfig')
.then(res => {
for (let param of this.params)
this.user[param] = res.data[param];
})
.finally(() => this.mergeParams());
}
mergeParams() {
for (let param of this.params) {
let local = this.local[param];
this[param] = local != null && local != ''
? local
: this.user[param];
}
}
setUser(param, value) {
this.user[param] = value;
this.mergeParams();
let params = {[param]: value};
return this.$http.post('api/UserConfigs/setUserConfig', params)
.then(() => this.showSaved());
}
setLocal(param, value) {
this.setItem(param, value);
this.local[param] = value;
this.mergeParams();
this.showSaved();
}
showSaved() {
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
}
getItem(key) {
let value = this.storage[key];
return value !== undefined ? JSON.parse(value) : value;
}
setItem(key, value) {
if (value == null)
this.storage.removeItem(key);
else
this.storage[key] = JSON.stringify(value);
}
}
Config.$inject = ['$http', 'vnApp', '$translate', '$window'];
ngModule.service('vnConfig', Config);

View File

@ -1,6 +1,8 @@
import './acl-service';
import './app';
import './auth';
import './token';
import './modules';
import './interceptor';
import './config';

View File

@ -64,14 +64,6 @@ vn-app {
box-sizing: border-box;
padding: $spacing-md;
height: inherit;
form vn-horizontal {
align-items: center;
& > * {
padding: .2em;
}
}
}
vn-main-block {
display: block;
@ -102,11 +94,6 @@ vn-app {
.content-block {
margin-left: 0;
margin-right: 0;
form vn-horizontal {
flex-direction: column;
align-items: initial;
}
}
vn-main-block {
padding-left: 0;
@ -117,3 +104,37 @@ vn-app {
}
}
}
form vn-horizontal {
align-items: center;
& > * {
box-sizing: border-box;
min-height: 2.8em;
padding: 0 $spacing-sm;
&:first-child {
padding-left: 0;
padding-right: $spacing-xs;
}
&:last-child {
padding-left: $spacing-xs;
padding-right: 0;
}
&:first-child:last-child {
padding: 0;
}
}
@media screen and (max-width: $mobile-width) {
flex-direction: column;
align-items: initial;
& > * {
&,
&:first-child,
&:last-child {
padding: 0;
}
}
}
}

View File

@ -6,6 +6,6 @@ import './background/background';
import './side-menu/side-menu';
import './left-menu/left-menu';
import './topbar/topbar';
import './user-configuration-popover';
import './user-popover';
import './descriptor';
import './summary';

View File

@ -3,20 +3,20 @@
<form name="form" ng-submit="$ctrl.submit()">
<vn-textfield
label="User"
model="$ctrl.user"
ng-model="$ctrl.user"
name="user"
vn-id="userField"
vn-focus>
</vn-textfield>
<vn-textfield
label="Password"
model="$ctrl.password"
ng-model="$ctrl.password"
name="password"
type="password">
</vn-textfield>
<vn-check
label="Do not close session"
field="$ctrl.remember"
ng-model="$ctrl.remember"
name="remember">
</vn-check>
<div class="footer">

View File

@ -32,10 +32,8 @@ vn-login {
& > form {
& > vn-textfield {
width: 100%;
margin: 1em 0;
}
& > vn-check {
padding-top: .5em;
display: block;
.md-label {
white-space: inherit;

View File

@ -1,6 +1,6 @@
<div>
<div
ng-click="$ctrl.openUserConfiguration($event)"
ng-click="userPopover.show($event)"
id="user"
class="unselectable">
{{$root.user.nickname}}
@ -15,7 +15,7 @@
id="logout"
icon="exit_to_app"
translate-attr="{title: 'Logout'}"
ng-click="$ctrl.onLogoutClick()">
ng-click="$ctrl.vnAuth.logout()">
</vn-icon-button>
</div>
<vn-menu vn-id="apps-menu">
@ -26,6 +26,6 @@
</li>
</ul>
</vn-menu>
<vn-user-configuration-popover
vn-id="popover">
</vn-user-configuration-popover>
<vn-user-popover
vn-id="user-popover">
</vn-user-popover>

View File

@ -21,14 +21,6 @@ export default class MainMenu {
window.localStorage.currentUserWorkerId = json.data.workerId;
});
}
openUserConfiguration(event) {
this.$.popover.show(event);
}
onLogoutClick() {
this.vnAuth.logout();
}
}
MainMenu.$inject = ['$scope', '$http', 'vnAuth', 'vnModules'];

View File

@ -1,158 +0,0 @@
import ngModule from '../../module';
import './style.scss';
let languages = {
es: 'Español',
en: 'English',
ca: 'Català',
pt: 'Português',
fr: 'Français',
nl: 'Nederlands',
mn: 'Монгол хэл'
};
class Controller {
constructor($scope, $http, $state, vnApp, $translate) {
this.$scope = $scope;
this.$http = $http;
this.$state = $state;
this.vnApp = vnApp;
this.$translate = $translate;
this.getUserConfig();
this.lang = $translate.use();
this.langs = [];
for (let code of $translate.getAvailableLanguageKeys()) {
this.langs.push({
code: code,
name: languages[code] ? languages[code] : code
});
}
}
$onInit() {
if (window.localStorage.localBankFk && window.localStorage.localBankFk !== 'null')
window.localStorage.defaultBankFk = window.localStorage.localBankFk;
else
localStorage.removeItem('defaultBankFk');
}
set lang(value) {
this._lang = value;
this.$translate.use(value);
}
get lang() {
return this._lang;
}
set localBankFk(value) {
window.localStorage.localBankFk = value;
window.localStorage.defaultBankFk = value;
this.showOk();
}
get localBankFk() {
return parseInt(window.localStorage.localBankFk);
}
set localWarehouseFk(value) {
window.localStorage.localWarehouseFk = value;
if (!value && this.warehouseFk)
window.localStorage.defaultWarehouseFk = this.warehouseFk;
else
window.localStorage.defaultWarehouseFk = value;
this.showOk();
}
get localWarehouseFk() {
return parseInt(window.localStorage.localWarehouseFk);
}
set localCompanyFk(value) {
window.localStorage.localCompanyFk = value;
if (!value && this.companyFk)
window.localStorage.defaultCompanyFk = this.companyFk;
else
window.localStorage.defaultCompanyFk = value;
this.showOk();
}
get localCompanyFk() {
return parseInt(window.localStorage.localCompanyFk);
}
set warehouseFk(value) {
this.warehouse = value;
if (value &&
(!window.localStorage.localWarehouseFk || window.localStorage.localWarehouseFk === 'null'))
window.localStorage.defaultWarehouseFk = value;
this.setUserConfig('warehouseFk', value);
}
get warehouseFk() {
return this.warehouse;
}
set companyFk(value) {
this.company = value;
if (value &&
(!window.localStorage.localCompanyFk || window.localStorage.localCompanyFk === 'null'))
window.localStorage.setItem('defaultCompanyFk', value);
this.setUserConfig('companyFk', value);
}
get companyFk() {
return this.company;
}
showOk() {
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
}
show(event) {
this.$scope.warehouses.refresh();
this.$scope.companies.refresh();
this.$scope.popover.parent = event.target;
this.$scope.popover.show();
}
getUserConfig() {
this.$http.get('/api/UserConfigs/getUserConfig')
.then(res => {
if (res.data && res.data.warehouseFk) {
this.warehouse = res.data.warehouseFk;
if (res.data.warehouseFk && !window.localStorage.localWarehouseFk)
window.localStorage.setItem('defaultWarehouseFk', res.data.warehouseFk);
}
if (res.data && res.data.companyFk) {
this.company = res.data.companyFk;
if (res.data.companyFk && !window.localStorage.localCompanyFk)
window.localStorage.setItem('defaultCompanyFk', res.data.companyFk);
}
});
}
setUserConfig(property, value) {
let params = {};
params[property] = value;
this.$http.post('/api/UserConfigs/setUserConfig', params)
.then(() => {
this.showOk();
});
}
searchLocalBank(a, b) {
return angular.equals(a.id, b.id);
}
}
Controller.$inject = ['$scope', '$http', '$state', 'vnApp', '$translate'];
ngModule.component('vnUserConfigurationPopover', {
template: require('./index.html'),
controller: Controller
});

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