diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 034d864ea3..e26b72c467 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -2282,7 +2282,7 @@ function columnWidthDisplayValue(value: CellValue, columnIndex: number): CellVal return showWhitespaceEnabled.value && typeof display === "string" ? gridCellDisplayValue(display, flatteningMultiLineEnabled.value, true) : display; } -const { initColumnWidths, onResizeStart, autoFitColumn, renderedColumnWidths, totalWidth, columnVars, getIsResizing } = useDataGridColumnResize({ +const { initColumnWidths, onResizeStart, autoFitColumn, autoFitAllColumns, renderedColumnWidths, totalWidth, columnVars, getIsResizing } = useDataGridColumnResize({ columns: visibleColumns, sourceRows: computed(() => props.result.rows), columnIndexes: visibleColumnIndexes, @@ -11524,6 +11524,7 @@ defineExpose({ hasCustomColumnOrder, moveDisplayableColumn, resetColumnOrder, + autoFitAllColumns, nullColumnsHidden, allNullColumnCount, canToggleAllNullColumns, diff --git a/apps/desktop/src/components/grid/DataGridColumnLayoutPopover.vue b/apps/desktop/src/components/grid/DataGridColumnLayoutPopover.vue index c51af7229e..7ad8b4d892 100644 --- a/apps/desktop/src/components/grid/DataGridColumnLayoutPopover.vue +++ b/apps/desktop/src/components/grid/DataGridColumnLayoutPopover.vue @@ -368,6 +368,12 @@ onBeforeUnmount(resetColumnDragState); {{ t("grid.noSearchResults") }} +
+ {{ t("grid.columnWidth") }} + +
{{ t("grid.columnVisibilityHint") }} diff --git a/apps/desktop/src/components/grid/__tests__/dataGridColumnLayoutPopover.spec.ts b/apps/desktop/src/components/grid/__tests__/dataGridColumnLayoutPopover.spec.ts index 597db006b8..00a4244de0 100644 --- a/apps/desktop/src/components/grid/__tests__/dataGridColumnLayoutPopover.spec.ts +++ b/apps/desktop/src/components/grid/__tests__/dataGridColumnLayoutPopover.spec.ts @@ -60,6 +60,7 @@ function createGrid(itemCount: number) { const options = columnLayoutOptions(itemCount); const toggleColumnVisibility = vi.fn(); const moveDisplayableColumn = vi.fn(); + const autoFitAllColumns = vi.fn(); const grid: DataGridColumnLayoutHandle = { visibleColumnCount: itemCount, displayableColumnCount: itemCount, @@ -76,10 +77,41 @@ function createGrid(itemCount: number) { hasCustomColumnOrder: false, moveDisplayableColumn, resetColumnOrder: vi.fn(), + autoFitAllColumns, }; - return { grid, moveDisplayableColumn, toggleColumnVisibility }; + return { grid, moveDisplayableColumn, toggleColumnVisibility, autoFitAllColumns }; } +// https://github.com/t8y2/dbx/issues/9813 +describe("column layout popover auto fit", () => { + it("offers an auto fit action that fits every visible column", async () => { + const { host, autoFitAllColumns } = await mountPopover(3); + + expect(host.textContent).toContain("Column Width"); + const autoFitButton = host.querySelector("[data-column-auto-fit-all]")!; + expect(autoFitButton).not.toBeNull(); + expect(autoFitButton.textContent?.trim()).toBe("Fit columns to content"); + + autoFitButton.click(); + expect(autoFitAllColumns).toHaveBeenCalledTimes(1); + }); + + it("hides the auto fit action for grids that cannot resize columns", async () => { + const gridState = createGrid(2); + delete (gridState.grid as { autoFitAllColumns?: unknown }).autoFitAllColumns; + const host = document.createElement("div"); + document.body.append(host); + const app = createApp(DataGridColumnLayoutPopover, { grid: gridState.grid }); + app.use(i18n); + app.mount(host); + mountedApps.push({ app, host }); + await nextTick(); + await nextTick(); + + expect(host.querySelector("[data-column-auto-fit-all]")).toBeNull(); + }); +}); + async function mountPopover(itemCount = 4) { const gridState = createGrid(itemCount); const host = document.createElement("div"); diff --git a/apps/desktop/src/components/grid/dataGridColumnLayoutPopover.ts b/apps/desktop/src/components/grid/dataGridColumnLayoutPopover.ts index 021dcaefa6..ab03f04b42 100644 --- a/apps/desktop/src/components/grid/dataGridColumnLayoutPopover.ts +++ b/apps/desktop/src/components/grid/dataGridColumnLayoutPopover.ts @@ -19,6 +19,9 @@ export interface DataGridColumnLayoutHandle { hasCustomColumnOrder: boolean; moveDisplayableColumn: (fromDisplayableIndex: number, toDisplayableIndex: number) => void; resetColumnOrder: () => void; + /** Fit every visible column to its content (#9813). Optional so grids that + * only expose the visibility/order surface keep working. */ + autoFitAllColumns?: () => void; } export interface DataGridColumnLayoutVirtualWindow { diff --git a/apps/desktop/src/composables/__tests__/useDataGridColumnResize.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridColumnResize.spec.ts index ca310f8c55..39ec10a19c 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridColumnResize.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridColumnResize.spec.ts @@ -62,6 +62,56 @@ describe("useDataGridColumnResize", () => { clearDataGridColumnWidthStates(); }); + it("fits every visible column to its content when auto fitting the whole grid", () => { + const longText = "x".repeat(300); + const state = createResizeState({ + columns: ["id", "note"], + rows: [ + [1, longText], + [2, longText], + ], + }); + + state.initColumnWidths(); + const before = state.renderedColumnWidths.value.slice(); + expect(Math.max(...before)).toBeLessThan(DATA_GRID_COL_AUTO_FIT_MAX_WIDTH); + + state.autoFitAllColumns(); + + const after = state.renderedColumnWidths.value; + // The long text column now uses the full auto-fit allowance instead of the + // density value limit that bounds the default grow-only sizing. + expect(after[1]).toBeGreaterThan(before[1]!); + expect(after[1]).toBeLessThanOrEqual(DATA_GRID_COL_AUTO_FIT_MAX_WIDTH); + expect(after[0]).toBeGreaterThanOrEqual(DATA_GRID_COL_MIN_WIDTH); + expect(after.length).toBe(before.length); + }); + + it("keeps auto fitted widths when the grid re-initialises for the same columns", () => { + const longText = "y".repeat(220); + const state = createResizeState({ + columns: ["id", "note", "extra"], + rows: [[1, longText, "short"]], + }); + + state.initColumnWidths(); + state.autoFitAllColumns(); + const fitted = state.renderedColumnWidths.value.slice(); + + state.initColumnWidths(); + + expect(state.renderedColumnWidths.value).toEqual(fitted); + }); + + it("does not change any width when the grid has no columns yet", () => { + const state = createResizeState({ columns: [], rows: [] }); + + state.initColumnWidths(); + state.autoFitAllColumns(); + + expect(state.renderedColumnWidths.value).toEqual([]); + }); + it("keeps compact query result columns at content width instead of filling the viewport", () => { const state = createResizeState({ columns: ["id", "user_id"], diff --git a/apps/desktop/src/composables/useDataGridColumnResize.ts b/apps/desktop/src/composables/useDataGridColumnResize.ts index 295acbc98a..d8ff7f714a 100644 --- a/apps/desktop/src/composables/useDataGridColumnResize.ts +++ b/apps/desktop/src/composables/useDataGridColumnResize.ts @@ -184,10 +184,13 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions) document.addEventListener("mouseup", onUp); } - function autoFitColumn(colIdx: number) { + /** Full-content width of one visible column: unlike the default grow-only + * sizing this ignores the density value truncation, so long text columns are + * sized for what the cell actually shows. */ + function autoFitColumnWidth(colIdx: number): number | undefined { const colName = columns.value[colIdx]; - if (!colName) return; - columnWidths.value[colIdx] = calculateDataGridColumnWidth({ + if (!colName) return undefined; + return calculateDataGridColumnWidth({ columnName: colName, sampleValues: sampleColumnValues(colIdx), maxWidth: DATA_GRID_COL_AUTO_FIT_MAX_WIDTH, @@ -198,10 +201,37 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions) headerTextWidth: measureHeaderText?.(colName), hasIndexIndicator: options.columnIndexIndicators?.value[colIdx] ?? false, }); + } + + function autoFitColumn(colIdx: number) { + const width = autoFitColumnWidth(colIdx); + if (width === undefined) return; + columnWidths.value[colIdx] = width; markColumnUserSized(colIdx); persistColumnWidths(); } + /** Fit every visible column to its content in one action (issue #9813), so a + * wide table does not need one double-click per column. Manual drag keeps + * working afterwards: the fitted columns are recorded as user-sized, which is + * exactly the state a drag leaves behind. */ + function autoFitAllColumns() { + if (columnWidths.value.length !== columns.value.length || columns.value.length === 0) return; + const next = columnWidths.value.slice(); + let changed = false; + for (let colIdx = 0; colIdx < columns.value.length; colIdx++) { + const width = autoFitColumnWidth(colIdx); + if (width === undefined) continue; + markColumnUserSized(colIdx); + if (next[colIdx] !== width) { + next[colIdx] = width; + changed = true; + } + } + if (changed) columnWidths.value = next; + persistColumnWidths(); + } + const renderedColumnWidths = computed(() => columnWidths.value.slice()); const resolvedRowNumberWidth = computed(() => options.rowNumberWidth?.value ?? DATA_GRID_ROW_NUM_WIDTH); @@ -245,6 +275,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions) growColumnWidthsToFitSamples, onResizeStart, autoFitColumn, + autoFitAllColumns, renderedColumnWidths, totalWidth, columnVars, diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index 87a84da7f4..edd96fe99e 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -2033,6 +2033,8 @@ export default withEnglishFallback({ localFiltersActive: "{count} yerli dəyər süzgəci", localFilterMoreValues: " +{count} əlavə", columnVisibility: "Sütunlar", + autoFitColumnWidths: "Məzmuna uyğunlaşdır", + autoFitColumnWidthsHint: "Bütün görünən sütunları xana məzmununa uyğun ölçüləndirir. Sonra da sütun kənarını sürükləyə bilərsiniz.", columnVisibilityHint: "Ən azı bir sütun görünən qalır.", columnReorderHint: "Sütunların sırasını dəyişmək üçün sürükləyin.", columnReorderSearchHint: "Sütunların sırasını dəyişmək üçün axtarışı təmizləyin.", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 7098c70d93..2e1e40b69e 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -2218,6 +2218,8 @@ export default { localFiltersActive: "{count} local value filters", localFilterMoreValues: " +{count} more", columnVisibility: "Columns", + autoFitColumnWidths: "Fit columns to content", + autoFitColumnWidthsHint: "Sizes every visible column to what its cells show. You can still drag a column border afterwards.", columnVisibilityHint: "At least one column stays visible.", columnReorderHint: "Drag to reorder columns.", columnReorderSearchHint: "Clear the search to reorder columns.", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 6486f08f98..2879c5a050 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -2079,6 +2079,8 @@ export default withEnglishFallback({ localFiltersActive: "{count} filtros locales de valores", localFilterMoreValues: " +{count} más", columnVisibility: "Columnas", + autoFitColumnWidths: "Ajustar al contenido", + autoFitColumnWidthsHint: "Ajusta cada columna visible al contenido de sus celdas. Después puedes arrastrar el borde de la columna.", columnVisibilityHint: "Al menos una columna permanece visible.", columnReorderHint: "Arrastra para reordenar las columnas.", columnReorderSearchHint: "Borra la búsqueda para reordenar las columnas.", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 6ed127dc48..83b3ea4359 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -2076,6 +2076,8 @@ export default withEnglishFallback({ localFiltersActive: "{count} filtri valori locali", localFilterMoreValues: " +{count} altri", columnVisibility: "Colonne", + autoFitColumnWidths: "Adatta al contenuto", + autoFitColumnWidthsHint: "Adatta ogni colonna visibile al contenuto delle celle. Puoi comunque trascinare il bordo della colonna.", columnVisibilityHint: "Almeno una colonna deve rimanere visibile.", columnReorderHint: "Trascina per riordinare le colonne.", columnReorderSearchHint: "Cancella la ricerca per riordinare le colonne.", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index c10b0967c9..4765cd0d91 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -2087,6 +2087,8 @@ export default withEnglishFallback({ localFiltersActive: "{count}件のローカル値フィルター", localFilterMoreValues: " +{count}件以上", columnVisibility: "列", + autoFitColumnWidths: "内容に合わせて調整", + autoFitColumnWidthsHint: "表示中のすべての列をセルの内容に合わせて調整します。その後も列の境界をドラッグできます。", columnVisibilityHint: "少なくとも1列は表示されます。", columnReorderHint: "ドラッグして列の順序を変更します。", columnReorderSearchHint: "列の順序を変更するには検索をクリアしてください。", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index b7fde570a5..a86770ba52 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -2057,6 +2057,8 @@ export default withEnglishFallback({ localFiltersActive: "로컬 값 필터 {count}개", localFilterMoreValues: " 외 {count}개", columnVisibility: "컬럼", + autoFitColumnWidths: "내용에 맞게 맞춤", + autoFitColumnWidthsHint: "표시된 모든 열을 셀 내용에 맞게 조정합니다. 이후에도 열 경계를 드래그할 수 있습니다.", columnVisibilityHint: "최소 한 개의 컬럼은 표시 상태를 유지합니다.", columnReorderHint: "드래그하여 컬럼 순서를 변경합니다.", columnReorderSearchHint: "컬럼 순서를 변경하려면 검색을 지우세요.", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index bc66671457..ccac0f5350 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -2078,6 +2078,8 @@ export default withEnglishFallback({ localFiltersActive: "{count} filtros locais de valores", localFilterMoreValues: " +{count} mais", columnVisibility: "Colunas", + autoFitColumnWidths: "Ajustar ao conteúdo", + autoFitColumnWidthsHint: "Ajusta cada coluna visível ao conteúdo das células. Você ainda pode arrastar a borda da coluna.", columnVisibilityHint: "Pelo menos uma coluna permanece visível.", columnReorderHint: "Arraste para reordenar as colunas.", columnReorderSearchHint: "Limpe a pesquisa para reordenar as colunas.", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 05a2069a0e..f182d21a40 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -2028,6 +2028,8 @@ export default withEnglishFallback({ localFiltersActive: "{count} yerel değer filtresi", localFilterMoreValues: " +{count} tane daha", columnVisibility: "Sütunlar", + autoFitColumnWidths: "İçeriğe sığdır", + autoFitColumnWidthsHint: "Görünen tüm sütunları hücre içeriğine göre ayarlar. Ardından sütun kenarını yine sürükleyebilirsiniz.", columnVisibilityHint: "En az bir sütun görünür kalır.", columnReorderHint: "Sütunları yeniden sıralamak için sürükleyin.", columnReorderSearchHint: "Sütunları yeniden sıralamak için aramayı temizleyin.", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 45aff239e7..f6712a7390 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -2131,6 +2131,8 @@ export default withEnglishFallback({ localFiltersActive: "{count} 个本地值筛选", localFilterMoreValues: ",还有 {count} 个", columnVisibility: "字段筛选", + autoFitColumnWidths: "自动适配列宽", + autoFitColumnWidthsHint: "按单元格内容调整所有可见列的宽度;之后仍可拖拽列边界手动微调。", columnVisibilityHint: "至少保留一列可见。", columnReorderHint: "拖动可调整字段顺序。", columnReorderSearchHint: "清除搜索后可调整字段顺序。", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index a37bb0876c..0cd4a83177 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -2076,6 +2076,8 @@ export default withEnglishFallback({ localFiltersActive: "{count} 個本機值篩選", localFilterMoreValues: ",還有 {count} 個", columnVisibility: "欄位篩選", + autoFitColumnWidths: "自動適配欄寬", + autoFitColumnWidthsHint: "依儲存格內容調整所有可見欄的寬度;之後仍可拖曳欄邊界手動微調。", columnVisibilityHint: "至少保留一欄可見。", columnReorderHint: "拖曳可調整欄位順序。", columnReorderSearchHint: "清除搜尋後可調整欄位順序。", diff --git a/docs/screenshots/issue-9813-after.png b/docs/screenshots/issue-9813-after.png new file mode 100644 index 0000000000..e9c54f8ebe Binary files /dev/null and b/docs/screenshots/issue-9813-after.png differ diff --git a/docs/screenshots/issue-9813-before.png b/docs/screenshots/issue-9813-before.png new file mode 100644 index 0000000000..b539162331 Binary files /dev/null and b/docs/screenshots/issue-9813-before.png differ diff --git a/docs/screenshots/issue-9813-popover.png b/docs/screenshots/issue-9813-popover.png new file mode 100644 index 0000000000..82b3f12ff9 Binary files /dev/null and b/docs/screenshots/issue-9813-popover.png differ