0
0
Fork 0

Merge pull request '4797-workerNotificationManager' (!107) from 4797-workerNotificationManager into dev

Reviewed-on: verdnatura/salix-front#107
Reviewed-by: Javi Gallego <jgallego@verdnatura.es>
This commit is contained in:
Alex Moreno 2023-11-10 09:39:52 +00:00
commit 222a970a26
12 changed files with 224 additions and 298 deletions

View File

@ -13,5 +13,6 @@
], ],
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
} },
"cSpell.words": ["axios"]
} }

View File

@ -15,7 +15,7 @@
"test:unit:ci": "vitest run" "test:unit:ci": "vitest run"
}, },
"dependencies": { "dependencies": {
"@quasar/cli": "^2.2.1", "@quasar/cli": "^2.3.0",
"@quasar/extras": "^1.16.4", "@quasar/extras": "^1.16.4",
"axios": "^1.4.0", "axios": "^1.4.0",
"chromium": "^3.0.3", "chromium": "^3.0.3",

View File

@ -89,6 +89,7 @@ async function fetch(data) {
watch(formData, () => (hasChanges.value = true), { deep: true }); watch(formData, () => (hasChanges.value = true), { deep: true });
emit('onFetch', data); emit('onFetch', data);
return data;
} }
async function reset() { async function reset() {

View File

@ -249,9 +249,6 @@ function goToAction() {
.grid-style-transition { .grid-style-transition {
transition: transform 0.28s, background-color 0.28s; transition: transform 0.28s, background-color 0.28s;
} }
.maxwidth {
width: 100%;
}
</style> </style>
<i18n> <i18n>

View File

@ -1,56 +0,0 @@
<script setup>
import { reactive, watch } from 'vue';
const customer = reactive({
name: '',
});
watch(() => customer.name);
</script>
<template>
<QPage class="q-pa-md">
<QCard class="q-pa-md">
<QForm @submit="onSubmit" @reset="onReset" class="q-gutter-md">
<QInput
filled
v-model="customer.name"
label="Your name *"
hint="Name and surname"
lazy-rules
:rules="[(val) => (val && val.length > 0) || 'Please type something']"
/>
<QInput
filled
type="number"
v-model="age"
label="Your age *"
lazy-rules
:rules="[
(val) => (val !== null && val !== '') || 'Please type your age',
(val) => (val > 0 && val < 100) || 'Please type a real age',
]"
/>
<div>
<QBtn label="Submit" type="submit" color="primary" />
<QBtn
label="Reset"
type="reset"
color="primary"
flat
class="q-ml-sm"
/>
</div>
</QForm>
</QCard>
</QPage>
</template>
<style lang="scss" scoped>
.card {
width: 100%;
max-width: 60em;
}
</style>

View File

@ -1,10 +1,12 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { computed, onMounted, onUpdated, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import CrudModel from 'components/CrudModel.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
type: Number, type: Number,
@ -12,131 +14,139 @@ const $props = defineProps({
default: null, default: null,
}, },
}); });
const entityId = computed(() => $props.id || route.params.id);
onMounted(() => fetch());
onUpdated(() => fetch());
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
const entityId = computed(() => $props.id || route.params.id);
const notifications = ref([]); const URL_KEY = 'NotificationSubscriptions';
const active = ref();
async function fetch() { const available = ref();
try {
await axios
.get(`NotificationSubscriptions/${entityId.value}/getList`)
.then(async (res) => {
if (res.data) {
notifications.value = res.data;
}
});
} catch (e) {
//
}
}
async function disableNotification(notification) {
await axios
.delete(`NotificationSubscriptions/${notification.id}`)
.catch(() => (notification.active = true))
.then((res) => {
if (res.data) {
notification.id = null;
notification.active = false;
quasar.notify({
type: 'positive',
message: t('worker.notificationsManager.unsubscribed'),
});
}
});
}
async function toggleNotification(notification) { async function toggleNotification(notification) {
if (!notification.active) { try {
await disableNotification(notification); if (!notification.active) {
} else { await axios.delete(`${URL_KEY}/${notification.id}`);
await axios swapEntry(active.value, available.value, notification.notificationFk);
.post(`NotificationSubscriptions`, { } else {
const { data } = await axios.post(URL_KEY, {
notificationFk: notification.notificationFk, notificationFk: notification.notificationFk,
userFk: entityId.value, userFk: entityId.value,
})
.catch(() => (notification.active = false))
.then((res) => {
if (res.data) {
notification.id = res.data.id;
quasar.notify({
type: 'positive',
message: t('worker.notificationsManager.subscribed'),
});
}
}); });
notification.id = data.id;
swapEntry(available.value, active.value, notification.notificationFk);
}
quasar.notify({
type: 'positive',
message: t(
`worker.notificationsManager.${notification.active ? '' : 'un'}subscribed`
),
});
} catch {
notification.active = !notification.active;
} }
} }
const swapEntry = (from, to, key) => {
const element = from.get(key);
to.set(key, element);
from.delete(key);
};
function setNotifications(data) {
active.value = new Map(data.active);
available.value = new Map(data.available);
}
</script> </script>
<template> <template>
<QPage> <CrudModel
<QCard class="q-pa-md"> auto-load
<QList> :data-key="URL_KEY"
<div :url="`${URL_KEY}/${entityId}/getList`"
v-show=" :default-reset="false"
notifications.filter( :default-remove="false"
(notification) => notification.active == true :default-save="false"
).length @on-fetch="setNotifications"
" >
> <template #body>
<QItemLabel header class="text-h6"> <div
{{ t('worker.notificationsManager.activeNotifications') }} v-for="(notifications, index) in [
</QItemLabel> [...active.values()],
<QItem> [...available.values()],
<div ]"
v-for="notification in notifications.filter( :key="notifications"
(notification) => notification.active == true >
)" <QList class="notificationList">
:key="notification.id" <TransitionGroup>
> <QCard
<QChip
:key="notification.id"
:label="notification.name"
text-color="white"
color="primary"
class="q-mr-sm"
removable
@remove="disableNotification(notification)"
/>
</div>
</QItem>
</div>
<div v-show="notifications.length">
<QItemLabel header class="text-h6">
{{ t('worker.notificationsManager.availableNotifications') }}
</QItemLabel>
<div class="row">
<QItem
class="col-3"
:key="notification.notificationFk"
v-for="notification in notifications" v-for="notification in notifications"
:key="notification.notificationFk"
class="q-pa-md"
> >
<QItemSection> <QItem>
<QItemLabel>{{ notification.name }}</QItemLabel> <QItemSection avatar>
<QItemLabel caption>{{ <QBtn
notification.description round
}}</QItemLabel> icon="mail"
</QItemSection> :color="notification.active ? 'green' : 'grey'"
<QItemSection side top> />
</QItemSection>
<QItemSection>
<QItemLabel>{{ notification.name }}</QItemLabel>
<QItemLabel caption>
{{ notification.description }}
</QItemLabel>
</QItemSection>
<QToggle <QToggle
checked-icon="check" checked-icon="check"
unchecked-icon="close" unchecked-icon="close"
indeterminate-icon="block"
v-model="notification.active" v-model="notification.active"
color="green"
@update:model-value="toggleNotification(notification)" @update:model-value="toggleNotification(notification)"
/> />
</QItemSection> </QItem>
</QItem> </QCard>
</div> </TransitionGroup>
</div> </QList>
</QList> <QSeparator
</QCard> color="primary"
</QPage> class="q-my-lg"
v-if="!index && available.size && active.size"
/>
</div>
</template>
</CrudModel>
</template> </template>
<style lang="scss" scoped>
.notificationList {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 10px;
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
}
@media (max-width: $breakpoint-md) {
.notificationList {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: $breakpoint-xs) {
.notificationList {
grid-template-columns: repeat(1, 1fr);
}
}
</style>

View File

@ -10,7 +10,7 @@ export default {
component: RouterView, component: RouterView,
redirect: { name: 'CustomerMain' }, redirect: { name: 'CustomerMain' },
menus: { menus: {
main: ['CustomerList', 'CustomerPayments', 'CustomerCreate'], main: ['CustomerList', 'CustomerPayments'],
card: ['CustomerBasicData'], card: ['CustomerBasicData'],
}, },
children: [ children: [
@ -27,7 +27,7 @@ export default {
title: 'list', title: 'list',
icon: 'view_list', icon: 'view_list',
}, },
component: () => import('src/pages/Customer/CustomerList.vue') component: () => import('src/pages/Customer/CustomerList.vue'),
}, },
{ {
path: 'payments', path: 'payments',
@ -36,17 +36,7 @@ export default {
title: 'webPayments', title: 'webPayments',
icon: 'vn:onlinepayment', icon: 'vn:onlinepayment',
}, },
component: () => import('src/pages/Customer/CustomerPayments.vue') component: () => import('src/pages/Customer/CustomerPayments.vue'),
},
{
path: 'create',
name: 'CustomerCreate',
meta: {
title: 'createCustomer',
icon: 'vn:addperson',
roles: ['developer'],
},
component: () => import('src/pages/Customer/CustomerCreate.vue'),
}, },
], ],
}, },
@ -63,7 +53,8 @@ export default {
title: 'summary', title: 'summary',
icon: 'launch', icon: 'launch',
}, },
component: () => import('src/pages/Customer/Card/CustomerSummary.vue'), component: () =>
import('src/pages/Customer/Card/CustomerSummary.vue'),
}, },
{ {
path: 'basic-data', path: 'basic-data',
@ -72,7 +63,8 @@ export default {
title: 'basicData', title: 'basicData',
icon: 'vn:settings', icon: 'vn:settings',
}, },
component: () => import('src/pages/Customer/Card/CustomerBasicData.vue'), component: () =>
import('src/pages/Customer/Card/CustomerBasicData.vue'),
}, },
], ],
}, },

View File

@ -11,7 +11,7 @@ export default {
redirect: { name: 'WorkerMain' }, redirect: { name: 'WorkerMain' },
menus: { menus: {
main: ['WorkerList'], main: ['WorkerList'],
// card: ['WorkerNotificationsManager'], card: ['WorkerNotificationsManager'],
}, },
children: [ children: [
{ {
@ -46,15 +46,16 @@ export default {
}, },
component: () => import('src/pages/Worker/Card/WorkerSummary.vue'), component: () => import('src/pages/Worker/Card/WorkerSummary.vue'),
}, },
// { {
// name: 'WorkerNotificationsManager', name: 'WorkerNotificationsManager',
// path: 'notifications', path: 'notifications',
// meta: { meta: {
// title: 'notifications', title: 'notifications',
// icon: 'notifications', icon: 'notifications',
// }, },
// component: () => import('src/pages/Worker/Card/WorkerNotificationsManager.vue'), component: () =>
// }, import('src/pages/Worker/Card/WorkerNotificationsManager.vue'),
},
], ],
}, },
], ],

View File

@ -1,36 +1,79 @@
xdescribe('WorkerNotificationsManager', () => { describe('WorkerNotificationsManager', () => {
const salesPersonId = 18;
const developerId = 9;
const activeList = ':nth-child(1) > .q-list';
const availableList = ':nth-child(2) > .q-list';
const firstActiveNotification =
':nth-child(1) > .q-list > :nth-child(1) > .q-item > .q-toggle > .q-toggle__inner';
const firstAvailableNotification =
':nth-child(2) > .q-list > :nth-child(1) > .q-item > .q-toggle > .q-toggle__inner';
beforeEach(() => { beforeEach(() => {
const workerId = 1110;
cy.viewport(1280, 720); cy.viewport(1280, 720);
cy.login('salesBoss');
cy.visit(`/#/worker/${workerId}/notifications`);
}); });
it('should unsubscribe 2 notifications, check the unsubscription has been saved, subscribe to other one and should check the data has been saved', () => { it('should throw an error if you try to change a notification that is not yours', () => {
cy.get('.q-chip').should('have.length', 3); cy.login('developer');
cy.get('.q-toggle__thumb').eq(0).click(); cy.visit(`/#/worker/${salesPersonId}/notifications`);
cy.get('.q-notification__message').should( cy.get(firstAvailableNotification).click();
'have.text', cy.notificationHas(
'Unsubscribed from the notification' '.q-notification__message',
'The notification subscription of this worker cant be modified'
); );
cy.get('.q-chip > .q-icon').eq(0).click(); });
cy.reload(); it('should active a notification that is yours', () => {
cy.login('developer');
cy.visit(`/#/worker/${developerId}/notifications`);
cy.waitForElement(activeList);
cy.waitForElement(availableList);
cy.get('.q-chip').should('have.length', 1); cy.get(activeList)
cy.get('.q-toggle__thumb').should('have.length', 3).eq(0).click(); .children()
cy.get('.q-notification__message').should( .its('length')
'have.text', .then((beforeSize) => {
'Subscribed to the notification' cy.get(firstAvailableNotification).click();
); cy.get(activeList)
cy.get('.q-toggle__thumb').should('have.length', 3).eq(1).click(); .children()
cy.get('.q-notification__message').should( .should('have.length', beforeSize + 1);
'have.text', });
'Subscribed to the notification' });
);
cy.reload(); it('should deactivate a notification that is yours', () => {
cy.login('developer');
cy.visit(`/#/worker/${developerId}/notifications`);
cy.waitForElement(activeList);
cy.waitForElement(availableList);
cy.get('.q-chip').should('have.length', 3); cy.get(availableList)
.children()
.its('length')
.then((beforeSize) => {
cy.get(firstActiveNotification).click();
cy.get(availableList)
.children()
.should('have.length', beforeSize + 1);
});
});
it('should active a notification if you are their boss', () => {
cy.login('salesBoss');
cy.visit(`/#/worker/${salesPersonId}/notifications`);
cy.waitForElement(activeList);
cy.waitForElement(availableList);
cy.get(activeList)
.children()
.its('length')
.then((beforeSize) => {
cy.get(firstAvailableNotification).click();
cy.get(activeList)
.children()
.should('have.length', beforeSize + 1);
//Rollback
cy.get(firstActiveNotification).click();
});
}); });
}); });

View File

@ -91,6 +91,10 @@ Cypress.Commands.add('clickConfirm', () => {
cy.get('.q-btn--unelevated > .q-btn__content > .block').click(); cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
}); });
Cypress.Commands.add('notificationHas', (selector, text) => {
cy.get(selector).should('have.text', text);
});
Cypress.Commands.add('fillRow', (rowSelector, data) => { Cypress.Commands.add('fillRow', (rowSelector, data) => {
// Usar el selector proporcionado para obtener la fila deseada // Usar el selector proporcionado para obtener la fila deseada
cy.waitForElement('tbody'); cy.waitForElement('tbody');

View File

@ -1,15 +1,15 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper'; import { createWrapper } from 'app/test/vitest/helper';
import WorkerNotificationsManager from 'src/pages/Worker/Card/WorkerNotificationsManager.vue'; import WorkerNotificationsManager from 'src/pages/Worker/Card/WorkerNotificationsManager.vue';
import { ref } from 'vue';
describe('WorkerNotificationsManager', () => { describe('WorkerNotificationsManager', () => {
let vm; let vm;
const entityId = 1110;
beforeAll(() => { beforeAll(() => {
vm = createWrapper(WorkerNotificationsManager, { vm = createWrapper(WorkerNotificationsManager, {
propsData: { global: {
id: entityId, stubs: ['CrudModel'],
}, },
}).vm; }).vm;
}); });
@ -18,83 +18,16 @@ describe('WorkerNotificationsManager', () => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
describe('fetch()', () => { describe('swapEntry()', () => {
it('should fetch notification subscriptions and role mappings', async () => { it('should swap notification', async () => {
vi.spyOn(axios, 'get') const from = ref(new Map());
.mockResolvedValueOnce({ const to = ref(new Map());
data: [ from.value.set(1, { notificationFk: 1 });
{ to.value.set(2, { notificationFk: 2 });
id: 1,
name: 'Name 1',
description: 'Description 1',
notificationFk: 1,
active: true
},
],
});
await vm.fetch();
expect(axios.get).toHaveBeenCalledWith(`NotificationSubscriptions/${entityId}/getList`); await vm.swapEntry(from.value, to.value, 1);
expect(vm.notifications).toEqual([ expect(to.value.size).toBe(2);
{ expect(from.value.size).toBe(0);
id: 1,
notificationFk: 1,
name: 'Name 1',
description: 'Description 1',
active: true,
},
]);
});
});
describe('disableNotification()', () => {
it('should disable the notification', async () => {
vi.spyOn(axios, 'delete').mockResolvedValue({ data: { count: 1 } });
vi.spyOn(vm.quasar, 'notify');
const subscriptionId = 1;
vm.notifications = [{ id: 1, active: true }];
await vm.disableNotification(vm.notifications[0]);
expect(axios.delete).toHaveBeenCalledWith(
`NotificationSubscriptions/${subscriptionId}`
);
expect(vm.notifications[0].id).toBeNull();
expect(vm.notifications[0].id).toBeFalsy();
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'positive' })
);
});
});
describe('toggleNotification()', () => {
it('should activate the notification', async () => {
vi.spyOn(axios, 'post').mockResolvedValue({
data: { id: 1, notificationFk: 1 },
});
vm.notifications = [{ id: null, active: true, notificationFk: 1 }];
await vm.toggleNotification(vm.notifications[0]);
expect(axios.post).toHaveBeenCalledWith('NotificationSubscriptions', {
notificationFk: 1,
userFk: entityId,
});
expect(vm.notifications[0].id).toBe(1);
expect(vm.notifications[0].active).toBeTruthy();
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'positive' })
);
});
it('should disable the notification', async () => {
vi.spyOn(vm, 'disableNotification');
vm.notifications = [{ id: 1, active: false, notificationFk: 1 }];
await vm.toggleNotification(vm.notifications[0]);
expect(vm.notifications[0].id).toBe(null);
expect(vm.notifications[0].active).toBeFalsy();
}); });
}); });
}); });