fix(mysql): edit enum column values

This commit is contained in:
t8y2 committed 2026-07-10 16:10:54 +08:00
1 parent ea8b3f9800
commit 8c7e34a6de
14 files changed
+283 -54

No files matched your search

+29 -6
View File
@@ -90,7 +90,7 @@ import { createColumnDrafts } from "@/lib/table/tableStructureEditorState";
import type { BuildSingleColumnAlterSqlOptions } from "@/lib/table/tableStructureEditorSql";
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/table/tableSelectSql";
import { uuid } from "@/lib/common/utils";
import { resolveHeaderColumnType } from "@/lib/dataGrid/dataGridColumnType";
import { compactHeaderColumnType, resolveHeaderColumnType } from "@/lib/dataGrid/dataGridColumnType";
import { canEditExistingTableRows, canInsertTableRows, canUseKeylessRowPredicate, hiveTablePropertiesIndicateTransactional, isClickHouseExistingRowReadonlyColumn, isHiddenGridColumn, isTdengineExistingRowReadonlyColumn, usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
import { buildDataGridColumnDistinctValuesSql, buildDataGridContextFilterCondition, buildDataGridCountSql, buildHiveTablePropertiesSql, type DataGridContextFilterMode } from "@/lib/dataGrid/dataGridSql";
import {
@@ -379,7 +379,7 @@ function headerColumnType(column: string, actualColIdx: number): string {
resultColumnTypes: props.result.column_types,
actualColIdx,
});
return resolved ? shortTypeName(resolved) : "";
return resolved ? shortTypeName(compactHeaderColumnType(resolved)) : "";
}
function shortTypeName(t: string): string {
@@ -4124,6 +4124,11 @@ function isEnumGridColumnNullable(columnIndex: number): boolean {
return tableColumnForGridColumn(columnIndex)?.is_nullable ?? false;
}
function isEnumEditorInitialNull(rowId: number | undefined, columnIndex: number): boolean {
if (rowId === undefined) return false;
return getRowItem(rowId)?.data[columnIndex] === null;
}
function cellEditInputModeForColumn(columnIndex: number): "decimal" | "numeric" | undefined {
const dataType = normalizedColumnDataType(tableColumnForGridColumn(columnIndex));
if (isIntegerColumnType(dataType)) return "numeric";
@@ -6752,8 +6757,8 @@ function deleteCurrentRow(): boolean {
return true;
}
function commitGridEdit() {
void commitEditAndMaybeAutoSave().finally(() => nextTick(() => gridRef.value?.focus({ preventScroll: true })));
function commitGridEdit(value?: string | null) {
void commitEditAndMaybeAutoSave(value === undefined ? undefined : { explicitValue: value }).finally(() => nextTick(() => gridRef.value?.focus({ preventScroll: true })));
}
async function commitEditFromCellBlur() {
@@ -9257,7 +9262,16 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
@cancel="cancelEdit"
@commit="commitGridEdit"
/>
<EnumCellEditor v-else-if="isEnumGridColumn(cell.valueIndex)" v-model="editValue" :values="enumValuesForGridColumn(cell.valueIndex)" :nullable="isEnumGridColumnNullable(cell.valueIndex)" cell-layout="transpose" @cancel="cancelEdit" @commit="commitGridEdit" />
<EnumCellEditor
v-else-if="isEnumGridColumn(cell.valueIndex)"
v-model="editValue"
:values="enumValuesForGridColumn(cell.valueIndex)"
:nullable="isEnumGridColumnNullable(cell.valueIndex)"
:initial-null="isEnumEditorInitialNull(displayItems[cell.recordIndex]?.id, cell.valueIndex)"
cell-layout="transpose"
@cancel="cancelEdit"
@commit="commitGridEdit"
/>
<textarea
v-else-if="cellUsesExpandedEditor(displayItems[cell.recordIndex]?.id, cell.valueIndex)"
v-model="editValue"
@@ -9744,6 +9758,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
v-model="editValue"
:values="enumValuesForGridColumn(canvasEditingCell.actualColIdx)"
:nullable="isEnumGridColumnNullable(canvasEditingCell.actualColIdx)"
:initial-null="isEnumEditorInitialNull(canvasEditingCell.rowId, canvasEditingCell.actualColIdx)"
@cancel="cancelEdit"
@commit="commitGridEdit"
/>
@@ -9889,7 +9904,15 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
@cancel="cancelEdit"
@commit="commitGridEdit"
/>
<EnumCellEditor v-else-if="isEnumGridColumn(col.actualColIdx)" v-model="editValue" :values="enumValuesForGridColumn(col.actualColIdx)" :nullable="isEnumGridColumnNullable(col.actualColIdx)" @cancel="cancelEdit" @commit="commitGridEdit" />
<EnumCellEditor
v-else-if="isEnumGridColumn(col.actualColIdx)"
v-model="editValue"
:values="enumValuesForGridColumn(col.actualColIdx)"
:nullable="isEnumGridColumnNullable(col.actualColIdx)"
:initial-null="isEnumEditorInitialNull(item.id, col.actualColIdx)"
@cancel="cancelEdit"
@commit="commitGridEdit"
/>
<textarea
v-else-if="cellUsesExpandedEditor(item.id, col.actualColIdx)"
v-model="editValue"
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import type { FocusOutsideEvent, PointerDownOutsideEvent } from "reka-ui";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
@@ -8,40 +9,47 @@ const props = withDefaults(
modelValue: string;
values: string[];
nullable?: boolean;
initialNull?: boolean;
cellLayout?: "grid" | "transpose";
}>(),
{
nullable: false,
initialNull: false,
cellLayout: "grid",
},
);
const emit = defineEmits<{
"update:modelValue": [value: string];
commit: [];
commit: [value?: string | null];
cancel: [];
}>();
const { t } = useI18n();
const open = ref(true);
const triggerRef = ref<HTMLButtonElement | null>(null);
let closeHandled = false;
const triggerClass = computed(() => ["cell-edit-input absolute inset-0 z-10 flex items-center gap-1 border-2 border-primary bg-background py-0 text-left text-xs outline-none", props.cellLayout === "transpose" ? "px-1.5" : "px-2.5"].join(" "));
const displayValue = computed(() => props.modelValue || "(NULL)");
const displayValue = computed(() => {
if (props.initialNull && props.modelValue === "") return t("grid.nullValue");
return props.modelValue === "" ? "''" : props.modelValue;
});
onMounted(() => {
nextTick(() => triggerRef.value?.focus());
});
function selectValue(value: string) {
emit("update:modelValue", value);
finishCommit();
function selectValue(value: string | null) {
if (value !== null) emit("update:modelValue", value);
finishCommit(value);
}
function finishCommit() {
function finishCommit(value?: string | null) {
closeHandled = true;
emit("commit");
emit("commit", value);
}
function finishCancel() {
@@ -87,9 +95,22 @@ function onPopoverInteractOutside(event: PointerDownOutsideEvent | FocusOutsideE
</PopoverTrigger>
<PopoverContent align="start" side="bottom" class="w-auto min-w-[120px] rounded-md p-1" @click.stop @keydown.stop="onKeydown" @interact-outside="onPopoverInteractOutside">
<div class="flex flex-col gap-0.5">
<button v-if="nullable" type="button" class="flex items-center rounded-sm px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-accent-foreground" :class="{ 'bg-accent text-accent-foreground': modelValue === '' }" @click="selectValue('')">(NULL)</button>
<button v-for="val in values" :key="val" type="button" class="flex items-center rounded-sm px-2 py-1 text-xs hover:bg-accent hover:text-accent-foreground" :class="{ 'bg-accent text-accent-foreground': modelValue === val }" @click="selectValue(val)">
{{ val }}
<button v-if="nullable" type="button" class="flex min-h-7 w-full items-center rounded-sm px-2 py-1 text-left text-xs italic text-muted-foreground hover:bg-accent hover:text-accent-foreground" :class="{ 'bg-accent text-accent-foreground': initialNull }" @click="selectValue(null)">
{{ t("grid.nullValue") }}
</button>
<button
v-for="val in values"
:key="val"
type="button"
class="flex min-h-7 w-full items-center rounded-sm px-2 py-1 text-left text-xs hover:bg-accent hover:text-accent-foreground"
:class="{ 'bg-accent text-accent-foreground': modelValue === val && !(initialNull && val === '') }"
@click="selectValue(val)"
>
<template v-if="val === ''">
<code>''</code>
<span class="ml-2 text-muted-foreground">{{ t("grid.emptyStringValue") }}</span>
</template>
<template v-else>{{ val }}</template>
</button>
</div>
</PopoverContent>
@@ -45,9 +45,11 @@ import {
getDataTypeOptions,
getDefaultLengthForType,
isDataTypeLengthDisabled,
isMysqlEnumDataType,
isMysqlCharacterDataType,
isProtectedManticoreIdColumn,
isSqlServerIdentityCompatibleDataType,
mysqlEnumDataType,
parseExtraToColumnExtra,
rehydrateColumnDraftsFromMetadata,
splitDataType,
@@ -1247,6 +1249,7 @@ async function addColumn() {
id: `new:${uuid()}`,
name: "",
dataType,
enumValues: [],
isNullable: true,
defaultValue: "",
comment: "",
@@ -1377,7 +1380,12 @@ function updateSqlServerIdentityIncrement(column: EditableStructureColumn, value
}
function updateColumnDataType(column: EditableStructureColumn, baseType: string) {
column.dataType = combineDataTypeForDatabase(databaseType.value, baseType, getDefaultLengthForType(databaseType.value, baseType));
if (isMysqlEnumDataType(databaseType.value, baseType)) {
if (!column.enumValues?.length) column.enumValues = [""];
column.dataType = mysqlEnumDataType(column.enumValues);
} else {
column.dataType = combineDataTypeForDatabase(databaseType.value, baseType, getDefaultLengthForType(databaseType.value, baseType));
}
syncSqlServerIdentityForDataType(column);
// Clear charset/collation when switching to a non-character MySQL type
if (showCharacterSet.value && !isMysqlCharacterDataType(column.dataType)) {
@@ -1386,6 +1394,24 @@ function updateColumnDataType(column: EditableStructureColumn, baseType: string)
}
}
function updateMysqlEnumValue(column: EditableStructureColumn, index: number, value: string | number) {
if (!column.enumValues || index < 0 || index >= column.enumValues.length) return;
column.enumValues[index] = String(value);
column.dataType = mysqlEnumDataType(column.enumValues);
}
function addMysqlEnumValue(column: EditableStructureColumn) {
column.enumValues ??= [];
column.enumValues.push("");
column.dataType = mysqlEnumDataType(column.enumValues);
}
function removeMysqlEnumValue(column: EditableStructureColumn, index: number) {
if (!column.enumValues || column.enumValues.length <= 1) return;
column.enumValues.splice(index, 1);
column.dataType = mysqlEnumDataType(column.enumValues);
}
function updateColumnDataTypeLength(column: EditableStructureColumn, value: string | number) {
column.dataType = combineDataTypeForDatabase(databaseType.value, splitDataType(column.dataType).baseType, String(value));
syncSqlServerIdentityForDataType(column);
@@ -2385,7 +2411,32 @@ watch(activeTab, (tab) => {
<Input v-else :model-value="splitDataType(column.dataType).baseType" :class="[structureMonoControlClass, 'w-full']" disabled />
</td>
<td v-if="columnEditorControls.length" :class="structureCellClass">
<Input :model-value="dataTypeLengthInputValue(databaseType, column.dataType)" :class="structureMonoControlClass" :disabled="isColumnLengthDisabled(column)" @update:model-value="updateColumnDataTypeLength(column, $event)" />
<Popover v-if="isMysqlEnumDataType(databaseType, column.dataType)">
<PopoverTrigger as-child>
<Button variant="outline" size="sm" :class="[structureMonoControlClass, 'w-full justify-between px-2']" :disabled="isColumnTypeDisabled(column)">
<span>{{ t("structureEditor.enumValueCount", { count: column.enumValues?.length ?? 0 }) }}</span>
<ListChevronsUpDown :class="structureIconClass" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-80 p-3" align="start">
<div class="mb-2 flex items-center justify-between gap-2">
<span class="text-sm font-medium">{{ t("structureEditor.enumValues") }}</span>
<Button variant="outline" size="sm" class="h-7 px-2" @click="addMysqlEnumValue(column)">
<Plus class="mr-1 h-3.5 w-3.5" />
{{ t("structureEditor.addEnumValue") }}
</Button>
</div>
<div class="max-h-64 space-y-1.5 overflow-y-auto pr-1">
<div v-for="(value, valueIndex) in column.enumValues" :key="valueIndex" class="flex items-center gap-1.5">
<Input :model-value="value" :class="structureMonoControlClass" :placeholder="t('structureEditor.enumValuePlaceholder')" @update:model-value="updateMysqlEnumValue(column, valueIndex, $event)" />
<Button variant="ghost" size="icon" class="h-8 w-8 shrink-0" :disabled="(column.enumValues?.length ?? 0) <= 1" :title="t('structureEditor.removeEnumValue')" @click="removeMysqlEnumValue(column, valueIndex)">
<Trash2 class="h-3.5 w-3.5" />
</Button>
</div>
</div>
</PopoverContent>
</Popover>
<Input v-else :model-value="dataTypeLengthInputValue(databaseType, column.dataType)" :class="structureMonoControlClass" :disabled="isColumnLengthDisabled(column)" @update:model-value="updateColumnDataTypeLength(column, $event)" />
</td>
<td v-if="columnEditorControls.nullable" :class="structureCellClass">
<label class="flex items-center gap-1.5">