mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
feat(structure): show comments and defaults in copy-fields dialog
This commit is contained in:
@@ -109,6 +109,8 @@ import {
|
||||
structureColumnNamesForCopy,
|
||||
tableStructureIdentifierComparisonKey,
|
||||
toColumnNames,
|
||||
copySourceColumnDetails,
|
||||
matchesCopySourceColumnSearch,
|
||||
} from "@/lib/table/tableStructureEditorState";
|
||||
import { CREATE_DATABASE_CHARSET_OPTIONS, createDatabaseCollationOptionsForCharset, fallbackCreateDatabaseCharsetMetadata, normalizeCreateDatabaseCharsetKey, parseCreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
|
||||
import type { CreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
|
||||
@@ -2501,14 +2503,14 @@ const copyableSourceColumns = computed(() => {
|
||||
const existingNames = new Set(columns.value.filter((column) => !column.markedForDrop).map((column) => tableStructureIdentifierComparisonKey(column.name, databaseType.value, databaseInfo)));
|
||||
return copySourceColumns.value.map((column) => ({
|
||||
column,
|
||||
details: copySourceColumnDetails(column, databaseType.value),
|
||||
alreadyExists: existingNames.has(tableStructureIdentifierComparisonKey(column.name, databaseType.value, databaseInfo)),
|
||||
}));
|
||||
});
|
||||
|
||||
const filteredCopyableSourceColumns = computed(() => {
|
||||
const search = normalizedColumnSearch(copySourceColumnSearch.value);
|
||||
if (!search) return copyableSourceColumns.value;
|
||||
return copyableSourceColumns.value.filter(({ column }) => [column.name, column.data_type, column.comment ?? ""].some((value) => normalizedColumnSearch(value).includes(search)));
|
||||
if (!normalizedColumnSearch(copySourceColumnSearch.value)) return copyableSourceColumns.value;
|
||||
return copyableSourceColumns.value.filter(({ column }) => matchesCopySourceColumnSearch(column, copySourceColumnSearch.value, databaseType.value));
|
||||
});
|
||||
|
||||
const copyableSourceColumnNames = computed(() => copyableSourceColumns.value.filter(({ alreadyExists }) => !alreadyExists).map(({ column }) => column.name));
|
||||
@@ -5441,10 +5443,18 @@ watch(
|
||||
{{ t("structureEditor.copyColumnsNoMatchingFields") }}
|
||||
</div>
|
||||
<div v-else class="max-h-72 overflow-y-auto rounded-md border">
|
||||
<label v-for="{ column, alreadyExists } in filteredCopyableSourceColumns" :key="column.name" class="flex cursor-pointer items-center gap-2 border-b px-3 py-2 last:border-b-0 hover:bg-muted/50" :class="alreadyExists ? 'cursor-not-allowed opacity-60' : ''">
|
||||
<input v-model="selectedCopySourceColumnNames" type="checkbox" :value="column.name" :disabled="alreadyExists" class="size-4 rounded border-input" />
|
||||
<span class="min-w-0 flex-1 truncate font-mono text-sm">{{ column.name }}</span>
|
||||
<span class="shrink-0 text-xs text-muted-foreground">{{ column.data_type }}</span>
|
||||
<label v-for="{ column, details, alreadyExists } in filteredCopyableSourceColumns" :key="column.name" class="flex cursor-pointer items-start gap-2 border-b px-3 py-2 last:border-b-0 hover:bg-muted/50" :class="alreadyExists ? 'cursor-not-allowed opacity-60' : ''">
|
||||
<input v-model="selectedCopySourceColumnNames" type="checkbox" :value="column.name" :disabled="alreadyExists" class="mt-0.5 size-4 shrink-0 rounded border-input" />
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="flex min-w-0 items-center gap-2">
|
||||
<span class="min-w-0 flex-1 truncate font-mono text-sm">{{ column.name }}</span>
|
||||
<span class="shrink-0 text-xs text-muted-foreground">{{ column.data_type }}</span>
|
||||
</span>
|
||||
<span v-if="(columnEditorControls.defaultValue && details.defaultValue) || (columnEditorControls.comment && details.comment)" class="mt-0.5 flex min-w-0 items-center gap-3 text-xs text-muted-foreground">
|
||||
<span v-if="columnEditorControls.defaultValue && details.defaultValue" class="min-w-0 truncate" :title="`${t('structureEditor.defaultValue')}: ${details.defaultValue}`">{{ t("structureEditor.defaultValue") }}: {{ details.defaultValue }}</span>
|
||||
<span v-if="columnEditorControls.comment && details.comment" class="min-w-0 truncate" :title="`${t('structureEditor.comment')}: ${details.comment}`">{{ t("structureEditor.comment") }}: {{ details.comment }}</span>
|
||||
</span>
|
||||
</span>
|
||||
<Badge v-if="alreadyExists" variant="secondary" class="shrink-0 text-[10px]">{{ t("structureEditor.copyColumnsAlreadyExists") }}</Badge>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
cloneColumnDraftAsNew,
|
||||
combineDataTypeForDatabase,
|
||||
combineDataTypeForDatabaseWithLengthUnit,
|
||||
copySourceColumnDetails,
|
||||
createCopiedColumnDrafts,
|
||||
createColumnDrafts,
|
||||
createTriggerDrafts,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
isMysqlCharacterDataType,
|
||||
isMysqlEnumDataType,
|
||||
isSqlServerIdentityCompatibleDataType,
|
||||
matchesCopySourceColumnSearch,
|
||||
mysqlEnumDataType,
|
||||
parseExtraToColumnExtra,
|
||||
rehydrateColumnDraftsFromMetadata,
|
||||
@@ -706,6 +708,54 @@ describe("tableStructureEditorState", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("copySourceColumnDetails", () => {
|
||||
it("keeps the comment and default shown in the copy-fields dialog", () => {
|
||||
expect(copySourceColumnDetails({ data_type: "varchar(20)", column_default: "'unknown'", comment: "名称" })).toEqual({ defaultValue: "'unknown'", comment: "名称" });
|
||||
});
|
||||
|
||||
it("keeps falsy-looking defaults such as 0 and empty strings", () => {
|
||||
expect(copySourceColumnDetails({ data_type: "int", column_default: "0", comment: null })).toEqual({ defaultValue: "0", comment: null });
|
||||
expect(copySourceColumnDetails({ data_type: "varchar(20)", column_default: "''", comment: null })).toEqual({ defaultValue: "''", comment: null });
|
||||
});
|
||||
|
||||
it("drops blank metadata instead of rendering an empty label", () => {
|
||||
expect(copySourceColumnDetails({ data_type: "int", column_default: null, comment: null })).toEqual({ defaultValue: null, comment: null });
|
||||
expect(copySourceColumnDetails({ data_type: "int", column_default: undefined, comment: undefined })).toEqual({ defaultValue: null, comment: null });
|
||||
expect(copySourceColumnDetails({ data_type: "int", column_default: " ", comment: " " })).toEqual({ defaultValue: null, comment: null });
|
||||
});
|
||||
|
||||
it("trims padded metadata for display", () => {
|
||||
expect(copySourceColumnDetails({ data_type: "int", column_default: " 1 ", comment: " 备注 " })).toEqual({ defaultValue: "1", comment: "备注" });
|
||||
});
|
||||
|
||||
it("normalizes defaults per database like the editor grid", () => {
|
||||
expect(copySourceColumnDetails({ data_type: "character varying", column_default: "'unknown'::character varying", comment: null }, "postgres")).toEqual({ defaultValue: "'unknown'", comment: null });
|
||||
expect(copySourceColumnDetails({ data_type: "int", column_default: "((0))", comment: null }, "sqlserver")).toEqual({ defaultValue: "0", comment: null });
|
||||
expect(copySourceColumnDetails({ data_type: "varchar(50)", column_default: "", comment: null }, "mysql")).toEqual({ defaultValue: "''", comment: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe("matchesCopySourceColumnSearch", () => {
|
||||
const column = { name: "status", data_type: "tinyint", column_default: "1", comment: "状态:1启用 0停用" };
|
||||
|
||||
it("matches everything for a blank query", () => {
|
||||
expect(matchesCopySourceColumnSearch(column, "")).toBe(true);
|
||||
expect(matchesCopySourceColumnSearch(column, " ")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches name and type case-insensitively", () => {
|
||||
expect(matchesCopySourceColumnSearch(column, "STAT")).toBe(true);
|
||||
expect(matchesCopySourceColumnSearch(column, "TINY")).toBe(true);
|
||||
expect(matchesCopySourceColumnSearch(column, "missing")).toBe(false);
|
||||
});
|
||||
|
||||
it("matches comments and default values too", () => {
|
||||
expect(matchesCopySourceColumnSearch(column, "启用")).toBe(true);
|
||||
expect(matchesCopySourceColumnSearch(column, "1")).toBe(true);
|
||||
expect(matchesCopySourceColumnSearch({ ...column, column_default: null, comment: null }, "1")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("structureColumnNamesForCopy", () => {
|
||||
const column = (name: string, markedForDrop = false) => ({ name, markedForDrop });
|
||||
|
||||
|
||||
@@ -1,6 +1,37 @@
|
||||
import type { ColumnInfo, DatabaseConnectionInfo, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo } from "@/types/database.ts";
|
||||
import type { ColumnExtra, EditableStructureColumn, EditableStructureForeignKey, EditableStructureIndex, EditableStructureTrigger } from "@/lib/table/tableStructureEditorSql.ts";
|
||||
|
||||
export interface CopySourceColumnDetails {
|
||||
/** Column default as shown in the copy-fields dialog, or null when there is none. */
|
||||
defaultValue: string | null;
|
||||
/** Column comment as shown in the copy-fields dialog, or null when there is none. */
|
||||
comment: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only summary rendered under a column name in the "copy fields from another
|
||||
* table" dialog. The source table is not open while copying, so the comment and
|
||||
* default value are the only hint about what an unfamiliar field means.
|
||||
*/
|
||||
export function copySourceColumnDetails(column: Pick<ColumnInfo, "column_default" | "comment" | "data_type">, databaseType?: DatabaseType): CopySourceColumnDetails {
|
||||
// Match the main grid and the editor drafts so the dialog, the grid, and the
|
||||
// copied result render the same normalized default for every database.
|
||||
const defaultValue = column.column_default == null ? "" : columnDefaultForEditor(column, databaseType);
|
||||
const rawComment = column.comment ?? "";
|
||||
return {
|
||||
defaultValue: defaultValue.trim() ? defaultValue.trim() : null,
|
||||
comment: rawComment.trim() ? rawComment.trim() : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Copy-dialog search matches comments and default values on top of name and type. */
|
||||
export function matchesCopySourceColumnSearch(column: Pick<ColumnInfo, "name" | "data_type" | "column_default" | "comment">, search: string, databaseType?: DatabaseType): boolean {
|
||||
const query = search.trim().toLowerCase();
|
||||
if (!query) return true;
|
||||
const details = copySourceColumnDetails(column, databaseType);
|
||||
return [column.name, column.data_type, details.defaultValue ?? "", details.comment ?? ""].some((value) => value.toLowerCase().includes(query));
|
||||
}
|
||||
|
||||
/**
|
||||
* Column names offered by the structure editor's "copy all column names" action.
|
||||
* Fields marked for drop disappear on save, so they are not offered.
|
||||
@@ -847,7 +878,7 @@ function stripSqlServerDefaultOuterParens(defaultValue: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
function columnDefaultForEditor(column: ColumnInfo, databaseType?: DatabaseType): string {
|
||||
function columnDefaultForEditor(column: Pick<ColumnInfo, "column_default" | "data_type">, databaseType?: DatabaseType): string {
|
||||
if (column.column_default === null) return "";
|
||||
const defaultValue = column.column_default;
|
||||
if (databaseType === "mysql" && defaultValue === "" && isMysqlCharacterDataType(column.data_type)) {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
Reference in New Issue
Block a user