4797-workerNotificationManager #107

Merged
alexm merged 9 commits from 4797-workerNotificationManager into dev 2023-11-10 09:39:53 +00:00
12 changed files with 224 additions and 298 deletions

View File

@ -13,5 +13,6 @@
],
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
},
"cSpell.words": ["axios"]
alexm marked this conversation as resolved
Review

Este archivo seguro que hay que subirlo?

Este archivo seguro que hay que subirlo?
Review

Creo que se puede poner a nivel de usuario.
Pero por si alguien mas lo usa. Que todos tengamos que darle la primera vez a que no te lo marque

Creo que se puede poner a nivel de usuario. Pero por si alguien mas lo usa. Que todos tengamos que darle la primera vez a que no te lo marque
Review

Creo que se puede poner a nivel de usuario.
Pero por si alguien mas lo usa. Que todos tengamos que darle la primera vez a que no te lo marque

Creo que se puede poner a nivel de usuario. Pero por si alguien mas lo usa. Que todos tengamos que darle la primera vez a que no te lo marque
}

View File

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

View File

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

View File

@ -249,9 +249,6 @@ function goToAction() {
.grid-style-transition {
transition: transform 0.28s, background-color 0.28s;
}
.maxwidth {
width: 100%;
}
</style>
<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>
import axios from 'axios';
import { useQuasar } from 'quasar';
import { computed, onMounted, onUpdated, ref } from 'vue';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import CrudModel from 'components/CrudModel.vue';
const $props = defineProps({
id: {
type: Number,
@ -12,131 +14,139 @@ const $props = defineProps({
default: null,
},
});
const entityId = computed(() => $props.id || route.params.id);
onMounted(() => fetch());
onUpdated(() => fetch());
const route = useRoute();
const { t } = useI18n();
const quasar = useQuasar();
const notifications = ref([]);
async function fetch() {
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'),
});
}
});
}
const entityId = computed(() => $props.id || route.params.id);
const URL_KEY = 'NotificationSubscriptions';
const active = ref();
const available = ref();
async function toggleNotification(notification) {
try {
if (!notification.active) {
await disableNotification(notification);
await axios.delete(`${URL_KEY}/${notification.id}`);
swapEntry(active.value, available.value, notification.notificationFk);
} else {
await axios
.post(`NotificationSubscriptions`, {
const { data } = await axios.post(URL_KEY, {
notificationFk: notification.notificationFk,
userFk: entityId.value,
})
.catch(() => (notification.active = false))
.then((res) => {
if (res.data) {
notification.id = res.data.id;
});
notification.id = data.id;
swapEntry(available.value, active.value, notification.notificationFk);
}
quasar.notify({
type: 'positive',
message: t('worker.notificationsManager.subscribed'),
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>
<template>
<QPage>
<QCard class="q-pa-md">
<QList>
<div
v-show="
notifications.filter(
(notification) => notification.active == true
).length
"
<CrudModel
auto-load
alexm marked this conversation as resolved
Review

NotificationSubscriptions aparece 4 veces en este fichero, como verias de hacer una const para que sólo esté 1 vez?

NotificationSubscriptions aparece 4 veces en este fichero, como verias de hacer una const para que sólo esté 1 vez?
:data-key="URL_KEY"
:url="`${URL_KEY}/${entityId}/getList`"
:default-reset="false"
:default-remove="false"
:default-save="false"
@on-fetch="setNotifications"
>
<QItemLabel header class="text-h6">
{{ t('worker.notificationsManager.activeNotifications') }}
</QItemLabel>
<QItem>
<template #body>
<div
v-for="notification in notifications.filter(
(notification) => notification.active == true
)"
:key="notification.id"
v-for="(notifications, index) in [
[...active.values()],
[...available.values()],
]"
:key="notifications"
>
<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"
<QList class="notificationList">
<TransitionGroup>
<QCard
v-for="notification in notifications"
:key="notification.notificationFk"
class="q-pa-md"
>
<QItem>
<QItemSection avatar>
<QBtn
round
icon="mail"
:color="notification.active ? 'green' : 'grey'"
/>
</QItemSection>
<QItemSection>
<QItemLabel>{{ notification.name }}</QItemLabel>
<QItemLabel caption>{{
notification.description
}}</QItemLabel>
<QItemLabel caption>
{{ notification.description }}
</QItemLabel>
</QItemSection>
<QItemSection side top>
<QToggle
checked-icon="check"
unchecked-icon="close"
indeterminate-icon="block"
v-model="notification.active"
color="green"
@update:model-value="toggleNotification(notification)"
/>
</QItemSection>
</QItem>
</div>
</div>
</QList>
</QCard>
</QPage>
</TransitionGroup>
</QList>
<QSeparator
color="primary"
class="q-my-lg"
v-if="!index && available.size && active.size"
/>
</div>
</template>
</CrudModel>
</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,
redirect: { name: 'CustomerMain' },
menus: {
main: ['CustomerList', 'CustomerPayments', 'CustomerCreate'],
main: ['CustomerList', 'CustomerPayments'],
card: ['CustomerBasicData'],
},
children: [
@ -27,7 +27,7 @@ export default {
title: 'list',
icon: 'view_list',
},
component: () => import('src/pages/Customer/CustomerList.vue')
component: () => import('src/pages/Customer/CustomerList.vue'),
},
{
path: 'payments',
@ -36,17 +36,7 @@ export default {
title: 'webPayments',
icon: 'vn:onlinepayment',
},
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'),
component: () => import('src/pages/Customer/CustomerPayments.vue'),
},
],
},
@ -63,7 +53,8 @@ export default {
title: 'summary',
icon: 'launch',
},
component: () => import('src/pages/Customer/Card/CustomerSummary.vue'),
component: () =>
import('src/pages/Customer/Card/CustomerSummary.vue'),
},
{
path: 'basic-data',
@ -72,7 +63,8 @@ export default {
title: 'basicData',
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' },
menus: {
main: ['WorkerList'],
// card: ['WorkerNotificationsManager'],
card: ['WorkerNotificationsManager'],
},
children: [
{
@ -46,15 +46,16 @@ export default {
},
component: () => import('src/pages/Worker/Card/WorkerSummary.vue'),
},
// {
// name: 'WorkerNotificationsManager',
// path: 'notifications',
// meta: {
// title: 'notifications',
// icon: 'notifications',
// },
// component: () => import('src/pages/Worker/Card/WorkerNotificationsManager.vue'),
// },
{
name: 'WorkerNotificationsManager',
path: 'notifications',
meta: {
title: 'notifications',
icon: 'notifications',
},
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(() => {
const workerId = 1110;
cy.viewport(1280, 720);
});
it('should throw an error if you try to change a notification that is not yours', () => {
cy.login('developer');
cy.visit(`/#/worker/${salesPersonId}/notifications`);
cy.get(firstAvailableNotification).click();
cy.notificationHas(
'.q-notification__message',
'The notification subscription of this worker cant be modified'
);
});
it('should active a notification that is yours', () => {
cy.login('developer');
cy.visit(`/#/worker/${developerId}/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);
});
});
it('should deactivate a notification that is yours', () => {
cy.login('developer');
cy.visit(`/#/worker/${developerId}/notifications`);
cy.waitForElement(activeList);
cy.waitForElement(availableList);
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/${workerId}/notifications`);
});
cy.visit(`/#/worker/${salesPersonId}/notifications`);
cy.waitForElement(activeList);
cy.waitForElement(availableList);
it('should unsubscribe 2 notifications, check the unsubscription has been saved, subscribe to other one and should check the data has been saved', () => {
cy.get('.q-chip').should('have.length', 3);
cy.get('.q-toggle__thumb').eq(0).click();
cy.get('.q-notification__message').should(
'have.text',
'Unsubscribed from the notification'
);
cy.get('.q-chip > .q-icon').eq(0).click();
cy.get(activeList)
.children()
.its('length')
.then((beforeSize) => {
cy.get(firstAvailableNotification).click();
cy.get(activeList)
.children()
.should('have.length', beforeSize + 1);
cy.reload();
cy.get('.q-chip').should('have.length', 1);
cy.get('.q-toggle__thumb').should('have.length', 3).eq(0).click();
cy.get('.q-notification__message').should(
'have.text',
'Subscribed to the notification'
);
cy.get('.q-toggle__thumb').should('have.length', 3).eq(1).click();
cy.get('.q-notification__message').should(
'have.text',
'Subscribed to the notification'
);
cy.reload();
cy.get('.q-chip').should('have.length', 3);
//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();
});
Cypress.Commands.add('notificationHas', (selector, text) => {
cy.get(selector).should('have.text', text);
});
Cypress.Commands.add('fillRow', (rowSelector, data) => {
// Usar el selector proporcionado para obtener la fila deseada
cy.waitForElement('tbody');

View File

@ -1,15 +1,15 @@
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 { ref } from 'vue';
describe('WorkerNotificationsManager', () => {
let vm;
const entityId = 1110;
beforeAll(() => {
vm = createWrapper(WorkerNotificationsManager, {
propsData: {
id: entityId,
global: {
stubs: ['CrudModel'],
},
}).vm;
});
@ -18,83 +18,16 @@ describe('WorkerNotificationsManager', () => {
vi.clearAllMocks();
});
describe('fetch()', () => {
it('should fetch notification subscriptions and role mappings', async () => {
vi.spyOn(axios, 'get')
.mockResolvedValueOnce({
data: [
{
id: 1,
name: 'Name 1',
description: 'Description 1',
notificationFk: 1,
active: true
},
],
});
await vm.fetch();
describe('swapEntry()', () => {
it('should swap notification', async () => {
const from = ref(new Map());
const to = ref(new Map());
from.value.set(1, { notificationFk: 1 });
to.value.set(2, { notificationFk: 2 });
expect(axios.get).toHaveBeenCalledWith(`NotificationSubscriptions/${entityId}/getList`);
expect(vm.notifications).toEqual([
{
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();
await vm.swapEntry(from.value, to.value, 1);
expect(to.value.size).toBe(2);
expect(from.value.size).toBe(0);
});
});
});