WIP: feat: refs #8463 cardDescriptorBeta #1291

Draft
alexm wants to merge 1 commits from 8463-CardDescriptor_useCard into dev
6 changed files with 358 additions and 64 deletions

View File

@ -1,5 +1,5 @@
<script setup>
import { onBeforeMount, computed } from 'vue';
import { onBeforeMount, computed, onMounted, watch, ref } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
@ -7,7 +7,10 @@ import useCardSize from 'src/composables/useCardSize';
import LeftMenu from 'components/LeftMenu.vue';
import VnSubToolbar from '../ui/VnSubToolbar.vue';
const emit = defineEmits(['onFetch']);
const props = defineProps({
id: { type: Number, required: false, default: null },
dataKey: { type: String, required: true },
baseUrl: { type: String, default: undefined },
customUrl: { type: String, default: undefined },
@ -18,52 +21,72 @@ const props = defineProps({
searchDataKey: { type: String, default: undefined },
searchbarProps: { type: Object, default: undefined },
redirectOnError: { type: Boolean, default: false },
visual: { type: Boolean, default: true },
});
const stateStore = useStateStore();
const route = useRoute();
const router = useRouter();
const arrayData = ref({});
const id = computed(() => props.id || route?.params?.id);
const url = computed(() => {
if (props.baseUrl) {
return `${props.baseUrl}/${route.params.id}`;
return `${props.baseUrl}/${id.value}`;
}
return props.customUrl;
});
const arrayData = useArrayData(props.dataKey, {
url: url.value,
filter: props.filter,
userFilter: props.userFilter,
});
onBeforeMount(async () => {
console.log('asd', id.value);
arrayData.value = useArrayData(props.dataKey);
if (!arrayData.value.store.data && !arrayData.value.isLoading.value) {
arrayData.value = useArrayData(props.dataKey, {
url: url.value,
filter: props.filter,
userFilter: props.userFilter,
});
}
if (props.baseUrl && props.visual) {
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
arrayData.value.store.url = `${props.baseUrl}/${to.params.id}`;
await fetch('router');
}
});
}
try {
if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id };
await arrayData.fetch({ append: false, updateRouter: false });
if (!props.baseUrl) arrayData.value.store.filter.where = { id: id.value };
await fetch('montar');
} catch {
if (!props.visual) return;
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
router.push({ path: path.replace(/:id.*/, '') });
}
});
if (props.baseUrl) {
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
arrayData.store.url = `${props.baseUrl}/${to.params.id}`;
await arrayData.fetch({ append: false, updateRouter: false });
}
});
watch(
() => arrayData?.value?.isLoading,
(loading) => !loading && emit('onFetch', arrayData.value.store.data),
Review

Con este watcher hacemos que cuando el card cambie de estado, emita los datos.

Con este watcher hacemos que cuando el card cambie de estado, emita los datos.
);
async function fetch() {
await arrayData.value.fetch({ append: false, updateRouter: false });
}
</script>
<template>
<Teleport to="#left-panel" v-if="stateStore.isHeaderMounted()">
<component :is="descriptor" />
<QSeparator />
<LeftMenu source="card" />
</Teleport>
<VnSubToolbar />
<div :class="[useCardSize(), $attrs.class]">
<RouterView :key="route.path" />
</div>
<span v-if="visual">
Review

No me acaba este enfoque, pero he visto y probado que con Vue no se puede acceder a funciones de un componente sin montarlo.
Seria esta opcion o dividir la logica del template...

No me acaba este enfoque, pero he visto y probado que con Vue no se puede acceder a funciones de un componente sin montarlo. Seria esta opcion o dividir la logica del template...
<Teleport to="#left-panel" v-if="stateStore.isHeaderMounted()">
<component :is="descriptor" />
<QSeparator />
<LeftMenu source="card" />
</Teleport>
<VnSubToolbar />
<div :class="[useCardSize(), $attrs.class]">
<RouterView :key="route?.path" />
</div>
</span>
</template>

View File

@ -0,0 +1,262 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useRoute } from 'vue-router';
import VnMoreOptions from './VnMoreOptions.vue';
const $props = defineProps({
id: {
type: Number,
default: false,
},
title: {
type: String,
default: '',
},
subtitle: {
type: Number,
default: null,
},
module: {
type: String,
default: null,
},
summary: {
type: Object,
default: null,
},
card: {
type: Object,
required: true,
},
width: {
type: String,
default: 'md-width',
},
});
const route = useRoute();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const entity = ref({});
const isLoading = ref(false);
const emit = defineEmits(['onFetch']);
function getValueFromPath(path) {
if (!path) return;
const keys = path.toString().split('.');
let current = entity.value;
for (const key of keys) {
if (current[key] === undefined) return undefined;
else current = current[key];
}
return current;
}
const iconModule = computed(() => route.matched[1].meta.icon);
const toModule = computed(() =>
route.matched[1].path.split('/').length > 2
? route.matched[1].redirect
: route.matched[1].children[0].redirect,
);
function setData(data) {
const newData = (Array.isArray(data) ? data[0] : data) ?? {};
entity.value = newData;
isLoading.value = false;
if (newData) emit('onFetch', newData);
}
</script>
<template>
{{ id }}
<component
Review

Usamos el card, pero pasandole visual a false

Usamos el card, pero pasandole visual a `false`
:is="card"
:id
:visual="false"
@on-fetch="(data) => setData(data)"
v-bind="$attrs"
/>
<div class="descriptor">
<template v-if="entity && !isLoading">
<div class="header bg-primary q-pa-sm justify-between">
<slot name="header-extra-action"
><QBtn
round
flat
dense
size="md"
:icon="iconModule"
color="white"
class="link"
:to="$attrs['to-module'] ?? toModule"
>
<QTooltip>
{{ t('globals.goToModuleIndex') }}
</QTooltip>
</QBtn></slot
>
<QBtn
@click.stop="viewSummary(entity.id, $props.summary, $props.width)"
round
flat
dense
size="md"
icon="preview"
color="white"
class="link"
v-if="summary"
>
<QTooltip>
{{ t('components.smartCard.openSummary') }}
</QTooltip>
</QBtn>
<RouterLink :to="{ name: `${module}Summary`, params: { id: entity.id } }">
<QBtn
class="link"
color="white"
dense
flat
icon="launch"
round
size="md"
>
<QTooltip>
{{ t('components.cardDescriptor.summary') }}
</QTooltip>
</QBtn>
</RouterLink>
<VnMoreOptions v-if="$slots.menu">
<template #menu="{ menuRef }">
<slot name="menu" :entity="entity" :menu-ref="menuRef" />
</template>
</VnMoreOptions>
</div>
<slot name="before" />
<div class="body q-py-sm">
<QList dense>
<QItemLabel header class="ellipsis text-h5" :lines="1">
<div class="title">
<span v-if="$props.title" :title="getValueFromPath(title)">
{{ getValueFromPath(title) ?? $props.title }}
</span>
<slot v-else name="description" :entity="entity">
<span :title="entity.name">
{{ entity.name }}
</span>
</slot>
</div>
</QItemLabel>
<QItem dense>
<QItemLabel class="subtitle" caption>
#{{ getValueFromPath(subtitle) ?? entity.id }}
</QItemLabel>
</QItem>
</QList>
<div class="list-box q-mt-xs">
<slot name="body" :entity="entity" />
</div>
</div>
<div class="icons">
<slot name="icons" :entity="entity" />
</div>
<div class="actions justify-center">
<slot name="actions" :entity="entity" />
</div>
<slot name="after" />
</template>
<!-- Skeleton -->
<SkeletonDescriptor v-if="!entity || isLoading" />
</div>
<QInnerLoading
:label="t('globals.pleaseWait')"
:showing="isLoading"
color="primary"
/>
</template>
<style lang="scss">
.body {
background-color: var(--vn-section-color);
.text-h5 {
font-size: 20px;
padding-top: 5px;
padding-bottom: 0px;
}
.q-item {
min-height: 20px;
.link {
margin-left: 10px;
}
}
.vn-label-value {
display: flex;
padding: 0px 16px;
.label {
color: var(--vn-label-color);
font-size: 14px;
&:not(:has(a))::after {
content: ':';
}
}
.value {
color: var(--vn-text-color);
font-size: 14px;
margin-left: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
.info {
margin-left: 5px;
}
}
}
</style>
<style lang="scss" scoped>
.title {
overflow: hidden;
text-overflow: ellipsis;
span {
color: var(--vn-text-color);
font-weight: bold;
}
}
.subtitle {
color: var(--vn-text-color);
font-size: 16px;
margin-bottom: 2px;
}
.list-box {
.q-item__label {
color: var(--vn-label-color);
padding-bottom: 0%;
}
}
.descriptor {
width: 256px;
.header {
display: flex;
align-items: center;
}
.icons {
margin: 0 10px;
display: flex;
justify-content: center;
.q-icon {
margin-right: 5px;
}
}
.actions {
margin: 0 5px;
justify-content: center !important;
}
}
</style>

View File

@ -1,12 +1,42 @@
<script setup>
import VnCardBeta from 'components/common/VnCardBeta.vue';
import OrderDescriptor from 'pages/Order/Card/OrderDescriptor.vue';
const userFilter = {
include: [
{ relation: 'agencyMode', scope: { fields: ['name'] } },
{
relation: 'address',
scope: { fields: ['nickname'] },
},
{ relation: 'rows', scope: { fields: ['id'] } },
{
relation: 'client',
scope: {
fields: [
'salesPersonFk',
'name',
'isActive',
'isFreezed',
'isTaxDataChecked',
],
include: {
relation: 'salesPersonUser',
scope: { fields: ['id', 'name'] },
},
},
},
],
};
</script>
<template>
<VnCardBeta
Review

Ahora el card hace la peticion mas reusable para los modulos

Ahora el card hace la peticion mas reusable para los modulos
data-key="Order"
base-url="Orders"
:userFilter
:descriptor="OrderDescriptor"
v-bind="$attrs"
v-on="$attrs"
/>
</template>

View File

@ -6,10 +6,11 @@ import { toCurrency, toDate } from 'src/filters';
import { useState } from 'src/composables/useState';
import useCardDescription from 'src/composables/useCardDescription';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import FetchData from 'components/FetchData.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import OrderCard from './OrderCard.vue';
import CardDescriptorBeta from 'src/components/ui/CardDescriptorBeta.vue';
const DEFAULT_ITEMS = 0;
@ -26,37 +27,13 @@ const state = useState();
const { t } = useI18n();
const data = ref(useCardDescription());
const getTotalRef = ref();
const total = ref(0);
const entityId = computed(() => {
return $props.id || route.params.id;
});
const filter = {
include: [
{ relation: 'agencyMode', scope: { fields: ['name'] } },
{
relation: 'address',
scope: { fields: ['nickname'] },
},
{ relation: 'rows', scope: { fields: ['id'] } },
{
relation: 'client',
scope: {
fields: [
'salesPersonFk',
'name',
'isActive',
'isFreezed',
'isTaxDataChecked',
],
include: {
relation: 'salesPersonUser',
scope: { fields: ['id', 'name'] },
},
},
},
],
};
const orderTotal = computed(() => state.get('orderTotal') ?? 0);
const setData = (entity) => {
if (!entity) return;
@ -68,9 +45,6 @@ const setData = (entity) => {
const getConfirmationValue = (isConfirmed) => {
return t(isConfirmed ? 'globals.confirmed' : 'order.summary.notConfirmed');
};
const orderTotal = computed(() => state.get('orderTotal') ?? 0);
const total = ref(0);
</script>
<template>
@ -83,15 +57,14 @@ const total = ref(0);
}
"
/>
<CardDescriptor
ref="descriptor"
:url="`Orders/${entityId}`"
:filter="filter"
<CardDescriptorBeta
v-bind="$attrs"
:id
:card="OrderCard"
module="Order"
:title="data.title"
:subtitle="data.subtitle"
@on-fetch="setData"
data-key="orderData"
>
<template #body="{ entity }">
<VnLv
@ -142,5 +115,5 @@ const total = ref(0);
</QBtn>
</QCardActions>
</template>
</CardDescriptor>
</CardDescriptorBeta>
</template>

View File

@ -12,6 +12,11 @@ const $props = defineProps({
<template>
<QPopupProxy>
<OrderDescriptor v-if="$props.id" :id="$props.id" :summary="OrderSummary" />
<OrderDescriptor
v-if="$props.id"
:id="$props.id"
:summary="OrderSummary"
data-key="orderDescriptor"
Review

hay que cambiar el dataKey cuando es popup. sino si estas en ticket y abres un ticketDescriptor, te usara el mismo arrayData para los dos

hay que cambiar el dataKey cuando es popup. sino si estas en ticket y abres un ticketDescriptor, te usara el mismo arrayData para los dos
/>
</QPopupProxy>
</template>

View File

@ -13,6 +13,7 @@ import FetchedTags from 'components/ui/FetchedTags.vue';
import VnTitle from 'src/components/common/VnTitle.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import OrderDescriptorMenu from 'pages/Order/Card/OrderDescriptorMenu.vue';
import OrderDescriptorProxy from './OrderDescriptorProxy.vue';
const { t } = useI18n();
const route = useRoute();
@ -106,7 +107,7 @@ async function handleConfirm() {
<template #value>
<span class="link">
{{ dashIfEmpty(entity?.address?.nickname) }}
<CustomerDescriptorProxy :id="entity?.clientFk" />
<OrderDescriptorProxy :id="1" />
Review

Lo he puesto para probar el funcionamiento en modo popup
Habria que quitarlo

Lo he puesto para probar el funcionamiento en modo popup Habria que quitarlo
</span>
</template>
</VnLv>