favorite modules positions

This commit is contained in:
Carlos Jimenez Ruiz 2021-06-01 22:18:13 +02:00
parent a594e63566
commit 9f00fd11d2
12 changed files with 459 additions and 43 deletions

View File

@ -12,17 +12,23 @@ module.exports = function(Self) {
} }
}); });
Self.getStarredModules = async ctx => { Self.getStarredModules = async(ctx, options) => {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const filter = { const filter = {
where: { where: {
workerFk: userId workerFk: userId
}, },
fields: ['moduleFk'] fields: ['id', 'workerFk', 'moduleFk', 'position'],
order: 'position ASC'
}; };
const starredModules = await Self.app.models.StarredModule.find(filter); return models.StarredModule.find(filter, myOptions);
return starredModules;
}; };
}; };

View File

@ -0,0 +1,98 @@
module.exports = function(Self) {
Self.remoteMethodCtx('setPosition', {
description: 'sets the position of a given module',
accessType: 'WRITE',
returns: {
type: 'object',
root: true
},
accepts: [
{
arg: 'moduleName',
type: 'string',
required: true,
description: 'The module name'
},
{
arg: 'action',
type: 'string',
required: true,
description: 'Whether to increases or decreases the module position, last if undefined'
}
],
http: {
path: `/setPosition`,
verb: 'post'
}
});
Self.setPosition = async(ctx, moduleName, action, options) => {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
let tx;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const filter = {
where: {
workerFk: userId,
moduleFk: moduleName
},
order: 'position DESC'
};
const [movingModule] = await models.StarredModule.find(filter, myOptions);
let operator;
let order;
switch (action) {
case 'increase':
operator = {lt: movingModule.position};
order = 'position DESC';
break;
case 'decrease':
operator = {gt: movingModule.position};
order = 'position ASC';
break;
default:
return;
}
const pushedModule = await models.StarredModule.findOne({
where: {
position: operator,
workerFk: userId
},
order: order
}, myOptions);
if (!pushedModule) return;
const movingPosition = pushedModule.position;
const pushingPosition = movingModule.position;
await movingModule.updateAttribute('position', movingPosition, myOptions);
await pushedModule.updateAttribute('position', pushingPosition, myOptions);
if (tx) await tx.commit();
return {
movingModule: movingModule,
pushedModule: pushedModule
};
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -19,7 +19,7 @@ describe('getStarredModules()', () => {
}); });
it(`should return the starred modules for a given user`, async() => { it(`should return the starred modules for a given user`, async() => {
const newStarred = await app.models.StarredModule.create({workerFk: 9, moduleFk: 'Clients'}); const newStarred = await app.models.StarredModule.create({workerFk: 9, moduleFk: 'Clients', position: 1});
const starredModules = await app.models.StarredModule.getStarredModules(ctx); const starredModules = await app.models.StarredModule.getStarredModules(ctx);
expect(starredModules.length).toEqual(1); expect(starredModules.length).toEqual(1);

View File

@ -0,0 +1,223 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('setPosition()', () => {
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 increase the orders module position by replacing it with clients and vice versa', async() => {
const tx = await app.models.StarredModule.beginTransaction({});
const filter = {
where: {
workerFk: ctx.req.accessToken.userId,
moduleFk: 'Orders'
}
};
try {
const options = {transaction: tx};
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
let orders = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Clients';
let clients = await app.models.StarredModule.findOne(filter, options);
expect(orders.position).toEqual(1);
expect(clients.position).toEqual(2);
await app.models.StarredModule.setPosition(ctx, 'Clients', 'increase', options);
filter.where.moduleFk = 'Clients';
clients = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Orders';
orders = await app.models.StarredModule.findOne(filter, options);
expect(clients.position).toEqual(1);
expect(orders.position).toEqual(2);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should decrease the orders module position by replacing it with clients and vice versa', async() => {
const tx = await app.models.StarredModule.beginTransaction({});
const filter = {
where: {
workerFk: ctx.req.accessToken.userId,
moduleFk: 'Orders'
}
};
try {
const options = {transaction: tx};
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
let orders = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Clients';
let clients = await app.models.StarredModule.findOne(filter, options);
expect(orders.position).toEqual(1);
expect(clients.position).toEqual(2);
await app.models.StarredModule.setPosition(ctx, 'Orders', 'decrease', options);
filter.where.moduleFk = 'Orders';
orders = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Clients';
clients = await app.models.StarredModule.findOne(filter, options);
expect(orders.position).toEqual(2);
expect(clients.position).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should switch two modules after adding and deleting several modules', async() => {
const tx = await app.models.StarredModule.beginTransaction({});
const filter = {
where: {
workerFk: ctx.req.accessToken.userId,
moduleFk: 'Items'
}
};
try {
const options = {transaction: tx};
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Items', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Claims', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Zones', options);
const items = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Claims';
const claims = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Clients';
let clients = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Orders';
let orders = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Zones';
const zones = await app.models.StarredModule.findOne(filter, options);
expect(items.position).toEqual(1);
expect(claims.position).toEqual(2);
expect(clients.position).toEqual(3);
expect(orders.position).toEqual(4);
expect(zones.position).toEqual(5);
await app.models.StarredModule.setPosition(ctx, 'Clients', 'decrease', options);
filter.where.moduleFk = 'Orders';
orders = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Clients';
clients = await app.models.StarredModule.findOne(filter, options);
expect(orders.position).toEqual(3);
expect(clients.position).toEqual(4);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should switch two modules after adding and deleting a module between them', async() => {
const tx = await app.models.StarredModule.beginTransaction({});
const filter = {
where: {
workerFk: ctx.req.accessToken.userId,
moduleFk: 'Items'
}
};
try {
const options = {transaction: tx};
await app.models.StarredModule.toggleStarredModule(ctx, 'Items', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Claims', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Zones', options);
const items = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Clients';
let clients = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Claims';
const claims = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Orders';
let orders = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Zones';
const zones = await app.models.StarredModule.findOne(filter, options);
expect(items.position).toEqual(1);
expect(clients.position).toEqual(2);
expect(claims.position).toEqual(3);
expect(orders.position).toEqual(4);
expect(zones.position).toEqual(5);
await app.models.StarredModule.toggleStarredModule(ctx, 'Claims', options);
await app.models.StarredModule.setPosition(ctx, 'Clients', 'decrease', options);
filter.where.moduleFk = 'Clients';
clients = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Orders';
orders = await app.models.StarredModule.findOne(filter, options);
expect(orders.position).toEqual(2);
expect(clients.position).toEqual(4);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -27,6 +27,7 @@ describe('toggleStarredModule()', () => {
expect(starredModules.length).toEqual(1); expect(starredModules.length).toEqual(1);
expect(starredModule.moduleFk).toEqual('Orders'); expect(starredModule.moduleFk).toEqual('Orders');
expect(starredModule.workerFk).toEqual(activeCtx.accessToken.userId); expect(starredModule.workerFk).toEqual(activeCtx.accessToken.userId);
expect(starredModule.position).toEqual(starredModules.length);
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders'); await app.models.StarredModule.toggleStarredModule(ctx, 'Orders');
starredModules = await app.models.StarredModule.getStarredModules(ctx); starredModules = await app.models.StarredModule.getStarredModules(ctx);

View File

@ -18,24 +18,61 @@ module.exports = function(Self) {
} }
}); });
Self.toggleStarredModule = async(ctx, moduleName) => { Self.toggleStarredModule = async(ctx, moduleName, options) => {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const filter = {
where: { let tx;
workerFk: userId, let myOptions = {};
moduleFk: moduleName
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const filter = {
where: {
workerFk: userId,
moduleFk: moduleName
}
};
const [starredModule] = await models.StarredModule.find(filter, myOptions);
delete filter.moduleName;
const allStarredModules = await models.StarredModule.getStarredModules(ctx, myOptions);
let addedModule;
if (starredModule)
await starredModule.destroy(myOptions);
else {
let highestPosition;
if (allStarredModules.length) {
allStarredModules.sort((a, b) => {
return a.position - b.position;
});
highestPosition = allStarredModules[allStarredModules.length - 1].position + 1;
} else
highestPosition = 1;
addedModule = await models.StarredModule.create({
workerFk: userId,
moduleFk: moduleName,
position: highestPosition
}, myOptions);
} }
};
const [starredModule] = await Self.app.models.StarredModule.find(filter); if (tx) await tx.commit();
if (starredModule) return addedModule;
await starredModule.destroy(); } catch (e) {
else { if (tx) await tx.rollback();
return Self.app.models.StarredModule.create({ throw e;
workerFk: userId,
moduleFk: moduleName
});
} }
}; };
}; };

View File

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

View File

@ -18,6 +18,10 @@
"moduleFk": { "moduleFk": {
"type": "string", "type": "string",
"required": true "required": true
},
"position": {
"type": "number",
"required": true
} }
}, },
"relations": { "relations": {

View File

@ -0,0 +1,2 @@
ALTER TABLE `vn`.`starredModule`
ADD `position` INT NOT NULL AFTER `moduleFk`;

View File

@ -10,17 +10,35 @@
</div> </div>
<div class="modules"> <div class="modules">
<a <a
ng-repeat="mod in ::$ctrl.modules" ng-repeat="mod in $ctrl.modules | orderBy: '+position'"
ng-animate-ref="{{mod.position}}"
ng-if='mod.starred' ng-if='mod.starred'
ui-sref="{{::mod.route.state}}" ui-sref="{{::mod.route.state}}"
translate-attr="{title: mod.name}" translate-attr="{title: mod.name}"
class="vn-shadow"> class="vn-shadow">
<div <span>
vn-tooltip="Remove from favorites" <div
class="pin" vn-tooltip="Move left"
ng-click="$ctrl.toggleStarredModule(mod, $event)"> class="small-icon"
<vn-icon icon="remove_circle"></vn-icon> ng-class="{hidden: $index === 0}"
</div> ng-click="$ctrl.moveModule(mod, $event, 'increase')">
<vn-icon icon="arrow_left"></vn-icon>
</div>
<div
vn-tooltip="Remove from favorites"
class="small-icon"
ng-click="$ctrl.toggleStarredModule(mod, $event)">
<vn-icon icon="remove_circle"></vn-icon>
</div>
<div
vn-tooltip="Move right"
class="small-icon"
ng-class="{hidden: $ctrl.starredCount == $index + 1}"
ng-click="$ctrl.moveModule(mod, $event, 'decrease')">
<vn-icon icon="arrow_right"></vn-icon>
</div>
in: {{$index}} - pos {{mod.position}}
</span>
<div> <div>
<vn-icon icon="{{::mod.icon || 'photo'}}"></vn-icon> <vn-icon icon="{{::mod.icon || 'photo'}}"></vn-icon>
</div> </div>
@ -41,17 +59,19 @@
</div> </div>
<div class="modules"> <div class="modules">
<a <a
ng-repeat="mod in ::$ctrl.modules" ng-repeat="mod in $ctrl.modules"
ng-if='!mod.starred' ng-if='!mod.starred'
ui-sref="{{::mod.route.state}}" ui-sref="{{::mod.route.state}}"
translate-attr="{title: mod.name}" translate-attr="{title: mod.name}"
class="vn-shadow"> class="vn-shadow">
<div <span>
vn-tooltip="Add to favorites" <div
class="pin" vn-tooltip="Add to favorites"
ng-click="$ctrl.toggleStarredModule(mod, $event)"> class="small-icon"
<vn-icon icon="push_pin"></vn-icon> ng-click="$ctrl.toggleStarredModule(mod, $event)">
</div> <vn-icon icon="push_pin"></vn-icon>
</div>
</span>
<div> <div>
<vn-icon icon="{{::mod.icon || 'photo'}}"></vn-icon> <vn-icon icon="{{::mod.icon || 'photo'}}"></vn-icon>
</div> </div>

View File

@ -35,6 +35,7 @@ export default class Controller extends Component {
for (let starredModule of res.data) { for (let starredModule of res.data) {
const module = this.modules.find(mod => mod.name === starredModule.moduleFk); const module = this.modules.find(mod => mod.name === starredModule.moduleFk);
module.starred = true; module.starred = true;
module.position = starredModule.position;
} }
this.countModules(); this.countModules();
}); });
@ -48,9 +49,10 @@ export default class Controller extends Component {
const params = {moduleName: module.name}; const params = {moduleName: module.name};
const query = `starredModules/toggleStarredModule`; const query = `starredModules/toggleStarredModule`;
this.$http.post(query, params).then(res => { this.$http.post(query, params).then(res => {
if (res.data) if (res.data) {
module.starred = true; module.starred = true;
else module.position = res.data.position;
} else
module.starred = false; module.starred = false;
this.vnApp.showSuccess(this.$t('Data saved!')); this.vnApp.showSuccess(this.$t('Data saved!'));
@ -74,6 +76,26 @@ export default class Controller extends Component {
return this.$sce.trustAsHtml(getName(mod)); return this.$sce.trustAsHtml(getName(mod));
} }
moveModule(module, event, action) {
if (event.defaultPrevented) return;
event.preventDefault();
event.stopPropagation();
const params = {moduleName: module.name, action: action};
const query = `starredModules/setPosition`;
this.$http.post(query, params).then(res => {
if (res.data) {
module.position = res.data.movingModule.position;
this.modules.forEach(mod => {
if (mod.name == res.data.pushedModule.moduleFk)
mod.position = res.data.pushedModule.position;
});
this.vnApp.showSuccess(this.$t('Data saved!'));
this.countModules();
}
});
}
} }
Controller.$inject = ['$element', '$scope', 'vnModules', '$sce']; Controller.$inject = ['$element', '$scope', 'vnModules', '$sce'];

View File

@ -66,19 +66,21 @@ vn-home {
background-color: $color-button; background-color: $color-button;
color: $color-font-dark; color: $color-font-dark;
& .pin { & .small-icon {
display: inline-flex;
opacity: 0; opacity: 0;
flex-direction: row;
justify-content: left;
height: 20px;
width: 20px;
vn-icon { vn-icon {
margin: auto; margin: auto;
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
&:hover .pin { &:hover .small-icon {
flex-direction: row;
opacity: 1; opacity: 1;
}
& .hidden {
visibility: hidden;
} }
& > div { & > div {