926 lines
31 KiB
Vue
926 lines
31 KiB
Vue
<script setup>
|
|
import { ref, onBeforeMount, onMounted, computed, watch } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useQuasar } from 'quasar';
|
|
import { useStateStore } from 'stores/useStateStore';
|
|
|
|
import CrudModel from 'src/components/CrudModel.vue';
|
|
import FormModelPopup from 'components/FormModelPopup.vue';
|
|
|
|
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
|
|
import VnTableColumn from 'components/VnTable/VnColumn.vue';
|
|
import VnFilter from 'components/VnTable/VnFilter.vue';
|
|
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';
|
|
|
|
const $props = defineProps({
|
|
columns: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
defaultMode: {
|
|
type: String,
|
|
default: 'table', // 'table', 'card'
|
|
},
|
|
columnSearch: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
rightSearch: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
rowClick: {
|
|
type: [Function, Boolean],
|
|
default: null,
|
|
},
|
|
rowCtrlClick: {
|
|
type: [Function, Boolean],
|
|
default: null,
|
|
},
|
|
redirect: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
create: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
createAsDialog: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
bottom: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
cardClass: {
|
|
type: String,
|
|
default: 'flex-one',
|
|
},
|
|
searchUrl: {
|
|
type: [String, Boolean],
|
|
default: 'table',
|
|
},
|
|
isEditable: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
useModel: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
hasSubToolbar: {
|
|
type: Boolean,
|
|
default: null,
|
|
},
|
|
disableOption: {
|
|
type: Object,
|
|
default: () => ({ card: false, table: false }),
|
|
},
|
|
withoutHeader: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
tableCode: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
table: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
crudModel: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
tableHeight: {
|
|
type: String,
|
|
default: '90vh',
|
|
},
|
|
chipLocale: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
footer: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
disabledAttr: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
});
|
|
const { t } = useI18n();
|
|
const stateStore = useStateStore();
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const quasar = useQuasar();
|
|
|
|
const CARD_MODE = 'card';
|
|
const TABLE_MODE = 'table';
|
|
const mode = ref(CARD_MODE);
|
|
const selected = ref([]);
|
|
const hasParams = ref(false);
|
|
const routeQuery = JSON.parse(route?.query[$props.searchUrl] ?? '{}');
|
|
const params = ref({ ...routeQuery, ...routeQuery.filter?.where });
|
|
const orders = ref(parseOrder(routeQuery.filter?.order));
|
|
const CrudModelRef = ref({});
|
|
const showForm = ref(false);
|
|
const splittedColumns = ref({ columns: [] });
|
|
const columnsVisibilitySkipped = ref();
|
|
const createForm = ref();
|
|
const tableFilterRef = ref([]);
|
|
const tableRef = ref();
|
|
|
|
const tableModes = [
|
|
{
|
|
icon: 'view_column',
|
|
title: t('table view'),
|
|
value: TABLE_MODE,
|
|
disable: $props.disableOption?.table,
|
|
},
|
|
{
|
|
icon: 'grid_view',
|
|
title: t('grid view'),
|
|
value: CARD_MODE,
|
|
disable: $props.disableOption?.card,
|
|
},
|
|
];
|
|
|
|
onBeforeMount(() => {
|
|
const urlParams = route.query[$props.searchUrl];
|
|
hasParams.value = urlParams && Object.keys(urlParams).length !== 0;
|
|
});
|
|
|
|
onMounted(() => {
|
|
mode.value =
|
|
quasar.platform.is.mobile && !$props.disableOption?.card
|
|
? CARD_MODE
|
|
: $props.defaultMode;
|
|
stateStore.rightDrawer = quasar.screen.gt.xs;
|
|
columnsVisibilitySkipped.value = [
|
|
...splittedColumns.value.columns
|
|
.filter((c) => c.visible === false)
|
|
.map((c) => c.name),
|
|
...['tableActions'],
|
|
];
|
|
createForm.value = $props.create;
|
|
if ($props.create && route?.query?.createForm) {
|
|
showForm.value = true;
|
|
createForm.value = {
|
|
...createForm.value,
|
|
...{ formInitialData: JSON.parse(route?.query?.createForm) },
|
|
};
|
|
}
|
|
});
|
|
|
|
watch(
|
|
() => $props.columns,
|
|
(value) => splitColumns(value),
|
|
{ immediate: true }
|
|
);
|
|
|
|
watch(
|
|
() => route.query[$props.searchUrl],
|
|
(val) => setUserParams(val),
|
|
{ immediate: true, deep: true }
|
|
);
|
|
|
|
const isTableMode = computed(() => mode.value == TABLE_MODE);
|
|
|
|
function setUserParams(watchedParams, watchedOrder) {
|
|
if (!watchedParams) return;
|
|
|
|
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
|
|
const filter =
|
|
typeof watchedParams?.filter == 'string'
|
|
? JSON.parse(watchedParams?.filter ?? '{}')
|
|
: watchedParams?.filter;
|
|
const where = filter?.where;
|
|
const order = watchedOrder ?? filter?.order;
|
|
|
|
watchedParams = { ...watchedParams, ...where };
|
|
delete watchedParams.filter;
|
|
delete params.value?.filter;
|
|
params.value = { ...params.value, ...sanitizer(watchedParams) };
|
|
orders.value = parseOrder(order);
|
|
}
|
|
|
|
function sanitizer(params) {
|
|
for (const [key, value] of Object.entries(params)) {
|
|
if (value && typeof value == 'object') {
|
|
const param = Object.values(value)[0];
|
|
if (typeof param == 'string') params[key] = param.replaceAll('%', '');
|
|
}
|
|
}
|
|
return params;
|
|
}
|
|
|
|
function splitColumns(columns) {
|
|
splittedColumns.value = {
|
|
columns: [],
|
|
chips: [],
|
|
create: [],
|
|
cardVisible: [],
|
|
};
|
|
|
|
for (const col of columns) {
|
|
if (col.name == 'tableActions') {
|
|
col.orderBy = false;
|
|
splittedColumns.value.actions = col;
|
|
}
|
|
if (col.chip) splittedColumns.value.chips.push(col);
|
|
if (col.isTitle) splittedColumns.value.title = col;
|
|
if (col.create) splittedColumns.value.create.push(col);
|
|
if (col.cardVisible) splittedColumns.value.cardVisible.push(col);
|
|
if ($props.isEditable && col.disable == null) col.disable = false;
|
|
if ($props.useModel && col.columnFilter !== false)
|
|
col.columnFilter = { inWhere: true, ...col.columnFilter };
|
|
splittedColumns.value.columns.push(col);
|
|
}
|
|
// Status column
|
|
if (splittedColumns.value.chips.length) {
|
|
splittedColumns.value.columnChips = splittedColumns.value.chips.filter(
|
|
(c) => !c.isId
|
|
);
|
|
if (splittedColumns.value.columnChips.length)
|
|
splittedColumns.value.columns.unshift({
|
|
align: 'left',
|
|
label: t('status'),
|
|
name: 'tableStatus',
|
|
columnFilter: false,
|
|
orderBy: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
const rowClickFunction = computed(() => {
|
|
if ($props.rowClick != undefined) return $props.rowClick;
|
|
if ($props.redirect) return ({ id }) => redirectFn(id);
|
|
return () => {};
|
|
});
|
|
|
|
const rowCtrlClickFunction = computed(() => {
|
|
if ($props.rowCtrlClick != undefined) return $props.rowCtrlClick;
|
|
if ($props.redirect)
|
|
return (evt, { id }) => {
|
|
stopEventPropagation(evt);
|
|
window.open(`/#/${$props.redirect}/${id}`, '_blank');
|
|
};
|
|
return () => {};
|
|
});
|
|
|
|
function redirectFn(id) {
|
|
router.push({ path: `/${$props.redirect}/${id}` });
|
|
}
|
|
|
|
function stopEventPropagation(event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
function reload(params) {
|
|
selected.value = [];
|
|
CrudModelRef.value.reload(params);
|
|
}
|
|
|
|
function columnName(col) {
|
|
const column = { ...col, ...col.columnFilter };
|
|
let name = column.name;
|
|
if (column.alias) name = column.alias + '.' + name;
|
|
return name;
|
|
}
|
|
|
|
function getColAlign(col) {
|
|
return 'text-' + (col.align ?? 'left');
|
|
}
|
|
|
|
function parseOrder(urlOrders) {
|
|
const orderObject = {};
|
|
if (!urlOrders) return orderObject;
|
|
if (typeof urlOrders == 'string') urlOrders = [urlOrders];
|
|
for (const [index, orders] of urlOrders.entries()) {
|
|
const [name, direction] = orders.split(' ');
|
|
orderObject[name] = { direction, index: index + 1 };
|
|
}
|
|
return orderObject;
|
|
}
|
|
|
|
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
|
|
defineExpose({
|
|
create: createForm,
|
|
reload,
|
|
redirect: redirectFn,
|
|
selected,
|
|
CrudModelRef,
|
|
params,
|
|
tableRef,
|
|
});
|
|
|
|
function handleOnDataSaved(_) {
|
|
if (_.onDataSaved) _.onDataSaved({ CrudModelRef: CrudModelRef.value });
|
|
else $props.create.onDataSaved(_);
|
|
}
|
|
|
|
function handleScroll() {
|
|
if ($props.crudModel.disableInfiniteScroll) return;
|
|
|
|
const tMiddle = tableRef.value.$el.querySelector('.q-table__middle');
|
|
const { scrollHeight, scrollTop, clientHeight } = tMiddle;
|
|
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) <= 40;
|
|
if (isAtBottom) CrudModelRef.value.vnPaginateRef.paginate();
|
|
}
|
|
|
|
function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|
if (evt?.shiftKey && added) {
|
|
const rowIndex = selectedRows[0].$index;
|
|
const selectedIndexes = new Set(selected.value.map((row) => row.$index));
|
|
for (const row of rows) {
|
|
if (row.$index == rowIndex) break;
|
|
if (!selectedIndexes.has(row.$index)) {
|
|
selected.value.push(row);
|
|
selectedIndexes.add(row.$index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<template>
|
|
<QDrawer
|
|
v-if="$props.rightSearch"
|
|
v-model="stateStore.rightDrawer"
|
|
side="right"
|
|
:width="256"
|
|
show-if-above
|
|
>
|
|
<QScrollArea class="fit">
|
|
<VnFilterPanel
|
|
:data-key="$attrs['data-key']"
|
|
:search-button="true"
|
|
v-model="params"
|
|
:search-url="searchUrl"
|
|
:redirect="!!redirect"
|
|
@set-user-params="setUserParams"
|
|
:disable-submit-event="true"
|
|
@remove="
|
|
(key) =>
|
|
tableFilterRef
|
|
.find((f) => f.props?.column.name == key)
|
|
?.addFilter()
|
|
"
|
|
>
|
|
<template #body>
|
|
<div
|
|
class="row no-wrap flex-center"
|
|
v-for="col of splittedColumns.columns.filter(
|
|
(c) => c.columnFilter ?? true
|
|
)"
|
|
:key="col.id"
|
|
>
|
|
<VnFilter
|
|
ref="tableFilterRef"
|
|
:column="col"
|
|
:data-key="$attrs['data-key']"
|
|
v-model="params[columnName(col)]"
|
|
:search-url="searchUrl"
|
|
/>
|
|
<VnTableOrder
|
|
v-if="
|
|
col?.columnFilter !== false &&
|
|
col?.name !== 'tableActions'
|
|
"
|
|
v-model="orders[col.orderBy ?? col.name]"
|
|
:name="col.orderBy ?? col.name"
|
|
:data-key="$attrs['data-key']"
|
|
:search-url="searchUrl"
|
|
:vertical="false"
|
|
/>
|
|
</div>
|
|
<slot
|
|
name="moreFilterPanel"
|
|
:params="params"
|
|
:columns="splittedColumns.columns"
|
|
/>
|
|
</template>
|
|
<template #tags="{ tag, formatFn }" v-if="chipLocale">
|
|
<div class="q-gutter-x-xs">
|
|
<strong>{{ t(`${chipLocale}.${tag.label}`) }}: </strong>
|
|
<span>{{ formatFn(tag.value) }}</span>
|
|
</div>
|
|
</template>
|
|
</VnFilterPanel>
|
|
</QScrollArea>
|
|
</QDrawer>
|
|
<CrudModel
|
|
v-bind="$attrs"
|
|
:class="$attrs['class'] ?? 'q-px-md'"
|
|
:limit="$attrs['limit'] ?? 20"
|
|
ref="CrudModelRef"
|
|
@on-fetch="(...args) => emit('onFetch', ...args)"
|
|
:search-url="searchUrl"
|
|
:disable-infinite-scroll="isTableMode"
|
|
@save-changes="reload"
|
|
:has-sub-toolbar="$props.hasSubToolbar ?? isEditable"
|
|
:auto-load="hasParams || $attrs['auto-load']"
|
|
>
|
|
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
|
|
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
|
|
</template>
|
|
<template #body="{ rows }">
|
|
<QTable
|
|
ref="tableRef"
|
|
v-bind="table"
|
|
class="vnTable"
|
|
:class="{ 'last-row-sticky': $props.footer }"
|
|
:columns="splittedColumns.columns"
|
|
:rows="rows"
|
|
v-model:selected="selected"
|
|
:grid="!isTableMode"
|
|
table-header-class="bg-header"
|
|
card-container-class="grid-three"
|
|
flat
|
|
:style="isTableMode && `max-height: ${tableHeight}`"
|
|
:virtual-scroll="isTableMode"
|
|
@virtual-scroll="handleScroll"
|
|
@row-click="(_, row) => rowClickFunction && rowClickFunction(row)"
|
|
@update:selected="emit('update:selected', $event)"
|
|
@selection="(details) => handleSelection(details, rows)"
|
|
>
|
|
<template #top-left v-if="!$props.withoutHeader">
|
|
<slot name="top-left"></slot>
|
|
</template>
|
|
<template #top-right v-if="!$props.withoutHeader">
|
|
<VnVisibleColumn
|
|
v-if="isTableMode"
|
|
v-model="splittedColumns.columns"
|
|
:table-code="tableCode ?? route.name"
|
|
:skip="columnsVisibilitySkipped"
|
|
/>
|
|
<QBtnToggle
|
|
v-model="mode"
|
|
toggle-color="primary"
|
|
class="bg-vn-section-color"
|
|
dense
|
|
:options="tableModes.filter((mode) => !mode.disable)"
|
|
/>
|
|
<QBtn
|
|
v-if="$props.rightSearch"
|
|
icon="filter_alt"
|
|
class="bg-vn-section-color q-ml-sm"
|
|
dense
|
|
@click="stateStore.toggleRightDrawer()"
|
|
/>
|
|
</template>
|
|
<template #header-cell="{ col }">
|
|
<QTh
|
|
v-if="col.visible ?? true"
|
|
:style="col.headerStyle"
|
|
:class="col.headerClass"
|
|
>
|
|
<div
|
|
class="column ellipsis"
|
|
:class="`text-${col?.align ?? 'left'}`"
|
|
:style="$props.columnSearch ? 'height: 75px' : ''"
|
|
>
|
|
<div class="row items-center no-wrap" style="height: 30px">
|
|
<QTooltip v-if="col.toolTip">{{ col.toolTip }}</QTooltip>
|
|
<VnTableOrder
|
|
v-model="orders[col.orderBy ?? col.name]"
|
|
:name="col.orderBy ?? col.name"
|
|
:label="col?.label"
|
|
:data-key="$attrs['data-key']"
|
|
:search-url="searchUrl"
|
|
/>
|
|
</div>
|
|
<VnFilter
|
|
v-if="$props.columnSearch"
|
|
:column="col"
|
|
:show-title="true"
|
|
:data-key="$attrs['data-key']"
|
|
v-model="params[columnName(col)]"
|
|
:search-url="searchUrl"
|
|
class="full-width"
|
|
/>
|
|
</div>
|
|
</QTh>
|
|
</template>
|
|
<template #header-cell-tableActions>
|
|
<QTh auto-width class="sticky" />
|
|
</template>
|
|
<template #body-cell-tableStatus="{ col, row }">
|
|
<QTd auto-width :class="getColAlign(col)">
|
|
<VnTableChip :columns="splittedColumns.columnChips" :row="row">
|
|
<template #afterChip>
|
|
<slot name="afterChip" :row="row"></slot>
|
|
</template>
|
|
</VnTableChip>
|
|
</QTd>
|
|
</template>
|
|
<template #body-cell="{ col, row, rowIndex }">
|
|
<!-- Columns -->
|
|
<QTd
|
|
auto-width
|
|
class="no-margin"
|
|
:class="[getColAlign(col), col.columnClass]"
|
|
:style="col.style"
|
|
v-if="col.visible ?? true"
|
|
@click.ctrl="
|
|
($event) =>
|
|
rowCtrlClickFunction && rowCtrlClickFunction($event, row)
|
|
"
|
|
>
|
|
<slot
|
|
:name="`column-${col.name}`"
|
|
:col="col"
|
|
:row="row"
|
|
:row-index="rowIndex"
|
|
>
|
|
<VnTableColumn
|
|
:column="col"
|
|
:row="row"
|
|
:is-editable="col.isEditable ?? isEditable"
|
|
v-model="row[col.name]"
|
|
component-prop="columnField"
|
|
/>
|
|
</slot>
|
|
</QTd>
|
|
</template>
|
|
<template #body-cell-tableActions="{ col, row }">
|
|
<QTd
|
|
auto-width
|
|
:class="getColAlign(col)"
|
|
class="sticky no-padding"
|
|
@click="stopEventPropagation($event)"
|
|
:style="col.style"
|
|
>
|
|
<QBtn
|
|
v-for="(btn, index) of col.actions"
|
|
v-show="btn.show ? btn.show(row) : true"
|
|
:key="index"
|
|
:title="btn.title"
|
|
:icon="btn.icon"
|
|
class="q-pa-xs"
|
|
flat
|
|
dense
|
|
:class="
|
|
btn.isPrimary ? 'text-primary-light' : 'color-vn-text '
|
|
"
|
|
:style="`visibility: ${
|
|
(btn.show && btn.show(row)) ?? true ? 'visible' : 'hidden'
|
|
}`"
|
|
@click="btn.action(row)"
|
|
/>
|
|
</QTd>
|
|
</template>
|
|
<template #bottom v-if="bottom">
|
|
<slot name="bottom-table">
|
|
<QBtn
|
|
@click="
|
|
() =>
|
|
createAsDialog
|
|
? (showForm = !showForm)
|
|
: handleOnDataSaved(create)
|
|
"
|
|
class="cursor-pointer fill-icon"
|
|
color="primary"
|
|
icon="add_circle"
|
|
size="md"
|
|
round
|
|
flat
|
|
shortcut="+"
|
|
:disabled="!disabledAttr"
|
|
/>
|
|
<QTooltip>
|
|
{{ createForm.title }}
|
|
</QTooltip>
|
|
</slot>
|
|
</template>
|
|
<template #item="{ row, colsMap }">
|
|
<component
|
|
:is="$props.redirect ? 'router-link' : 'span'"
|
|
:to="`/${$props.redirect}/` + row.id"
|
|
>
|
|
<QCard
|
|
bordered
|
|
flat
|
|
class="row no-wrap justify-between cursor-pointer"
|
|
@click="
|
|
(_, row) => {
|
|
$props.rowClick && $props.rowClick(row);
|
|
}
|
|
"
|
|
style="height: 100%"
|
|
>
|
|
<QCardSection
|
|
vertical
|
|
class="no-margin no-padding"
|
|
:class="colsMap.tableActions ? 'w-80' : 'fit'"
|
|
>
|
|
<!-- Chips -->
|
|
<QCardSection
|
|
v-if="splittedColumns.chips.length"
|
|
class="no-margin q-px-xs q-py-none"
|
|
>
|
|
<VnTableChip
|
|
:columns="splittedColumns.chips"
|
|
:row="row"
|
|
>
|
|
<template #afterChip>
|
|
<slot name="afterChip" :row="row"></slot>
|
|
</template>
|
|
</VnTableChip>
|
|
</QCardSection>
|
|
<!-- Title -->
|
|
<QCardSection
|
|
v-if="splittedColumns.title"
|
|
class="q-pl-sm q-py-none text-primary-light text-bold text-h6 cardEllipsis"
|
|
>
|
|
<span
|
|
:title="row[splittedColumns.title.name]"
|
|
@click="stopEventPropagation($event)"
|
|
class="cursor-text"
|
|
>
|
|
{{ row[splittedColumns.title.name] }}
|
|
</span>
|
|
</QCardSection>
|
|
<!-- Fields -->
|
|
<QCardSection
|
|
class="q-pl-sm q-pr-lg q-py-xs"
|
|
:class="$props.cardClass"
|
|
>
|
|
<div
|
|
v-for="(
|
|
col, index
|
|
) of splittedColumns.cardVisible"
|
|
:key="col.name"
|
|
class="fields"
|
|
>
|
|
<VnLv :label="col.label + ':'">
|
|
<template #value>
|
|
<span
|
|
@click="stopEventPropagation($event)"
|
|
>
|
|
<slot
|
|
:name="`column-${col.name}`"
|
|
:col="col"
|
|
:row="row"
|
|
:row-index="index"
|
|
>
|
|
<VnTableColumn
|
|
:column="col"
|
|
:row="row"
|
|
:is-editable="false"
|
|
v-model="row[col.name]"
|
|
component-prop="columnField"
|
|
:show-label="true"
|
|
/>
|
|
</slot>
|
|
</span>
|
|
</template>
|
|
</VnLv>
|
|
</div>
|
|
</QCardSection>
|
|
</QCardSection>
|
|
<!-- Actions -->
|
|
<QCardSection
|
|
v-if="colsMap.tableActions"
|
|
class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs"
|
|
@click="stopEventPropagation($event)"
|
|
>
|
|
<QBtn
|
|
v-for="(btn, index) of splittedColumns.actions
|
|
.actions"
|
|
:key="index"
|
|
:title="btn.title"
|
|
:icon="btn.icon"
|
|
class="q-pa-xs"
|
|
flat
|
|
:class="
|
|
btn.isPrimary
|
|
? 'text-primary-light'
|
|
: 'color-vn-text '
|
|
"
|
|
@click="btn.action(row)"
|
|
/>
|
|
</QCardSection>
|
|
</QCard>
|
|
</component>
|
|
</template>
|
|
<template #bottom-row="{ cols }" v-if="$props.footer">
|
|
<QTr v-if="rows.length" style="height: 30px">
|
|
<QTh
|
|
v-for="col of cols.filter((cols) => cols.visible ?? true)"
|
|
:key="col?.id"
|
|
class="text-center"
|
|
:class="getColAlign(col)"
|
|
>
|
|
<slot :name="`column-footer-${col.name}`" />
|
|
</QTh>
|
|
</QTr>
|
|
</template>
|
|
</QTable>
|
|
</template>
|
|
</CrudModel>
|
|
<QPageSticky v-if="$props.create" :offset="[20, 20]" style="z-index: 2">
|
|
<QBtn
|
|
@click="
|
|
() =>
|
|
createAsDialog ? (showForm = !showForm) : handleOnDataSaved(create)
|
|
"
|
|
color="primary"
|
|
fab
|
|
icon="add"
|
|
shortcut="+"
|
|
data-cy="vnTableCreateBtn"
|
|
/>
|
|
<QTooltip self="top right">
|
|
{{ createForm?.title }}
|
|
</QTooltip>
|
|
</QPageSticky>
|
|
<QDialog v-model="showForm" transition-show="scale" transition-hide="scale">
|
|
<FormModelPopup
|
|
v-bind="createForm"
|
|
:model="$attrs['data-key'] + 'Create'"
|
|
@on-data-saved="(_, res) => createForm.onDataSaved(res)"
|
|
>
|
|
<template #form-inputs="{ data }">
|
|
<div class="grid-create">
|
|
<slot
|
|
v-for="column of splittedColumns.create"
|
|
:key="column.name"
|
|
:name="`column-create-${column.name}`"
|
|
:data="data"
|
|
:column-name="column.name"
|
|
:label="column.label"
|
|
>
|
|
<VnTableColumn
|
|
:column="column"
|
|
:row="{}"
|
|
default="input"
|
|
v-model="data[column.name]"
|
|
:show-label="true"
|
|
component-prop="columnCreate"
|
|
/>
|
|
</slot>
|
|
<slot name="more-create-dialog" :data="data" />
|
|
</div>
|
|
</template>
|
|
</FormModelPopup>
|
|
</QDialog>
|
|
</template>
|
|
<i18n>
|
|
en:
|
|
status: Status
|
|
table view: Table view
|
|
grid view: Grid view
|
|
es:
|
|
status: Estados
|
|
table view: Vista en tabla
|
|
grid view: Vista en cuadrícula
|
|
</i18n>
|
|
|
|
<style lang="scss">
|
|
.bg-chip-secondary {
|
|
background-color: var(--vn-page-color);
|
|
color: var(--vn-text-color);
|
|
}
|
|
|
|
.bg-header {
|
|
background-color: var(--vn-accent-color);
|
|
color: var(--vn-text-color);
|
|
}
|
|
|
|
.color-vn-text {
|
|
color: var(--vn-text-color);
|
|
}
|
|
|
|
.grid-three {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(350px, max-content));
|
|
max-width: 100%;
|
|
grid-gap: 20px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.grid-create {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
|
|
max-width: 100%;
|
|
grid-gap: 20px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.flex-one {
|
|
display: flex;
|
|
flex-flow: row wrap;
|
|
div.fields {
|
|
width: 100%;
|
|
.vn-label-value {
|
|
display: flex;
|
|
gap: 2%;
|
|
}
|
|
}
|
|
}
|
|
|
|
.q-table {
|
|
th {
|
|
padding: 0;
|
|
}
|
|
|
|
&__top {
|
|
padding: 12px 0px;
|
|
top: 0;
|
|
}
|
|
}
|
|
|
|
.vnTable {
|
|
thead tr th {
|
|
position: sticky;
|
|
z-index: 2;
|
|
}
|
|
thead tr:first-child th {
|
|
top: 0;
|
|
}
|
|
.q-table__top {
|
|
top: 0;
|
|
padding: 12px 0;
|
|
}
|
|
.sticky {
|
|
position: sticky;
|
|
right: 0;
|
|
}
|
|
td.sticky {
|
|
background-color: var(--vn-section-color);
|
|
z-index: 1;
|
|
}
|
|
table tbody th {
|
|
position: relative;
|
|
}
|
|
}
|
|
|
|
.last-row-sticky {
|
|
tbody:nth-last-child(1) {
|
|
@extend .bg-header;
|
|
position: sticky;
|
|
z-index: 2;
|
|
bottom: 0;
|
|
}
|
|
}
|
|
|
|
.vn-label-value {
|
|
display: flex;
|
|
flex-direction: row;
|
|
color: var(--vn-text-color);
|
|
.value {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
pointer-events: all;
|
|
cursor: text;
|
|
user-select: all;
|
|
}
|
|
}
|
|
|
|
.cardEllipsis {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.grid-two {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
|
|
max-width: 100%;
|
|
margin: 0 auto;
|
|
overflow: scroll;
|
|
white-space: wrap;
|
|
width: 100%;
|
|
}
|
|
|
|
.w-80 {
|
|
width: 80%;
|
|
}
|
|
|
|
.w-20 {
|
|
width: 20%;
|
|
}
|
|
|
|
.cursor-text {
|
|
pointer-events: all;
|
|
cursor: text;
|
|
user-select: all;
|
|
}
|
|
|
|
.q-table__container {
|
|
background-color: transparent;
|
|
}
|
|
|
|
.q-table__middle.q-virtual-scroll.q-virtual-scroll--vertical.scroll {
|
|
background-color: var(--vn-section-color);
|
|
}
|
|
</style>
|