mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-09 08:00:27 +00:00
refactor: select bind (#13993)
* refactor: select bind * refactor: simplify table selection state handling --------- Co-authored-by: ssongliu <sloooop1x@gmail.com>
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
<fu-table
|
||||
v-if="currentViewMode === 'table'"
|
||||
v-bind="$attrs"
|
||||
:data="props.data"
|
||||
ref="tableRef"
|
||||
@select="handleSelect"
|
||||
@selection-change="handleSelectionChange"
|
||||
@@ -175,7 +176,7 @@ import { useResponsivePagination } from './useResponsivePagination';
|
||||
import { useTableSelection } from './useTableSelection';
|
||||
const slots = useSlots();
|
||||
const attrs = useAttrs();
|
||||
const { isMobile, openMenuTabs } = useGlobalStore();
|
||||
const { isMobile, openMenuTabs, currentNode } = useGlobalStore();
|
||||
|
||||
defineOptions({ name: 'ComplexTable' });
|
||||
export type DropdownProps = FuTableOperationButton;
|
||||
@@ -184,6 +185,8 @@ type ViewMode = 'table' | 'card';
|
||||
|
||||
const props = defineProps({
|
||||
header: String,
|
||||
data: { type: Array as PropType<any[]>, default: () => [] },
|
||||
selectionContext: { type: [String, Number, Array, Object, Function], default: undefined },
|
||||
paginationConfig: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@@ -239,7 +242,7 @@ const selectionColumn = computed(() =>
|
||||
columnNodes.value.find((column) => (column.props as Record<string, any> | null)?.type === 'selection'),
|
||||
);
|
||||
const tableData = computed(() => {
|
||||
const data = attrs.data;
|
||||
const data = props.data;
|
||||
return Array.isArray(data) ? data : [];
|
||||
});
|
||||
const cardContentStyle = computed(() => {
|
||||
@@ -295,7 +298,7 @@ const {
|
||||
isDisabled: isRightButtonDisabled,
|
||||
click: rightButtonClick,
|
||||
} = useContextMenu(() => props.rightButtons);
|
||||
const getTableData = () => (Array.isArray(attrs.data) ? attrs.data : []);
|
||||
const getTableData = () => props.data || [];
|
||||
const isRowSelectable = (row: any) => {
|
||||
const selectable = (selectionColumn.value?.props as Record<string, any> | null)?.selectable;
|
||||
return typeof selectable === 'function' ? selectable(row) : true;
|
||||
@@ -304,7 +307,6 @@ const {
|
||||
selectedRows,
|
||||
isRowSelected,
|
||||
clearSelects,
|
||||
pruneSelection,
|
||||
toggleSelection,
|
||||
selectRow,
|
||||
syncTableSelection,
|
||||
@@ -313,7 +315,18 @@ const {
|
||||
handleRowClick,
|
||||
handleKeyDown: handleSelectionKeyDown,
|
||||
handleKeyUp,
|
||||
} = useTableSelection(tableRef, getTableData, (rows) => emit('update:selects', rows), isRowSelectable);
|
||||
} = useTableSelection(
|
||||
tableRef,
|
||||
getTableData,
|
||||
(rows) => emit('update:selects', rows),
|
||||
isRowSelectable,
|
||||
(row) => {
|
||||
const key = attrs.rowKey ?? attrs['row-key'];
|
||||
if (typeof key === 'function') return row == null ? undefined : key(row);
|
||||
if (typeof key === 'string') return key.split('.').reduce((value, part) => value?.[part], row);
|
||||
return row?.id;
|
||||
},
|
||||
);
|
||||
const toggleCardSelection = (row: any, selected: boolean) => {
|
||||
selectRow(row, selected);
|
||||
};
|
||||
@@ -446,7 +459,26 @@ watch([currentViewMode, tableData, () => props.syncCardContentHeight], scheduleC
|
||||
flush: 'post',
|
||||
});
|
||||
|
||||
watch(tableData, pruneSelection);
|
||||
watch([() => props.paginationConfig?.currentPage, () => props.paginationConfig?.pageSize], clearSelects, {
|
||||
flush: 'sync',
|
||||
});
|
||||
|
||||
watch(
|
||||
[
|
||||
currentNode,
|
||||
() =>
|
||||
JSON.stringify(
|
||||
typeof props.selectionContext === 'function' ? props.selectionContext() : props.selectionContext,
|
||||
),
|
||||
],
|
||||
() => {
|
||||
clearSelects();
|
||||
if (props.paginationConfig) {
|
||||
props.paginationConfig.currentPage = 1;
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', calcHeight);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { shallowRef, ref, toRaw, type Ref } from 'vue';
|
||||
import { shallowRef, ref, toRaw, watch, type Ref } from 'vue';
|
||||
|
||||
type TableRef = { refElTable?: any } | undefined;
|
||||
|
||||
@@ -7,6 +7,7 @@ export const useTableSelection = (
|
||||
getTableData: () => any[],
|
||||
onSelectionChange: (rows: any[]) => void,
|
||||
isRowSelectable: (row: any) => boolean = () => true,
|
||||
getRowKey: (row: any) => unknown = (row) => row?.id,
|
||||
) => {
|
||||
const selectedRows = shallowRef<any[]>([]);
|
||||
const shiftPressed = ref(false);
|
||||
@@ -15,8 +16,13 @@ export const useTableSelection = (
|
||||
let isSyncingTableSelection = false;
|
||||
let skipNextSelectionChange = false;
|
||||
|
||||
// Element Plus can return reactive proxies for rows supplied as plain objects.
|
||||
const sameRow = (left: any, right: any) => toRaw(left) === toRaw(right);
|
||||
let isRefreshingData = false;
|
||||
const sameRow = (left: any, right: any) => {
|
||||
if (left == null || right == null) return left === right;
|
||||
const leftKey = getRowKey(left);
|
||||
const rightKey = getRowKey(right);
|
||||
return leftKey != null && rightKey != null ? leftKey === rightKey : toRaw(left) === toRaw(right);
|
||||
};
|
||||
const hasRow = (rows: any[], row: any) => rows.some((item) => sameRow(item, row));
|
||||
const isRowSelected = (row: any) => hasRow(selectedRows.value, row);
|
||||
const getTable = () => tableRef.value?.refElTable;
|
||||
@@ -39,8 +45,12 @@ export const useTableSelection = (
|
||||
const tableData = getTableData();
|
||||
const nextRows = selectedRows.value.filter((row) => hasRow(tableData, row) && isRowSelectable(row));
|
||||
const currentRows = table.getSelectionRows();
|
||||
currentRows.filter((row) => !hasRow(nextRows, row)).forEach((row) => table.toggleRowSelection(row, false));
|
||||
nextRows.filter((row) => !hasRow(currentRows, row)).forEach((row) => table.toggleRowSelection(row, true));
|
||||
currentRows
|
||||
.filter((row) => !nextRows.some((item) => toRaw(item) === toRaw(row)))
|
||||
.forEach((row) => table.toggleRowSelection(row, false));
|
||||
nextRows
|
||||
.filter((row) => !currentRows.some((item) => toRaw(item) === toRaw(row)))
|
||||
.forEach((row) => table.toggleRowSelection(row, true));
|
||||
} finally {
|
||||
isSyncingTableSelection = false;
|
||||
}
|
||||
@@ -73,7 +83,7 @@ export const useTableSelection = (
|
||||
return true;
|
||||
};
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
if (isSyncingTableSelection) {
|
||||
if (isSyncingTableSelection || isRefreshingData) {
|
||||
return;
|
||||
}
|
||||
if (skipNextSelectionChange) {
|
||||
@@ -104,16 +114,35 @@ export const useTableSelection = (
|
||||
rangeBaseRows.value = [];
|
||||
};
|
||||
const pruneSelection = () => {
|
||||
const nextRows = selectedRows.value.filter((row) => hasRow(getTableData(), row));
|
||||
if (nextRows.length !== selectedRows.value.length) {
|
||||
const data = getTableData();
|
||||
const nextRows = data.filter((row) => hasRow(selectedRows.value, row) && isRowSelectable(row));
|
||||
if (nextRows.length !== selectedRows.value.length || nextRows.some((row, i) => row !== selectedRows.value[i])) {
|
||||
setSelectedRows(nextRows);
|
||||
}
|
||||
if (lastSelectedRow.value && !hasRow(nextRows, lastSelectedRow.value)) {
|
||||
lastSelectedRow.value = null;
|
||||
rangeBaseRows.value = [];
|
||||
}
|
||||
syncTableSelection();
|
||||
lastSelectedRow.value = nextRows.find((row) => sameRow(row, lastSelectedRow.value)) ?? null;
|
||||
rangeBaseRows.value = lastSelectedRow.value ? nextRows.filter((row) => hasRow(rangeBaseRows.value, row)) : [];
|
||||
};
|
||||
// Guard before the child table reacts to replacement data and emits an empty selection.
|
||||
watch(
|
||||
getTableData,
|
||||
() => {
|
||||
isRefreshingData = true;
|
||||
skipNextSelectionChange = false;
|
||||
pruneSelection();
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
watch(
|
||||
getTableData,
|
||||
() => {
|
||||
try {
|
||||
syncTableSelection();
|
||||
} finally {
|
||||
isRefreshingData = false;
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
const toggleSelection = () => {
|
||||
const selectableRows = getTableData().filter(isRowSelectable);
|
||||
const allSelected = selectableRows.length > 0 && selectableRows.every(isRowSelected);
|
||||
@@ -155,7 +184,6 @@ export const useTableSelection = (
|
||||
selectedRows,
|
||||
isRowSelected,
|
||||
clearSelects,
|
||||
pruneSelection,
|
||||
toggleSelection,
|
||||
selectRow,
|
||||
syncTableSelection,
|
||||
|
||||
@@ -65,6 +65,8 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="id"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:class="{ mask: maskShow }"
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName, activeTag, includeAppStore, props.filters]"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:view-mode="viewMode"
|
||||
v-model:selects="selects"
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="id"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="id"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="name"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName, searchGroupID]"
|
||||
row-key="id"
|
||||
:pagination-config="paginationConfig"
|
||||
:default-sort="
|
||||
paginationConfig.order !== 'null'
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchInfo, group]"
|
||||
row-key="id"
|
||||
v-model:selects="selects"
|
||||
:pagination-config="paginationConfig"
|
||||
:data="data"
|
||||
|
||||
@@ -57,6 +57,18 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName, fireName]"
|
||||
:row-key="
|
||||
(row) =>
|
||||
JSON.stringify([
|
||||
row.family,
|
||||
row.protocol,
|
||||
row.port,
|
||||
row.targetIP,
|
||||
row.targetPort,
|
||||
row.interface,
|
||||
])
|
||||
"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
@search="search"
|
||||
|
||||
@@ -185,6 +185,7 @@
|
||||
<template #main>
|
||||
<div v-loading="loading">
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName, selectedRuleFilters, visibleIptablesChains]"
|
||||
v-model:selects="selects"
|
||||
:pagination-config="paginationConfig"
|
||||
:data="allRows"
|
||||
@@ -661,7 +662,6 @@ const loadRules = async (refreshUsage: boolean) => {
|
||||
paginationConfig.total = total;
|
||||
inventoryTotal.value = response.data.allTotal || 0;
|
||||
managedTotal.value = response.data.managedTotal || 0;
|
||||
selects.value = [];
|
||||
} finally {
|
||||
if (requestID === searchRequestID) loading.value = false;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="id"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
</el-card>
|
||||
<template #main v-if="clamStatus.isExist">
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="id"
|
||||
:class="{ mask: !clamStatus.isRunning }"
|
||||
v-if="!isSettingShow"
|
||||
:pagination-config="paginationConfig"
|
||||
|
||||
@@ -66,6 +66,8 @@
|
||||
</template>
|
||||
<template v-if="!openNginxConfig" #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [req.name, req.type, req.websiteGroupId]"
|
||||
row-key="id"
|
||||
:pagination-config="paginationConfig"
|
||||
:default-sort="tableSort.order ? tableSort : undefined"
|
||||
v-model:view-mode="viewMode"
|
||||
@@ -498,7 +500,6 @@ const search = async () => {
|
||||
req.pageSize = paginationConfig.pageSize;
|
||||
|
||||
loading.value = true;
|
||||
data.value = [];
|
||||
await searchWebsites(req)
|
||||
.then((res) => {
|
||||
data.value = res.data.items;
|
||||
|
||||
Reference in New Issue
Block a user