feat(grid): add split toolbar layout option

This commit is contained in:
_Mr丶s ゛😁® authored and GitHub committed 2026-09-23 19:17:08 +08:00
1 parent 6dcd31c0d2
commit 645adff972
19 files changed
+182 -16

No files matched your search

@@ -88,6 +88,7 @@ import {
type DeleteConnectionTabHandlingMode,
type DataTabReuseMode,
type DataGridFilterEditorView,
type DataGridToolbarLayout,
type MultiStatementDefaultView,
type OpenTabsRestoreMode,
type AppCloseUnsavedTabsMode,
@@ -665,6 +666,7 @@ const editShowIndexIndicatorsInHeader = ref(settingsStore.editorSettings.showInd
const editCompactColumnHeaderActions = ref(settingsStore.editorSettings.compactColumnHeaderActions);
const editDataGridQuickEntry = ref(settingsStore.editorSettings.dataGridQuickEntry);
const editDataGridFilterEditorView = ref<DataGridFilterEditorView>(settingsStore.editorSettings.dataGridFilterEditorView);
const editDataGridToolbarLayout = ref<DataGridToolbarLayout>(settingsStore.editorSettings.dataGridToolbarLayout);
const editDataGridKeepFilterEditorExpanded = ref(settingsStore.editorSettings.dataGridKeepFilterEditorExpanded);
const dataGridFilterViewPreviewExpanded = ref(true);
const editDataGridTextFilterPanelHeight = ref(settingsStore.editorSettings.dataGridTextFilterPanelHeight);
@@ -1006,6 +1008,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
compactColumnHeaderActions: editCompactColumnHeaderActions.value,
dataGridQuickEntry: editDataGridQuickEntry.value,
dataGridFilterEditorView: editDataGridFilterEditorView.value,
dataGridToolbarLayout: editDataGridToolbarLayout.value,
dataGridKeepFilterEditorExpanded: editDataGridKeepFilterEditorExpanded.value,
dataGridTextFilterPanelHeight: editDataGridTextFilterPanelHeight.value,
defaultAutoKeepResults: editDefaultAutoKeepResults.value,
@@ -1649,6 +1652,7 @@ function syncEditorSettingsDraftFromStore() {
editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions;
editDataGridQuickEntry.value = settingsStore.editorSettings.dataGridQuickEntry;
editDataGridFilterEditorView.value = settingsStore.editorSettings.dataGridFilterEditorView;
editDataGridToolbarLayout.value = settingsStore.editorSettings.dataGridToolbarLayout;
editDataGridKeepFilterEditorExpanded.value = settingsStore.editorSettings.dataGridKeepFilterEditorExpanded;
editDataGridTextFilterPanelHeight.value = settingsStore.editorSettings.dataGridTextFilterPanelHeight;
editDefaultAutoKeepResults.value = settingsStore.editorSettings.defaultAutoKeepResults;
@@ -1780,6 +1784,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
compactColumnHeaderActions: editCompactColumnHeaderActions,
dataGridQuickEntry: editDataGridQuickEntry,
dataGridFilterEditorView: editDataGridFilterEditorView,
dataGridToolbarLayout: editDataGridToolbarLayout,
dataGridKeepFilterEditorExpanded: editDataGridKeepFilterEditorExpanded,
dataGridTextFilterPanelHeight: editDataGridTextFilterPanelHeight,
defaultAutoKeepResults: editDefaultAutoKeepResults,
@@ -2302,6 +2307,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
editDataGridFilterEditorView.value = DEFAULT_EDITOR_SETTINGS.dataGridFilterEditorView;
editDataGridToolbarLayout.value = DEFAULT_EDITOR_SETTINGS.dataGridToolbarLayout;
editDataGridKeepFilterEditorExpanded.value = DEFAULT_EDITOR_SETTINGS.dataGridKeepFilterEditorExpanded;
editDataGridTextFilterPanelHeight.value = DEFAULT_EDITOR_SETTINGS.dataGridTextFilterPanelHeight;
editDefaultAutoKeepResults.value = DEFAULT_EDITOR_SETTINGS.defaultAutoKeepResults;
@@ -2407,6 +2413,7 @@ function resetAllDefaults() {
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
editDataGridFilterEditorView.value = DEFAULT_EDITOR_SETTINGS.dataGridFilterEditorView;
editDataGridToolbarLayout.value = DEFAULT_EDITOR_SETTINGS.dataGridToolbarLayout;
editDataGridKeepFilterEditorExpanded.value = DEFAULT_EDITOR_SETTINGS.dataGridKeepFilterEditorExpanded;
editDataGridTextFilterPanelHeight.value = DEFAULT_EDITOR_SETTINGS.dataGridTextFilterPanelHeight;
editDefaultAutoKeepResults.value = DEFAULT_EDITOR_SETTINGS.defaultAutoKeepResults;
@@ -7537,6 +7544,21 @@ onUnmounted(() => {
<Button type="button" variant="outline" size="sm" @click="historyRetention.load">{{ t("common.retry") }}</Button>
</div>
</div>
<div id="data-grid-toolbar-layout" data-settings-search-id="data-grid-toolbar-layout" :class="['settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2', settingsSearchTargetClass('data-grid-toolbar-layout')]">
<div class="min-w-0 space-y-1">
<Label for="data-grid-toolbar-layout-select">{{ t("settings.dataGridToolbarLayout") }}</Label>
<p class="text-xs text-muted-foreground">{{ t("settings.dataGridToolbarLayoutDescription") }}</p>
</div>
<Select v-model="editDataGridToolbarLayout">
<SelectTrigger id="data-grid-toolbar-layout-select" class="w-48 shrink-0">
<SelectValue :placeholder="t('settings.dataGridToolbarLayout')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="split">{{ t("settings.dataGridToolbarLayoutSplit") }}</SelectItem>
<SelectItem value="single">{{ t("settings.dataGridToolbarLayoutSingle") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div data-settings-search-id="data-grid-filter-view" :class="['overflow-hidden rounded-md border bg-muted/20', settingsSearchTargetClass('data-grid-filter-view')]">
<div class="space-y-3 p-3">
<div class="flex items-start justify-between gap-4">
+26 -4
View File
@@ -758,6 +758,7 @@ const compactColumnHeaderActions = computed(() => settingsStore.editorSettings.c
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode);
const compactDataGridToolbar = computed(() => dataGridTopbarOverflowCompact.value || isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH));
const splitDataGridToolbar = computed(() => settingsStore.editorSettings.dataGridToolbarLayout === "split");
const responsiveDataGridToolbarActionCount = computed(() => dataGridToolbarActionCollapseCount(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH));
const compactDataGridToolbarActionCount = computed(() => Math.max(responsiveDataGridToolbarActionCount.value, dataGridTopbarOverflowActionCount.value));
const infiniteScrollEnabled = computed(() => props.paginationEnabled && settingsStore.editorSettings.infiniteScroll);
@@ -11974,12 +11975,18 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
<div v-if="hasData || canShowWhereSearch" class="flex-1 flex flex-col overflow-hidden" @contextmenu="onContextMenu">
<!-- Search bar -->
<!-- Leave real vertical space around the 28px controls instead of fitting them against the border. -->
<div ref="dataGridTopbarRef" v-if="showDataGridTopbar" class="data-grid-topbar-shell flex h-8 min-w-0 shrink-0 items-center border-b bg-muted/20">
<div v-if="hasResultToolbarLeadingSlot" class="flex shrink-0 items-center border-r">
<div
ref="dataGridTopbarRef"
v-if="showDataGridTopbar"
:data-grid-toolbar-layout="settingsStore.editorSettings.dataGridToolbarLayout"
class="data-grid-topbar-shell min-w-0 shrink-0 border-b bg-muted/20"
:class="splitDataGridToolbar ? 'grid h-16 grid-cols-[auto_minmax(0,1fr)] grid-rows-2' : 'flex h-8 items-center'"
>
<div v-if="hasResultToolbarLeadingSlot" data-grid-topbar-row="actions" class="flex shrink-0 items-center border-r" :class="splitDataGridToolbar ? 'col-start-1 row-start-1' : ''">
<slot name="result-toolbar-leading" :compact="compactDataGridToolbar" />
</div>
<!-- Clip both axes instead of creating a hidden scroll container around the toolbar controls. -->
<div class="data-grid-topbar-scroll min-w-0 flex-1 overflow-clip">
<div data-grid-topbar-row="filters" class="data-grid-topbar-scroll min-w-0 overflow-clip" :class="splitDataGridToolbar ? 'data-grid-topbar-scroll--row-divider col-span-2 row-start-2' : 'flex-1'">
<div class="data-grid-topbar flex items-stretch relative" :class="{ 'data-grid-topbar--compact': compactDataGridToolbar }">
<div v-if="useTransaction && editable && hasDataGridSaveTarget" class="flex items-center px-2 py-0.5 border-r shrink-0">
<Select :model-value="rowStatusFilter" @update:model-value="(value: any) => setRowStatusFilter(String(value))">
@@ -12049,7 +12056,8 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
</div>
<DataGridToolbar
class="ml-auto"
data-grid-topbar-row="actions"
:class="splitDataGridToolbar ? 'col-start-2 row-start-1' : 'ml-auto'"
:compact-action-count="compactDataGridToolbarActionCount"
:navigation-visible="props.result.columns.length > 0"
:refresh="refreshToolbarCapability"
@@ -14343,6 +14351,20 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
scrollbar-gutter: auto;
}
.data-grid-topbar-scroll--row-divider {
position: relative;
}
.data-grid-topbar-scroll--row-divider::before {
position: absolute;
inset: 0 0 auto;
z-index: 1;
height: 1px;
background-color: var(--border);
content: "";
pointer-events: none;
}
.data-grid-topbar-scroll::-webkit-scrollbar {
display: none;
}
@@ -50,7 +50,7 @@ const RecycleScroller = defineComponent({
},
});
function mountGrid(options: { displayableColumns?: boolean; columns?: string[] } = {}) {
function mountGrid(options: { displayableColumns?: boolean; columns?: string[]; slots?: Record<string, () => ReturnType<typeof h>>; withTableMetadata?: boolean } = {}) {
const pinia = createPinia();
setActivePinia(pinia);
const settingsStore = useSettingsStore();
@@ -79,11 +79,22 @@ function mountGrid(options: { displayableColumns?: boolean; columns?: string[] }
{ delayDuration: 0 },
{
default: () =>
h(DataGrid, {
result,
databaseType: "mysql",
context: "table-data",
}),
h(
DataGrid,
{
result,
databaseType: "mysql",
context: "table-data",
...(options.withTableMetadata
? {
connectionId: "test-connection",
database: "test-database",
tableMeta: { tableName: "users", schema: "test-database", columns: [], primaryKeys: [] },
}
: {}),
},
options.slots,
),
},
);
},
@@ -95,7 +106,7 @@ function mountGrid(options: { displayableColumns?: boolean; columns?: string[] }
app.mount(host);
const mounted = { app, host };
mountedApps.push(mounted);
return mounted;
return { ...mounted, settingsStore };
}
async function settle() {
@@ -129,6 +140,7 @@ function goToColumnEvent(target: HTMLElement): KeyboardEvent {
}
afterEach(() => {
vi.restoreAllMocks();
for (const { app, host } of mountedApps.splice(0)) {
app.unmount();
host.remove();
@@ -142,6 +154,58 @@ function functionBody(name: string, nextName: string): string {
}
describe("DataGrid go-to-column shortcut", () => {
it("switches between the split and original single-row toolbar layouts", async () => {
const { host, settingsStore } = mountGrid();
await settle();
const topbar = host.querySelector<HTMLElement>("[data-grid-toolbar-layout]");
expect(topbar?.dataset.gridToolbarLayout).toBe("single");
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
await settle();
expect(topbar?.dataset.gridToolbarLayout).toBe("split");
expect(topbar?.classList.contains("grid")).toBe(true);
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "single" });
await settle();
expect(topbar?.dataset.gridToolbarLayout).toBe("single");
expect(topbar?.classList.contains("flex")).toBe(true);
expect(host.querySelector('[data-grid-topbar-row="actions"].ml-auto')).not.toBeNull();
settingsStore.updateEditorSettings({ dataGridToolbarLayout: "split" });
await settle();
expect(topbar?.dataset.gridToolbarLayout).toBe("split");
expect(topbar?.classList.contains("grid")).toBe(true);
expect(host.querySelector('[data-grid-topbar-row="filters"].data-grid-topbar-scroll--row-divider')).not.toBeNull();
});
it("does not show a DDL action in the result action toolbar", async () => {
const { host } = mountGrid({ withTableMetadata: true });
await settle();
expect(host.querySelector('[data-toolbar-action="tableInfo"]')).toBeNull();
});
it("keeps result actions and query filters in their separate toolbar rows", async () => {
const { host } = mountGrid({
slots: {
"result-toolbar-leading": () => h("span", { "data-testid": "result-view" }, "结果视图"),
"result-toolbar-actions": () => h("span", { "data-testid": "result-actions" }, "结果操作"),
"search-bar": () => h("span", { "data-testid": "search-controls" }, "文档筛选"),
},
});
await settle();
const actionRowSelector = '[data-grid-topbar-row="actions"]';
const filterRowSelector = '[data-grid-topbar-row="filters"]';
expect(host.querySelector('[data-testid="result-view"]')?.closest(actionRowSelector)).not.toBeNull();
expect(host.querySelector('[data-testid="result-actions"]')?.closest(actionRowSelector)).not.toBeNull();
expect(host.querySelector('[data-testid="search-controls"]')?.closest(filterRowSelector)).not.toBeNull();
});
it("opens and consumes the configured shortcut when a column is displayable", async () => {
const { host } = mountGrid();
await settle();