mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 08:13:34 +00:00
fix(postgres): support editing array cells
This commit is contained in:
@@ -79,6 +79,7 @@ import ImagePreviewDialog from "@/components/grid/ImagePreviewDialog.vue";
|
||||
import TemporalCellEditor from "@/components/grid/TemporalCellEditor.vue";
|
||||
import type { QueryResult, ColumnInfo, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo } from "@/types/database";
|
||||
import * as api from "@/lib/api";
|
||||
import { coerceDataGridCellValue, dataGridCellDisplayText, dataGridCellEditorText } from "@/lib/dataGridCellCoercion";
|
||||
import { createColumnDrafts } from "@/lib/tableStructureEditorState";
|
||||
import type { BuildSingleColumnAlterSqlOptions } from "@/lib/tableStructureEditorSql";
|
||||
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
|
||||
@@ -246,11 +247,11 @@ const props = defineProps<{
|
||||
onExecuteSql?: (sql: string) => Promise<void>;
|
||||
fullExportResult?: () => Promise<QueryResult | undefined>;
|
||||
customSave?: (changes: {
|
||||
dirtyRows: Map<number, Map<number, string | number | boolean | null>>;
|
||||
newRows: (string | number | boolean | null)[][];
|
||||
dirtyRows: Map<number, Map<number, CellValue>>;
|
||||
newRows: CellValue[][];
|
||||
deletedRows: Set<number>;
|
||||
columns: string[];
|
||||
rows: (string | number | boolean | null)[][];
|
||||
rows: CellValue[][];
|
||||
}) => Promise<void>;
|
||||
}>();
|
||||
|
||||
@@ -2193,7 +2194,6 @@ const {
|
||||
saveChanges,
|
||||
discardChanges,
|
||||
rowDataWithChanges,
|
||||
coerceCellValue,
|
||||
canEditColumn,
|
||||
resetGridVerticalScroll,
|
||||
getResetScrollAfterResult,
|
||||
@@ -2249,6 +2249,15 @@ function tableColumnForGridColumn(columnIndex: number): ColumnInfo | undefined {
|
||||
return props.tableMeta?.columns.find((column) => column.name.toLowerCase() === columnName.toLowerCase());
|
||||
}
|
||||
|
||||
function coerceDetailCellValue(value: string, oldValue: CellValue | undefined, columnIndex: number): CellValue {
|
||||
return coerceDataGridCellValue({
|
||||
value,
|
||||
oldValue,
|
||||
databaseType: props.databaseType,
|
||||
columnInfo: tableColumnForGridColumn(columnIndex),
|
||||
}) as CellValue;
|
||||
}
|
||||
|
||||
function temporalEditorKindForColumn(columnIndex: number): TemporalCellEditorKind | undefined {
|
||||
return temporalCellEditorKind(tableColumnForGridColumn(columnIndex)?.data_type, props.databaseType);
|
||||
}
|
||||
@@ -2904,7 +2913,11 @@ watch(activeCellDetail, (detail) => {
|
||||
resetDetailEdit();
|
||||
return;
|
||||
}
|
||||
detailEditValue.value = cellDetailEditorText(detail.value, detail.type);
|
||||
detailEditValue.value = dataGridCellEditorText({
|
||||
value: detail.value,
|
||||
databaseType: props.databaseType,
|
||||
columnInfo: tableColumnForGridColumn(detail.colIndex),
|
||||
});
|
||||
syncEditorFromDetailEdit();
|
||||
isEditingDetail.value = true;
|
||||
});
|
||||
@@ -2984,7 +2997,11 @@ function closeCellDetails() {
|
||||
function startDetailEdit() {
|
||||
const detail = activeCellDetail.value;
|
||||
if (!detail || !detail.isEditable) return;
|
||||
detailEditValue.value = cellDetailEditorText(detail.value, detail.type);
|
||||
detailEditValue.value = dataGridCellEditorText({
|
||||
value: detail.value,
|
||||
databaseType: props.databaseType,
|
||||
columnInfo: tableColumnForGridColumn(detail.colIndex),
|
||||
});
|
||||
isEditingDetail.value = true;
|
||||
}
|
||||
|
||||
@@ -2998,7 +3015,11 @@ function commitDetailEdit() {
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
const oldVal = newRows.value[item.newIndex]?.[detail.colIndex];
|
||||
newRows.value[item.newIndex][detail.colIndex] = coerceCellValue(detailEditValue.value, oldVal);
|
||||
newRows.value[item.newIndex][detail.colIndex] = coerceDetailCellValue(
|
||||
detailEditValue.value,
|
||||
oldVal,
|
||||
detail.colIndex,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3006,7 +3027,7 @@ function commitDetailEdit() {
|
||||
if (!canEditExistingRows.value) return;
|
||||
|
||||
const oldVal = props.result.rows[item.sourceIndex]?.[detail.colIndex];
|
||||
const newVal = coerceCellValue(detailEditValue.value, oldVal);
|
||||
const newVal = coerceDetailCellValue(detailEditValue.value, oldVal, detail.colIndex);
|
||||
if (newVal !== oldVal) {
|
||||
if (!dirtyRows.value.has(item.sourceIndex)) dirtyRows.value.set(item.sourceIndex, new Map());
|
||||
dirtyRows.value.get(item.sourceIndex)!.set(detail.colIndex, newVal);
|
||||
@@ -3035,7 +3056,11 @@ function syncEditorFromDetailEdit() {
|
||||
function cancelValueEditorEdit() {
|
||||
const detail = activeCellDetail.value;
|
||||
if (!detail || !detail.isEditable) return;
|
||||
detailEditValue.value = cellDetailEditorText(detail.value, detail.type);
|
||||
detailEditValue.value = dataGridCellEditorText({
|
||||
value: detail.value,
|
||||
databaseType: props.databaseType,
|
||||
columnInfo: tableColumnForGridColumn(detail.colIndex),
|
||||
});
|
||||
syncEditorFromDetailEdit();
|
||||
isEditingDetail.value = true;
|
||||
}
|
||||
@@ -3067,7 +3092,11 @@ function restoreDetailOriginalValue() {
|
||||
dirtyRows.value = new Map(dirtyRows.value);
|
||||
}
|
||||
|
||||
detailEditValue.value = cellDetailEditorText(restoredValue, detail.type);
|
||||
detailEditValue.value = dataGridCellEditorText({
|
||||
value: restoredValue,
|
||||
databaseType: props.databaseType,
|
||||
columnInfo: tableColumnForGridColumn(detail.colIndex),
|
||||
});
|
||||
syncEditorFromDetailEdit();
|
||||
isEditingDetail.value = activeCellDetailTab.value === "valueEditor";
|
||||
detailCell.value = { ...detailCell.value! };
|
||||
@@ -3297,9 +3326,17 @@ function primitiveCellFormatKey(value: CellValue, columnIndex?: number): string
|
||||
function formatCell(value: CellValue, columnIndex?: number): string {
|
||||
const formatter = columnIndex === undefined ? undefined : resolvedColumnFormatters.value[columnIndex];
|
||||
const columnName = columnIndex === undefined ? undefined : props.result.columns[columnIndex];
|
||||
const columnInfo = columnIndex === undefined ? undefined : tableColumnForGridColumn(columnIndex);
|
||||
const arrayDisplay = formatter
|
||||
? undefined
|
||||
: dataGridCellDisplayText({ value, databaseType: props.databaseType, columnInfo });
|
||||
if (arrayDisplay !== undefined) return arrayDisplay;
|
||||
const binaryDisplay = formatter
|
||||
? null
|
||||
: binaryCellDisplayText(value, columnName ? columnTypeMap.value.get(columnName) : undefined);
|
||||
: binaryCellDisplayText(
|
||||
value,
|
||||
columnInfo?.data_type ?? (columnName ? columnTypeMap.value.get(columnName) : undefined),
|
||||
);
|
||||
if (binaryDisplay) return binaryDisplay;
|
||||
const s = applyColumnFormatter(value, formatter);
|
||||
return s.length > CELL_DISPLAY_MAX_LENGTH ? s.slice(0, CELL_DISPLAY_MAX_LENGTH) : s;
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
type Ref,
|
||||
} from "vue";
|
||||
import * as api from "@/lib/api";
|
||||
import type { CellValue } from "@/lib/cellValue";
|
||||
import { coerceDataGridCellValue, dataGridCellEditorText } from "@/lib/dataGridCellCoercion";
|
||||
import { normalizeDataGridSaveError } from "@/lib/dataGridSql";
|
||||
import { rowStatusFilterAfterAddingRow, type RowStatusFilter } from "@/lib/gridRowStatus";
|
||||
import { supportsDataGridTransaction } from "@/lib/tableEditing";
|
||||
@@ -20,8 +22,6 @@ import { useHistoryStore } from "@/stores/historyStore";
|
||||
import type { ColumnInfo, DatabaseType } from "@/types/database";
|
||||
import { DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_COLUMN } from "@/lib/tableEditing";
|
||||
|
||||
type CellValue = string | number | boolean | null;
|
||||
|
||||
interface RowItem {
|
||||
id: number;
|
||||
sourceIndex?: number;
|
||||
@@ -378,40 +378,19 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
}
|
||||
|
||||
// --- Cell value coercion ---
|
||||
function isNull(value: unknown): boolean {
|
||||
return value === null;
|
||||
function coerceCellValue(value: string, oldValue: CellValue | undefined, columnIndex: number): CellValue {
|
||||
return coerceDataGridCellValue({
|
||||
value,
|
||||
oldValue,
|
||||
databaseType: databaseType.value,
|
||||
columnInfo: tableColumnForGridColumn(columnIndex),
|
||||
}) as CellValue;
|
||||
}
|
||||
|
||||
function coerceCellValue(value: string, oldVal: CellValue | undefined): CellValue {
|
||||
if (value.toUpperCase() === "NULL") return null;
|
||||
if (value === "" && isNull(oldVal)) return null;
|
||||
if (typeof oldVal === "number") {
|
||||
const num = Number(value);
|
||||
if (!Number.isNaN(num)) return num;
|
||||
}
|
||||
if (typeof oldVal === "boolean") {
|
||||
return value === "true" || value === "1";
|
||||
}
|
||||
return normalizeSmartQuotedJsonInput(value);
|
||||
}
|
||||
|
||||
function normalizeSmartQuotedJsonInput(value: string): string {
|
||||
if (!/[“”]/.test(value)) return value;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value;
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return value;
|
||||
} catch {
|
||||
// macOS smart punctuation can turn JSON delimiters into Chinese-style quotes.
|
||||
}
|
||||
const normalized = value.replace(/[“”]/g, '"');
|
||||
try {
|
||||
JSON.parse(normalized);
|
||||
return normalized;
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
function tableColumnForGridColumn(columnIndex: number): ColumnInfo | undefined {
|
||||
const columnName = sourceColumns.value?.[columnIndex] ?? result.value.columns[columnIndex];
|
||||
if (!columnName) return undefined;
|
||||
return tableMeta.value?.columns.find((column) => column.name.toLowerCase() === columnName.toLowerCase());
|
||||
}
|
||||
|
||||
function canEditColumn(columnIndex: number): boolean {
|
||||
@@ -437,7 +416,11 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
suppressNextBlurCommit = false;
|
||||
editingCell.value = { rowId, col: colIdx };
|
||||
const val = item?.data[colIdx] ?? null;
|
||||
editValue.value = val === null ? "" : typeof val === "object" ? JSON.stringify(val) : String(val);
|
||||
editValue.value = dataGridCellEditorText({
|
||||
value: val,
|
||||
databaseType: databaseType.value,
|
||||
columnInfo: tableColumnForGridColumn(colIdx),
|
||||
});
|
||||
focusEditInput();
|
||||
}
|
||||
|
||||
@@ -453,7 +436,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
const oldVal = newRows.value[item.newIndex]?.[col];
|
||||
const newVal = coerceCellValue(editValue.value, oldVal);
|
||||
const newVal = coerceCellValue(editValue.value, oldVal, col);
|
||||
if (newRows.value[item.newIndex]) {
|
||||
newRows.value[item.newIndex][col] = newVal;
|
||||
}
|
||||
@@ -471,7 +454,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
}
|
||||
|
||||
const oldVal = result.value.rows[item.sourceIndex]?.[col];
|
||||
const newVal = coerceCellValue(editValue.value, oldVal);
|
||||
const newVal = coerceCellValue(editValue.value, oldVal, col);
|
||||
if (newVal !== oldVal) {
|
||||
if (!dirtyRows.value.has(item.sourceIndex)) dirtyRows.value.set(item.sourceIndex, new Map());
|
||||
dirtyRows.value.get(item.sourceIndex)!.set(col, newVal);
|
||||
@@ -501,7 +484,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
const oldVal = newRows.value[item.newIndex]?.[col];
|
||||
newRows.value[item.newIndex][col] = value === null ? null : coerceCellValue(value, oldVal);
|
||||
newRows.value[item.newIndex][col] = value === null ? null : coerceCellValue(value, oldVal, col);
|
||||
newRows.value = [...newRows.value];
|
||||
return;
|
||||
}
|
||||
@@ -510,7 +493,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
if (!canEditExistingRows.value) return;
|
||||
|
||||
const oldVal = result.value.rows[item.sourceIndex]?.[col];
|
||||
const newVal = value === null ? null : coerceCellValue(value, oldVal);
|
||||
const newVal = value === null ? null : coerceCellValue(value, oldVal, col);
|
||||
if (newVal !== oldVal) {
|
||||
if (!dirtyRows.value.has(item.sourceIndex)) dirtyRows.value.set(item.sourceIndex, new Map());
|
||||
dirtyRows.value.get(item.sourceIndex)!.set(col, newVal);
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { GridCellValue } from "@/lib/dataGridSql";
|
||||
import type { DatabaseType, ColumnInfo } from "@/types/database";
|
||||
|
||||
export interface CoerceDataGridCellValueOptions {
|
||||
value: string;
|
||||
oldValue: GridCellValue | undefined;
|
||||
databaseType: DatabaseType | undefined;
|
||||
columnInfo: Pick<ColumnInfo, "data_type"> | undefined;
|
||||
}
|
||||
|
||||
export function coerceDataGridCellValue(options: CoerceDataGridCellValueOptions): GridCellValue {
|
||||
const { value, oldValue } = options;
|
||||
if (value.toUpperCase() === "NULL") return null;
|
||||
if (value === "" && oldValue === null) return null;
|
||||
const postgresArrayValue = coercePostgresArrayValue(options);
|
||||
if (postgresArrayValue !== undefined) return postgresArrayValue;
|
||||
if (typeof oldValue === "number") {
|
||||
const num = Number(value);
|
||||
if (!Number.isNaN(num)) return num;
|
||||
}
|
||||
if (typeof oldValue === "boolean") {
|
||||
return value === "true" || value === "1";
|
||||
}
|
||||
return normalizeSmartQuotedJsonInput(value);
|
||||
}
|
||||
|
||||
export function dataGridCellEditorText(options: {
|
||||
value: GridCellValue | undefined;
|
||||
databaseType: DatabaseType | undefined;
|
||||
columnInfo: Pick<ColumnInfo, "data_type"> | undefined;
|
||||
}): string {
|
||||
const value = options.value ?? null;
|
||||
if (value === null) return "";
|
||||
if (Array.isArray(value) && options.databaseType === "postgres" && isPostgresArrayColumn(options.columnInfo, value)) {
|
||||
return formatPostgresArrayText(value);
|
||||
}
|
||||
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
||||
}
|
||||
|
||||
export function dataGridCellDisplayText(options: {
|
||||
value: GridCellValue;
|
||||
databaseType: DatabaseType | undefined;
|
||||
columnInfo: Pick<ColumnInfo, "data_type"> | undefined;
|
||||
}): string | undefined {
|
||||
if (
|
||||
Array.isArray(options.value) &&
|
||||
options.databaseType === "postgres" &&
|
||||
isPostgresArrayColumn(options.columnInfo, options.value)
|
||||
) {
|
||||
return formatPostgresArrayText(options.value);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function coercePostgresArrayValue(options: CoerceDataGridCellValueOptions): unknown[] | undefined {
|
||||
if (options.databaseType !== "postgres") return undefined;
|
||||
if (!isPostgresArrayColumn(options.columnInfo, options.oldValue)) return undefined;
|
||||
const trimmed = options.value.trim();
|
||||
if (!trimmed.startsWith("[")) return undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(normalizeSmartQuotes(trimmed));
|
||||
return Array.isArray(parsed) ? parsed : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isPostgresArrayColumn(
|
||||
columnInfo: Pick<ColumnInfo, "data_type"> | undefined,
|
||||
oldValue: GridCellValue | undefined,
|
||||
): boolean {
|
||||
if (Array.isArray(oldValue)) return true;
|
||||
const dataType = columnInfo?.data_type.trim().toLowerCase() ?? "";
|
||||
return dataType === "array" || dataType.endsWith("[]") || dataType.startsWith("_");
|
||||
}
|
||||
|
||||
function normalizeSmartQuotedJsonInput(value: string): string {
|
||||
if (!/[“”]/.test(value)) return value;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value;
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return value;
|
||||
} catch {
|
||||
// macOS smart punctuation can turn JSON delimiters into Chinese-style quotes.
|
||||
}
|
||||
const normalized = normalizeSmartQuotes(value);
|
||||
try {
|
||||
JSON.parse(normalized);
|
||||
return normalized;
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSmartQuotes(value: string): string {
|
||||
return value.replace(/[“”]/g, '"');
|
||||
}
|
||||
|
||||
function formatPostgresArrayText(value: unknown[]): string {
|
||||
return `{${value.map(formatPostgresArrayElement).join(",")}}`;
|
||||
}
|
||||
|
||||
function formatPostgresArrayElement(value: unknown): string {
|
||||
if (Array.isArray(value)) return formatPostgresArrayText(value);
|
||||
if (value === null) return "NULL";
|
||||
if (typeof value === "boolean") return value ? "true" : "false";
|
||||
if (typeof value === "number") return String(value);
|
||||
const text = typeof value === "string" ? value : JSON.stringify(value);
|
||||
if (text === undefined) return "";
|
||||
if (!needsQuotedPostgresArrayElement(text)) return text;
|
||||
return `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
|
||||
function needsQuotedPostgresArrayElement(value: string): boolean {
|
||||
return value === "" || /[\s,"{}\\]/.test(value) || value.toUpperCase() === "NULL";
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import * as api from "@/lib/api";
|
||||
|
||||
export type GridCellValue = string | number | boolean | null;
|
||||
export type GridCellValue = string | number | boolean | null | unknown[] | { [key: string]: unknown };
|
||||
|
||||
export interface DataGridTableMeta {
|
||||
schema?: string;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import {
|
||||
coerceDataGridCellValue,
|
||||
dataGridCellDisplayText,
|
||||
dataGridCellEditorText,
|
||||
} from "../../apps/desktop/src/lib/dataGridCellCoercion.ts";
|
||||
|
||||
test("formats Postgres array cells with native brace syntax", () => {
|
||||
const columnInfo = { data_type: "_text" };
|
||||
|
||||
assert.equal(
|
||||
dataGridCellDisplayText({
|
||||
value: ["draft", "发布", "needs space"],
|
||||
databaseType: "postgres",
|
||||
columnInfo,
|
||||
}),
|
||||
'{draft,发布,"needs space"}',
|
||||
);
|
||||
assert.equal(
|
||||
dataGridCellEditorText({
|
||||
value: ["draft", "发布", "needs space"],
|
||||
databaseType: "postgres",
|
||||
columnInfo,
|
||||
}),
|
||||
'{draft,发布,"needs space"}',
|
||||
);
|
||||
});
|
||||
|
||||
test("coerces JSON style input for Postgres array columns", () => {
|
||||
assert.deepEqual(
|
||||
coerceDataGridCellValue({
|
||||
value: `["draft","发布"]`,
|
||||
oldValue: "{legacy}",
|
||||
databaseType: "postgres",
|
||||
columnInfo: { data_type: "_text" },
|
||||
}),
|
||||
["draft", "发布"],
|
||||
);
|
||||
});
|
||||
@@ -3,11 +3,10 @@ import { test } from "vitest";
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { useDataGridEditor } from "../../apps/desktop/src/composables/useDataGridEditor.ts";
|
||||
import type { CellValue } from "../../apps/desktop/src/lib/cellValue.ts";
|
||||
import type { DataGridSaveStatementOptions } from "../../apps/desktop/src/lib/dataGridSql.ts";
|
||||
import type { ColumnInfo } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
type CellValue = string | number | boolean | null;
|
||||
|
||||
function installBrowserTestGlobals() {
|
||||
globalThis.document = { querySelector: () => null } as unknown as Document;
|
||||
globalThis.localStorage = {
|
||||
@@ -82,10 +81,23 @@ function formatGridSqlLiteral(value: CellValue, databaseType?: string): string {
|
||||
if (value === null) return "NULL";
|
||||
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
||||
if (typeof value === "number") return String(value);
|
||||
if (Array.isArray(value) && databaseType === "postgres") {
|
||||
return `'${formatPgArrayLiteral(value)}'`;
|
||||
}
|
||||
const escaped = `'${String(value).replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
|
||||
return databaseType === "sqlserver" ? `N${escaped}` : escaped;
|
||||
}
|
||||
|
||||
function formatPgArrayLiteral(value: CellValue[]): string {
|
||||
return `{${value
|
||||
.map((item) => {
|
||||
if (Array.isArray(item)) return formatPgArrayLiteral(item);
|
||||
if (item === null) return "NULL";
|
||||
return `"${String(item).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
||||
})
|
||||
.join(",")}}`;
|
||||
}
|
||||
|
||||
function column(name: string, isPrimaryKey = false, extra: string | null = null): ColumnInfo {
|
||||
return {
|
||||
name,
|
||||
@@ -493,6 +505,61 @@ test("saving manually typed JSON from a MySQL grid normalizes smart quotes", asy
|
||||
]);
|
||||
});
|
||||
|
||||
test("saving manually typed JSON arrays from a Postgres array column uses array values", async () => {
|
||||
setActivePinia(createPinia());
|
||||
installBrowserTestGlobals();
|
||||
|
||||
const result = computed(() => ({
|
||||
columns: ["id", "tags"],
|
||||
rows: [[1, "{legacy}"] as CellValue[]],
|
||||
}));
|
||||
const rowStatusFilter = ref<"all" | "changed" | "edited" | "new" | "deleted">("all");
|
||||
const executedSql: string[] = [];
|
||||
|
||||
const editor = useDataGridEditor({
|
||||
result,
|
||||
editable: computed(() => true),
|
||||
databaseType: computed(() => "postgres"),
|
||||
connectionId: computed(() => undefined),
|
||||
database: computed(() => undefined),
|
||||
tableMeta: computed(() => ({
|
||||
tableName: "articles",
|
||||
columns: [column("id", true), { ...column("tags"), data_type: "_text" }],
|
||||
primaryKeys: ["id"],
|
||||
})),
|
||||
onExecuteSql: computed(() => async (sql: string) => {
|
||||
executedSql.push(sql);
|
||||
}),
|
||||
customSave: computed(() => undefined),
|
||||
sql: computed(() => "SELECT id, tags FROM articles"),
|
||||
searchText: ref(""),
|
||||
whereFilterInput: ref(""),
|
||||
orderByInput: ref(""),
|
||||
currentWhereInput: computed(() => undefined),
|
||||
rowStatusFilter,
|
||||
pageSize: ref(50),
|
||||
currentPage: ref(1),
|
||||
getRowItem: (rowId) => {
|
||||
if (rowId !== 0) return undefined;
|
||||
return {
|
||||
id: 0,
|
||||
sourceIndex: 0,
|
||||
data: result.value.rows[0],
|
||||
isNew: false,
|
||||
isDeleted: false,
|
||||
isDirtyCol: [false, false],
|
||||
status: "clean",
|
||||
};
|
||||
},
|
||||
emit: () => {},
|
||||
});
|
||||
|
||||
editor.applyCellValue(0, 1, `["draft","发布"]`);
|
||||
await editor.saveChanges();
|
||||
|
||||
assert.deepEqual(executedSql, [`UPDATE "articles" SET "tags" = '{"draft","发布"}' WHERE "id" = 1;`]);
|
||||
});
|
||||
|
||||
test("failed table data save records a failed history entry", async () => {
|
||||
setActivePinia(createPinia());
|
||||
installBrowserTestGlobals();
|
||||
|
||||
Reference in New Issue
Block a user