WIP: #8224 - ContextMenu #1035

Draft
wbuezas wants to merge 6 commits from wbuezas/salix-front-mindshore-fork2:8224-ContextMenu into dev
8 changed files with 384 additions and 16 deletions

View File

@ -15,6 +15,7 @@ import VnTableChip from 'components/VnTable/VnChip.vue';
import VnVisibleColumn from 'src/components/VnTable/VnVisibleColumn.vue';
import VnLv from 'components/ui/VnLv.vue';
import VnTableOrder from 'src/components/VnTable/VnOrder.vue';
import ContextMenu from 'src/components/common/ContextMenu.vue';
const $props = defineProps({
columns: {
@ -113,6 +114,10 @@ const $props = defineProps({
type: Boolean,
default: false,
},
contextMenuItems: {
type: Array,
default: () => [],
},
});
const { t } = useI18n();
const stateStore = useStateStore();
@ -544,6 +549,14 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
component-prop="columnField"
/>
</slot>
<ContextMenu
v-if="contextMenuItems && contextMenuItems.length > 0"
:data-key="$attrs['data-key']"
:table-row="row"
:table-col="col"
:expr-builder="$attrs['expr-builder']"
:context-menu-items="$props.contextMenuItems"
/>
</QTd>
</template>
<template #body-cell-tableActions="{ col, row }">
@ -774,6 +787,7 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
</FormModelPopup>
</QDialog>
</template>
<i18n>
en:
status: Status

View File

@ -0,0 +1,186 @@
<script setup>
import { computed, onMounted } from 'vue';
import { useArrayData } from 'composables/useArrayData';
import { buildFilter } from 'filters/filterPanel';
const $props = defineProps({
contextMenuItems: {
type: Array,
default: () => [],
},
tableRow: {
type: Object,
default: () => ({}),
},
dataKey: {
type: String,
default: '',
},
tableCol: {
type: Object,
default: () => ({}),
},
exprBuilder: {
type: Function,
default: null,
},
});
const arrayData = useArrayData($props.dataKey, {
exprBuilder: $props.exprBuilder,
url: 'Tickets/filter',
});
const store = arrayData.store;
const contextMenuProps = computed(() => $props.tableCol.contextMenuProps);
const isMenuEnabled = computed(() => contextMenuProps.value.menuEnabled);
const isFilterAllowed = computed(() => contextMenuProps.value.filterEnabled);
const isActionAllowed = computed(() => contextMenuProps.value.actionEnabled);
const fieldName = computed(() => $props.tableCol.name);
const fieldValue = computed(() => $props.tableRow[fieldName.value]);
const menuItems = computed(() => {
// If item does not have type, it will be displayed
return $props.contextMenuItems.filter((item) => {
return (
(item.type === 'filter' && isFilterAllowed.value) ||
(item.type === 'action' && isActionAllowed.value) ||
!item.type
);
});
});
/**
* Filter by current field selection
*/
const filterBySelection = () => {
const where = $props.exprBuilder
? $props.exprBuilder(fieldName.value, fieldValue.value)
: { [fieldName.value]: fieldValue.value };
arrayData.addFilterWhere(where);
};
/**
* Exclude by current field selection
*/
const excludeSelection = () => {
let where = { [fieldName.value]: { neq: fieldValue.value } };
if ($props.exprBuilder) {
where = { [fieldName.value]: fieldValue.value };
where = buildFilter(where, (param, value) => {
const expr = $props.exprBuilder(param, value);
const props = Object.keys(expr);
let newExpr = {};
for (let prop of props) {
if (expr[prop].like) {
const operator = expr[prop].like;
newExpr[prop] = { nlike: operator };
} else if (expr[prop].between) {
const operator = expr[prop].between;
newExpr = {
or: [
{ [prop]: { lt: operator[0] } },
{ [prop]: { gt: operator[1] } },
],
};
} else newExpr[prop] = { neq: fieldValue.value };
}
return newExpr;
});
}
const filter = { where };
arrayData.addFilter({ filter });
};
const removeFilter = () => {
const userFilter = store.userFilter;
const userParams = store.userParams;
const where = userFilter?.where;
let filterKey = fieldName.value;
if ($props.exprBuilder) {
const param = $props.exprBuilder(fieldName.value, null);
if (param) [filterKey] = Object.keys(param);
}
if (!where) return;
const whereKeys = Object.keys(where);
for (let key of whereKeys) {
removeProp(where, filterKey, key);
if (!Object.keys(where)) delete userFilter.where;
}
function removeProp(obj, targetProp, prop) {
if (prop == targetProp) delete obj[prop];
if (prop === 'and' || prop === 'or') {
const arrayCopy = obj[prop].slice();
for (let param of arrayCopy) {
const [key] = Object.keys(param);
const index = obj[prop].findIndex((param) => {
return Object.keys(param)[0] == key;
});
if (key == targetProp) obj[prop].splice(index, 1);
if (param[key] instanceof Array) removeProp(param, filterKey, key);
if (Object.keys(param).length == 0) obj[prop].splice(index, 1);
}
if (obj[prop].length == 0) delete obj[prop];
}
}
arrayData.applyFilter({ filter: userFilter, params: userParams });
};
/**
* Removes all applied filters
*/
const removeAllFilters = () => {
const filter = { where: null };
arrayData.applyFilter({ filter });
};
/**
* Copies the current field
* value to the clipboard
*/
const copyValue = () => {
if ($props.tableCol?.format)
navigator.clipboard.writeText($props.tableCol?.format($props.tableRow));
};
const menuActions = {
filterBySelection: () => filterBySelection(),
excludeSelection: () => excludeSelection(),
removeFilter: () => removeFilter(),
removeAllFilters: () => removeAllFilters(),
copyValue: () => copyValue(),
};
onMounted(() => {
console.log('row data', $props.tableRow);
console.log('col data', $props.tableCol);
console.log('test: ', $props.tableCol.format($props.tableRow));
});
</script>
<template>
<QMenu v-if="isMenuEnabled" touch-position context-menu v-bind="$attrs">
<QList dense style="min-width: 100px">
<QItem
v-for="(item, index) in menuItems"
:key="index"
clickable
v-close-popup
@click="menuActions[item.action]"
>
<QItemSection>{{ item.label }}</QItemSection>
</QItem>
</QList>
</QMenu>
</template>

View File

@ -1,9 +1,9 @@
import { useI18n } from 'vue-i18n';
import { i18n } from 'src/boot/i18n';
export default function (value, symbol = 'EUR', fractionSize = 2) {
if (value == null || value === '') value = 0;
const { locale } = useI18n();
const locale = i18n.global?.locale?.value;
const options = {
style: 'currency',

View File

@ -1,4 +1,4 @@
import { useI18n } from 'vue-i18n';
import { i18n } from 'src/boot/i18n';
export default function (value, options = {}) {
if (!value) return;
@ -9,8 +9,7 @@ export default function (value, options = {}) {
options.year = 'numeric';
}
const { locale } = useI18n();
const date = new Date(value);
const locale = i18n.global?.locale?.value;
return new Intl.DateTimeFormat(locale.value, options).format(date);
}

View File

@ -833,6 +833,12 @@ item:
specie: Specie
components:
topbar: {}
contextMenu:
filterBySelection: Filter by selection
excludeSelection: Exclude selection
removeFilter: Remove filter
removeAllFilters: Remove all filters
copyValue: Copy value
itemsFilterPanel:
typeFk: Type
value: Value

View File

@ -827,6 +827,12 @@ item:
concept: Concepto
components:
topbar: {}
contextMenu:
filterBySelection: Filtro por selección
excludeSelection: Excluir selección
removeFilter: Quitar filtro por selección
removeAllFilters: Eliminar todos los filtros
copyValue: Copiar valor
itemsFilterPanel:
typeFk: Tipo
value: Valor

View File

@ -47,7 +47,23 @@ const getGroupedStates = (data) => {
/>
<FetchData url="AgencyModes" @on-fetch="(data) => (agencies = data)" auto-load />
<FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load />
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
:hidden-tags="[
'ts.stateFk',
'c.salesPersonFk',
'a.provinceFk',
'z.hour',
't.shipped',
't.id',
't.refFk',
't.zoneFk',
't.nickname',
't.agencyModeFk',
't.warehouseFk',
]"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>

View File

@ -5,7 +5,7 @@ import { useRoute, useRouter } from 'vue-router';
import { useStateStore } from 'stores/useStateStore';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { toDate, toCurrency, dashIfEmpty } from 'src/filters/index';
import { toDate, toCurrency, dashIfEmpty, dateRange } from 'src/filters/index';
import useNotify from 'src/composables/useNotify';
import TicketSummary from './Card/TicketSummary.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
@ -46,12 +46,14 @@ const userParams = {
from: null,
to: null,
};
onMounted(() => {
initializeFromQuery();
stateStore.rightDrawer = true;
if (!route.query.createForm) return;
onClientSelected(JSON.parse(route.query.createForm));
});
const initializeFromQuery = () => {
const query = route.query.table ? JSON.parse(route.query.table) : {};
from.value = query.from || from.toISOString();
@ -67,6 +69,60 @@ const companiesOptions = ref([]);
const accountingOptions = ref([]);
const amountToReturn = ref();
const exprBuilder = (param, value) => {
switch (param) {
case 'stateFk':
return { 'ts.stateFk': value };
case 'salesPersonFk':
return { 'c.salesPersonFk': value };
case 'provinceFk':
return { 'a.provinceFk': value };
case 'hour':
return { 'z.hour': value };
case 'shipped':
return {
't.shipped': {
between: dateRange(value),
},
};
case 'id':
case 'refFk':
case 'zoneFk':
case 'nickname':
case 'agencyModeFk':
case 'warehouseFk':
return { [`t.${param}`]: value };
}
};
const contextMenuItems = [
Review

Estos valores son estaticos no? Quiero decir, son todas las opciones que tendrá este modulo
Bien, si ahora queremos esto mismo, hasta el mismo numero de elementos, habria que copiarlo, no?

Propongo, que si el conentido de esta variable es etatica, moverlo a un archivo js dentro de utils/filter o hermano de ContextMenu y desde ahi consumir

Estos valores son estaticos no? Quiero decir, son todas las opciones que tendrá este modulo Bien, si ahora queremos esto mismo, hasta el mismo numero de elementos, habria que copiarlo, no? Propongo, que si el conentido de esta variable es etatica, moverlo a un archivo js dentro de utils/filter o hermano de ContextMenu y desde ahi consumir
{
label: t('components.contextMenu.filterBySelection'),
action: 'filterBySelection',
type: 'filter',
},
{
label: t('components.contextMenu.excludeSelection'),
action: 'excludeSelection',
type: 'filter',
},
{
label: t('components.contextMenu.removeFilter'),
action: 'removeFilter',
type: 'filter',
},
{
label: t('components.contextMenu.removeAllFilters'),
action: 'removeAllFilters',
type: null,
},
{
label: t('components.contextMenu.copyValue'),
action: 'copyValue',
type: 'action',
},
];
const columns = computed(() => [
{
align: 'left',
@ -74,6 +130,11 @@ const columns = computed(() => [
hidden: true,
format: () => '',
columnClass: 'expand',
contextMenuProps: {
menuEnabled: true,
filterEnabled: false,
actionEnabled: false,
},
},
{
align: 'left',
@ -83,6 +144,12 @@ const columns = computed(() => [
condition: () => true,
},
isId: true,
contextMenuProps: {
menuEnabled: true,
filterEnabled: true,
actionEnabled: true,
},
format: (row) => row.id,
},
{
align: 'left',
@ -100,11 +167,17 @@ const columns = computed(() => [
component: null,
},
columnClass: 'expand',
format: (row, dashIfEmpty) => dashIfEmpty(row.salesPerson),
contextMenuProps: {
menuEnabled: true,
filterEnabled: true,
actionEnabled: true,
copyValueField: 'salesPerson',
},
format: (row) => dashIfEmpty(row.salesPerson),
},
{
align: 'left',
name: 'shippedDate',
name: 'shipped',
cardVisible: true,
label: t('ticketList.shipped'),
columnFilter: {
@ -112,37 +185,70 @@ const columns = computed(() => [
alias: 't',
inWhere: true,
},
contextMenuProps: {
menuEnabled: true,
filterEnabled: true,
actionEnabled: true,
},
format: ({ shippedDate }) => toDate(shippedDate),
},
{
align: 'left',
name: 'shipped',
name: 'shippedHour',
label: t('ticketList.hour'),
contextMenuProps: {
menuEnabled: true,
filterEnabled: false,
actionEnabled: false,
},
format: (row) => toTimeFormat(row.shipped),
},
{
align: 'left',
name: 'zoneLanding',
label: t('ticketList.closure'),
format: (row, dashIfEmpty) => dashIfEmpty(toTimeFormat(row.zoneLanding)),
contextMenuProps: {
menuEnabled: true,
filterEnabled: true,
actionEnabled: true,
},
format: (row) => dashIfEmpty(toTimeFormat(row.zoneLanding)),
},
{
align: 'left',
name: 'nickname',
label: t('ticketList.nickname'),
columnClass: 'expand',
contextMenuProps: {
menuEnabled: true,
filterEnabled: true,
actionEnabled: true,
},
format: (row) => row.nickname,
},
{
align: 'left',
name: 'addressNickname',
label: t('ticketList.addressNickname'),
columnClass: 'expand',
contextMenuProps: {
menuEnabled: true,
filterEnabled: false,
actionEnabled: true,
},
format: (row) => row.addressNickname,
},
{
align: 'left',
name: 'province',
name: 'provinceFk',
label: t('ticketList.province'),
columnClass: 'expand',
contextMenuProps: {
menuEnabled: true,
filterEnabled: true,
actionEnabled: true,
},
format: (row) => row.province,
},
{
align: 'left',
@ -157,6 +263,12 @@ const columns = computed(() => [
},
},
columnClass: 'expand',
contextMenuProps: {
menuEnabled: true,
filterEnabled: true,
actionEnabled: true,
},
format: (row) => row.state,
},
{
align: 'left',
@ -172,13 +284,24 @@ const columns = computed(() => [
inWhere: true,
},
columnClass: 'expand',
format: (row, dashIfEmpty) => dashIfEmpty(row.zoneName),
contextMenuProps: {
menuEnabled: true,
filterEnabled: true,
actionEnabled: true,
},
format: (row) => dashIfEmpty(row.zoneName),
},
{
align: 'left',
name: 'warehouse',
name: 'warehouseFk',
label: t('ticketList.warehouse'),
columnClass: 'expand',
contextMenuProps: {
menuEnabled: true,
filterEnabled: true,
actionEnabled: true,
},
format: (row) => row.warehouse,
},
{
align: 'left',
@ -189,13 +312,23 @@ const columns = computed(() => [
component: 'number',
inWhere: true,
},
contextMenuProps: {
menuEnabled: true,
filterEnabled: false,
actionEnabled: true,
},
format: (row) => toCurrency(row.totalWithVat),
},
{
align: 'left',
name: 'packing',
label: t('ticketSale.packaging'),
format: (row, dashIfEmpty) => dashIfEmpty(row.packing),
contextMenuProps: {
menuEnabled: true,
filterEnabled: false,
actionEnabled: false,
},
format: (row) => dashIfEmpty(row.packing),
},
{
align: 'right',
@ -221,6 +354,11 @@ const columns = computed(() => [
},
},
],
contextMenuProps: {
menuEnabled: false,
filterEnabled: false,
actionEnabled: false,
},
},
]);
function redirectToLines(id) {
@ -463,6 +601,7 @@ function setReference(data) {
<TicketFilter data-key="TicketList" />
</template>
</RightMenu>
<VnTable
ref="tableRef"
data-key="TicketList"
@ -485,6 +624,8 @@ function setReference(data) {
selection: 'multiple',
}"
data-cy="ticketListTable"
:context-menu-items="contextMenuItems"
:expr-builder="exprBuilder"
>
<template #column-statusIcons="{ row }">
<TicketProblems :row="row" />
@ -495,7 +636,7 @@ function setReference(data) {
<CustomerDescriptorProxy :id="row.salesPersonFk" />
</span>
</template>
<template #column-shippedDate="{ row }">
<template #column-shipped="{ row }">
<span v-if="getDateColor(row.shipped)">
<QChip :class="getDateColor(row.shipped)" dense square>
{{ toDate(row.shippedDate) }}