feat(grid): flatten multiline cell text
This commit is contained in:
17 files changed
+97
-30
No files matched your search
@@ -321,6 +321,7 @@ const editQueryResultMaxRows = ref(settingsStore.editorSettings.queryResultMaxRo
|
||||
const editInfiniteScroll = ref(settingsStore.editorSettings.infiniteScroll);
|
||||
const editRegexMaxMatchCount = ref(settingsStore.editorSettings.regexMaxMatchCount);
|
||||
const editAutoCalculateTotalRows = ref(settingsStore.editorSettings.autoCalculateTotalRows);
|
||||
const editFlatteningMultiLineText = ref(settingsStore.editorSettings.flatteningMultiLineText);
|
||||
const editTableColumnTemplateRows = ref<TableColumnTemplateGridRow[]>(tableColumnTemplateRowsFromSettings(settingsStore.editorSettings.tableColumnTemplateFields));
|
||||
const editTableColumnTemplateDatabaseType = ref<DatabaseType>(TABLE_COLUMN_TEMPLATE_DATABASE_TYPES[0] ?? "mysql");
|
||||
const editSqlVariableSyntaxOverrides = ref<SqlVariableSyntaxOverrides>(normalizeSqlVariableSyntaxOverrides(settingsStore.editorSettings.sqlVariableSyntaxOverrides));
|
||||
@@ -488,6 +489,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
||||
compactColumnHeaderActions: editCompactColumnHeaderActions.value,
|
||||
dataGridQuickEntry: editDataGridQuickEntry.value,
|
||||
dataGridAutoTransposeSingleRow: editDataGridAutoTransposeSingleRow.value,
|
||||
flatteningMultiLineText: editFlatteningMultiLineText.value,
|
||||
pageSize: editPageSize.value,
|
||||
tableOpenPageSize: editTableOpenPageSize.value,
|
||||
queryResultMaxRowsEnabled: editQueryResultMaxRowsEnabled.value,
|
||||
@@ -761,6 +763,7 @@ function syncEditorSettingsDraftFromStore() {
|
||||
editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions;
|
||||
editDataGridQuickEntry.value = settingsStore.editorSettings.dataGridQuickEntry;
|
||||
editDataGridAutoTransposeSingleRow.value = settingsStore.editorSettings.dataGridAutoTransposeSingleRow;
|
||||
editFlatteningMultiLineText.value = settingsStore.editorSettings.flatteningMultiLineText;
|
||||
editPageSize.value = settingsStore.editorSettings.pageSize;
|
||||
editTableOpenPageSize.value = settingsStore.editorSettings.tableOpenPageSize;
|
||||
editQueryResultMaxRowsEnabled.value = settingsStore.editorSettings.queryResultMaxRowsEnabled;
|
||||
@@ -1006,6 +1009,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
|
||||
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
|
||||
editDataGridAutoTransposeSingleRow.value = DEFAULT_EDITOR_SETTINGS.dataGridAutoTransposeSingleRow;
|
||||
editFlatteningMultiLineText.value = DEFAULT_EDITOR_SETTINGS.flatteningMultiLineText;
|
||||
editPageSize.value = DEFAULT_EDITOR_SETTINGS.pageSize;
|
||||
editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize;
|
||||
editQueryResultMaxRowsEnabled.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRowsEnabled;
|
||||
@@ -1074,6 +1078,7 @@ function resetAllDefaults() {
|
||||
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
|
||||
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
|
||||
editDataGridAutoTransposeSingleRow.value = DEFAULT_EDITOR_SETTINGS.dataGridAutoTransposeSingleRow;
|
||||
editFlatteningMultiLineText.value = DEFAULT_EDITOR_SETTINGS.flatteningMultiLineText;
|
||||
editPageSize.value = DEFAULT_EDITOR_SETTINGS.pageSize;
|
||||
editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize;
|
||||
editQueryResultMaxRowsEnabled.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRowsEnabled;
|
||||
@@ -4877,6 +4882,17 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<Switch id="data-grid-auto-transpose-single-row" v-model="editDataGridAutoTransposeSingleRow" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="flattening-multi-line">
|
||||
{{ t("settings.flatteningMultiLineText") }}
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.flatteningMultiLineTextDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch id="flattening-multi-line" v-model="editFlatteningMultiLineText" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!isWeb">
|
||||
|
||||
@@ -557,6 +557,7 @@ const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridS
|
||||
const compactDataGridToolbar = computed(() => isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH));
|
||||
const infiniteScrollEnabled = computed(() => props.paginationEnabled && settingsStore.editorSettings.infiniteScroll);
|
||||
const infiniteScrollMaxRows = computed(() => continuousQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows));
|
||||
const flatteningMultiLineEnabled = computed(() => settingsStore.editorSettings.flatteningMultiLineText);
|
||||
const expandedCellEditor = ref<{ rowId: number; col: number } | null>(null);
|
||||
|
||||
function headerColumnComment(column: string): string {
|
||||
@@ -5840,7 +5841,7 @@ function booleanNullTextHitFromCanvasEvent(item: RowItem, hit: { rowIndex: numbe
|
||||
const canvasRect = canvas?.getBoundingClientRect();
|
||||
const cellRect = canvasCellViewportRect(hit.rowIndex, hit.visibleColIdx);
|
||||
if (!canvasRect || !cellRect) return false;
|
||||
const text = firstLineCellDisplayValue(formatCellCached(item.data[actualColIdx], actualColIdx));
|
||||
const text = firstLineCellDisplayValue(formatCellCached(item.data[actualColIdx], actualColIdx), flatteningMultiLineEnabled.value);
|
||||
if (!text) return false;
|
||||
const textWidth = measureCellTextWidthCached(text, `italic 400 ${tableFontSize.value}px ${tableFontFamily.value}`);
|
||||
if (textWidth <= 0) return false;
|
||||
@@ -6105,6 +6106,7 @@ function drawCanvasGrid() {
|
||||
rightAlignedActionCell: canvasRightAlignedActionCell.value,
|
||||
columnIsBoolean: isBooleanGridColumn,
|
||||
booleanDisplayMode: booleanDisplayMode.value,
|
||||
flatteningMultiLineEnabled: flatteningMultiLineEnabled.value,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6122,6 +6124,7 @@ watch(showDataGridTopbar, () => nextTick(observeDataGridTopbarWidth), {
|
||||
});
|
||||
watch(columnAligns, () => scheduleCanvasDraw());
|
||||
watch(booleanDisplayMode, () => scheduleCanvasDraw());
|
||||
watch(flatteningMultiLineEnabled, () => scheduleCanvasDraw());
|
||||
watch(colorizeDataGridCellTypes, () => scheduleCanvasDraw());
|
||||
watch(
|
||||
[
|
||||
@@ -10284,7 +10287,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
<template v-if="draftCellPlaceholder(displayItems[cell.recordIndex], cell.valueIndex)">
|
||||
<span class="text-muted-foreground/70 italic">{{ draftCellPlaceholder(displayItems[cell.recordIndex], cell.valueIndex) }}</span>
|
||||
</template>
|
||||
<template v-else>{{ firstLineCellDisplayValue(cell.display) }}</template>
|
||||
<template v-else>{{ firstLineCellDisplayValue(cell.display, flatteningMultiLineEnabled) }}</template>
|
||||
<div v-if="cellDetailButtonVisible(cell.recordIndex, cell.valueIndex)" class="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-1">
|
||||
<LightDropdownMenu
|
||||
v-if="canQuickDownloadCellValue(cell.recordIndex, cell.valueIndex)"
|
||||
@@ -11051,13 +11054,13 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="booleanCellsUseCheckbox && isBooleanGridCell(item, col.actualColIdx) && item.data[col.actualColIdx] === null && canEditCellItem(item, col.actualColIdx)">
|
||||
<span class="italic text-muted-foreground cursor-pointer select-none" @click.stop="cycleBooleanGridCell(item, col.actualColIdx, $event)">{{ firstLineCellDisplayValue(formatCellCached(item.data[col.actualColIdx], col.actualColIdx)) }}</span>
|
||||
<span class="italic text-muted-foreground cursor-pointer select-none" @click.stop="cycleBooleanGridCell(item, col.actualColIdx, $event)">{{ firstLineCellDisplayValue(formatCellCached(item.data[col.actualColIdx], col.actualColIdx), flatteningMultiLineEnabled) }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="draftCellPlaceholder(item, col.actualColIdx)">
|
||||
<span class="text-muted-foreground/70 italic">{{ draftCellPlaceholder(item, col.actualColIdx) }}</span>
|
||||
</template>
|
||||
<template v-else>{{ firstLineCellDisplayValue(formatCellCached(item.data[col.actualColIdx], col.actualColIdx)) }}</template>
|
||||
<template v-else>{{ firstLineCellDisplayValue(formatCellCached(item.data[col.actualColIdx], col.actualColIdx), flatteningMultiLineEnabled) }}</template>
|
||||
<div v-if="cellDetailButtonVisible(item.displayIndex, col.actualColIdx)" class="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-1">
|
||||
<LightDropdownMenu
|
||||
v-if="canQuickDownloadCellValue(item.displayIndex, col.actualColIdx)"
|
||||
|
||||
@@ -5621,6 +5621,8 @@ export default {
|
||||
autoCalculateTotalRows: "Auto-calculate total row count",
|
||||
autoCalculateTotalRowsDescription: "Run COUNT(*) automatically after each query to show the total matching rows. Off by default to keep large queries fast — calculate it on demand from the result footer.",
|
||||
infiniteScrollDescription: "Automatically load the next batch when scrolling to the bottom.",
|
||||
flatteningMultiLineText: "Flattening Multi-line Text",
|
||||
flatteningMultiLineTextDescription: "Multi-line text merged into single line display, with line breaks shown as ¶ .",
|
||||
regexMaxMatchCount: "Maximum select-all matches",
|
||||
regexMaxMatchCountDescription: "Limits the number of selections created by Select All Matches; search counting remains complete (100–10000).",
|
||||
tableColumnTemplateFields: "New Table Preset Fields",
|
||||
|
||||
@@ -5376,6 +5376,8 @@ export default withEnglishFallback({
|
||||
autoCalculateTotalRows: "Calcular automáticamente el total de filas",
|
||||
autoCalculateTotalRowsDescription: "Ejecuta COUNT(*) automáticamente tras cada consulta para mostrar el total de filas coincidentes. Desactivado por defecto para mantener rápidas las consultas grandes; puedes calcularlo cuando quieras desde el pie de resultados.",
|
||||
infiniteScrollDescription: "Carga automáticamente el siguiente lote al desplazarse hasta el final.",
|
||||
flatteningMultiLineText: "Aplastar texto multilínea",
|
||||
flatteningMultiLineTextDescription: "El texto multilínea se fusiona en una sola línea de visualización, con los saltos de línea mostrados como ¶ .",
|
||||
regexMaxMatchCount: "Máximo de coincidencias al seleccionar todas",
|
||||
regexMaxMatchCountDescription: "Limita el número de selecciones creadas por Seleccionar todas las coincidencias; el conteo de búsqueda permanece completo (100–10000).",
|
||||
tableColumnTemplateFields: "Campos predefinidos para tablas nuevas",
|
||||
|
||||
@@ -5376,6 +5376,8 @@ export default withEnglishFallback({
|
||||
autoCalculateTotalRows: "Calcola automaticamente il totale delle righe",
|
||||
autoCalculateTotalRowsDescription: "Esegue COUNT(*) automaticamente dopo ogni query per mostrare il totale delle righe corrispondenti. Disattivato per impostazione predefinita per mantenere veloci le query grandi; puoi calcolarlo all'occorrenza dal piè di pagina dei risultati.",
|
||||
infiniteScrollDescription: "Carica automaticamente il blocco successivo quando scorri fino in fondo.",
|
||||
flatteningMultiLineText: "Pianifica automaticamente i testi multi-riga",
|
||||
flatteningMultiLineTextDescription: "Se attivo, i testi multi-riga nei risultati vengono automaticamente pianificati in una singola colonna.",
|
||||
regexMaxMatchCount: "Numero massimo di corrispondenze per Seleziona tutte",
|
||||
regexMaxMatchCountDescription: "Limita il numero di selezioni create da Seleziona tutte le corrispondenze; il conteggio della ricerca rimane completo (100–10000).",
|
||||
tableColumnTemplateFields: "Campi predefiniti per nuove tabelle",
|
||||
|
||||
@@ -5801,6 +5801,8 @@ export default withEnglishFallback({
|
||||
autoCalculateTotalRows: "総行数を自動計算",
|
||||
autoCalculateTotalRowsDescription: "クエリごとに COUNT(*) を自動実行し、一致する総行数を表示します。大きなクエリを高速に保つため既定はオフです。結果フッターから必要に応じて計算できます。",
|
||||
infiniteScrollDescription: "末尾までスクロールしたときに、次のデータを自動的に読み込みます。",
|
||||
flatteningMultiLineText: "多行テキストを展開",
|
||||
flatteningMultiLineTextDescription: "複数行のテキストを1行に結合して表示し、改行文字は¶として表示する。",
|
||||
regexMaxMatchCount: "すべての一致を選択する最大数",
|
||||
regexMaxMatchCountDescription: "「すべての一致を選択」で一度に作成する選択範囲の数を制限します。検索の一致数は完全に集計されます(100〜10000)。",
|
||||
exportRowLimitEnabled: "エクスポート行数を制限",
|
||||
|
||||
@@ -5119,6 +5119,8 @@ export default withEnglishFallback({
|
||||
autoCalculateTotalRows: "전체 행 수 자동 집계",
|
||||
autoCalculateTotalRowsDescription: "각 쿼리 후 자동으로 COUNT(*)를 실행하여 일치하는 전체 행 수를 표시합니다. 대규모 쿼리를 빠르게 유지하기 위해 기본적으로 꺼져 있으며, 결과 푸터에서 요청 시 집계할 수 있습니다.",
|
||||
infiniteScrollDescription: "목록 하단으로 스크롤할 때 다음 데이터 묶음을 자동으로 불러옵니다.",
|
||||
flatteningMultiLineText: "멀티라인 텍스트 단순화",
|
||||
flatteningMultiLineTextDescription: "다중 행 텍스트를 단일 행으로 병합하여 표시하고, 개행 문자는 ¶로 표시합니다.",
|
||||
tableColumnTemplateFields: "새 테이블 미리 정의된 필드",
|
||||
tableColumnTemplateFieldsDescription: "데이터베이스 유형을 선택한 다음 새 테이블을 만들 때 사용할 미리 정의된 필드 타입을 구성하세요.",
|
||||
tableColumnTemplateAdd: "필드 추가",
|
||||
|
||||
@@ -5378,6 +5378,8 @@ export default withEnglishFallback({
|
||||
autoCalculateTotalRows: "Calcular automaticamente o total de linhas",
|
||||
autoCalculateTotalRowsDescription: "Executa COUNT(*) automaticamente após cada consulta para mostrar o total de linhas correspondentes. Desativado por padrão para manter consultas grandes rápidas; você pode calculá-lo quando quiser no rodapé dos resultados.",
|
||||
infiniteScrollDescription: "Carregar automaticamente o próximo lote ao rolar até o final.",
|
||||
flatteningMultiLineText: "Achatar texto multilinha",
|
||||
flatteningMultiLineTextDescription: "Exibir texto multilinha em uma única linha.",
|
||||
regexMaxMatchCount: "Máximo de correspondências para selecionar todas",
|
||||
regexMaxMatchCountDescription: "Limita o número de seleções criadas por Selecionar todas as correspondências; a contagem da pesquisa permanece completa (100–10000).",
|
||||
tableColumnTemplateFields: "Campos predefinidos para novas tabelas",
|
||||
|
||||
@@ -5543,6 +5543,8 @@ export default withEnglishFallback({
|
||||
autoCalculateTotalRows: "自动统计总行数",
|
||||
autoCalculateTotalRowsDescription: "每次查询后自动执行 COUNT(*) 显示匹配的总行数。默认关闭以保证大查询速度 —— 可在结果栏按需手动统计。",
|
||||
infiniteScrollDescription: "滚动到底部时自动加载下一批数据,无需手动翻页或加载。",
|
||||
flatteningMultiLineText: "扁平化多行文本",
|
||||
flatteningMultiLineTextDescription: "多行文本合并为单行显示,换行符显示为 ¶ 。",
|
||||
regexMaxMatchCount: "全部选中最大数量",
|
||||
regexMaxMatchCountDescription: "限制“全部选中”一次创建的选区数量;搜索匹配仍完整统计(100–10000)。",
|
||||
tableColumnTemplateFields: "新建表预设字段",
|
||||
|
||||
@@ -4716,6 +4716,8 @@ export default withEnglishFallback({
|
||||
autoCalculateTotalRows: "自動統計總筆數",
|
||||
autoCalculateTotalRowsDescription: "每次查詢後自動執行 COUNT(*) 顯示符合的總筆數。預設關閉以確保大型查詢速度 —— 可在結果列按需手動統計。",
|
||||
infiniteScrollDescription: "滾動到底部時自動載入下一批資料,無需手動翻頁或載入。",
|
||||
flatteningMultiLineText: "扁平多行文本",
|
||||
flatteningMultiLineTextDescription: "多行文本合併為單行顯示,換行符顯示為 ¶ 。",
|
||||
regexMaxMatchCount: "全部選取最大數量",
|
||||
regexMaxMatchCountDescription: "限制「全部選取」一次建立的選取範圍數量;搜尋符合結果仍會完整統計(100–10000)。",
|
||||
tableColumnTemplateFields: "新建表預設欄位",
|
||||
|
||||
+26
-10
@@ -3,22 +3,38 @@ import { firstLineCellDisplayValue } from "@/lib/dataGrid/cellValue";
|
||||
|
||||
describe("firstLineCellDisplayValue", () => {
|
||||
it("shows only the first line in fixed-height cells", () => {
|
||||
expect(firstLineCellDisplayValue("111\n222")).toBe("111");
|
||||
expect(firstLineCellDisplayValue("111\r\n222")).toBe("111");
|
||||
expect(firstLineCellDisplayValue("111\r222")).toBe("111");
|
||||
expect(firstLineCellDisplayValue(null, false)).toBe(null);
|
||||
expect(firstLineCellDisplayValue("111\n222", false)).toBe("111");
|
||||
expect(firstLineCellDisplayValue("111\r\n222", false)).toBe("111");
|
||||
expect(firstLineCellDisplayValue("111\r222", false)).toBe("111");
|
||||
expect(firstLineCellDisplayValue("single line")).toBe("single line");
|
||||
|
||||
expect(firstLineCellDisplayValue(null, true)).toBe(null);
|
||||
expect(firstLineCellDisplayValue("111\n222", true)).toBe("111¶222");
|
||||
expect(firstLineCellDisplayValue("111\r\n222", true)).toBe("111¶222");
|
||||
expect(firstLineCellDisplayValue("111\r222", true)).toBe("111¶222");
|
||||
expect(firstLineCellDisplayValue("single line", true)).toBe("single line");
|
||||
});
|
||||
|
||||
it("skips leading blank lines without stripping content indentation", () => {
|
||||
expect(firstLineCellDisplayValue("\nINNODB MONITOR OUTPUT\nbody")).toBe("INNODB MONITOR OUTPUT");
|
||||
expect(firstLineCellDisplayValue("\r\nINNODB MONITOR OUTPUT\r\nbody")).toBe("INNODB MONITOR OUTPUT");
|
||||
expect(firstLineCellDisplayValue("\rINNODB MONITOR OUTPUT\rbody")).toBe("INNODB MONITOR OUTPUT");
|
||||
expect(firstLineCellDisplayValue(" \n indented content\nbody")).toBe(" indented content");
|
||||
expect(firstLineCellDisplayValue("\nINNODB MONITOR OUTPUT\nbody", false)).toBe("INNODB MONITOR OUTPUT");
|
||||
expect(firstLineCellDisplayValue("\r\nINNODB MONITOR OUTPUT\r\nbody", false)).toBe("INNODB MONITOR OUTPUT");
|
||||
expect(firstLineCellDisplayValue("\rINNODB MONITOR OUTPUT\rbody", false)).toBe("INNODB MONITOR OUTPUT");
|
||||
expect(firstLineCellDisplayValue(" \n indented content\nbody", false)).toBe(" indented content");
|
||||
|
||||
expect(firstLineCellDisplayValue("\nINNODB MONITOR OUTPUT\nbody", true)).toBe("¶INNODB MONITOR OUTPUT¶body");
|
||||
expect(firstLineCellDisplayValue("\r\nINNODB MONITOR OUTPUT\r\nbody", true)).toBe("¶INNODB MONITOR OUTPUT¶body");
|
||||
expect(firstLineCellDisplayValue("\rINNODB MONITOR OUTPUT\rbody", true)).toBe("¶INNODB MONITOR OUTPUT¶body");
|
||||
expect(firstLineCellDisplayValue(" \n indented content\nbody", true)).toBe(" ¶ indented content¶body");
|
||||
});
|
||||
|
||||
it("preserves values that do not contain a content line", () => {
|
||||
expect(firstLineCellDisplayValue("")).toBe("");
|
||||
expect(firstLineCellDisplayValue(" ")).toBe(" ");
|
||||
expect(firstLineCellDisplayValue("\n\r\n \r")).toBe("\n\r\n \r");
|
||||
expect(firstLineCellDisplayValue("", false)).toBe("");
|
||||
expect(firstLineCellDisplayValue(" ", false)).toBe(" ");
|
||||
expect(firstLineCellDisplayValue("\n\r\n \r", false)).toBe("\n\r\n \r");
|
||||
|
||||
expect(firstLineCellDisplayValue("", true)).toBe("");
|
||||
expect(firstLineCellDisplayValue(" ", true)).toBe(" ");
|
||||
expect(firstLineCellDisplayValue("\n\r\n \r", true)).toBe("¶¶ ¶");
|
||||
});
|
||||
});
|
||||
@@ -30,6 +30,10 @@ describe("dataGridRuntime", () => {
|
||||
expect(dataGridSource).toContain(":title=\"t('grid.queryError')\"");
|
||||
});
|
||||
|
||||
it("redraws canvas rows when multiline flattening changes", () => {
|
||||
expect(dataGridSource).toContain("watch(flatteningMultiLineEnabled, () => scheduleCanvasDraw());");
|
||||
});
|
||||
|
||||
it("disposes registered resources in reverse order and only once", () => {
|
||||
const scope = createDataGridRuntimeScope();
|
||||
const calls: string[] = [];
|
||||
|
||||
@@ -95,6 +95,7 @@ export interface DrawCanvasDataGridOptions {
|
||||
colorizeDataTypes?: boolean;
|
||||
rightAlignedActionCell?: CanvasRightAlignedActionCell | null;
|
||||
booleanDisplayMode?: "checkbox" | "dropdown";
|
||||
flatteningMultiLineEnabled: boolean;
|
||||
}
|
||||
|
||||
type NumericCanvasContext = CanvasRenderingContext2D & {
|
||||
@@ -353,6 +354,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
||||
rightAlignedActionCell,
|
||||
columnIsBoolean,
|
||||
booleanDisplayMode = "dropdown",
|
||||
flatteningMultiLineEnabled,
|
||||
} = options;
|
||||
// 框选热路径:整次绘制只判断一次。常见情况(单矩形 / 多列且每段都是多格)可跳过逐格 kind 查询
|
||||
const paintSelectionOuterFrame = dataGridSelectionUsesOuterFrame(selectionFrames);
|
||||
@@ -575,7 +577,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
||||
}
|
||||
} else {
|
||||
const rawDisplayText = item.isDraft && value === null ? (draftCellPlaceholder ?? "") : formatCell(value, actualColIdx);
|
||||
const displayText = isEditingThisCell ? "" : firstLineCellDisplayValue(rawDisplayText);
|
||||
const displayText = isEditingThisCell ? "" : firstLineCellDisplayValue(rawDisplayText, flatteningMultiLineEnabled);
|
||||
const text = isEditingThisCell ? displayText : fitCanvasText(ctx, displayText, cellMaxWidth, isBooleanNullCell ? "left" : isRightAlign ? "right" : "left");
|
||||
const anchorX = isBooleanNullCell ? alignCanvasPixel(drawX + colWidth / 2, scaleX) : textAnchorX;
|
||||
ctx.fillText(text, anchorX, textY);
|
||||
|
||||
@@ -10,26 +10,30 @@ export function displayCellValue(value: CellValue): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function firstLineCellDisplayValue(value: string): string {
|
||||
export function firstLineCellDisplayValue(value: string, flatteningMultiLine: boolean): string {
|
||||
const lineBreakPattern = /\r\n|\r|\n/g;
|
||||
const firstLineBreak = lineBreakPattern.exec(value);
|
||||
if (!firstLineBreak) return value;
|
||||
if (flatteningMultiLine) {
|
||||
return value && value.replaceAll(lineBreakPattern, "¶");
|
||||
} else {
|
||||
const firstLineBreak = lineBreakPattern.exec(value);
|
||||
if (!firstLineBreak) return value;
|
||||
|
||||
const firstLine = value.slice(0, firstLineBreak.index);
|
||||
if (/\S/u.test(firstLine)) return firstLine;
|
||||
const firstLine = value.slice(0, firstLineBreak.index);
|
||||
if (/\S/u.test(firstLine)) return firstLine;
|
||||
|
||||
let lineStart = lineBreakPattern.lastIndex;
|
||||
let lineStart = lineBreakPattern.lastIndex;
|
||||
|
||||
while (lineStart <= value.length) {
|
||||
const lineBreak = lineBreakPattern.exec(value);
|
||||
const lineEnd = lineBreak?.index ?? value.length;
|
||||
const line = value.slice(lineStart, lineEnd);
|
||||
if (/\S/u.test(line)) return line;
|
||||
if (!lineBreak) return value;
|
||||
lineStart = lineBreakPattern.lastIndex;
|
||||
while (lineStart <= value.length) {
|
||||
const lineBreak = lineBreakPattern.exec(value);
|
||||
const lineEnd = lineBreak?.index ?? value.length;
|
||||
const line = value.slice(lineStart, lineEnd);
|
||||
if (/\S/u.test(line)) return line;
|
||||
if (!lineBreak) return value;
|
||||
lineStart = lineBreakPattern.lastIndex;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function limitDataGridCellDisplay(value: string, maxLength = DATA_GRID_CELL_DISPLAY_MAX_LENGTH): string {
|
||||
|
||||
@@ -43,6 +43,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"infiniteScroll",
|
||||
"regexMaxMatchCount",
|
||||
"autoCalculateTotalRows",
|
||||
"flatteningMultiLineText",
|
||||
"tableColumnTemplateFields",
|
||||
"shortcuts",
|
||||
"sqlFormatter",
|
||||
|
||||
@@ -180,6 +180,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
|
||||
{ id: "data-grid-infinite-scroll", category: "data", titleKey: "settings.infiniteScroll", descriptionKey: "settings.infiniteScrollDescription", targetId: "data" },
|
||||
{ id: "data-grid-auto-transpose", category: "data", titleKey: "settings.dataGridAutoTransposeSingleRow", descriptionKey: "settings.dataGridAutoTransposeSingleRowDescription", targetId: "data" },
|
||||
{ id: "data-grid-quick-entry", category: "data", titleKey: "settings.dataGridQuickEntry", descriptionKey: "settings.dataGridQuickEntryDescription", targetId: "data" },
|
||||
{ id: "data-grid-flattening-multi-line", category: "data", titleKey: "settings.flatteningMultiLineText", descriptionKey: "settings.flatteningMultiLineTextDescription", targetId: "data" },
|
||||
{ id: "appearance-toolbar", category: "appearance", titleKey: "settings.toolbarTitle", descriptionKey: "settings.toolbarHiddenHint", targetId: "appearance" },
|
||||
{ id: "appearance-exclusive-sidebar-panels", category: "appearance", titleKey: "settings.exclusiveRightSidebarPanels", descriptionKey: "settings.exclusiveRightSidebarPanelsDescription", targetId: "appearance" },
|
||||
...createToolbarVisibilitySettingsSearchDefinitions(),
|
||||
|
||||
@@ -533,6 +533,7 @@ export interface EditorSettings {
|
||||
infiniteScroll: boolean;
|
||||
/** Preserved for downgrade compatibility; current clients use queryResultMaxRows. */
|
||||
infiniteScrollMaxRows: number;
|
||||
flatteningMultiLineText: boolean;
|
||||
regexMaxMatchCount: number;
|
||||
autoCalculateTotalRows: boolean;
|
||||
mongoViewMode: "document" | "table";
|
||||
@@ -725,6 +726,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
queryResultMaxRows: DEFAULT_QUERY_RESULT_MAX_ROWS,
|
||||
infiniteScroll: false,
|
||||
infiniteScrollMaxRows: 5000,
|
||||
flatteningMultiLineText: false,
|
||||
regexMaxMatchCount: 1000,
|
||||
autoCalculateTotalRows: false,
|
||||
mongoViewMode: "document",
|
||||
@@ -1062,6 +1064,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
queryResultMaxRows: normalizeQueryResultMaxRows(settings.queryResultMaxRows),
|
||||
infiniteScroll: settings.infiniteScroll ?? DEFAULT_EDITOR_SETTINGS.infiniteScroll,
|
||||
infiniteScrollMaxRows: typeof settings.infiniteScrollMaxRows === "number" && settings.infiniteScrollMaxRows >= 1000 && settings.infiniteScrollMaxRows <= 50000 ? Math.round(settings.infiniteScrollMaxRows) : DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows,
|
||||
flatteningMultiLineText: settings.flatteningMultiLineText ?? DEFAULT_EDITOR_SETTINGS.flatteningMultiLineText,
|
||||
regexMaxMatchCount: typeof settings.regexMaxMatchCount === "number" && Number.isFinite(settings.regexMaxMatchCount) && settings.regexMaxMatchCount >= 100 && settings.regexMaxMatchCount <= 10000 ? Math.round(settings.regexMaxMatchCount) : DEFAULT_EDITOR_SETTINGS.regexMaxMatchCount,
|
||||
autoCalculateTotalRows: settings.autoCalculateTotalRows ?? DEFAULT_EDITOR_SETTINGS.autoCalculateTotalRows,
|
||||
mongoViewMode: settings.mongoViewMode === "table" ? "table" : DEFAULT_EDITOR_SETTINGS.mongoViewMode,
|
||||
@@ -1706,6 +1709,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.continueOnErrorOnBatch !== undefined) editorSettings.value.continueOnErrorOnBatch = partial.continueOnErrorOnBatch === true;
|
||||
if (partial.clickTableNavigationTarget !== undefined) editorSettings.value.clickTableNavigationTarget = normalizeClickTableNavigationTarget(partial.clickTableNavigationTarget);
|
||||
if (partial.completionTriggerMode !== undefined) editorSettings.value.completionTriggerMode = normalizeCompletionTriggerMode(partial.completionTriggerMode);
|
||||
if (partial.flatteningMultiLineText !== undefined) editorSettings.value.flatteningMultiLineText = partial.flatteningMultiLineText;
|
||||
}
|
||||
|
||||
function updateEditorSettings(partial: Partial<EditorSettings>) {
|
||||
|
||||
Reference in new issue
Block a user