feat(grid): add enum column select editor for MySQL
This commit is contained in:
1 parent
f10d152c53
commit
ca1d420d91
4 files changed
+220
No files matched your search
@@ -62,6 +62,7 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import ImagePreviewDialog from "@/components/grid/ImagePreviewDialog.vue";
|
||||
import TemporalCellEditor from "@/components/grid/TemporalCellEditor.vue";
|
||||
import EnumCellEditor from "@/components/grid/EnumCellEditor.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";
|
||||
@@ -94,6 +95,7 @@ import { renderWktOnCanvas, isHexGeometry } from "@/lib/geometryPreview";
|
||||
import { buildDataGridCellDetail, buildDataGridColumnDetail, buildDataGridRowDetail, dataGridColumnDetailJson, dataGridColumnDetailTsv, dataGridRowDetailJson, dataGridRowDetailTsv, filterDataGridDetailFields, type DataGridCellDetail } from "@/lib/dataGridDetail";
|
||||
import { applyColumnFormatter, buildColumnFormatterKey, normalizeColumnFormatter, resolveColumnFormatter, type ColumnFormatterConfig, type DateTimeFormatterUnit } from "@/lib/columnFormatter";
|
||||
import { temporalCellEditorKind, type TemporalCellEditorKind } from "@/lib/dataGridTemporalEditor";
|
||||
import { isEnumColumn, enumValuesForColumn } from "@/lib/dataGridEnumEditor";
|
||||
import { isCancelSearchShortcut, isCopyCurrentRowShortcut, isDeleteCurrentRowShortcut, isFocusSearchShortcut, isModRShortcut, isToggleTransposeShortcut } from "@/lib/keyboardShortcuts";
|
||||
import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGridScrollGutter";
|
||||
import { canGoNextDataGridPage } from "@/lib/dataGridPagination";
|
||||
@@ -2109,6 +2111,18 @@ function temporalEditorKindForColumn(columnIndex: number): TemporalCellEditorKin
|
||||
return temporalCellEditorKind(tableColumnForGridColumn(columnIndex)?.data_type, props.databaseType);
|
||||
}
|
||||
|
||||
function enumValuesForGridColumn(columnIndex: number): string[] {
|
||||
return enumValuesForColumn(tableColumnForGridColumn(columnIndex));
|
||||
}
|
||||
|
||||
function isEnumGridColumn(columnIndex: number): boolean {
|
||||
return isEnumColumn(tableColumnForGridColumn(columnIndex));
|
||||
}
|
||||
|
||||
function isEnumGridColumnNullable(columnIndex: number): boolean {
|
||||
return tableColumnForGridColumn(columnIndex)?.is_nullable ?? false;
|
||||
}
|
||||
|
||||
function canDeleteRowItem(item: RowItem | undefined): boolean {
|
||||
return !!props.editable && !!item && !item.isDeleted && (item.isNew || canEditExistingRows.value);
|
||||
}
|
||||
@@ -6076,6 +6090,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
>
|
||||
<template v-if="editingCell?.rowId === displayItems[cell.recordIndex]?.id && editingCell?.col === cell.valueIndex">
|
||||
<TemporalCellEditor v-if="temporalEditorKindForColumn(cell.valueIndex)" v-model="editValue" :kind="temporalEditorKindForColumn(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)" cell-layout="transpose" @cancel="cancelEdit" @commit="commitGridEdit" />
|
||||
<input
|
||||
v-else
|
||||
v-model="editValue"
|
||||
@@ -6464,6 +6479,14 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
>
|
||||
<div v-if="canvasEditingCell" class="absolute pointer-events-auto z-20" :class="{ 'tabular-nums': canvasEditingCellIsNumeric }" :style="canvasEditingCellStyle" @mousedown.stop @click.stop>
|
||||
<TemporalCellEditor v-if="temporalEditorKindForColumn(canvasEditingCell.actualColIdx)" v-model="editValue" :kind="temporalEditorKindForColumn(canvasEditingCell.actualColIdx)!" @cancel="cancelEdit" @commit="commitGridEdit" />
|
||||
<EnumCellEditor
|
||||
v-else-if="isEnumGridColumn(canvasEditingCell.actualColIdx)"
|
||||
v-model="editValue"
|
||||
:values="enumValuesForGridColumn(canvasEditingCell.actualColIdx)"
|
||||
:nullable="isEnumGridColumnNullable(canvasEditingCell.actualColIdx)"
|
||||
@cancel="cancelEdit"
|
||||
@commit="commitGridEdit"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
v-model="editValue"
|
||||
@@ -6564,6 +6587,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
>
|
||||
<template v-if="editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx">
|
||||
<TemporalCellEditor v-if="temporalEditorKindForColumn(col.actualColIdx)" v-model="editValue" :kind="temporalEditorKindForColumn(col.actualColIdx)!" @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" />
|
||||
<input
|
||||
v-else
|
||||
v-model="editValue"
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref } from "vue";
|
||||
import type { FocusOutsideEvent, PointerDownOutsideEvent } from "reka-ui";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string;
|
||||
values: string[];
|
||||
nullable?: boolean;
|
||||
cellLayout?: "grid" | "transpose";
|
||||
}>(),
|
||||
{
|
||||
nullable: false,
|
||||
cellLayout: "grid",
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: string];
|
||||
commit: [];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
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)");
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => triggerRef.value?.focus());
|
||||
});
|
||||
|
||||
function selectValue(value: string) {
|
||||
emit("update:modelValue", value);
|
||||
finishCommit();
|
||||
}
|
||||
|
||||
function finishCommit() {
|
||||
closeHandled = true;
|
||||
emit("commit");
|
||||
}
|
||||
|
||||
function finishCancel() {
|
||||
closeHandled = true;
|
||||
emit("cancel");
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
finishCommit();
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
finishCancel();
|
||||
}
|
||||
}
|
||||
|
||||
function onPopoverInteractOutside(event: PointerDownOutsideEvent | FocusOutsideEvent) {
|
||||
const originalEvent = event.detail.originalEvent;
|
||||
if (originalEvent instanceof FocusEvent) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
finishCommit();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover
|
||||
:open="open"
|
||||
@update:open="
|
||||
(v) => {
|
||||
open = v;
|
||||
if (!v && !closeHandled) finishCommit();
|
||||
}
|
||||
"
|
||||
>
|
||||
<PopoverTrigger as-child>
|
||||
<button ref="triggerRef" type="button" :class="triggerClass" @keydown.stop="onKeydown" @click.stop>
|
||||
<span class="min-w-0 flex-1 truncate">{{ displayValue }}</span>
|
||||
</button>
|
||||
</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>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { ColumnInfo } from "@/types/database";
|
||||
|
||||
/**
|
||||
* Check if the column's data_type is a MySQL ENUM type definition.
|
||||
* Matches patterns like `enum('value1','value2')` (case-insensitive).
|
||||
*/
|
||||
export function isEnumColumn(columnInfo: Pick<ColumnInfo, "data_type"> | undefined): boolean {
|
||||
if (!columnInfo?.data_type) return false;
|
||||
return /^enum\s*\(/i.test(columnInfo.data_type.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse MySQL ENUM values from a data_type string like `enum('a','b',...)`.
|
||||
* Handles escaped single quotes ('' → ').
|
||||
* Returns an empty array for unparseable input.
|
||||
*/
|
||||
export function enumValuesForColumn(columnInfo: Pick<ColumnInfo, "data_type"> | undefined): string[] {
|
||||
if (!isEnumColumn(columnInfo)) return [];
|
||||
const raw = columnInfo!.data_type.trim();
|
||||
const inner = raw.match(/^enum\s*\(\s*(.+?)\s*\)\s*$/is)?.[1];
|
||||
if (!inner) return [];
|
||||
|
||||
const values: string[] = [];
|
||||
let i = 0;
|
||||
while (i < inner.length) {
|
||||
// Skip whitespace and commas between values
|
||||
while (i < inner.length && (inner[i] === " " || inner[i] === ",")) i++;
|
||||
if (i >= inner.length) break;
|
||||
|
||||
if (inner[i] !== "'") break; // unexpected token
|
||||
|
||||
i++; // skip opening quote
|
||||
let value = "";
|
||||
while (i < inner.length) {
|
||||
if (inner[i] === "'") {
|
||||
if (i + 1 < inner.length && inner[i + 1] === "'") {
|
||||
// Escaped single quote: ''
|
||||
value += "'";
|
||||
i += 2;
|
||||
} else {
|
||||
// Closing quote
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
value += inner[i];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
values.push(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
Reference in new issue
Block a user