1274 lines
42 KiB
Vue
1274 lines
42 KiB
Vue
<script setup>
|
|
import {
|
|
ref,
|
|
onBeforeMount,
|
|
onMounted,
|
|
onUnmounted,
|
|
computed,
|
|
watch,
|
|
h,
|
|
render,
|
|
inject,
|
|
useAttrs,
|
|
nextTick,
|
|
} from 'vue';
|
|
import { useArrayData } from 'src/composables/useArrayData';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useQuasar, date } from 'quasar';
|
|
import { useStateStore } from 'stores/useStateStore';
|
|
import { useFilterParams } from 'src/composables/useFilterParams';
|
|
import { dashIfEmpty, toDate } from 'src/filters';
|
|
|
|
import CrudModel from 'src/components/CrudModel.vue';
|
|
import FormModelPopup from 'components/FormModelPopup.vue';
|
|
|
|
import VnColumn 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';
|
|
import VnTableFilter from './VnTableFilter.vue';
|
|
import { getColAlign } from 'src/composables/getColAlign';
|
|
import RightMenu from '../common/RightMenu.vue';
|
|
|
|
const arrayData = useArrayData(useAttrs()['data-key']);
|
|
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: Boolean,
|
|
default: false,
|
|
},
|
|
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',
|
|
},
|
|
footer: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
disabledAttr: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
withFilters: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
overlay: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
createComplement: {
|
|
type: Object,
|
|
},
|
|
dataCy: {
|
|
type: String,
|
|
default: 'vn-table',
|
|
},
|
|
});
|
|
|
|
const { t } = useI18n();
|
|
const stateStore = useStateStore();
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const quasar = useQuasar();
|
|
const $attrs = useAttrs();
|
|
|
|
const CARD_MODE = 'card';
|
|
const TABLE_MODE = 'table';
|
|
const mode = ref(CARD_MODE);
|
|
const selected = ref([]);
|
|
const hasParams = ref(false);
|
|
const CrudModelRef = ref({});
|
|
const showForm = ref(false);
|
|
const splittedColumns = ref({ columns: [] });
|
|
const columnsVisibilitySkipped = ref();
|
|
const createForm = ref();
|
|
const createRef = ref(null);
|
|
const tableRef = ref();
|
|
const params = ref(useFilterParams($attrs['data-key']).params);
|
|
const orders = ref(useFilterParams($attrs['data-key']).orders);
|
|
const app = inject('app');
|
|
|
|
const editingRow = ref(null);
|
|
const editingField = ref(null);
|
|
const isTableMode = computed(() => mode.value == TABLE_MODE);
|
|
const selectRegex = /select/;
|
|
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
|
|
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(async () => {
|
|
if ($props.isEditable) document.addEventListener('click', clickHandler);
|
|
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) },
|
|
};
|
|
}
|
|
});
|
|
|
|
onUnmounted(async () => {
|
|
if ($props.isEditable) document.removeEventListener('click', clickHandler);
|
|
});
|
|
|
|
watch(
|
|
() => $props.columns,
|
|
(value) => splitColumns(value),
|
|
{ immediate: true },
|
|
);
|
|
|
|
defineExpose({
|
|
create: createForm,
|
|
reload,
|
|
redirect: redirectFn,
|
|
selected,
|
|
CrudModelRef,
|
|
params,
|
|
tableRef,
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
splittedColumns.value.create = createOrderSort(splittedColumns.value.create);
|
|
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
|
|
function createOrderSort(columns) {
|
|
const orderedColumn = columns
|
|
.map((column, index) =>
|
|
column.createOrder !== undefined ? { ...column, originalIndex: index } : null,
|
|
)
|
|
.filter((item) => item !== null);
|
|
|
|
orderedColumn.sort((a, b) => a.createOrder - b.createOrder);
|
|
|
|
const filteredColumns = columns.filter((col) => col.createOrder === undefined);
|
|
|
|
orderedColumn.forEach((col) => {
|
|
filteredColumns.splice(col.createOrder, 0, col);
|
|
});
|
|
|
|
return filteredColumns;
|
|
}
|
|
|
|
const rowClickFunction = computed(() => {
|
|
if ($props.rowClick != undefined) return $props.rowClick;
|
|
if ($props.redirect) return ({ id }) => redirectFn(id);
|
|
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 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));
|
|
const minIndex = selectedIndexes.size
|
|
? Math.min(...selectedIndexes, rowIndex)
|
|
: 0;
|
|
const maxIndex = Math.max(...selectedIndexes, rowIndex);
|
|
|
|
for (let i = minIndex; i <= maxIndex; i++) {
|
|
const row = rows[i];
|
|
if (row.$index == rowIndex) continue;
|
|
if (!selectedIndexes.has(row.$index)) {
|
|
selected.value.push(row);
|
|
selectedIndexes.add(row.$index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function isEditableColumn(column) {
|
|
const isEditableCol = column?.isEditable ?? true;
|
|
const isVisible = column?.visible ?? true;
|
|
const hasComponent = column?.component;
|
|
|
|
return $props.isEditable && isVisible && hasComponent && isEditableCol;
|
|
}
|
|
|
|
function hasEditableFormat(column) {
|
|
if (isEditableColumn(column)) return 'editable-text';
|
|
}
|
|
|
|
const clickHandler = async (event) => {
|
|
const clickedElement = event.target.closest('td');
|
|
const isDateElement = event.target.closest('.q-date');
|
|
const isTimeElement = event.target.closest('.q-time');
|
|
const isQSelectDropDown = event.target.closest('.q-select__dropdown-icon');
|
|
|
|
if (isDateElement || isTimeElement || isQSelectDropDown) return;
|
|
|
|
if (clickedElement === null) {
|
|
await destroyInput(editingRow.value, editingField.value);
|
|
return;
|
|
}
|
|
const rowIndex = clickedElement.getAttribute('data-row-index');
|
|
const colField = clickedElement.getAttribute('data-col-field');
|
|
const column = $props.columns.find((col) => col.name === colField);
|
|
|
|
if (editingRow.value !== null && editingField.value !== null) {
|
|
if (editingRow.value == rowIndex && editingField.value == colField) return;
|
|
|
|
await destroyInput(editingRow.value, editingField.value);
|
|
}
|
|
|
|
if (isEditableColumn(column)) {
|
|
await renderInput(Number(rowIndex), colField, clickedElement);
|
|
}
|
|
};
|
|
|
|
async function handleTabKey(event, rowIndex, colField) {
|
|
if (editingRow.value == rowIndex && editingField.value == colField)
|
|
await destroyInput(editingRow.value, editingField.value);
|
|
|
|
const direction = event.shiftKey ? -1 : 1;
|
|
const { nextRowIndex, nextColumnName } = await handleTabNavigation(
|
|
rowIndex,
|
|
colField,
|
|
direction,
|
|
);
|
|
|
|
if (nextRowIndex < 0 || nextRowIndex >= arrayData.store.data.length) return;
|
|
|
|
event.preventDefault();
|
|
await renderInput(nextRowIndex, nextColumnName, null);
|
|
}
|
|
|
|
async function renderInput(rowId, field, clickedElement) {
|
|
editingField.value = field;
|
|
editingRow.value = rowId;
|
|
|
|
const originalColumn = $props.columns.find((col) => col.name === field);
|
|
const column = { ...originalColumn, ...{ label: '' } };
|
|
const row = CrudModelRef.value.formData[rowId];
|
|
const oldValue = CrudModelRef.value.formData[rowId][column?.name];
|
|
|
|
if (!clickedElement)
|
|
clickedElement = document.querySelector(
|
|
`[data-row-index="${rowId}"][data-col-field="${field}"]`,
|
|
);
|
|
|
|
Array.from(clickedElement.childNodes).forEach((child) => {
|
|
child.style.visibility = 'hidden';
|
|
child.style.position = 'relative';
|
|
});
|
|
|
|
const isSelect = selectRegex.test(column?.component);
|
|
if (isSelect) column.attrs = { ...column.attrs, 'emit-value': false };
|
|
|
|
const node = h(VnColumn, {
|
|
row: row,
|
|
class: 'temp-input',
|
|
column: column,
|
|
modelValue: row[column.name],
|
|
componentProp: 'columnField',
|
|
autofocus: true,
|
|
focusOnMount: true,
|
|
eventHandlers: {
|
|
'update:modelValue': async (value) => {
|
|
if (isSelect && value) {
|
|
await updateSelectValue(value, column, row, oldValue);
|
|
} else row[column.name] = value;
|
|
await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row);
|
|
},
|
|
keyup: async (event) => {
|
|
if (event.key === 'Enter')
|
|
await destroyInput(rowId, field, clickedElement);
|
|
},
|
|
keydown: async (event) => {
|
|
switch (event.key) {
|
|
case 'Tab':
|
|
await handleTabKey(event, rowId, field);
|
|
event.stopPropagation();
|
|
break;
|
|
case 'Escape':
|
|
await destroyInput(rowId, field, clickedElement);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
click: (event) => {
|
|
column?.cellEvent?.['click']?.(event, row);
|
|
},
|
|
},
|
|
});
|
|
|
|
node.appContext = app._context;
|
|
render(node, clickedElement);
|
|
|
|
if (['toggle'].includes(column?.component))
|
|
node.el?.querySelector('span > div').focus();
|
|
|
|
if (['checkbox', undefined].includes(column?.component))
|
|
node.el?.querySelector('span > div > div').focus();
|
|
}
|
|
|
|
async function updateSelectValue(value, column, row, oldValue) {
|
|
row[column.name] = value[column.attrs?.optionValue ?? 'id'];
|
|
|
|
row[column?.name + 'VnTableTextValue'] = value[column.attrs?.optionLabel ?? 'name'];
|
|
|
|
if (column?.attrs?.find?.label)
|
|
row[column?.attrs?.find?.label] = value[column.attrs?.optionLabel ?? 'name'];
|
|
|
|
await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row);
|
|
}
|
|
|
|
async function destroyInput(rowIndex, field, clickedElement) {
|
|
if (!clickedElement)
|
|
clickedElement = document.querySelector(
|
|
`[data-row-index="${rowIndex}"][data-col-field="${field}"]`,
|
|
);
|
|
if (clickedElement) {
|
|
await nextTick();
|
|
render(null, clickedElement);
|
|
Array.from(clickedElement.childNodes).forEach((child) => {
|
|
child.style.visibility = 'visible';
|
|
child.style.position = '';
|
|
});
|
|
}
|
|
if (editingRow.value !== rowIndex || editingField.value !== field) return;
|
|
editingRow.value = null;
|
|
editingField.value = null;
|
|
}
|
|
|
|
async function handleTabNavigation(rowIndex, colName, direction) {
|
|
const columns = $props.columns;
|
|
const totalColumns = columns.length;
|
|
let currentColumnIndex = columns.findIndex((col) => col.name === colName);
|
|
|
|
let iterations = 0;
|
|
let newColumnIndex = currentColumnIndex;
|
|
|
|
do {
|
|
iterations++;
|
|
newColumnIndex = (newColumnIndex + direction + totalColumns) % totalColumns;
|
|
|
|
if (isEditableColumn(columns[newColumnIndex])) break;
|
|
} while (iterations < totalColumns);
|
|
|
|
if (iterations >= totalColumns + 1) return;
|
|
|
|
if (direction === 1 && newColumnIndex <= currentColumnIndex) {
|
|
rowIndex++;
|
|
} else if (direction === -1 && newColumnIndex >= currentColumnIndex) {
|
|
rowIndex--;
|
|
}
|
|
return { nextRowIndex: rowIndex, nextColumnName: columns[newColumnIndex].name };
|
|
}
|
|
|
|
function getCheckboxIcon(value) {
|
|
switch (typeof value) {
|
|
case 'boolean':
|
|
return value ? 'check' : 'close';
|
|
case 'number':
|
|
return value === 0 ? 'close' : 'check';
|
|
case 'undefined':
|
|
return 'indeterminate_check_box';
|
|
default:
|
|
return 'indeterminate_check_box';
|
|
}
|
|
}
|
|
|
|
function getToggleIcon(value) {
|
|
if (value === null) return 'help_outline';
|
|
return value ? 'toggle_on' : 'toggle_off';
|
|
}
|
|
|
|
function formatColumnValue(col, row, dashIfEmpty) {
|
|
if (col?.format || row[col?.name + 'VnTableTextValue']) {
|
|
if (selectRegex.test(col?.component) && row[col?.name + 'VnTableTextValue']) {
|
|
return dashIfEmpty(row[col?.name + 'VnTableTextValue']);
|
|
} else {
|
|
return col.format(row, dashIfEmpty);
|
|
}
|
|
}
|
|
|
|
if (col?.component === 'date') return dashIfEmpty(toDate(row[col?.name]));
|
|
|
|
if (col?.component === 'time')
|
|
return row[col?.name] >= 5
|
|
? dashIfEmpty(date.formatDate(new Date(row[col?.name]), 'HH:mm'))
|
|
: row[col?.name];
|
|
|
|
if (selectRegex.test(col?.component) && $props.isEditable) {
|
|
const { find, url } = col.attrs;
|
|
const urlRelation = url?.charAt(0)?.toLocaleLowerCase() + url?.slice(1, -1);
|
|
|
|
if (col?.attrs.options) {
|
|
const find = col?.attrs.options.find((option) => option.id === row[col.name]);
|
|
if (!col.attrs?.optionLabel || !find) return dashIfEmpty(row[col?.name]);
|
|
return dashIfEmpty(find[col.attrs?.optionLabel ?? 'name']);
|
|
}
|
|
|
|
if (typeof row[urlRelation] == 'object') {
|
|
if (typeof find == 'object')
|
|
return dashIfEmpty(row[urlRelation][find?.label ?? 'name']);
|
|
|
|
return dashIfEmpty(row[urlRelation][col?.attrs.optionLabel ?? 'name']);
|
|
}
|
|
if (typeof row[urlRelation] == 'string') return dashIfEmpty(row[urlRelation]);
|
|
}
|
|
return dashIfEmpty(row[col?.name]);
|
|
}
|
|
|
|
function cardClick(_, row) {
|
|
if ($props.redirect) router.push({ path: `/${$props.redirect}/${row.id}` });
|
|
}
|
|
|
|
function removeTextValue(data, getChanges) {
|
|
let changes = data.updates;
|
|
if (!changes) return data;
|
|
|
|
for (const change of changes) {
|
|
for (const key in change.data) {
|
|
if (key.endsWith('VnTableTextValue')) {
|
|
delete change.data[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
data.updates = changes.filter((change) => Object.keys(change.data).length > 0);
|
|
|
|
if ($attrs?.beforeSaveFn) data = $attrs.beforeSaveFn(data, getChanges);
|
|
|
|
return data;
|
|
}
|
|
|
|
function handleRowClick(event, row) {
|
|
if (event.ctrlKey) return rowCtrlClickFunction.value(event, row);
|
|
if (rowClickFunction.value) rowClickFunction.value(row);
|
|
}
|
|
|
|
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 () => {};
|
|
});
|
|
</script>
|
|
<template>
|
|
<RightMenu v-if="$props.rightSearch" :overlay="overlay">
|
|
<template #right-panel>
|
|
<VnTableFilter
|
|
:data-key="$attrs['data-key']"
|
|
:columns="columns"
|
|
:redirect="redirect"
|
|
>
|
|
<template
|
|
v-for="(_, slotName) in $slots"
|
|
#[slotName]="slotData"
|
|
:key="slotName"
|
|
>
|
|
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
|
|
</template>
|
|
</VnTableFilter>
|
|
</template>
|
|
</RightMenu>
|
|
<CrudModel
|
|
v-bind="$attrs"
|
|
:class="$attrs['class'] ?? 'q-px-md'"
|
|
:limit="$attrs['limit'] ?? 100"
|
|
ref="CrudModelRef"
|
|
@on-fetch="(...args) => emit('onFetch', ...args)"
|
|
:search-url="searchUrl"
|
|
:disable-infinite-scroll="isTableMode"
|
|
:before-save-fn="removeTextValue"
|
|
@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',
|
|
table ? 'selection-cell' : '',
|
|
$props.footer ? 'last-row-sticky' : '',
|
|
]"
|
|
wrap-cells
|
|
: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="(event, row) => handleRowClick(event, row)"
|
|
@update:selected="emit('update:selected', $event)"
|
|
@selection="(details) => handleSelection(details, rows)"
|
|
:hide-selected-banner="true"
|
|
:data-cy="$props.dataCy ?? 'vnTable'"
|
|
>
|
|
<template #top-left v-if="!$props.withoutHeader">
|
|
<slot name="top-left"> </slot>
|
|
</template>
|
|
<template #top-right v-if="!$props.withoutHeader">
|
|
<slot name="top-right"></slot>
|
|
<VnVisibleColumn
|
|
v-if="isTableMode"
|
|
v-model="splittedColumns.columns"
|
|
:table-code="tableCode ?? route.name"
|
|
:skip="columnsVisibilitySkipped"
|
|
/>
|
|
<QBtnToggle
|
|
v-if="!tableModes.some((mode) => mode.disable)"
|
|
v-model="mode"
|
|
toggle-color="primary"
|
|
class="bg-vn-section-color"
|
|
dense
|
|
:options="tableModes.filter((mode) => !mode.disable)"
|
|
/>
|
|
</template>
|
|
<template #header-cell="{ col }">
|
|
<QTh
|
|
v-if="col.visible ?? true"
|
|
v-bind:class="col.headerClass"
|
|
class="body-cell"
|
|
:style="col?.width ? `max-width: ${col?.width}` : ''"
|
|
>
|
|
<div
|
|
class="no-padding"
|
|
:style="[
|
|
withFilters && $props.columnSearch ? 'height: 75px' : '',
|
|
]"
|
|
>
|
|
<div 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?.labelAbbreviation ?? col?.label"
|
|
:data-key="$attrs['data-key']"
|
|
:search-url="searchUrl"
|
|
:align="getColAlign(col)"
|
|
/>
|
|
</div>
|
|
<VnFilter
|
|
v-if="
|
|
$props.columnSearch &&
|
|
col.columnSearch !== false &&
|
|
withFilters
|
|
"
|
|
:column="col"
|
|
:show-title="true"
|
|
:data-key="$attrs['data-key']"
|
|
v-model="params[columnName(col)]"
|
|
:search-url="searchUrl"
|
|
customClass="header-filter"
|
|
/>
|
|
</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 }">
|
|
<QTd
|
|
class="no-margin q-px-xs"
|
|
v-if="col.visible ?? true"
|
|
:style="{
|
|
'max-width': col?.width ?? false,
|
|
position: 'relative',
|
|
}"
|
|
:class="[
|
|
col.columnClass,
|
|
'body-cell no-margin no-padding',
|
|
getColAlign(col),
|
|
]"
|
|
:data-row-index="rowIndex"
|
|
:data-col-field="col?.name"
|
|
>
|
|
<div
|
|
class="no-padding no-margin peter"
|
|
style="
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
"
|
|
>
|
|
<slot
|
|
:name="`column-${col.name}`"
|
|
:col="col"
|
|
:row="row"
|
|
:row-index="rowIndex"
|
|
>
|
|
<QIcon
|
|
v-if="col?.component === 'toggle'"
|
|
:name="
|
|
col?.getIcon
|
|
? col.getIcon(row[col?.name])
|
|
: getToggleIcon(row[col?.name])
|
|
"
|
|
style="color: var(--vn-text-color)"
|
|
:class="hasEditableFormat(col)"
|
|
size="14px"
|
|
/>
|
|
<QIcon
|
|
v-else-if="col?.component === 'checkbox'"
|
|
:name="getCheckboxIcon(row[col?.name])"
|
|
style="color: var(--vn-text-color)"
|
|
:class="hasEditableFormat(col)"
|
|
size="14px"
|
|
/>
|
|
<span
|
|
v-else
|
|
:class="hasEditableFormat(col)"
|
|
:style="
|
|
typeof col?.style == 'function'
|
|
? col.style(row)
|
|
: col?.style
|
|
"
|
|
style="bottom: 0"
|
|
>
|
|
{{ formatColumnValue(col, row, dashIfEmpty) }}
|
|
</span>
|
|
</slot>
|
|
</div>
|
|
</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-label'
|
|
"
|
|
:style="`visibility: ${
|
|
((btn.show && btn.show(row)) ?? true)
|
|
? 'visible'
|
|
: 'hidden'
|
|
}`"
|
|
@click="btn.action(row)"
|
|
:data-cy="btn?.name ?? `tableAction-${index}`"
|
|
/>
|
|
</QTd>
|
|
</template>
|
|
<template #item="{ row, colsMap }">
|
|
<component
|
|
v-bind:is="'div'"
|
|
@click="(event) => cardClick(event, row)"
|
|
>
|
|
<QCard
|
|
bordered
|
|
flat
|
|
class="row no-wrap justify-between cursor-pointer q-pa-sm"
|
|
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-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"
|
|
>
|
|
<VnColumn
|
|
: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"
|
|
@click="stopEventPropagation($event)"
|
|
>
|
|
<QBtn
|
|
v-for="(btn, index) of splittedColumns.actions
|
|
.actions"
|
|
:key="index"
|
|
:title="btn.title"
|
|
:icon="btn.icon"
|
|
data-cy="cardBtn"
|
|
class="q-pa-xs"
|
|
:class="
|
|
btn.isPrimary
|
|
? 'text-primary-light'
|
|
: 'color-vn-label'
|
|
"
|
|
flat
|
|
@click="btn.action(row)"
|
|
/>
|
|
</QCardSection>
|
|
</QCard>
|
|
</component>
|
|
</template>
|
|
<template #bottom-row="{ cols }" v-if="$props.footer">
|
|
<QTr v-if="rows.length" style="height: 45px">
|
|
<QTh v-if="table.selection" />
|
|
<QTh
|
|
v-for="col of cols.filter((cols) => cols.visible ?? true)"
|
|
:key="col?.id"
|
|
:class="getColAlign(col)"
|
|
>
|
|
<slot
|
|
:name="`column-footer-${col.name}`"
|
|
:isEditableColumn="isEditableColumn(col)"
|
|
/>
|
|
</QTh>
|
|
</QTr>
|
|
</template>
|
|
</QTable>
|
|
<div class="full-width bottomButton" v-if="bottom">
|
|
<QBtn
|
|
@click="
|
|
() =>
|
|
createAsDialog
|
|
? (showForm = !showForm)
|
|
: handleOnDataSaved(create)
|
|
"
|
|
class="cursor-pointer fill-icon"
|
|
color="primary"
|
|
icon="add_circle"
|
|
size="md"
|
|
round
|
|
flat
|
|
v-shortcut="'+'"
|
|
:disabled="!disabledAttr"
|
|
/>
|
|
<QTooltip>
|
|
{{ createForm.title }}
|
|
</QTooltip>
|
|
</div>
|
|
</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"
|
|
v-shortcut="'+'"
|
|
data-cy="vnTableCreateBtn"
|
|
/>
|
|
<QTooltip self="top right">
|
|
{{ createForm?.title }}
|
|
</QTooltip>
|
|
</QPageSticky>
|
|
<QDialog
|
|
v-model="showForm"
|
|
transition-show="scale"
|
|
transition-hide="scale"
|
|
:full-width="createComplement?.isFullWidth ?? false"
|
|
data-cy="vn-table-create-dialog"
|
|
>
|
|
<FormModelPopup
|
|
ref="createRef"
|
|
v-bind="createForm"
|
|
:model="$attrs['data-key'] + 'Create'"
|
|
@on-data-saved="(_, res) => createForm.onDataSaved(res)"
|
|
>
|
|
<template #form-inputs="{ data }">
|
|
<div :style="createComplement?.containerStyle">
|
|
<div
|
|
:style="createComplement?.previousStyle"
|
|
v-if="!quasar.screen.xs"
|
|
>
|
|
<slot name="previous-create-dialog" :data="data" />
|
|
</div>
|
|
<div class="grid-create" :style="createComplement?.columnGridStyle">
|
|
<slot
|
|
v-for="column of splittedColumns.create"
|
|
:key="column.name"
|
|
:name="`column-create-${column.name}`"
|
|
:data="data"
|
|
:column-name="column.name"
|
|
:label="column.label"
|
|
>
|
|
<VnColumn
|
|
:column="{
|
|
...column,
|
|
...{ disable: column?.createDisable ?? false },
|
|
}"
|
|
:row="{}"
|
|
default="input"
|
|
v-model="data[column.name]"
|
|
:show-label="true"
|
|
component-prop="columnCreate"
|
|
:data-cy="`${column.name}-create-popup`"
|
|
/>
|
|
</slot>
|
|
<slot name="more-create-dialog" :data="data" />
|
|
</div>
|
|
</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">
|
|
.selection-cell {
|
|
table td:first-child {
|
|
padding: 0px;
|
|
}
|
|
}
|
|
.side-padding {
|
|
padding-left: 1px;
|
|
padding-right: 1px;
|
|
}
|
|
.editable-text:hover {
|
|
border-bottom: 1px dashed var(--q-primary);
|
|
@extend .side-padding;
|
|
}
|
|
.editable-text {
|
|
border-bottom: 1px dashed var(--vn-label-color);
|
|
@extend .side-padding;
|
|
}
|
|
.cell-input {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
padding-top: 0px !important;
|
|
}
|
|
.q-field--labeled .q-field__native,
|
|
.q-field--labeled .q-field__prefix,
|
|
.q-field--labeled .q-field__suffix {
|
|
padding-top: 20px;
|
|
}
|
|
|
|
.body-cell {
|
|
padding-left: 4px !important;
|
|
padding-right: 4px !important;
|
|
position: relative;
|
|
}
|
|
.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(300px, max-content));
|
|
width: 100%;
|
|
grid-gap: 20px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.grid-create {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
max-width: 100%;
|
|
grid-gap: 20px;
|
|
margin: 0 auto;
|
|
.col-span-2 {
|
|
grid-column: span 2;
|
|
}
|
|
}
|
|
|
|
.flex-one {
|
|
display: flex;
|
|
flex-flow: row wrap;
|
|
div.fields {
|
|
width: 100%;
|
|
.vn-label-value {
|
|
display: flex;
|
|
gap: 2%;
|
|
}
|
|
}
|
|
}
|
|
.q-table tbody tr td {
|
|
position: relative;
|
|
}
|
|
.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;
|
|
align-items: center;
|
|
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: 2fr 2fr;
|
|
.vn-label-value {
|
|
flex-direction: column;
|
|
white-space: nowrap;
|
|
.fields {
|
|
display: flex;
|
|
}
|
|
}
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
.temp-input {
|
|
top: 0;
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
}
|
|
|
|
label.header-filter > .q-field__inner > .q-field__control {
|
|
padding: inherit;
|
|
}
|
|
</style>
|