This commit is contained in:
Carlos Jimenez Ruiz 2019-11-15 10:37:42 +01:00
commit d1b114de59
302 changed files with 3885 additions and 3479 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
coverage
node_modules
dist/*
dist
e2e/dms/*/
!e2e/dms/c4c
!e2e/dms/c81

View File

@ -1,10 +1,12 @@
USE `vn`;
DROP procedure IF EXISTS `zone_getEvents`;
DROP PROCEDURE IF EXISTS `vn`.`zone_getEvents`;
DELIMITER $$
CREATE DEFINER=`root`@`%` PROCEDURE `vn`.`zone_getEvents`(
vAgencyModeFk INT,
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `zone_getEvents`(
vProvinceFk INT,
vPostCode VARCHAR(255))
vPostCode VARCHAR(255),
vAgencyModeFk INT)
BEGIN
/**
* Returns available events for the passed province/postcode and agency.
@ -13,9 +15,9 @@ BEGIN
* @param vProvinceFk The province id
* @param vPostCode The postcode or %NULL to use the province
*/
DECLARE vGeoFk INT;
DECLARE vGeoFk INT;
IF vPostCode IS NOT NULL THEN
IF vPostCode IS NOT NULL THEN
SELECT p.geoFk INTO vGeoFk
FROM postCode p
JOIN town t ON t.id = p.townFk
@ -24,24 +26,27 @@ BEGIN
ELSE
SELECT geoFk INTO vGeoFk
FROM province
WHERE id = vProvinceFk;
END IF;
WHERE id = vProvinceFk;
END IF;
CALL zone_getFromGeo(vGeoFk);
CALL zone_getFromGeo(vGeoFk);
DELETE t FROM tmp.zone t
JOIN zone z ON z.id = t.id
WHERE z.agencyModeFk != vAgencyModeFk;
IF vAgencyModeFk IS NOT NULL THEN
DELETE t FROM tmp.zone t
JOIN zone z ON z.id = t.id
WHERE z.agencyModeFk != vAgencyModeFk;
END IF;
SELECT e.`from`, e.`to`, e.weekDays
SELECT e.`from`, e.`to`, e.weekDays
FROM tmp.zone t
JOIN zoneEvent e ON e.zoneFk = t.id;
SELECT DISTINCT e.`day`
SELECT DISTINCT e.`day`
FROM tmp.zone t
JOIN zoneExclusion e ON e.zoneFk = t.id;
DROP TEMPORARY TABLE tmp.zone;
END$$
DELIMITER ;

View File

@ -7,8 +7,6 @@ import config from './config.js';
let currentUser;
let actions = {
// Generic extensions
clickIfExists: async function(selector) {
let exists = await this.exists(selector);
if (exists) await this.click(selector);
@ -25,8 +23,6 @@ let actions = {
}, selector);
},
// Salix specific extensions
changeLanguageToEnglish: async function() {
let langSelector = '.user-popover vn-autocomplete[ng-model="$ctrl.lang"]';
@ -49,14 +45,15 @@ let actions = {
login: async function(userName) {
if (currentUser !== userName) {
let logoutClicked = await this.clickIfExists('#logout');
let accountClicked = await this.clickIfExists('#user');
if (logoutClicked) {
if (accountClicked) {
let buttonSelector = '.vn-dialog.shown button[response=accept]';
await this.wait(buttonSelector => {
return document.querySelector(buttonSelector) != null
|| location.hash == '#!/login';
}, buttonSelector);
await this.waitToClick('#logout')
.wait(buttonSelector => {
return document.querySelector(buttonSelector) != null
|| location.hash == '#!/login';
}, buttonSelector);
await this.clickIfExists(buttonSelector);
}
@ -72,7 +69,7 @@ let actions = {
currentUser = userName;
} else
await this.waitToClick('vn-topbar a[ui-sref="home"]');
await this.waitToClick('a[ui-sref=home]');
},
waitForLogin: async function(userName) {
@ -189,7 +186,7 @@ let actions = {
let element = selectorMatches[0];
if (selectorMatches.length > 1)
throw new Error(`multiple matches of ${elementSelector} found`);
throw new Error(`Multiple matches of ${elementSelector} found`);
let isVisible = false;
if (element) {
@ -347,7 +344,7 @@ let actions = {
.write('vn-searchbar input', searchValue)
.click('vn-searchbar vn-icon[icon="search"]')
.wait(100)
.waitForNumberOfElements('.searchResult', 1)
.waitForNumberOfElements('.search-result', 1)
.evaluate(() => {
return document.querySelector('ui-view vn-card vn-table') != null;
})
@ -362,15 +359,16 @@ let actions = {
accessToSection: function(sectionRoute) {
return this.wait(`vn-left-menu`)
.evaluate(sectionRoute => {
return document.querySelector(`vn-left-menu ul li ul li > a[ui-sref="${sectionRoute}"]`) != null;
return document.querySelector(`vn-left-menu li li > a[ui-sref="${sectionRoute}"]`) != null;
}, sectionRoute)
.then(nested => {
if (!nested)
return this.waitToClick(`vn-left-menu li > a[ui-sref="${sectionRoute}"]`);
if (nested) {
this.waitToClick('vn-left-menu vn-item-section > vn-icon[icon=keyboard_arrow_down]')
.wait('vn-left-menu .expanded');
}
return this.waitToClick('vn-left-menu .collapsed')
.wait('vn-left-menu .expanded')
.waitToClick(`vn-left-menu li > a[ui-sref="${sectionRoute}"]`);
return this.waitToClick(`vn-left-menu li > a[ui-sref="${sectionRoute}"]`)
.waitForSpinnerLoad();
});
},
@ -429,10 +427,7 @@ let actions = {
},
waitForSpinnerLoad: function() {
return this.wait(() => {
const element = document.querySelector('vn-spinner > div');
return element.style.display == 'none';
});
return this.waitUntilNotPresent('vn-topbar vn-spinner');
}
};

View File

@ -13,9 +13,7 @@ module.exports = function createNightmare(width = 1280, height = 720) {
x: 0,
y: 0,
waitTimeout: 2000,
// openDevTools: {
// mode: 'detach'
// }
// openDevTools: {mode: 'detach'}
}).viewport(width, height);
nightmare.on('console', (type, message, ...args) => {

View File

@ -9,7 +9,7 @@ export default {
invoiceOutButton: '.modules-menu > li[ui-sref="invoiceOut.index"]',
claimsButton: '.modules-menu > li[ui-sref="claim.index"]',
returnToModuleIndexButton: 'a[ui-sref="order.index"]',
userMenuButton: 'vn-topbar #user',
userMenuButton: '#user',
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"]',
@ -23,7 +23,7 @@ export default {
clientsIndex: {
searchClientInput: `vn-textfield input`,
searchButton: 'vn-searchbar vn-icon[icon="search"]',
searchResult: 'vn-client-index .vn-list-item',
searchResult: 'vn-client-index .vn-item',
createClientButton: `vn-float-button`,
othersButton: 'vn-left-menu li[name="Others"] > a'
},
@ -180,7 +180,7 @@ export default {
acceptDeleteButton: '.vn-confirm.shown button[response="accept"]'
},
itemsIndex: {
searchIcon: 'vn-item-index vn-searchbar vn-icon[icon="search"]',
searchIcon: 'vn-searchbar vn-icon[icon="search"]',
createItemButton: `vn-float-button`,
searchResult: 'vn-item-index a.vn-tr',
searchResultPreviewButton: 'vn-item-index .buttons > [icon="desktop_windows"]',
@ -221,7 +221,7 @@ export default {
moreMenuRegularizeButton: '.vn-drop-down.shown li[name="Regularize stock"]',
regularizeQuantityInput: '.vn-dialog.shown tpl-body > div > vn-textfield input',
regularizeWarehouseAutocomplete: '.vn-dialog.shown vn-autocomplete[ng-model="$ctrl.warehouseFk"]',
editButton: 'vn-item-card vn-item-descriptor vn-float-button[icon="edit"]',
editButton: 'vn-item-descriptor vn-float-button[icon="edit"]',
regularizeSaveButton: '.vn-dialog.shown tpl-buttons > button',
inactiveIcon: 'vn-item-descriptor vn-icon[icon="icon-unavailable"]',
navigateBackToIndex: 'vn-item-descriptor vn-icon[icon="chevron_left"]'
@ -324,20 +324,20 @@ export default {
setOk: 'vn-ticket-summary vn-button[label="SET OK"] > button'
},
ticketsIndex: {
openAdvancedSearchButton: 'vn-ticket-index vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
openAdvancedSearchButton: '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 > 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 vn-textfield input`,
searchWeeklyTicketInput: `vn-ticket-weekly-index vn-textfield input`,
searchWeeklyClearInput: 'vn-ticket-weekly-index vn-searchbar vn-icon[icon=clear]',
searchTicketInput: `vn-searchbar input`,
searchWeeklyTicketInput: `vn-searchbar input`,
searchWeeklyClearInput: 'vn-searchbar vn-icon[icon=clear]',
advancedSearchButton: 'vn-ticket-search-panel button[type=submit]',
searchButton: 'vn-ticket-index vn-searchbar vn-icon[icon="search"]',
searchWeeklyButton: 'vn-ticket-weekly-index vn-searchbar vn-icon[icon="search"]',
searchButton: 'vn-searchbar vn-icon[icon="search"]',
searchWeeklyButton: 'vn-searchbar vn-icon[icon="search"]',
moreMenu: 'vn-ticket-index vn-icon-menu[icon=more_vert]',
moreMenuWeeklyTickets: '.vn-drop-down.shown li:nth-child(2)',
menuWeeklyTickets: 'vn-left-menu [ui-sref="ticket.weekly.index"]',
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"]',
@ -472,7 +472,7 @@ export default {
zoneAutocomplete: 'vn-autocomplete[ng-model="$ctrl.zoneId"]',
nextStepButton: 'vn-step-control .buttons > section:last-child vn-button',
finalizeButton: 'vn-step-control .buttons > section:last-child button[type=submit]',
stepTwoTotalPriceDif: 'vn-ticket-basic-data-step-two > form > vn-card > vn-horizontal > table > tfoot > tr > td:nth-child(4)',
stepTwoTotalPriceDif: 'vn-ticket-basic-data-step-two vn-tfoot > vn-tr > :nth-child(6)',
chargesReasonAutocomplete: 'vn-autocomplete[ng-model="$ctrl.ticket.option"]',
},
ticketComponents: {
@ -516,9 +516,9 @@ export default {
saveStateButton: `button[type=submit]`
},
claimsIndex: {
searchClaimInput: `vn-claim-index vn-textfield input`,
searchClaimInput: `vn-searchbar input`,
searchResult: 'vn-claim-index vn-card > vn-table > div > vn-tbody > a',
searchButton: 'vn-claim-index vn-searchbar vn-icon[icon="search"]'
searchButton: 'vn-searchbar vn-icon[icon="search"]'
},
claimDescriptor: {
moreMenu: 'vn-claim-descriptor vn-icon-menu[icon=more_vert]',
@ -584,7 +584,7 @@ export default {
searchResultDate: 'vn-order-index vn-table vn-tbody > a:nth-child(1) > vn-td:nth-child(4)',
searchResultAddress: 'vn-order-index vn-table vn-tbody > a:nth-child(1) > vn-td:nth-child(6)',
searchOrderInput: `vn-order-index vn-textfield input`,
searchButton: 'vn-order-index vn-searchbar vn-icon[icon="search"]',
searchButton: 'vn-searchbar vn-icon[icon="search"]',
createOrderButton: `vn-float-button`,
},
orderDescriptor: {
@ -722,8 +722,8 @@ export default {
acceptDeleteDialog: '.vn-confirm.shown button[response="accept"]'
},
invoiceOutIndex: {
searchInvoiceOutInput: `vn-invoice-out-index vn-textfield input`,
searchButton: 'vn-invoice-out-index vn-searchbar vn-icon[icon="search"]',
searchInvoiceOutInput: `vn-searchbar input`,
searchButton: 'vn-searchbar vn-icon[icon="search"]',
searchResult: 'vn-invoice-out-index vn-card > vn-table > div > vn-tbody > a.vn-tr',
},
invoiceOutDescriptor: {

View File

@ -30,7 +30,7 @@ describe('Login path', () => {
it('should log in', async() => {
const url = await nightmare
.doLogin('employee', null)
.wait('#logout')
.wait('#user')
.parsedUrl();
expect(url.hash).toEqual('#!/');

View File

@ -80,7 +80,7 @@ describe('Item summary path', () => {
it('should search for other item', async() => {
const result = await nightmare
.clearInput('vn-item-index vn-searchbar input')
.clearInput('vn-searchbar input')
.waitToClick(selectors.itemsIndex.searchButton)
.write(selectors.itemsIndex.searchItemInput, 'Melee weapon combat fist 15cm')
.waitToClick(selectors.itemsIndex.searchButton)

View File

@ -7,7 +7,7 @@ describe('Ticket expeditions and log path', () => {
beforeAll(() => {
return nightmare
.loginAndModule('production', 'ticket')
.accessToSearchResult('id:1')
.accessToSearchResult('1')
.accessToSection('ticket.card.expedition');
});

View File

@ -7,7 +7,7 @@ describe('Ticket Create packages path', () => {
beforeAll(() => {
return nightmare
.loginAndModule('employee', 'ticket')
.accessToSearchResult('id:1')
.accessToSearchResult('1')
.accessToSection('ticket.card.package');
});

View File

@ -8,7 +8,7 @@ describe('Ticket Create new tracking state path', () => {
beforeAll(() => {
return nightmare
.loginAndModule('production', 'ticket')
.accessToSearchResult('id:1')
.accessToSearchResult('1')
.accessToSection('ticket.card.tracking.index');
});
@ -44,7 +44,7 @@ describe('Ticket Create new tracking state path', () => {
beforeAll(() => {
return nightmare
.loginAndModule('salesPerson', 'ticket')
.accessToSearchResult('id:1')
.accessToSearchResult('1')
.accessToSection('ticket.card.tracking.index');
});

View File

@ -7,7 +7,7 @@ describe('Ticket List components path', () => {
beforeAll(() => {
return nightmare
.loginAndModule('employee', 'ticket')
.accessToSearchResult('id:1')
.accessToSearchResult('1')
.accessToSection('ticket.card.components');
});

View File

@ -11,8 +11,7 @@ describe('Ticket descriptor path', () => {
it('should count the amount of tickets in the turns section', async() => {
const result = await nightmare
.waitToClick(selectors.ticketsIndex.moreMenu)
.waitToClick(selectors.ticketsIndex.moreMenuWeeklyTickets)
.waitToClick(selectors.ticketsIndex.menuWeeklyTickets)
.wait(selectors.ticketsIndex.weeklyTicket)
.countElement(selectors.ticketsIndex.weeklyTicket);
@ -72,8 +71,7 @@ describe('Ticket descriptor path', () => {
it('should confirm the ticket 11 was added on thursday', async() => {
const result = await nightmare
.waitToClick(selectors.ticketsIndex.moreMenu)
.waitToClick(selectors.ticketsIndex.moreMenuWeeklyTickets)
.waitToClick(selectors.ticketsIndex.menuWeeklyTickets)
.waitToGetProperty(selectors.ticketsIndex.sixthWeeklyTicket, 'value');
expect(result).toEqual('Thursday');
@ -132,8 +130,7 @@ describe('Ticket descriptor path', () => {
it('should confirm the ticket 11 was added on saturday', async() => {
const result = await nightmare
.waitToClick(selectors.ticketsIndex.moreMenu)
.waitToClick(selectors.ticketsIndex.moreMenuWeeklyTickets)
.waitToClick(selectors.ticketsIndex.menuWeeklyTickets)
.waitToGetProperty(selectors.ticketsIndex.sixthWeeklyTicket, 'value');
expect(result).toEqual('Saturday');
@ -160,7 +157,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 vn-icon[icon=clear]')
.waitToClick('vn-searchbar vn-icon[icon=clear]')
.waitToClick(selectors.ticketsIndex.searchWeeklyButton)
.waitForNumberOfElements(selectors.ticketsIndex.searchWeeklyResult, 5)
.countElement(selectors.ticketsIndex.searchWeeklyResult);

View File

@ -7,7 +7,7 @@ describe('Ticket purchase request path', () => {
beforeAll(() => {
nightmare
.loginAndModule('salesPerson', 'ticket')
.accessToSearchResult('id:16')
.accessToSearchResult('16')
.accessToSection('ticket.card.request.index');
});

View File

@ -1,5 +1,4 @@
import selectors from '../../helpers/selectors.js';
import config from '../../helpers/config.js';
import createNightmare from '../../helpers/nightmare';
describe('Ticket diary path', () => {
@ -31,12 +30,9 @@ describe('Ticket diary path', () => {
});
it(`should navigate to the item diary from the 1st sale item id descriptor popover`, async() => {
const itemId = 2;
const url = await nightmare
.waitToClick(selectors.ticketSummary.firstSaleItemId)
.waitToClick(selectors.ticketSummary.popoverDiaryButton)
.waitForLogin('employee')
.goto(`${config.url}#!/item/${itemId}/diary?warehouseFk=1&ticketFk=1`)
.parsedUrl();
expect(url.hash).toContain('/diary');

View File

@ -21,7 +21,7 @@ describe('Ticket services path', () => {
.isDisabled(selectors.ticketService.firstAddDescriptionButton);
expect(result).toBeTruthy();
});
}, 100000);
it('should receive an error if you attempt to save a service without access rights', async() => {
const result = await nightmare

View File

@ -208,11 +208,9 @@ export default class Autocomplete extends Field {
onContainerKeyDown(event) {
if (event.defaultPrevented) return;
switch (event.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'Enter':
this.showDropDown();
break;
default:

View File

@ -7,7 +7,7 @@ export default class ButtonMenu extends Button {
constructor($element, $scope, $transclude) {
super($element, $scope);
this.$transclude = $transclude;
$element.on('click', e => this.onClick(e));
$element.on('click', e => this.onButtonClick(e));
}
get model() {
@ -41,8 +41,7 @@ export default class ButtonMenu extends Button {
Object.assign(this.$.dropDown, props);
}
onClick(event) {
if (this.disabled) return;
onButtonClick(event) {
if (event.defaultPrevented) return;
this.emit('open');
this.showDropDown();

View File

@ -14,7 +14,7 @@ describe('Component vnButtonMenu', () => {
$element.remove();
});
describe('onClick(event)', () => {
describe('onButtonClick(event)', () => {
it(`should emit the open event`, () => {
spyOn(controller, 'emit');
@ -23,7 +23,7 @@ describe('Component vnButtonMenu', () => {
bubbles: true,
cancelable: true
});
controller.onClick(event);
controller.onButtonClick(event);
expect(controller.emit).toHaveBeenCalledWith('open');
});

View File

@ -39,19 +39,19 @@
}
&.colored {
color: white;
background-color: $color-main;
background-color: $color-button;
box-shadow: 0 .15em .15em 0 rgba(0, 0, 0, .3);
transition: background 200ms ease-in-out;
&:not(.disabled) {
&:hover,
&:focus {
background-color: lighten($color-main, 10%);
background-color: lighten($color-button, 10%);
}
}
}
&.flat {
color: $color-main;
color: $color-button;
background-color: transparent;
box-shadow: none;
transition: background 200ms ease-in-out;

View File

@ -19,7 +19,7 @@
}
&.checked > .btn {
border-color: transparent;
background-color: $color-main;
background-color: $color-button;
& > .mark {
top: 0;

View File

@ -14,7 +14,9 @@ class DatePicker extends Field {
let value = this.input.value;
if (value) {
date = new Date(value);
let ymd = value.split('-')
.map(e => parseInt(e));
date = new Date(ymd[0], ymd[1] - 1, ymd[2]);
if (this.field) {
let orgDate = this.field instanceof Date

View File

@ -3,6 +3,10 @@ describe('Component vnDatePicker', () => {
let $element;
let $ctrl;
let today;
today = new Date();
today.setHours(0, 0, 0, 0);
beforeEach(ngModule('vnCore'));
beforeEach(angular.mock.inject(($compile, $rootScope, _$filter_) => {
@ -18,14 +22,19 @@ describe('Component vnDatePicker', () => {
describe('field() setter', () => {
it(`should display the formated the date`, () => {
let today;
today = new Date();
today.setHours(0, 0, 0, 0);
$ctrl.field = today;
let displayed = $filter('date')(today, 'yyyy-MM-dd');
expect($ctrl.value).toEqual(displayed);
});
});
describe('onValueUpdate()', () => {
it(`should change the picker value to selected date`, () => {
$ctrl.value = $filter('date')(today, 'yyyy-MM-dd');
$ctrl.input.dispatchEvent(new Event('change'));
expect($ctrl.field).toEqual(today);
});
});
});

View File

@ -29,6 +29,10 @@ export default class Dialog extends Popup {
* @return {Promise} A promise that will be resolved with response when dialog is closed
*/
show(data, responseHandler) {
if (this.shown)
return this.$q.reject(new Error('Dialog already shown'));
super.show();
if (typeof data == 'function') {
responseHandler = data;
data = null;
@ -36,27 +40,27 @@ export default class Dialog extends Popup {
this.data = data;
this.showHandler = responseHandler;
super.show();
return this.$q(resolve => {
this.resolve = resolve;
});
}
/**
* Hides the dialog.
* Hides the dialog resolving the promise returned by show().
*
* @param {String} response The response
*/
hide(response) {
if (!this.shown) return;
this.showHandler = null;
super.hide();
this.showHandler = null;
if (this.resolve)
this.resolve(response);
}
/**
* Calls the response handler.
* Calls the response handlers.
*
* @param {String} response The response code
* @return {Boolean} The response handler return

View File

@ -28,14 +28,6 @@ describe('Component vnDialog', () => {
expect(called).toBeTruthy();
});
it(`should hide the dialog when response is given`, () => {
controller.show();
spyOn(controller, 'hide');
controller.respond('answer');
expect(controller.hide).toHaveBeenCalledWith('answer');
});
it(`should not hide the dialog when false is returned from response handler`, () => {
controller.show(() => false);
spyOn(controller, 'hide');
@ -46,12 +38,13 @@ describe('Component vnDialog', () => {
});
describe('hide()', () => {
it(`should do nothing if it's already hidden`, () => {
controller.onResponse = () => {};
spyOn(controller, 'onResponse');
it(`should resolve the promise returned by show`, () => {
let resolved = true;
controller.show().then(() => resolved = true);
controller.hide();
$scope.$apply();
expect(controller.onResponse).not.toHaveBeenCalledWith();
expect(resolved).toBeTruthy();
});
});
@ -94,7 +87,7 @@ describe('Component vnDialog', () => {
expect(controller.onAccept).toHaveBeenCalledWith({$response: 'accept'});
});
it(`should resolve the promise returned by show() with response when hidden`, () => {
it(`should resolve the promise returned by show() with response`, () => {
let response;
controller.show().then(res => response = res);
controller.respond('response');

View File

@ -36,7 +36,7 @@
background-color: transparent;
border: none;
border-radius: .1em;
color: $color-main;
color: $color-button;
font-family: vn-font-bold;
padding: .7em;
margin: -0.7em;

View File

@ -5,6 +5,7 @@ import template from './index.html';
import ArrayModel from '../array-model/array-model';
import CrudModel from '../crud-model/crud-model';
import {mergeWhere} from 'vn-loopback/util/filter';
import focus from '../../lib/focus';
/**
* @event select Thrown when model item is selected
@ -86,9 +87,11 @@ export default class DropDown extends Popover {
* @param {String} search The initial search term or %null
*/
show(parent, search) {
this._activeOption = -1;
if (this.shown) return;
super.show(parent);
this._activeOption = -1;
this.list = this.popup.querySelector('.list');
this.ul = this.popup.querySelector('ul');
@ -102,21 +105,25 @@ export default class DropDown extends Popover {
this.search = search;
this.buildList();
let input = this.popup.querySelector('input');
setTimeout(() => input.focus());
focus(this.popup.querySelector('input'));
}
onClose() {
hide() {
if (!this.shown) return;
super.hide();
this.document.removeEventListener('keydown', this.docKeyDownHandler);
this.docKeyDownHandler = null;
this.list.removeEventListener('scroll', this.listScrollHandler);
this.listScrollHandler = null;
}
onClose() {
this.destroyList();
this.list = null;
this.ul = null;
this.destroyList();
super.onClose();
}
@ -387,7 +394,7 @@ export default class DropDown extends Popover {
let filter = {
order,
limit: this.limit || 8
limit: this.limit || 30
};
if (model instanceof CrudModel) {

View File

@ -28,8 +28,7 @@
ng-transclude="append"
class="append">
</div>
<div class="icons post">
</div>
<div class="icons post"></div>
<div class="underline blur"></div>
<div class="underline focus"></div>
</div>

View File

@ -50,7 +50,7 @@ export default class Field extends FormInput {
}
set value(value) {
this.field = value;
this.input.value = value;
}
get value() {

View File

@ -2,6 +2,7 @@
.vn-field {
display: inline-block;
box-sizing: border-box;
width: 100%;
& > .container {
@ -22,7 +23,7 @@
top: 18px;
line-height: 20px;
pointer-events: none;
color: $color-font-secondary;
color: $color-font-bg-marginal;
transition-property: top, color, font-size;
transition-duration: 400ms;
transition-timing-function: cubic-bezier(.4, 0, .2, 1);
@ -67,7 +68,11 @@
}
& > input {
position: relative;
color: $color-font;
&::placeholder {
color: $color-font-bg;
}
&[type=time],
&[type=date],
&[type=password] {
@ -121,12 +126,13 @@
& > .icons {
display: flex;
align-items: center;
color: $color-font-secondary;
color: $color-font-bg-marginal;
}
& > .prepend > prepend,
& > .append > append,
& > .icons {
display: flex;
align-items: center;
& > vn-icon {
font-size: 24px;
@ -159,7 +165,7 @@
}
&.focus {
height: 2px;
background-color: $color-main;
background-color: $color-primary;
left: 50%;
width: 0;
transition-property: width, left, background-color;
@ -190,13 +196,57 @@
}
}
}
&.standout {
border-radius: .1em;
background-color: rgba(255, 255, 255, .1);
padding: 0 12px;
transition-property: background-color, color;
transition-duration: 200ms;
transition-timing-function: ease-in-out;
& > .container {
& > .underline {
display: none;
}
& > .infix > .control > * {
color: $color-font-dark;
&::placeholder {
color: $color-font-bg-dark;
}
}
& > .prepend,
& > .append,
& > .icons {
color: $color-font-bg-dark-marginal;
}
}
&.focused {
background-color: $color-bg-panel;
& > .container {
& > .infix > .control > * {
color: $color-font;
&::placeholder {
color: $color-font-bg;
}
}
& > .prepend,
& > .append,
& > .icons {
color: $color-font-bg-marginal;
}
}
}
}
&.not-empty,
&.focused,
&.invalid {
& > .container > .infix {
& > label {
top: 5px;
color: $color-main;
color: $color-primary;
padding: 0;
font-size: 12px;
}

View File

@ -2,7 +2,7 @@
.vn-icon-button {
@extend %clickable-light;
color: $color-main;
color: $color-button;
& > button {
padding: .2em !important;

View File

@ -41,6 +41,7 @@ import './list';
import './popover';
import './popup';
import './radio';
import './slot';
import './submit';
import './table';
import './td-editable';

View File

@ -1,45 +1,102 @@
@import "./effects";
/*
ul.menu {
list-style-type: none;
padding: 0;
padding-top: $spacing-md;
margin: 0;
font-size: inherit;
& > li > a {
@extend %clickable;
display: block;
color: inherit;
padding: .6em 2em;
}
}
*/
vn-list,
.vn-list {
display: block;
max-width: $width-sm;
margin: 0 auto;
padding: 0;
list-style-type: none;
a.vn-list-item {
@extend %clickable;
vn-list,
.vn-list {
vn-item,
.vn-item {
padding-left: $spacing-lg;
}
}
.vn-list-item {
border-bottom: $border-thin-light;
display: block;
text-decoration: none;
color: inherit;
& > vn-horizontal {
&.separated {
vn-item,
.vn-item {
border-bottom: $border-thin-light;
padding: $spacing-md;
& > vn-one {
overflow: hidden;
&:last-child {
border-bottom: none;
}
& > .buttons {
align-items: center;
}
}
}
vn-icon-button {
opacity: .4;
margin-left: .5em;
transition: opacity 250ms ease-out;
padding: 0;
font-size: 1.2em;
vn-item,
.vn-item {
@extend %clickable;
display: flex;
align-items: center;
color: inherit;
padding: $spacing-sm $spacing-md;
text-decoration: none;
min-height: 40px;
box-sizing: border-box;
&:hover {
opacity: 1;
}
&.separated {
border-bottom: $border-thin-light;
&:last-child {
border-bottom: none;
}
}
&.active {
@extend %active;
}
& > vn-item-section {
overflow: hidden;
flex: 1;
&[avatar] {
display: flex;
flex: none;
align-items: center;
margin-right: $spacing-md;
& > .vn-icon {
font-size: 1.2em;
}
}
&[side] {
display: flex;
flex: none;
align-items: center;
& > .vn-button {
opacity: .4;
margin-left: .5em;
transition: opacity 250ms ease-out;
padding: 0;
font-size: 1.05em;
&:hover {
opacity: 1;
}
}
}
}
vn-empty-rows {
display: block;
text-align: center;
padding: 1.5em;
box-sizing: border-box;
}
}

View File

@ -98,8 +98,9 @@ export default class Popup extends Component {
onClose() {
this.closeTimeout = null;
this.popup.remove();
this.$contentScope.$destroy();
this.popup = null;
this.$contentScope.$destroy();
this.$contentScope = null;
this.windowEl = null;
this.emit('close');
}

View File

@ -9,7 +9,7 @@
}
}
&.checked > .btn {
border-color: $color-main;
border-color: $color-button;
& > .mark {
position: absolute;
@ -19,7 +19,7 @@
transform: translate(-50%, -50%);
width: 10px;
height: 10px;
background-color: $color-main;
background-color: $color-button;
}
}
&.disabled.checked > .btn > .mark {

View File

@ -5,7 +5,7 @@
-webkit-appearance: none;
margin-top: -5px;
border-radius: 50%;
background: $color-main;
background: $color-button;
border: none;
height: 12px;
width: 12px;
@ -15,7 +15,7 @@
transition-timing-function: ease-out;
}
&:focus::#{$thumb-selector} {
box-shadow: 0 0 0 10px rgba($color-main, .2);
box-shadow: 0 0 0 10px rgba($color-button, .2);
}
&:active::#{$thumb-selector} {
transform: scale(1.5);
@ -29,7 +29,7 @@
width: 100%;
height: 3px;
cursor: inherit;
background: $color-secondary;
background: $color-marginal;
border-radius: 2px;
border: none;
}
@ -40,7 +40,7 @@
font-size: 12px;
&.main {
color: $color-main;
color: $color-button;
}
&.min-label {
float: left;

View File

@ -1,17 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Component vnSearchbar doSearch() should define searchString, call onSearch and the model applyFilter method 1`] = `"function search() { [native code] } 0:i 1:d 2:: 3:( ) 4:1 5:2 6:3 7:4 8:5 9:6"`;
exports[`Component vnSearchbar onPanelSubmit() should hide the popover, and set the filter as it wasn't defined 1`] = `Object {}`;
exports[`Component vnSearchbar onPanelSubmit() should hide the popover, compact the filter 1`] = `
Object {
"id": 999,
}
`;
exports[`Component vnSearchbar onSubmit() should define the controller's filter 1`] = `
Object {
"search": "id: 999",
}
`;

View File

@ -1,14 +1,26 @@
<form ng-submit="$ctrl.onSubmit()">
<vn-textfield
class="dense"
class="dense standout"
placeholder="{{::'Search' | translate}}"
ng-model="$ctrl.searchString">
<prepend>
<vn-icon
icon="search"
ng-click="$ctrl.clearFilter(); $ctrl.onSubmit()"
ng-click="$ctrl.onSubmit()"
pointer>
</vn-icon>
<div class="search-params">
<span
ng-repeat="param in $ctrl.params"
class="search-param"
title="{{param.chip}}">
<vn-icon
icon="close"
ng-click="$ctrl.removeParam($index)">
</vn-icon>
{{param.chip}}
</span>
</div>
</prepend>
<append>
<vn-icon

View File

@ -1,7 +1,8 @@
import ngModule from '../../module';
import Component from '../../lib/component';
import './style.scss';
import {buildFilter} from 'vn-loopback/util/filter';
import focus from '../../lib/focus';
import './style.scss';
/**
* An input specialized to perform searches, it allows to use a panel
@ -11,56 +12,66 @@ import {buildFilter} from 'vn-loopback/util/filter';
* by calling the exprBuilder function for each non-null parameter.
*
* @property {Object} filter A key-value object with filter parameters
* @property {Function} onSearch Function to call when search is submited
* @property {SearchPanel} panel The panel used for advanced searches
* @property {CrudModel} model The model used for searching
* @property {Function} exprBuilder If defined, is used to build each non-null param expresion
*/
export default class Controller extends Component {
constructor($element, $scope, $compile, $state, $transitions) {
super($element, $scope);
this.$element = $element;
this.$compile = $compile;
this.$state = $state;
this.$ = $scope;
let criteria = {to: this.$state.current.name};
this.deregisterCallback = $transitions.onSuccess(criteria,
() => this.onStateChange());
constructor($element, $) {
super($element, $);
this.searchState = '.';
this._filter = null;
this.autoLoad = false;
let criteria = {};
this.deregisterCallback = this.$transitions.onSuccess(
criteria, () => this.onStateChange());
}
$postLink() {
if (this.filter === null)
this.onStateChange();
this.onStateChange();
}
set filter(value) {
this._filter = value;
this.$state.go('.', {q: JSON.stringify(value)}, {location: 'replace'});
$onDestroy() {
this.deregisterCallback();
}
get filter() {
return this._filter;
}
onStateChange() {
this._filter = null;
if (this.$state.params.q) {
try {
this._filter = JSON.parse(this.$state.params.q);
} catch (e) {
console.error(e);
}
}
this.doSearch();
set filter(value) {
this._filter = value;
this.toBar(value);
}
get shownFilter() {
return this._filter != null ? this._filter : this.suggestedFilter;
return this.filter != null
? this.filter
: this.suggestedFilter;
}
get searchString() {
return this._searchString;
}
set searchString(value) {
this._searchString = value;
if (value == null) this.params = [];
}
onStateChange() {
let filter = null;
if (this.$state.is(this.searchState)) {
if (this.$params.q) {
try {
filter = JSON.parse(this.$params.q);
} catch (e) {
console.error(e);
}
}
focus(this.element.querySelector('vn-textfield input'));
}
this.filter = filter;
}
openPanel(event) {
@ -88,21 +99,136 @@ export default class Controller extends Component {
onPanelSubmit(filter) {
this.$.popover.hide();
filter = compact(filter);
this.filter = filter != null ? filter : {};
let element = this.element.querySelector('vn-textfield input');
element.select();
element.focus();
filter = filter != null ? filter : {};
this.doSearch(filter);
}
onSubmit() {
this.filter = this.getObjectFromString(this.searchString);
this.doSearch(this.fromBar());
}
removeParam(index) {
this.params.splice(index, 1);
this.doSearch(this.fromBar());
}
doSearch(filter) {
this.filter = filter;
let opts = this.$state.is(this.searchState)
? {location: 'replace'} : null;
this.$state.go(this.searchState,
{q: JSON.stringify(filter)}, opts);
}
fromBar() {
let filter = {};
if (this.searchString)
filter.search = this.searchString;
if (this.params) {
for (let param of this.params)
filter[param.key] = param.value;
}
return filter;
}
toBar(filter) {
this.params = [];
this.searchString = filter && filter.search;
if (!filter) return;
let keys = Object.keys(filter);
keys.forEach(key => {
if (key == 'search') return;
let value = filter[key];
let chip;
if (typeof value == 'string'
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value))
value = new Date(value);
switch (typeof value) {
case 'boolean':
chip = `${value ? '' : 'not '}${key}`;
break;
case 'number':
case 'string':
chip = `${key}: ${value}`;
break;
default:
if (value instanceof Date) {
let format = 'yyyy-MM-dd';
if (value.getHours() || value.getMinutes())
format += ' HH:mm';
chip = `${key}: ${this.$filter('date')(value, format)}`;
} else
chip = key;
}
this.params.push({chip, key, value});
});
}
}
ngModule.vnComponent('vnSearchbar', {
controller: Controller,
template: require('./searchbar.html'),
bindings: {
searchState: '@?',
filter: '<?',
suggestedFilter: '<?',
panel: '@',
info: '@?'
}
});
/**
* @property {CrudModel} model The model used for searching
* @property {Function} exprBuilder If defined, is used to build each non-null param expresion
* @property {Function} onSearch Function to call when search is submited
*/
class AutoSearch {
constructor($state, $transitions) {
this.$state = $state;
this.$transitions = $transitions;
let criteria = {to: this.$state.current.name};
this.deregisterCallback = this.$transitions.onSuccess(criteria,
() => this.onStateChange());
this.fetchFilter();
}
$postLink() {
if (this.filter !== null)
this.doSearch();
}
$onDestroy() {
this.deregisterCallback();
}
fetchFilter() {
if (this.$state.params.q) {
try {
this.filter = JSON.parse(this.$state.params.q);
} catch (e) {
console.error(e);
}
} else
this.filter = null;
}
onStateChange() {
this.fetchFilter();
this.doSearch();
}
doSearch() {
this.searchString = this.getStringFromObject(this.shownFilter);
let filter = this._filter;
let filter = this.filter;
if (filter == null && this.autoload)
filter = {};
@ -141,94 +267,17 @@ export default class Controller extends Component {
exprBuilder(param, value) {
return {[param]: value};
}
/**
* Finds pattern key:value or key:(extra value) and passes it to object.
*
* @param {String} searchString The search string
* @return {Object} The parsed object
*/
getObjectFromString(searchString) {
let result = {};
if (searchString) {
let regex = /((([\w_]+):([\w_]+))|([\w_]+):\(([\w_ ]+)\))/gi;
let findPattern = searchString.match(regex);
let remnantString = searchString.replace(regex, '').trim();
if (findPattern) {
for (let i = 0; i < findPattern.length; i++) {
let aux = findPattern[i].split(':');
let property = aux[0];
let value = aux[1].replace(/\(|\)/g, '');
result[property] = value.trim();
}
}
if (remnantString)
result.search = remnantString;
}
return result;
}
/**
* Passes an object to pattern key:value or key:(extra value).
*
* @param {Object} searchObject The search object
* @return {String} The passed string
*/
getStringFromObject(searchObject) {
let search = [];
if (searchObject) {
let keys = Object.keys(searchObject);
keys.forEach(key => {
if (key == 'search') return;
let value = searchObject[key];
let valueString;
if (typeof value === 'string' && value.indexOf(' ') !== -1)
valueString = `(${value})`;
else if (value instanceof Date)
valueString = value.toJSON();
else {
switch (typeof value) {
case 'number':
case 'string':
case 'boolean':
valueString = `${value}`;
}
}
if (valueString)
search.push(`${key}:${valueString}`);
});
if (searchObject.search)
search.unshift(searchObject.search);
}
return search.length ? search.join(' ') : '';
}
$onDestroy() {
this.deregisterCallback();
}
}
Controller.$inject = ['$element', '$scope', '$compile', '$state', '$transitions'];
AutoSearch.$inject = ['$state', '$transitions'];
ngModule.component('vnSearchbar', {
template: require('./searchbar.html'),
ngModule.vnComponent('vnAutoSearch', {
controller: AutoSearch,
bindings: {
filter: '<?',
suggestedFilter: '<?',
onSearch: '&?',
panel: '@',
model: '<?',
onSearch: '&?',
exprBuilder: '&?',
paramBuilder: '&?',
autoLoad: '<?',
info: '@?'
},
controller: Controller
paramBuilder: '&?'
}
});
/**
@ -251,7 +300,7 @@ function compact(obj) {
} else if (typeof obj == 'object' && obj.constructor == Object) {
let keys = Object.keys(obj);
for (let key of keys) {
if (key.substr(0, 2) == '$$' || compact(obj[key]) === undefined)
if (key.charAt(0) == '$' || compact(obj[key]) === undefined)
delete obj[key];
}
if (Object.keys(obj).length == 0)

View File

@ -4,164 +4,149 @@ describe('Component vnSearchbar', () => {
let controller;
let $element;
let $state;
let $params;
let $scope;
let filter = {id: 1, search: 'needle'};
beforeEach(ngModule('vnCore'));
beforeEach(angular.mock.inject(($componentController, _$state_, $rootScope) => {
beforeEach(angular.mock.inject(($componentController, $rootScope, _$state_) => {
$scope = $rootScope.$new();
$state = _$state_;
$element = angular.element(`<vn-textfield><input></input></vn-textfield>`);
$params = $state.params;
$params.q = JSON.stringify(filter);
$element = angular.element(`<div></div>`);
controller = $componentController('vnSearchbar', {$element, $scope});
controller.panel = 'vn-client-search-panel';
}));
describe('$postLink()', () => {
it(`should not call onStateChange() if filter is defined`, () => {
spyOn(controller, 'onStateChange');
controller.filter = {};
it(`should fetch the filter from the state if it's in the filter state`, () => {
controller.$postLink();
expect(controller.onStateChange).not.toHaveBeenCalledWith();
expect(controller.filter).toEqual(filter);
expect(controller.searchString).toBe('needle');
expect(controller.params.length).toBe(1);
});
it(`should call onStateChange() if filter is null`, () => {
spyOn(controller, 'onStateChange');
controller.filter = null;
it(`should not fetch the filter from the state if not in the filter state`, () => {
controller.searchState = 'other.state';
controller.$postLink();
expect(controller.onStateChange).toHaveBeenCalledWith();
});
});
describe('onStateChange()', () => {
it(`should set a formated _filter in the controller`, () => {
spyOn(controller, 'doSearch');
Object.assign($state.params, {q: '{"id": 999}'});
controller.onStateChange();
expect(controller._filter).toEqual({id: 999});
expect(controller.doSearch).toHaveBeenCalledWith();
});
});
describe('shownFilter() getter', () => {
it(`should return the _filter if not NULL`, () => {
controller.filter = '{"id": 999}';
let shownFilter = controller.shownFilter;
expect(shownFilter).toEqual(controller._filter);
expect(shownFilter).not.toEqual(controller.suggestedFilter);
});
it(`should return the suggested filter if filter is NULL`, () => {
controller.suggestedFilter = '{"id": 888}';
let shownFilter = controller.shownFilter;
expect(shownFilter).not.toEqual(controller._filter);
expect(shownFilter).toEqual(controller.suggestedFilter);
expect(controller.filter).toBeNull();
expect(controller.searchString).toBeNull();
expect(controller.params.length).toBe(0);
});
});
describe('filter() setter', () => {
it(`should call $state.go() to replace the current state location instead of creating a new one`, () => {
controller._filter = {};
spyOn($state, 'go');
controller.filter = {expected: 'filter'};
it(`should update the bar params and search`, () => {
let withoutHours = new Date(2000, 1, 1);
let withHours = new Date(withoutHours.getTime());
withHours.setHours(12, 30, 15, 10);
expect(controller._filter).toEqual(controller.filter);
expect($state.go).toHaveBeenCalledWith('.', Object({q: '{"expected":"filter"}'}), Object({location: 'replace'}));
controller.filter = {
search: 'needle',
withHours: withHours.toJSON(),
withoutHours: withoutHours.toJSON(),
boolean: true,
negated: false,
myObjectProp: {myProp: 1}
};
let chips = {};
for (let param of controller.params || [])
chips[param.key] = param.chip;
expect(controller.searchString).toBe('needle');
expect(chips.withHours).toBe('withHours: 2000-02-01 12:30');
expect(chips.withoutHours).toBe('withoutHours: 2000-02-01');
expect(chips.boolean).toBe('boolean');
expect(chips.negated).toBe('not negated');
expect(chips.myObjectProp).toBe('myObjectProp');
});
});
describe('shownFilter() getter', () => {
it(`should return the _filter if not null`, () => {
controller.filter = filter;
controller.suggestedFilter = {sugestedParam: 'suggestedValue'};
expect(controller.shownFilter).toEqual(filter);
});
it(`should return the suggested filter if filter is null`, () => {
controller.filter = null;
controller.suggestedFilter = {sugestedParam: 'suggestedValue'};
expect(controller.shownFilter).toEqual(controller.suggestedFilter);
});
});
describe('searchString() setter', () => {
it(`should clear the whole filter when it's null`, () => {
controller.filter = filter;
controller.searchString = null;
expect(controller.searchString).toBeNull();
expect(controller.params.length).toBe(0);
});
});
describe('onPanelSubmit()', () => {
it(`should hide the popover, and set the filter as it wasn't defined`, () => {
it(`should compact and define the filter`, () => {
controller.$.popover = {hide: jasmine.createSpy('hide')};
const filter = undefined;
const filter = {
id: 1,
thisKeyShouldBePurged: null,
alsoThis: [],
andThis: {emptyProp: undefined, nullProp: null},
dontForgetThis: [null, undefined],
myObject: {keepThis: true, butNotThis: null},
myArray: [null, undefined, true]
};
controller.onPanelSubmit(filter);
expect(controller.filter).toMatchSnapshot();
});
it(`should hide the popover, compact the filter`, () => {
controller.$.popover = {hide: jasmine.createSpy('hide')};
const filter = {id: 999, thisKeyShouldBePurged: null};
controller.onPanelSubmit(filter);
expect(controller.filter).toMatchSnapshot();
expect(controller.filter).toEqual({
id: 1,
myObject: {keepThis: true},
myArray: [true]
});
});
});
describe('onSubmit()', () => {
it(`should define the controller's filter`, () => {
controller.searchString = 'id: 999';
it(`should define the filter`, () => {
controller.filter = filter;
controller.searchString = 'mySearch';
controller.onSubmit();
expect(controller.filter).toMatchSnapshot();
expect(controller.filter).toEqual({id: 1, search: 'mySearch'});
});
});
describe('removeParam()', () => {
it(`should remove the parameter from the filter`, () => {
controller.filter = filter;
controller.removeParam(0);
expect(controller.filter).toEqual({search: 'needle'});
});
});
describe('doSearch()', () => {
it(`should define searchString, call onSearch and the model applyFilter method`, () => {
controller._filter = 'id: 123456';
controller.autoload = true;
controller.onSearch = jasmine.createSpy('onSearch');
controller.model = {applyFilter: jasmine.createSpy('applyFilter')};
controller.paramBuilder = jasmine.createSpy('paramBuilder').and.returnValue({'param': `${controller._filter}`});
it(`should go to the search state and pass the filter as query param`, () => {
spyOn($state, 'go');
controller.searchState = 'search.state';
controller.doSearch(filter);
let queryParams = {q: JSON.stringify(filter)};
controller.doSearch();
expect(controller.searchString).toMatchSnapshot();
expect(controller.onSearch).toHaveBeenCalledWith({'$params': `${controller._filter}`});
expect(controller.model.applyFilter).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Object));
});
it(`should define searchString, call onSearch and the model clear method`, () => {
controller._filter = null;
controller.autoload = false;
controller.onSearch = jasmine.createSpy('onSearch');
controller.model = {clear: jasmine.createSpy('clear')};
controller.paramBuilder = jasmine.createSpy('paramBuilder').and.returnValue({'param': `${controller._filter}`});
controller.doSearch();
expect(controller.searchString).toEqual('');
expect(controller.onSearch).toHaveBeenCalledWith({'$params': null});
expect(controller.model.clear).toHaveBeenCalledWith();
});
});
describe('getObjectFromString()', () => {
it(`should return a formated object based on the string received for basic search`, () => {
let result = controller.getObjectFromString('Bruce Wayne');
expect(result).toEqual({search: 'Bruce Wayne'});
});
it(`should return a formated object based on the string received for advanced search`, () => {
let result = controller.getObjectFromString('id:101 name:(Bruce Wayne)');
expect(result).toEqual({id: '101', name: 'Bruce Wayne'});
});
it(`should format the object grouping any unmatched part of the instring of the string to the search property`, () => {
let string = 'I am the search id:101 name:(Bruce Wayne) concatenated value';
let result = controller.getObjectFromString(string);
expect(result).toEqual({
id: '101',
name: 'Bruce Wayne',
search: 'I am the search concatenated value'
});
expect($state.go).toHaveBeenCalledWith('search.state', queryParams, null);
expect(controller.filter).toEqual(filter);
});
});
});

View File

@ -2,7 +2,40 @@
vn-searchbar {
display: block;
width: 100%;
max-width: 35em;
margin: 0 auto;
.search-params {
flex: 1;
margin: .05em 0;
overflow: visible;
display: flex;
max-width: 24em;
& > .search-param {
color: rgba(0, 0, 0, .6);
background-color: rgba(0, 0, 0, .1);
padding: .1em .4em;
margin-left: .2em;
display: inline-block;
border-radius: .8em;
max-width: 12em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
& > vn-icon {
font-size: inherit;
vertical-align: middle;
cursor: pointer;
border-radius: 50%;
&:hover {
color: rgba(0, 0, 0, .8);
}
}
}
}
}
.search-panel {

View File

@ -0,0 +1,81 @@
import ngModule from '../../module';
import './portal';
import './style.scss';
export class Slot {
constructor($element, vnSlotService) {
this.$element = $element;
this.vnSlotService = vnSlotService;
this.$content = null;
}
$onDestroy() {
this.unregister();
}
set name(value) {
this.unregister();
this._name = value;
this.vnSlotService.slots[value] = this;
}
get name() {
return this._name;
}
unregister() {
if (this.name)
this.vnSlotService.slots[this.name] = undefined;
}
setContent($content) {
if (this.$content) {
this.$content.detach();
this.$content = null;
}
this.$content = $content;
if (this.$content) this.$element.append($content);
this.$element[0].style.display = $content ? 'block' : 'none';
}
}
Slot.$inject = ['$element', 'vnSlotService'];
ngModule.vnComponent('vnSlot', {
controller: Slot,
bindings: {
name: '@?'
}
});
export class SlotService {
constructor() {
this.stacks = {};
this.slots = {};
}
push(slot, $transclude) {
if (!this.stacks[slot]) this.stacks[slot] = [];
this.stacks[slot].unshift($transclude);
this.refreshContent(slot);
}
pop(slot) {
let $content = this.stacks[slot].shift();
this.refreshContent(slot);
if ($content && typeof $content == 'object')
$content.remove();
}
refreshContent(slot) {
if (!this.slots[slot]) return;
let $content = this.stacks[slot][0];
if (typeof $content == 'function') {
$content(clone => {
$content = this.stacks[slot][0] = clone;
});
}
this.slots[slot].setContent($content);
}
}
ngModule.service('vnSlotService', SlotService);

View File

@ -0,0 +1,28 @@
import ngModule from '../../module';
/**
* Component used to fill slots with content.
*/
export default class Portal {
constructor($transclude, vnSlotService) {
this.$transclude = $transclude;
this.vnSlotService = vnSlotService;
}
$postLink() {
this.vnSlotService.push(this.slot, this.$transclude);
}
$onDestroy() {
this.vnSlotService.pop(this.slot);
}
}
Portal.$inject = ['$transclude', 'vnSlotService'];
ngModule.component('vnPortal', {
controller: Portal,
transclude: true,
bindings: {
slot: '@'
}
});

View File

@ -0,0 +1,3 @@
vn-slot {
display: block;
}

View File

@ -6,6 +6,7 @@
cy="50"
r="20"
fill="none"
stroke="currentColor"
stroke-miterlimit="10">
</circle>
</svg>

View File

@ -4,6 +4,7 @@ vn-spinner {
display: inline-block;
min-height: 28px;
min-width: 28px;
color: $color-main;
& > .loader {
position: relative;
@ -29,7 +30,6 @@ vn-spinner {
margin: auto;
& > .path {
stroke: $color-main;
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
stroke-linecap: square;

View File

@ -3,8 +3,8 @@
vn-table {
display: block;
overflow: auto;
width: 100%;
overflow: auto;
}
.vn-table {
width: 100%;

View File

@ -40,10 +40,10 @@
background-color: rgba(0, 0, 0, .1);
}
&.checked > .btn {
border-color: $color-main;
border-color: $color-button;
& > .focus-mark {
background-color: rgba($color-main, .15);
background-color: rgba($color-button, .15);
}
}
&.disabled {

View File

@ -36,13 +36,14 @@ vn-treeview-childs {
}
vn-treeview-child {
line-height: 38px;
font-size: 16px;
display: block;
.node {
@extend %clickable;
display: flex;
padding: 5px;
padding: 2px;
align-items: center;
}
& > div > .arrow {

View File

@ -20,7 +20,7 @@
background-color: rgba(0, 0, 0, .05);
&.marked {
background: $color-main;
background: $color-button;
color: $color-font-dark;
}
}

View File

@ -12,14 +12,23 @@ export function directive() {
restrict: 'A',
link: function($scope, $element, $attrs) {
let id = kebabToCamel($attrs.vnId);
let $ctrl = $element[0].$ctrl
? $element[0].$ctrl
: $element.controller($element[0].tagName.toLowerCase());
if (!id)
throw new Error(`vnId: Attribute can't be null`);
$scope[id] = $ctrl || $element[0];
let $ctrl = $element[0].$ctrl
? $element[0].$ctrl
: $element.controller($element[0].tagName.toLowerCase());
let ctrl = $ctrl || $element[0];
$scope[id] = ctrl;
if (!$scope.hasOwnProperty('$ctrl')) {
while ($scope && !$scope.hasOwnProperty('$ctrl'))
$scope = Object.getPrototypeOf($scope);
if ($scope) $scope[id] = ctrl;
}
}
};
}

View File

@ -107,6 +107,7 @@ function runFn(
$compile,
$filter,
$interpolate,
$window,
vnApp) {
Object.assign(Component.prototype, {
$translate,
@ -119,6 +120,7 @@ function runFn(
$compile,
$filter,
$interpolate,
$window,
vnApp
});
}
@ -133,6 +135,7 @@ runFn.$inject = [
'$compile',
'$filter',
'$interpolate',
'$window',
'vnApp'
];

16
front/core/lib/focus.js Normal file
View File

@ -0,0 +1,16 @@
import isMobile from './is-mobile';
export default function focus(element) {
if (isMobile) return;
setTimeout(() => element.focus(), 10);
}
export function select(element) {
if (isMobile) return;
setTimeout(() => {
element.focus();
if (element.select)
element.select();
}, 10);
}

View File

@ -2,8 +2,9 @@ import './module-loader';
import './crud';
import './copy';
import './equals';
import './focus';
import './get-main-route';
import './modified';
import './key-codes';
import './http-error';
import './user-error';
import './get-main-route';

View File

@ -39,7 +39,7 @@ describe('Service acl', () => {
expect(hasAny).toBeTruthy();
});
it('should return true when user has not any of the passed roles', () => {
it('should return false when user has not any of the passed roles', () => {
let hasAny = aclService.hasAny(['inventedRole', 'nonExistent']);
expect(hasAny).toBeFalsy();

View File

@ -1,9 +1,6 @@
import {ng, ngDeps} from './vendor';
import {camelToKebab} from './lib/string';
const ngModule = ng.module('vnCore', ngDeps);
export default ngModule;
/**
* Acts like native Module.component() function but merging component options
* with parent component options. This method establishes the $options property
@ -17,7 +14,7 @@ export default ngModule;
* @param {Object} options The component options
* @return {angularModule} The same angular module
*/
ngModule.vnComponent = function(name, options) {
function vnComponent(name, options) {
let controller = options.controller;
let parent = Object.getPrototypeOf(controller);
let parentOptions = parent.$options || {};
@ -57,10 +54,21 @@ ngModule.vnComponent = function(name, options) {
controller.$classNames = classNames;
return this.component(name, mergedOptions);
}
const ngModuleFn = ng.module;
ng.module = function(...args) {
let ngModule = ngModuleFn.apply(this, args);
ngModule.vnComponent = vnComponent;
return ngModule;
};
config.$inject = ['$translateProvider', '$translatePartialLoaderProvider'];
export function config($translateProvider, $translatePartialLoaderProvider) {
const ngModule = ng.module('vnCore', ngDeps);
export default ngModule;
config.$inject = ['$translateProvider', '$translatePartialLoaderProvider', '$animateProvider'];
export function config($translateProvider, $translatePartialLoaderProvider, $animateProvider) {
// For CSS browser targeting
document.documentElement.setAttribute('data-browser', navigator.userAgent);
@ -91,5 +99,8 @@ export function config($translateProvider, $translatePartialLoaderProvider) {
return langAliases[locale];
return fallbackLang;
});
$animateProvider.customFilter(
node => node.tagName == 'UI-VIEW');
}
ngModule.config(config);

View File

@ -13,11 +13,15 @@ function interceptor($q, vnApp, vnToken, $translate) {
vnApp.pushLoader();
if (config.url.charAt(0) !== '/' && apiPath)
config.url = `${apiPath}/${config.url}`;
config.url = `${apiPath}${config.url}`;
if (vnToken.token)
config.headers.Authorization = vnToken.token;
if ($translate.use())
config.headers['Accept-Language'] = $translate.use();
if (config.filter) {
if (!config.params) config.params = {};
config.params.filter = config.filter;
}
return config;
},

View File

@ -0,0 +1,30 @@
@keyframes nothing {}
@keyframes slideIn {
from {
transform: translate3d(-2em, 0, 0);
opacity: 0;
}
to {
transform: translate3d(0, 0, 0);
opacity: 1;
}
}
@keyframes amplify {
from {
opacity: 0;
transform: scale3d(0, 0, 0);
}
to {
opacity: 1;
transform: scale3d(1, 1, 1);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View File

@ -41,3 +41,7 @@ a {
color: $color-font-link;
text-decoration: none;
}
.ng-leave,
.ng-enter {
transition: none;
}

View File

@ -1,3 +1,4 @@
import './animations.scss';
import './background.scss';
import './border.scss';
import './font-family.scss';

View File

@ -19,27 +19,36 @@ $spacing-xs: 4px;
$spacing-sm: 8px;
$spacing-md: 16px;
$spacing-lg: 32px;
$spacing-xl: 100px;
$spacing-xl: 70px;
// Light theme
$color-header: #3d3d3d;
$color-bg: #e5e5e5;
$color-bg-dark: #3d3d3d;
$color-primary: #f7931e;
$color-secondary: $color-primary;
$color-font: #222;
$color-font-light: #555;
$color-font-secondary: #9b9b9b;
$color-font-dark: white;
$color-font-bg: rgba(0, 0, 0, .7);
$color-font-link: #005a9a;
$color-font-bg: rgba(0, 0, 0, .7);
$color-font-bg-marginal: rgba(0, 0, 0, .4);
$color-font-bg-dark: rgba(255, 255, 255, .7);
$color-font-bg-dark-marginal: rgba(255, 255, 255, .4);
$color-header: #3d3d3d;
$color-menu-header: #3d3d3d;
$color-bg: #e5e5e5;
$color-bg-dark: #3d3d3d;
$color-active: #3d3d3d;
$color-active-font: white;
$color-active-font: $color-font-dark;
$color-bg-panel: white;
$color-main: #f7931e;
$color-secondary: #ccc;
$color-main: $color-primary;
$color-marginal: #ccc;
$color-success: #a3d131;
$color-notice: #32b1ce;
$color-alert: #f42121;
$color-button: $color-secondary;
$color-spacer: rgba(0, 0, 0, .3);
$color-spacer-light: rgba(0, 0, 0, .12);
@ -78,7 +87,7 @@ $color-active: #666;
$color-active-font: white;
$color-bg-panel: #3c3b3b;
$color-main: #f7931e;
$color-secondary: #ccc;
$color-marginal: #ccc;
$color-success: #a3d131;
$color-notice: #32b1ce;
$color-alert: #f42121;

View File

@ -2,6 +2,7 @@ import '@babel/polyfill';
import * as ng from 'angular';
export {ng};
import 'angular-animate';
import 'angular-translate';
import 'angular-translate-loader-partial';
import '@uirouter/angularjs';
@ -9,6 +10,7 @@ import 'mg-crud';
import 'oclazyload';
export const ngDeps = [
'ngAnimate',
'pascalprecht.translate',
'ui.router',
'mgCrud',

View File

@ -31,6 +31,11 @@
"resolved": "https://registry.npmjs.org/angular/-/angular-1.7.5.tgz",
"integrity": "sha512-760183yxtGzni740IBTieNuWLtPNAoMqvmC0Z62UoU0I3nqk+VJuO3JbQAXOyvo3Oy/ZsdNQwrSTh/B0OQZjNw=="
},
"angular-animate": {
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.7.8.tgz",
"integrity": "sha512-bINtzizq7TbJzfVrDpwLfTxVl0Qd7fRNWFb5jKYI1vaFZobQNX/QONXlLow6ySsDbZ6eLECycB7mvWtfh1YYaw=="
},
"angular-translate": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.18.1.tgz",

View File

@ -12,6 +12,7 @@
"@babel/polyfill": "^7.2.5",
"@uirouter/angularjs": "^1.0.20",
"angular": "^1.7.5",
"angular-animate": "^1.7.8",
"angular-translate": "^2.18.1",
"angular-translate-loader-partial": "^2.18.1",
"js-yaml": "^3.13.1",

View File

@ -1,29 +1,9 @@
<vn-topbar ng-if="$ctrl.showTopbar">
<a class="logo" ui-sref="home" title="{{'Home' | translate}}">
<img src="./logo.svg" alt="Logo"></img>
</a>
<vn-icon
icon="menu"
class="show-menu"
ng-if="$ctrl.leftMenu"
ng-click="$ctrl.leftMenu.show()">
</vn-icon>
<div class="main-title" translate>
{{$ctrl.$state.current.description}}
</div>
<vn-spinner enable="$ctrl.vnApp.loading"></vn-spinner>
<vn-main-menu></vn-main-menu>
<vn-icon
icon="menu"
class="show-menu"
ng-if="$ctrl.rightMenu"
ng-click="$ctrl.rightMenu.show()">
</vn-icon>
</vn-topbar>
<div ui-view
class="main-view"
ng-class="{padding: $ctrl.showTopbar}">
<vn-home></vn-home>
</div>
<vn-layout
ng-if="$ctrl.showLayout">
</vn-layout>
<ui-view
name="login"
ng-if="!$ctrl.showLayout">
</ui-view>
<vn-snackbar vn-id="snackbar"></vn-snackbar>
<vn-debug-info></vn-debug-info>

View File

@ -1,5 +1,6 @@
import ngModule from '../../module';
import './style.scss';
import Component from 'core/lib/component';
/**
* The main graphical application component.
@ -7,28 +8,21 @@ import './style.scss';
* @property {SideMenu} leftMenu The left menu, if it's present
* @property {SideMenu} rightMenu The left menu, if it's present
*/
export default class App {
constructor($, $state, vnApp) {
Object.assign(this, {
$,
$state,
vnApp
});
}
export default class App extends Component {
$postLink() {
this.vnApp.logger = this;
}
$onDestroy() {
this.vnApp.logger = null;
}
get showTopbar() {
get showLayout() {
let state = this.$state.current.name;
return state && state != 'login';
}
$onDestroy() {
this.deregisterCallback();
this.vnApp.logger = null;
}
showMessage(message) {
this.$.snackbar.show({message: message});
}
@ -41,9 +35,8 @@ export default class App {
this.$.snackbar.showError({message: message});
}
}
App.$inject = ['$scope', '$state', 'vnApp'];
ngModule.component('vnApp', {
ngModule.vnComponent('vnApp', {
template: require('./app.html'),
controller: App
});

View File

@ -1,99 +1,17 @@
@import "variables";
vn-app {
height: inherit;
display: block;
height: inherit;
& > vn-topbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 10;
box-shadow: 0 .1em .2em $color-shadow;
height: $topbar-height;
padding: .4em;
& > header {
& > * {
padding: .3em;
}
& > .logo > img {
height: 1.4em;
display: block;
}
& > .show-menu {
display: none;
font-size: 1.8em;
cursor: pointer;
&:hover {
color: $color-main;
}
}
& > .main-title {
font-size: 1.6em;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding-left: .6em;
}
& > vn-spinner {
padding: 0 .4em;
}
& > vn-main-menu {
flex: 1;
}
}
}
& > .main-view {
ui-view {
display: block;
box-sizing: border-box;
height: inherit;
&.padding {
padding-top: $topbar-height;
}
.content-block {
box-sizing: border-box;
padding: $spacing-md;
height: inherit;
}
vn-main-block {
display: block;
margin: 0 auto;
padding-left: $menu-width;
height: 100%
}
.main-with-right-menu {
padding-right: $menu-width;
@media screen and (max-width: 800px) {
padding-right: 0;
}
}
}
@media screen and (max-width: $mobile-width) {
& > vn-topbar > header {
& > .logo {
display: none;
}
& > .show-menu {
display: block;
}
}
& > .main-view {
.content-block {
margin-left: 0;
margin-right: 0;
}
vn-main-block {
padding-left: 0;
}
.main-with-right-menu {
padding-right: 0;
}
&.ng-enter {
animation-name: nothing, slideIn;
animation-duration: 400ms, 200ms;
}
}
}

View File

@ -50,7 +50,7 @@
& > vn-icon {
padding: $spacing-sm;
color: $color-secondary;
color: $color-marginal;
font-size: 1.5em;
&.bright {

View File

@ -1,4 +1,4 @@
<div ng-if="$ctrl.$state.current.name === 'home'">
<div>
<div class="modules">
<a
ng-repeat="mod in ::$ctrl.modules"

View File

@ -23,7 +23,7 @@ vn-home {
@extend %clickable-light;
overflow:hidden;
border-radius: 6px;
background-color: $color-main;
background-color: $color-button;
color: $color-font-dark;
display: flex;
flex-direction: column;

View File

@ -2,10 +2,13 @@ import './app/app';
import './background/background';
import './descriptor';
import './home/home';
import './layout';
import './left-menu/left-menu';
import './login/login';
import './main-menu/main-menu';
import './topbar/topbar';
import './module-card';
import './module-main';
import './side-menu/side-menu';
import './section';
import './summary';
import './topbar/topbar';
import './user-popover';

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