mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-29 08:11:06 +00:00
feat(grid): improve table context menu
This commit is contained in:
@@ -32,6 +32,8 @@ import {
|
||||
RefreshCcw,
|
||||
RotateCcw,
|
||||
Pencil,
|
||||
Filter,
|
||||
FileDown,
|
||||
} from "lucide-vue-next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -39,6 +41,9 @@ import {
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu";
|
||||
import {
|
||||
@@ -827,6 +832,14 @@ const selectedCellCount = computed(() => selectedCells.value.columns.length * se
|
||||
const hasCellSelection = computed(() => selectedCellCount.value > 0);
|
||||
const selectionSummary = computed(() => t("grid.selectedCells", { count: selectedCellCount.value }));
|
||||
const contextRowItem = computed(() => (contextCell.value ? getRowItem(contextCell.value.rowId) : undefined));
|
||||
const contextColumn = computed(() => {
|
||||
if (!contextCell.value || contextCell.value.col < 0) return null;
|
||||
return props.result.columns[contextCell.value.col] ?? null;
|
||||
});
|
||||
const contextCellValue = computed<CellValue | null>(() => {
|
||||
if (!contextCell.value || contextCell.value.col < 0) return null;
|
||||
return contextRowItem.value?.data[contextCell.value.col] ?? null;
|
||||
});
|
||||
const activeCellDetail = computed(() => {
|
||||
const cell = detailCell.value;
|
||||
if (!cell) return null;
|
||||
@@ -954,6 +967,50 @@ function toggleSort(colName: string, colIdx: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyContextSort(direction: "asc" | "desc" | null) {
|
||||
if (!contextColumn.value || !contextCell.value) return;
|
||||
const column = contextColumn.value;
|
||||
const columnIndex = contextCell.value.col;
|
||||
orderByInput.value = "";
|
||||
currentPage.value = 1;
|
||||
if (direction) {
|
||||
sortCol.value = column;
|
||||
sortColIndex.value = columnIndex;
|
||||
sortDir.value = direction;
|
||||
} else {
|
||||
sortCol.value = null;
|
||||
sortColIndex.value = null;
|
||||
sortDir.value = "asc";
|
||||
}
|
||||
emit("sort", column, columnIndex, direction, currentWhereInput());
|
||||
}
|
||||
|
||||
function contextFilterCondition(mode: "equals" | "not-equals" | "is-null" | "is-not-null"): string | null {
|
||||
if (!contextColumn.value) return null;
|
||||
const column = quoteIdent(contextColumn.value);
|
||||
const value = contextCellValue.value;
|
||||
|
||||
if (mode === "is-null") return `${column} IS NULL`;
|
||||
if (mode === "is-not-null") return `${column} IS NOT NULL`;
|
||||
if (value === null) return mode === "equals" ? `${column} IS NULL` : `${column} IS NOT NULL`;
|
||||
return mode === "equals" ? `${column} = ${escapeVal(value)}` : `${column} <> ${escapeVal(value)}`;
|
||||
}
|
||||
|
||||
async function applyContextFilter(mode: "equals" | "not-equals" | "is-null" | "is-not-null") {
|
||||
if (!canUseWhereSearch.value) return;
|
||||
const condition = contextFilterCondition(mode);
|
||||
if (!condition) return;
|
||||
const existing = whereFilterInput.value.trim();
|
||||
whereFilterInput.value = existing ? `(${existing}) AND (${condition})` : condition;
|
||||
await applyWhereFilter();
|
||||
}
|
||||
|
||||
async function clearContextFilter() {
|
||||
if (!canUseWhereSearch.value) return;
|
||||
whereFilterInput.value = "";
|
||||
await applyWhereFilter();
|
||||
}
|
||||
|
||||
async function applyOrderBySearch() {
|
||||
if (!props.tableMeta || !props.onExecuteSql) return;
|
||||
const orderByClause = orderByInput.value.trim() || undefined;
|
||||
@@ -1326,6 +1383,12 @@ function selectSingleCell(rowIndex: number, colIndex: number) {
|
||||
selectionFocus.value = cell;
|
||||
}
|
||||
|
||||
function selectRow(rowIndex: number) {
|
||||
if (props.result.columns.length === 0) return;
|
||||
selectionAnchor.value = { rowIndex, colIndex: 0 };
|
||||
selectionFocus.value = { rowIndex, colIndex: props.result.columns.length - 1 };
|
||||
}
|
||||
|
||||
function finishCellSelection() {
|
||||
isSelectingCells.value = false;
|
||||
document.removeEventListener("mouseup", finishCellSelection);
|
||||
@@ -1445,8 +1508,13 @@ function onCellContext(rowId: number, rowIndex: number, colIdx: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function onRowContext(rowId: number, rowIndex: number) {
|
||||
contextCell.value = { rowId, rowIndex, col: -1 };
|
||||
selectRow(rowIndex);
|
||||
}
|
||||
|
||||
function copyCell() {
|
||||
if (!contextCell.value) return;
|
||||
if (!contextCell.value || contextCell.value.col < 0) return;
|
||||
const item = getRowItem(contextCell.value.rowId);
|
||||
const val = item?.data[contextCell.value.col] ?? null;
|
||||
copyText(formatCell(val));
|
||||
@@ -1983,9 +2051,11 @@ defineExpose({
|
||||
:style="{ height: '26px', width: 'var(--total-w)' }"
|
||||
>
|
||||
<div
|
||||
class="shrink-0 px-2 py-1 border-r border-border text-center select-none"
|
||||
class="shrink-0 px-2 py-1 border-r border-border text-center select-none cursor-default hover:bg-accent/50"
|
||||
:class="rowNumberStatusClass(item)"
|
||||
:style="{ width: 'var(--row-num-w)' }"
|
||||
@click="selectRow(index)"
|
||||
@contextmenu="onRowContext(item.id, index)"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
@@ -2245,24 +2315,58 @@ defineExpose({
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
|
||||
<ContextMenuContent class="w-60">
|
||||
<ContextMenuItem @click="copyCell">{{ t("grid.copyCell") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyRow">{{ t("grid.copyRow") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyRowAsInsert">{{ t("grid.copyRowInsert") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyAll">{{ t("grid.copyAll") }}</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuContent class="w-64">
|
||||
<template v-if="contextColumn">
|
||||
<ContextMenuItem @click="applyContextSort('asc')">
|
||||
<ArrowUp class="w-3.5 h-3.5 mr-2" /> {{ t("grid.sortAscending") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="applyContextSort('desc')">
|
||||
<ArrowDown class="w-3.5 h-3.5 mr-2" /> {{ t("grid.sortDescending") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="sortCol" @click="applyContextSort(null)">
|
||||
<ArrowUpDown class="w-3.5 h-3.5 mr-2" /> {{ t("grid.clearSort") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSub v-if="canUseWhereSearch">
|
||||
<ContextMenuSubTrigger> <Filter class="w-3.5 h-3.5 mr-2" /> {{ t("grid.filter") }} </ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent class="w-56">
|
||||
<ContextMenuItem @click="applyContextFilter('equals')">{{ t("grid.filterByValue") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="applyContextFilter('not-equals')">
|
||||
{{ t("grid.filterExcludeValue") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="applyContextFilter('is-null')">{{ t("grid.filterIsNull") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="applyContextFilter('is-not-null')">
|
||||
{{ t("grid.filterIsNotNull") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem @click="clearContextFilter">{{ t("grid.clearFilter") }}</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
<ContextMenuSeparator />
|
||||
</template>
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger> <Copy class="w-3.5 h-3.5 mr-2" /> {{ t("grid.copy") }} </ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent class="w-56">
|
||||
<ContextMenuItem v-if="contextColumn" @click="copyCell">{{ t("grid.copyCell") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyRow">{{ t("grid.copyRow") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyRowAsInsert">{{ t("grid.copyRowInsert") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyAll">{{ t("grid.copyAll") }}</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
<ContextMenuItem v-if="contextCell" @click="openTranspose(contextCell.rowIndex)">
|
||||
<Rows3 class="w-3.5 h-3.5 mr-2" /> {{ t("grid.transpose") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSub v-if="hasCellSelection">
|
||||
<ContextMenuSubTrigger>{{ t("grid.selection") }}</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent class="w-60">
|
||||
<ContextMenuItem @click="copySelectionTsv">{{ t("grid.copySelectionTsv") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copySelectionCsv">{{ t("grid.copySelectionCsv") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copySelectionJson">{{ t("grid.copySelectionJson") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copySelectionSqlInList">{{ t("grid.copySelectionSql") }}</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem @click="clearCellSelection">{{ t("grid.clearSelection") }}</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
<ContextMenuSeparator />
|
||||
<template v-if="hasCellSelection">
|
||||
<ContextMenuItem @click="copySelectionTsv">{{ t("grid.copySelectionTsv") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copySelectionCsv">{{ t("grid.copySelectionCsv") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copySelectionJson">{{ t("grid.copySelectionJson") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copySelectionSqlInList">{{ t("grid.copySelectionSql") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="clearCellSelection">{{ t("grid.clearSelection") }}</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
</template>
|
||||
<template v-if="editable && contextRowItem">
|
||||
<ContextMenuItem v-if="contextRowItem.isDeleted" @click="restoreRow(contextRowItem.id)">
|
||||
<Undo2 class="w-3.5 h-3.5 mr-2" /> {{ t("grid.restoreRow") }}
|
||||
@@ -2272,9 +2376,14 @@ defineExpose({
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
</template>
|
||||
<ContextMenuItem @click="exportCsv">{{ t("grid.exportCsv") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="exportJson">{{ t("grid.exportJson") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="exportMarkdown">{{ t("grid.exportMarkdown") }}</ContextMenuItem>
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger> <FileDown class="w-3.5 h-3.5 mr-2" /> {{ t("grid.export") }} </ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent class="w-44">
|
||||
<ContextMenuItem @click="exportCsv">{{ t("grid.exportCsv") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="exportJson">{{ t("grid.exportJson") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="exportMarkdown">{{ t("grid.exportMarkdown") }}</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
|
||||
|
||||
@@ -172,18 +172,30 @@ export default {
|
||||
noSearchResultsDescription: "Try a different keyword.",
|
||||
noFilteredRows: "No rows match the filter",
|
||||
noFilteredRowsDescription: "Adjust the search text or row status filter.",
|
||||
copy: "Copy",
|
||||
copyCell: "Copy Cell",
|
||||
copyRow: "Copy Row (JSON)",
|
||||
copyRowInsert: "Copy as INSERT",
|
||||
copyAll: "Copy All (TSV)",
|
||||
selection: "Selection",
|
||||
copySelectionTsv: "Copy Selection (TSV)",
|
||||
copySelectionCsv: "Copy Selection (CSV)",
|
||||
copySelectionJson: "Copy Selection (JSON)",
|
||||
copySelectionSql: "Copy Selection as SQL IN List",
|
||||
clearSelection: "Clear Selection",
|
||||
export: "Export",
|
||||
exportCsv: "Export CSV",
|
||||
exportJson: "Export JSON",
|
||||
exportMarkdown: "Export Markdown",
|
||||
filter: "Filter",
|
||||
filterByValue: "Filter by This Value",
|
||||
filterExcludeValue: "Exclude This Value",
|
||||
filterIsNull: "Show NULL Values",
|
||||
filterIsNotNull: "Show Non-NULL Values",
|
||||
clearFilter: "Clear Filter",
|
||||
sortAscending: "Sort Ascending",
|
||||
sortDescending: "Sort Descending",
|
||||
clearSort: "Clear Sort",
|
||||
copied: "Copied!",
|
||||
search: "Search...",
|
||||
searchOrWhere: "Search, or enter a WHERE clause...",
|
||||
|
||||
@@ -171,18 +171,30 @@ export default {
|
||||
noSearchResultsDescription: "换个关键词试试。",
|
||||
noFilteredRows: "没有符合条件的行",
|
||||
noFilteredRowsDescription: "调整搜索词或状态筛选试试。",
|
||||
copy: "复制",
|
||||
copyCell: "复制单元格",
|
||||
copyRow: "复制行 (JSON)",
|
||||
copyRowInsert: "复制为 INSERT 语句",
|
||||
copyAll: "复制全部 (TSV)",
|
||||
selection: "选区",
|
||||
copySelectionTsv: "复制选区 (TSV)",
|
||||
copySelectionCsv: "复制选区 (CSV)",
|
||||
copySelectionJson: "复制选区 (JSON)",
|
||||
copySelectionSql: "复制选区为 SQL IN 列表",
|
||||
clearSelection: "清除选区",
|
||||
export: "导出",
|
||||
exportCsv: "导出 CSV",
|
||||
exportJson: "导出 JSON",
|
||||
exportMarkdown: "导出 Markdown",
|
||||
filter: "筛选",
|
||||
filterByValue: "筛选此值",
|
||||
filterExcludeValue: "排除此值",
|
||||
filterIsNull: "仅显示 NULL",
|
||||
filterIsNotNull: "仅显示非 NULL",
|
||||
clearFilter: "清除筛选",
|
||||
sortAscending: "升序排序",
|
||||
sortDescending: "降序排序",
|
||||
clearSort: "清除排序",
|
||||
copied: "已复制!",
|
||||
search: "搜索...",
|
||||
searchOrWhere: "搜索,或输入 WHERE 条件...",
|
||||
|
||||
Reference in New Issue
Block a user