Merge pull request '2585-pinned_module_icons' (#560) from 2585-pinned_module_icons into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #560
Reviewed-by: Joan Sanchez <joan@verdnatura.es>
This commit is contained in:
Joan Sanchez 2021-03-02 09:38:31 +00:00
commit f3373ce98c
21 changed files with 626 additions and 680 deletions

View File

@ -0,0 +1,28 @@
module.exports = function(Self) {
Self.remoteMethodCtx('getStarredModules', {
description: 'returns the starred modules for the current user',
accessType: 'READ',
returns: {
type: 'object',
root: true
},
http: {
path: `/getStarredModules`,
verb: 'get'
}
});
Self.getStarredModules = async ctx => {
const userId = ctx.req.accessToken.userId;
const filter = {
where: {
workerFk: userId
},
fields: ['moduleFk']
};
const starredModules = await Self.app.models.StarredModule.find(filter);
return starredModules;
};
};

View File

@ -0,0 +1,31 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('getStarredModules()', () => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
const ctx = {req: activeCtx};
beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it(`should return the starred modules for a given user`, async() => {
const newStarred = await app.models.StarredModule.create({workerFk: 9, moduleFk: 'Clients'});
const starredModules = await app.models.StarredModule.getStarredModules(ctx);
expect(starredModules.length).toEqual(1);
expect(starredModules[0].moduleFk).toEqual('Clients');
// restores
await app.models.StarredModule.destroyById(newStarred.id);
});
});

View File

@ -0,0 +1,36 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('toggleStarredModule()', () => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
const ctx = {
req: activeCtx
};
beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should create a new starred module and then remove it by calling the method again with same args', async() => {
const starredModule = await app.models.StarredModule.toggleStarredModule(ctx, 'Orders');
let starredModules = await app.models.StarredModule.getStarredModules(ctx);
expect(starredModules.length).toEqual(1);
expect(starredModule.moduleFk).toEqual('Orders');
expect(starredModule.workerFk).toEqual(activeCtx.accessToken.userId);
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders');
starredModules = await app.models.StarredModule.getStarredModules(ctx);
expect(starredModules.length).toEqual(0);
});
});

View File

@ -0,0 +1,41 @@
module.exports = function(Self) {
Self.remoteMethodCtx('toggleStarredModule', {
description: 'creates or deletes a starred module for the current user',
accessType: 'WRITE',
returns: {
type: 'object',
root: true
},
accepts: {
arg: 'moduleName',
type: 'string',
required: true,
description: 'The module name'
},
http: {
path: `/toggleStarredModule`,
verb: 'post'
}
});
Self.toggleStarredModule = async(ctx, moduleName) => {
const userId = ctx.req.accessToken.userId;
const filter = {
where: {
workerFk: userId,
moduleFk: moduleName
}
};
const [starredModule] = await Self.app.models.StarredModule.find(filter);
if (starredModule)
await starredModule.destroy();
else {
return Self.app.models.StarredModule.create({
workerFk: userId,
moduleFk: moduleName
});
}
};
};

View File

@ -59,6 +59,9 @@
"Language": {
"dataSource": "vn"
},
"Module": {
"dataSource": "vn"
},
"Province": {
"dataSource": "vn"
},
@ -71,6 +74,9 @@
"SageWithholding": {
"dataSource": "vn"
},
"StarredModule": {
"dataSource": "vn"
},
"TempContainer": {
"dataSource": "tempStorage"
},

23
back/models/module.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "Module",
"base": "VnModel",
"options": {
"mysql": {
"table": "salix.module"
}
},
"properties": {
"code": {
"type": "string",
"id": true
}
},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -0,0 +1,4 @@
module.exports = Self => {
require('../methods/starred-module/getStarredModules')(Self);
require('../methods/starred-module/toggleStarredModule')(Self);
};

View File

@ -0,0 +1,35 @@
{
"name": "StarredModule",
"base": "VnModel",
"options": {
"mysql": {
"table": "vn.starredModule"
}
},
"properties": {
"id": {
"type": "number",
"id": true
},
"workerFk": {
"type": "number",
"required": true
},
"moduleFk": {
"type": "string",
"required": true
}
},
"relations": {
"worker": {
"type": "belongsTo",
"model": "Worker",
"foreignKey": "workerFk"
},
"module": {
"type": "belongsTo",
"model": "Module",
"foreignKey": "moduleFk"
}
}
}

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('StarredModule', '*', '*', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,20 @@
CREATE TABLE `salix`.`module` (
`code` VARCHAR(45) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `salix`.`module`(`code`)
VALUES
('Items'),
('Orders'),
('Clients'),
('Entries'),
('Travels'),
('Invoices out'),
('Suppliers'),
('Claims'),
('Routes'),
('Tickets'),
('Workers'),
('Users'),
('Zones');

View File

@ -0,0 +1,10 @@
CREATE TABLE `vn`.`starredModule` (
`id` INT(11) unsigned NOT NULL AUTO_INCREMENT,
`workerFk` INT(10) NOT NULL,
`moduleFk` VARCHAR(45) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`),
KEY `starred_workerFk` (`workerFk`),
KEY `starred_moduleFk` (`moduleFk`),
CONSTRAINT `starred_workerFk` FOREIGN KEY (`workerFk`) REFERENCES `vn`.`worker` (`id`) ON UPDATE CASCADE,
CONSTRAINT `starred_moduleFk` FOREIGN KEY (`moduleFk`) REFERENCES `salix`.`module` (`code`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

View File

@ -23,6 +23,11 @@ export default {
acceptButton: '.vn-confirm.shown button[response=accept]',
searchButton: 'vn-searchbar vn-icon[icon="search"]'
},
moduleIndex: {
anyStarredModule: 'vn-home > div:nth-child(1) > div.modules > a',
firstModulePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="push_pin"]',
firstModuleRemovePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="remove_circle"]'
},
clientsIndex: {
createClientButton: `vn-float-button`
},

View File

@ -0,0 +1,43 @@
import selectors from '../../helpers/selectors';
import getBrowser from '../../helpers/puppeteer';
describe('Starred modules path', async() => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.login('employee');
});
afterAll(async() => {
await browser.close();
});
it('should make sure there are no modules pinned yet', async() => {
const count = await page.countElement(selectors.moduleIndex.anyStarredModule);
expect(count).toEqual(0);
});
it('should set a module as favore', async() => {
await page.waitToClick(selectors.moduleIndex.firstModulePinIcon);
const message = await page.waitForSnackbar();
const count = await page.countElement(selectors.moduleIndex.anyStarredModule);
expect(message.text).toContain('Data saved!');
expect(count).toEqual(1);
});
it('should remove the module from favores', async() => {
await page.waitToClick(selectors.moduleIndex.firstModuleRemovePinIcon);
const message = await page.waitForSnackbar();
const count = await page.countElement(selectors.moduleIndex.anyStarredModule);
expect(message.text).toContain('Data saved!');
expect(count).toEqual(0);
});
});

View File

@ -1,20 +1,67 @@
<div>
<div class="top-border">
<span translate>Favorites</span>
</div>
<div
ng-if="!$ctrl.starredCount"
class="starred-info vn-py-md">
<span translate class="empty">You can set modules as favorites by clicking their icon</span>
<vn-icon icon="push_pin"></vn-icon>
</div>
<div class="modules">
<a
ng-repeat="mod in ::$ctrl.modules"
ui-sref="{{::mod.route.state}}"
translate-attr="{title: mod.name}"
class="vn-shadow">
<div>
<vn-icon icon="{{::mod.icon || 'photo'}}"></vn-icon>
</div>
<h4 ng-bind-html="$ctrl.getModuleName(mod)"></h4>
<span
ng-show="::mod.keyBind"
vn-tooltip="Ctrl + Alt + {{::mod.keyBind}}">
({{::mod.keyBind}})
</span>
<span ng-show="::!mod.keyBind">&nbsp;</span>
</a>
<a
ng-repeat="mod in ::$ctrl.modules"
ng-if='mod.starred'
ui-sref="{{::mod.route.state}}"
translate-attr="{title: mod.name}"
class="vn-shadow">
<div
vn-tooltip="Remove from favorites"
class="pin"
ng-click="$ctrl.toggleStarredModule(mod, $event)">
<vn-icon icon="remove_circle"></vn-icon>
</div>
<div>
<vn-icon icon="{{::mod.icon || 'photo'}}"></vn-icon>
</div>
<h4 ng-bind-html="$ctrl.getModuleName(mod)"></h4>
<span
ng-show="::mod.keyBind"
vn-tooltip="Ctrl + Alt + {{::mod.keyBind}}">
({{::mod.keyBind}})
</span>
<span ng-show="::!mod.keyBind">&nbsp;</span>
</a>
</div>
</div>
<div>
<div
class="top-border"
ng-if="$ctrl.regularCount > 0 && $ctrl.starredCount > 0">
</div>
<div class="modules">
<a
ng-repeat="mod in ::$ctrl.modules"
ng-if='!mod.starred'
ui-sref="{{::mod.route.state}}"
translate-attr="{title: mod.name}"
class="vn-shadow">
<div
vn-tooltip="Add to favorites"
class="pin"
ng-click="$ctrl.toggleStarredModule(mod, $event)">
<vn-icon icon="push_pin"></vn-icon>
</div>
<div>
<vn-icon icon="{{::mod.icon || 'photo'}}"></vn-icon>
</div>
<h4 ng-bind-html="$ctrl.getModuleName(mod)"></h4>
<span
ng-show="::mod.keyBind"
vn-tooltip="Ctrl + Alt + {{::mod.keyBind}}">
({{::mod.keyBind}})
</span>
<span ng-show="::!mod.keyBind">&nbsp;</span>
</a>
</div>
</div>

View File

@ -9,6 +9,55 @@ export default class Controller extends Component {
this.$sce = $sce;
}
get modules() {
return this._modules;
}
set modules(value) {
this._modules = value;
this.getStarredModules();
}
countModules() {
this.starredCount = 0;
this.regularCount = 0;
this.modules.forEach(module => {
if (module.starred) this.starredCount ++;
else this.regularCount ++;
});
}
getStarredModules() {
this.$http.get('starredModules/getStarredModules')
.then(res => {
if (!res.data.length) return;
for (let starredModule of res.data) {
const module = this.modules.find(mod => mod.name === starredModule.moduleFk);
module.starred = true;
}
this.countModules();
});
}
toggleStarredModule(module, event) {
if (event.defaultPrevented) return;
event.preventDefault();
event.stopPropagation();
const params = {moduleName: module.name};
const query = `starredModules/toggleStarredModule`;
this.$http.post(query, params).then(res => {
if (res.data)
module.starred = true;
else
module.starred = false;
this.vnApp.showSuccess(this.$t('Data saved!'));
this.countModules();
});
}
getModuleName(mod) {
let getName = mod => {
let name = this.$t(mod.name);

View File

@ -0,0 +1,75 @@
import './home';
describe('Salix Component vnHome', () => {
let controller;
let $httpBackend;
let $scope;
let $element;
beforeEach(ngModule('salix'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _vnApp_, $window) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
$element = angular.element('<vn-home></vn-home>');
$window.routes = [{module: 'client', name: 'Clients'}];
controller = $componentController('vnHome', {$element, $scope, $window});
}));
describe('getStarredModules()', () => {
it('should not set any of the modules as starred if there are no starred modules for the user', () => {
const expectedResponse = [];
controller._modules = [{module: 'client', name: 'Clients'}];
$httpBackend.whenRoute('GET', 'starredModules/getStarredModules').respond(expectedResponse);
$httpBackend.expectGET('starredModules/getStarredModules').respond(expectedResponse);
controller.getStarredModules();
$httpBackend.flush();
expect(controller._modules.length).toEqual(1);
expect(controller._modules[0].starred).toBeUndefined();
});
it('should set the example module as starred since its the starred module for the user', () => {
const expectedResponse = [{id: 1, moduleFk: 'Clients', workerFk: 9}];
controller._modules = [{module: 'client', name: 'Clients'}];
$httpBackend.whenRoute('GET', 'starredModules/getStarredModules').respond(expectedResponse);
$httpBackend.expectGET('starredModules/getStarredModules').respond(expectedResponse);
controller.getStarredModules();
$httpBackend.flush();
expect(controller._modules.length).toEqual(1);
expect(controller._modules[0].starred).toBe(true);
});
});
describe('toggleStarredModule()', () => {
it(`should set the received module as starred if it wasn't starred`, () => {
const expectedResponse = [{id: 1, moduleFk: 'Clients', workerFk: 9}];
const event = new Event('target');
controller._modules = [{module: 'client', name: 'Clients'}];
$httpBackend.whenRoute('GET', 'starredModules/getStarredModules').respond(expectedResponse);
$httpBackend.expectPOST('starredModules/toggleStarredModule').respond(expectedResponse);
controller.toggleStarredModule(controller._modules[0], event);
$httpBackend.flush();
expect(controller._modules.length).toEqual(1);
expect(controller._modules[0].starred).toBe(true);
});
it(`should set the received module as regular if it was starred`, () => {
const event = new Event('target');
controller._modules = [{module: 'client', name: 'Clients', starred: true}];
$httpBackend.whenRoute('GET', 'starredModules/getStarredModules').respond([]);
$httpBackend.expectPOST('starredModules/toggleStarredModule').respond(undefined);
controller.toggleStarredModule(controller._modules[0], event);
$httpBackend.flush();
expect(controller._modules.length).toEqual(1);
expect(controller._modules[0].starred).toBe(false);
});
});
});

View File

@ -0,0 +1,4 @@
Favorites: Favoritos
You can set modules as favorites by clicking their icon: Puedes establecer módulos como favoritos haciendo clic en el icono
Add to favorites: Añadir a favoritos.
Remove from favorites: Quitar de favoritos.

View File

@ -10,28 +10,76 @@ vn-home {
text-align: center;
margin-bottom: 15px;
}
& .starred-info{
display: block;
text-align: center;
box-sizing: border-box;
color: $color-font-secondary;
font-size: 1.375rem;
& > .empty {
text-align: center;
box-sizing: border-box;
color: $color-font-secondary;
font-size: 1em;
}
& > vn-icon {
font-size: 1.2rem;
}
}
& > .top-border {
margin: 0 auto;
flex-direction: row;
float: center;
max-width: 690px;
border-bottom: 2px solid $color-font-secondary;
line-height: 2px;
> span {
height: 10px;
margin-left: 30px;
background-color: $color-bg;
padding:0 11px;
}
}
& > .modules {
padding: 10px 0 10px 0;
display: flex;
flex: 1;
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
flex: 1;
max-width: 704px;
margin: 0 auto;
& > a {
@extend %clickable-light;
overflow:hidden;
border-radius: 6px;
background-color: $color-button;
color: $color-font-dark;
display: flex;
flex-direction: column;
overflow:hidden;
justify-content: center;
border-radius: 6px;
height: 128px;
width: 128px;
margin: 8px;
padding: 16px;
justify-content: center;
background-color: $color-button;
color: $color-font-dark;
& .pin {
opacity: 0;
flex-direction: row;
justify-content: left;
height: 20px;
width: 20px;
vn-icon {
margin: auto;
font-size: 1.5rem;
}
}
&:hover .pin {
opacity: 1;
}
& > div {
height: 70px;
@ -56,10 +104,6 @@ vn-home {
color: inherit;
margin: 0;
line-height: 24px;
/* & > .bind-letter {
color: #FD0;
} */
}
}
}

744
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -32,6 +32,7 @@
"node-ssh": "^11.0.0",
"object-diff": "0.0.4",
"object.pick": "^1.3.0",
"puppeteer": "^7.1.0",
"read-chunk": "^3.2.0",
"request": "^2.88.0",
"request-promise-native": "^1.0.8",
@ -88,7 +89,6 @@
"node-sass": "^4.14.1",
"nodemon": "^1.19.4",
"plugin-error": "^1.0.1",
"puppeteer": "^5.5.0",
"raw-loader": "^1.0.0",
"sass-loader": "^7.3.1",
"style-loader": "^0.23.1",