feat(structure): search and paginate source table picker for copy columns
This commit is contained in:
14 files changed
+460
-60
No files matched your search
@@ -7,7 +7,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { AlertTriangle, Check, ChevronDown, ChevronUp, Copy, Database, Info, KeyRound, ListChevronsUpDown, Loader2, Maximize2, Plus, RefreshCw, Save, Search, Settings, SlidersHorizontal, Trash2, UserRound, X } from "@lucide/vue";
|
||||
import { AlertTriangle, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Copy, Database, Info, KeyRound, ListChevronsUpDown, Loader2, Maximize2, Plus, RefreshCw, Save, Search, Settings, SlidersHorizontal, Trash2, UserRound, X } from "@lucide/vue";
|
||||
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
@@ -205,13 +205,21 @@ const columns = ref<EditableStructureColumn[]>([]);
|
||||
const copyColumnsDialogOpen = ref(false);
|
||||
const copySourceTables = ref<TableInfo[]>([]);
|
||||
const copySourceTableName = ref("");
|
||||
const copySourceTableSearch = ref("");
|
||||
const copySourceColumns = ref<ColumnInfo[]>([]);
|
||||
const copySourceColumnSearch = ref("");
|
||||
const selectedCopySourceColumnNames = ref<string[]>([]);
|
||||
const copySourceTablesLoading = ref(false);
|
||||
const copySourceTablesOffset = ref(0);
|
||||
const copySourceTablesHasMore = ref(false);
|
||||
const copySourceColumnsLoading = ref(false);
|
||||
const copySourceError = ref("");
|
||||
const COPY_SOURCE_TABLE_PAGE_SIZE = 100;
|
||||
const COPY_SOURCE_TABLE_PAGE_PROBE_SIZE = COPY_SOURCE_TABLE_PAGE_SIZE + 2;
|
||||
const COPY_SOURCE_TABLE_SEARCH_DEBOUNCE_MS = 250;
|
||||
let copySourceTablesRequestId = 0;
|
||||
let copySourceColumnsRequestId = 0;
|
||||
let copySourceTableSearchTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
const indexes = ref<EditableStructureIndex[]>([]);
|
||||
/** PostgreSQL partitioned parent (`relkind = 'p'`): `CREATE INDEX CONCURRENTLY`
|
||||
* is rejected by the server on such tables, so the option is disabled here and
|
||||
@@ -1907,11 +1915,75 @@ const selectedCopySourceColumns = computed(() => {
|
||||
return copyableSourceColumns.value.filter(({ column, alreadyExists }) => !alreadyExists && selected.has(column.name)).map(({ column }) => column);
|
||||
});
|
||||
const allCopyableSourceColumnsSelected = computed(() => copyableSourceColumnNames.value.length > 0 && copyableSourceColumnNames.value.every((name) => selectedCopySourceColumnNames.value.includes(name)));
|
||||
const copySourceTablesHasPreviousPage = computed(() => copySourceTablesOffset.value > 0);
|
||||
|
||||
function clearCopySourceTableSearchTimer() {
|
||||
if (copySourceTableSearchTimer === undefined) return;
|
||||
clearTimeout(copySourceTableSearchTimer);
|
||||
copySourceTableSearchTimer = undefined;
|
||||
}
|
||||
|
||||
function isCopySourceTable(table: TableInfo): boolean {
|
||||
const databaseInfo = connection.value?.database_info;
|
||||
return isCreateMode.value || tableStructureIdentifierComparisonKey(table.name, databaseType.value, databaseInfo) !== tableStructureIdentifierComparisonKey(props.tableName, databaseType.value, databaseInfo);
|
||||
}
|
||||
|
||||
function clearCopySourceTableSelection() {
|
||||
copySourceTableName.value = "";
|
||||
copySourceColumns.value = [];
|
||||
copySourceColumnSearch.value = "";
|
||||
selectedCopySourceColumnNames.value = [];
|
||||
copySourceColumnsRequestId++;
|
||||
copySourceColumnsLoading.value = false;
|
||||
}
|
||||
|
||||
async function loadCopySourceTables(offset = 0) {
|
||||
if (!props.connectionId || !props.database) return;
|
||||
clearCopySourceTableSelection();
|
||||
const requestId = ++copySourceTablesRequestId;
|
||||
copySourceTablesLoading.value = true;
|
||||
copySourceError.value = "";
|
||||
try {
|
||||
await store.ensureConnected(props.connectionId);
|
||||
const tables = await api.listTables(props.connectionId, props.database, metadataSchema.value, copySourceTableSearch.value.trim() || undefined, COPY_SOURCE_TABLE_PAGE_PROBE_SIZE, offset, ["TABLE"], props.catalog);
|
||||
if (requestId !== copySourceTablesRequestId) return;
|
||||
copySourceTables.value = tables.slice(0, COPY_SOURCE_TABLE_PAGE_SIZE).filter(isCopySourceTable);
|
||||
copySourceTablesOffset.value = offset;
|
||||
// The current table is excluded locally. Probe the next two rows so its
|
||||
// presence immediately after a full page does not create an empty next page.
|
||||
copySourceTablesHasMore.value = tables.slice(COPY_SOURCE_TABLE_PAGE_SIZE).some(isCopySourceTable);
|
||||
} catch (error: any) {
|
||||
if (requestId !== copySourceTablesRequestId) return;
|
||||
copySourceTables.value = [];
|
||||
copySourceTablesHasMore.value = false;
|
||||
copySourceError.value = error?.message || String(error);
|
||||
} finally {
|
||||
if (requestId === copySourceTablesRequestId) copySourceTablesLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateCopySourceTableSearch(value: string | number) {
|
||||
copySourceTableSearch.value = String(value);
|
||||
clearCopySourceTableSelection();
|
||||
clearCopySourceTableSearchTimer();
|
||||
copySourceTableSearchTimer = setTimeout(() => {
|
||||
copySourceTableSearchTimer = undefined;
|
||||
void loadCopySourceTables();
|
||||
}, COPY_SOURCE_TABLE_SEARCH_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
watch(copyColumnsDialogOpen, (open) => {
|
||||
if (open) return;
|
||||
clearCopySourceTableSearchTimer();
|
||||
copySourceTablesRequestId++;
|
||||
copySourceColumnsRequestId++;
|
||||
});
|
||||
|
||||
async function openCopyColumnsDialog() {
|
||||
if (!canAddColumn.value || !props.connectionId || !props.database) return;
|
||||
copyColumnsDialogOpen.value = true;
|
||||
copySourceTableName.value = "";
|
||||
copySourceTableSearch.value = "";
|
||||
copySourceColumns.value = [];
|
||||
copySourceColumnSearch.value = "";
|
||||
selectedCopySourceColumnNames.value = [];
|
||||
@@ -1919,21 +1991,10 @@ async function openCopyColumnsDialog() {
|
||||
copySourceColumnsRequestId++;
|
||||
copySourceColumnsLoading.value = false;
|
||||
copySourceTables.value = [];
|
||||
copySourceTablesLoading.value = true;
|
||||
try {
|
||||
await store.ensureConnected(props.connectionId);
|
||||
const databaseInfo = connection.value?.database_info;
|
||||
const currentTableIdentifierKey = tableStructureIdentifierComparisonKey(props.tableName, databaseType.value, databaseInfo);
|
||||
copySourceTables.value = (await api.listTables(props.connectionId, props.database, metadataSchema.value, undefined, undefined, undefined, undefined, props.catalog)).filter((table) => {
|
||||
const tableType = table.table_type.toUpperCase();
|
||||
return tableType !== "VIEW" && tableType !== "MATERIALIZED_VIEW" && (isCreateMode.value || tableStructureIdentifierComparisonKey(table.name, databaseType.value, databaseInfo) !== currentTableIdentifierKey);
|
||||
});
|
||||
} catch (error: any) {
|
||||
copySourceTables.value = [];
|
||||
copySourceError.value = error?.message || String(error);
|
||||
} finally {
|
||||
copySourceTablesLoading.value = false;
|
||||
}
|
||||
copySourceTablesOffset.value = 0;
|
||||
copySourceTablesHasMore.value = false;
|
||||
clearCopySourceTableSearchTimer();
|
||||
await loadCopySourceTables();
|
||||
}
|
||||
|
||||
async function loadCopySourceColumns(tableName: string) {
|
||||
@@ -3265,6 +3326,7 @@ onDeactivated(() => {
|
||||
stopStructureHorizontalScrollbarDrag();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
clearCopySourceTableSearchTimer();
|
||||
stopColumnDragTracking();
|
||||
stopStructureHorizontalScrollbarDrag();
|
||||
structureHorizontalScrollbarObserverGeneration += 1;
|
||||
@@ -4335,18 +4397,10 @@ watch([activeTab, ddlLoading], ([tab, loading]) => {
|
||||
</DialogHeader>
|
||||
|
||||
<div class="space-y-3 overflow-hidden">
|
||||
<label class="grid gap-1.5 text-sm font-medium">
|
||||
{{ t("structureEditor.copyColumnsSourceTable") }}
|
||||
<select
|
||||
:value="copySourceTableName"
|
||||
class="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="copySourceTablesLoading || copySourceTables.length === 0"
|
||||
@change="loadCopySourceColumns(($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="" disabled>{{ t("structureEditor.copyColumnsSelectSourceTable") }}</option>
|
||||
<option v-for="table in copySourceTables" :key="table.name" :value="table.name">{{ table.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="grid gap-1.5 text-sm font-medium">
|
||||
<label for="copy-source-table-search">{{ t("structureEditor.copyColumnsSourceTable") }}</label>
|
||||
<Input id="copy-source-table-search" :model-value="copySourceTableSearch" :placeholder="t('structureEditor.copyColumnsSearchSourceTables')" @update:model-value="updateCopySourceTableSearch" />
|
||||
</div>
|
||||
|
||||
<div v-if="copySourceTablesLoading" class="flex items-center gap-2 py-5 text-sm text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
@@ -4356,9 +4410,47 @@ watch([activeTab, ddlLoading], ([tab, loading]) => {
|
||||
{{ copySourceError }}
|
||||
</div>
|
||||
<div v-else-if="copySourceTables.length === 0" class="rounded-md border border-dashed px-3 py-5 text-center text-sm text-muted-foreground">
|
||||
{{ t("structureEditor.copyColumnsNoSourceTables") }}
|
||||
{{ copySourceTableSearch ? t("structureEditor.copyColumnsNoMatchingSourceTables") : t("structureEditor.copyColumnsNoSourceTables") }}
|
||||
</div>
|
||||
<template v-else-if="copySourceTableName">
|
||||
<template v-else>
|
||||
<div class="max-h-52 overflow-y-auto rounded-md border" :aria-label="t('structureEditor.copyColumnsSourceTable')">
|
||||
<button
|
||||
v-for="table in copySourceTables"
|
||||
:key="table.name"
|
||||
type="button"
|
||||
:aria-pressed="table.name === copySourceTableName"
|
||||
:class="['flex h-9 w-full items-center px-3 text-left font-mono text-sm hover:bg-muted/50 focus-visible:bg-muted focus-visible:outline-none', table.name === copySourceTableName ? 'bg-muted' : '']"
|
||||
@click="loadCopySourceColumns(table.name)"
|
||||
>
|
||||
<span class="truncate" :title="table.name">{{ table.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="copySourceTablesHasPreviousPage || copySourceTablesHasMore" class="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:disabled="copySourceTablesLoading || !copySourceTablesHasPreviousPage"
|
||||
:title="t('structureEditor.copyColumnsPreviousSourceTablePage')"
|
||||
:aria-label="t('structureEditor.copyColumnsPreviousSourceTablePage')"
|
||||
@click="loadCopySourceTables(Math.max(0, copySourceTablesOffset - COPY_SOURCE_TABLE_PAGE_SIZE))"
|
||||
>
|
||||
<ChevronLeft class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:disabled="copySourceTablesLoading || !copySourceTablesHasMore"
|
||||
:title="t('structureEditor.copyColumnsNextSourceTablePage')"
|
||||
:aria-label="t('structureEditor.copyColumnsNextSourceTablePage')"
|
||||
@click="loadCopySourceTables(copySourceTablesOffset + COPY_SOURCE_TABLE_PAGE_SIZE)"
|
||||
>
|
||||
<ChevronRight class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="copySourceTableName">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-sm font-medium">{{ t("structureEditor.copyColumnsSelectFields") }}</span>
|
||||
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs" :disabled="copySourceColumnsLoading || copyableSourceColumnNames.length === 0" @click="toggleCopySourceColumns">
|
||||
|
||||
@@ -3671,12 +3671,15 @@ export default {
|
||||
copyColumn: "Copy Column",
|
||||
copyColumnsTitle: "Copy Columns from Another Table",
|
||||
copyColumnsSourceTable: "Source table",
|
||||
copyColumnsSelectSourceTable: "Select a source table",
|
||||
copyColumnsSearchSourceTables: "Search source tables",
|
||||
copyColumnsSelectFields: "Select fields",
|
||||
copyColumnsSearchFields: "Search fields or types",
|
||||
copyColumnsSelectAll: "Select all",
|
||||
copyColumnsClearSelection: "Clear selection",
|
||||
copyColumnsNoSourceTables: "No other tables are available in this schema.",
|
||||
copyColumnsNoMatchingSourceTables: "No source tables match the search.",
|
||||
copyColumnsPreviousSourceTablePage: "Previous source table page",
|
||||
copyColumnsNextSourceTablePage: "Next source table page",
|
||||
copyColumnsNoFields: "The selected table has no fields.",
|
||||
copyColumnsNoMatchingFields: "No fields match the search.",
|
||||
copyColumnsAlreadyExists: "Already exists",
|
||||
|
||||
@@ -3592,12 +3592,15 @@ export default withEnglishFallback({
|
||||
copyColumn: "Copiar columna",
|
||||
copyColumnsTitle: "Copiar columnas de otra tabla",
|
||||
copyColumnsSourceTable: "Tabla de origen",
|
||||
copyColumnsSelectSourceTable: "Selecciona una tabla de origen",
|
||||
copyColumnsSearchSourceTables: "Buscar tablas de origen",
|
||||
copyColumnsSelectFields: "Selecciona campos",
|
||||
copyColumnsSearchFields: "Buscar campos o tipos",
|
||||
copyColumnsSelectAll: "Seleccionar todo",
|
||||
copyColumnsClearSelection: "Borrar selección",
|
||||
copyColumnsNoSourceTables: "No hay otras tablas disponibles en este esquema.",
|
||||
copyColumnsNoMatchingSourceTables: "Ninguna tabla de origen coincide con la búsqueda.",
|
||||
copyColumnsPreviousSourceTablePage: "Página anterior de tablas de origen",
|
||||
copyColumnsNextSourceTablePage: "Página siguiente de tablas de origen",
|
||||
copyColumnsNoFields: "La tabla seleccionada no tiene campos.",
|
||||
copyColumnsNoMatchingFields: "Ningún campo coincide con la búsqueda.",
|
||||
copyColumnsAlreadyExists: "Ya existe",
|
||||
|
||||
@@ -3590,12 +3590,15 @@ export default withEnglishFallback({
|
||||
copyColumn: "Copia colonna",
|
||||
copyColumnsTitle: "Copia colonne da un'altra tabella",
|
||||
copyColumnsSourceTable: "Tabella di origine",
|
||||
copyColumnsSelectSourceTable: "Seleziona una tabella di origine",
|
||||
copyColumnsSearchSourceTables: "Cerca tabelle di origine",
|
||||
copyColumnsSelectFields: "Seleziona campi",
|
||||
copyColumnsSearchFields: "Cerca campi o tipi",
|
||||
copyColumnsSelectAll: "Seleziona tutto",
|
||||
copyColumnsClearSelection: "Cancella selezione",
|
||||
copyColumnsNoSourceTables: "Non sono disponibili altre tabelle in questo schema.",
|
||||
copyColumnsNoMatchingSourceTables: "Nessuna tabella di origine corrisponde alla ricerca.",
|
||||
copyColumnsPreviousSourceTablePage: "Pagina precedente delle tabelle di origine",
|
||||
copyColumnsNextSourceTablePage: "Pagina successiva delle tabelle di origine",
|
||||
copyColumnsNoFields: "La tabella selezionata non ha campi.",
|
||||
copyColumnsNoMatchingFields: "Nessun campo corrisponde alla ricerca.",
|
||||
copyColumnsAlreadyExists: "Già esistente",
|
||||
|
||||
@@ -3652,12 +3652,15 @@ export default withEnglishFallback({
|
||||
copyColumn: "列をコピー",
|
||||
copyColumnsTitle: "別のテーブルから列をコピー",
|
||||
copyColumnsSourceTable: "コピー元テーブル",
|
||||
copyColumnsSelectSourceTable: "コピー元テーブルを選択",
|
||||
copyColumnsSearchSourceTables: "コピー元テーブルを検索",
|
||||
copyColumnsSelectFields: "フィールドを選択",
|
||||
copyColumnsSearchFields: "フィールドまたは型を検索",
|
||||
copyColumnsSelectAll: "すべて選択",
|
||||
copyColumnsClearSelection: "選択を解除",
|
||||
copyColumnsNoSourceTables: "このスキーマには他に選択可能なテーブルがありません。",
|
||||
copyColumnsNoMatchingSourceTables: "検索に一致するコピー元テーブルはありません。",
|
||||
copyColumnsPreviousSourceTablePage: "コピー元テーブルの前のページ",
|
||||
copyColumnsNextSourceTablePage: "コピー元テーブルの次のページ",
|
||||
copyColumnsNoFields: "選択したテーブルにはフィールドがありません。",
|
||||
copyColumnsNoMatchingFields: "検索に一致するフィールドはありません。",
|
||||
copyColumnsAlreadyExists: "既に存在します",
|
||||
|
||||
@@ -3441,12 +3441,15 @@ export default withEnglishFallback({
|
||||
copyColumn: "컬럼 복사",
|
||||
copyColumnsTitle: "다른 테이블에서 컬럼 복사",
|
||||
copyColumnsSourceTable: "원본 테이블",
|
||||
copyColumnsSelectSourceTable: "원본 테이블 선택",
|
||||
copyColumnsSearchSourceTables: "원본 테이블 검색",
|
||||
copyColumnsSelectFields: "필드 선택",
|
||||
copyColumnsSearchFields: "필드 또는 유형 검색",
|
||||
copyColumnsSelectAll: "모두 선택",
|
||||
copyColumnsClearSelection: "선택 해제",
|
||||
copyColumnsNoSourceTables: "이 스키마에 선택할 수 있는 다른 테이블이 없습니다.",
|
||||
copyColumnsNoMatchingSourceTables: "검색과 일치하는 원본 테이블이 없습니다.",
|
||||
copyColumnsPreviousSourceTablePage: "원본 테이블 이전 페이지",
|
||||
copyColumnsNextSourceTablePage: "원본 테이블 다음 페이지",
|
||||
copyColumnsNoFields: "선택한 테이블에 필드가 없습니다.",
|
||||
copyColumnsNoMatchingFields: "검색과 일치하는 필드가 없습니다.",
|
||||
copyColumnsAlreadyExists: "이미 존재함",
|
||||
|
||||
@@ -3592,12 +3592,15 @@ export default withEnglishFallback({
|
||||
copyColumn: "Copiar coluna",
|
||||
copyColumnsTitle: "Copiar colunas de outra tabela",
|
||||
copyColumnsSourceTable: "Tabela de origem",
|
||||
copyColumnsSelectSourceTable: "Selecione uma tabela de origem",
|
||||
copyColumnsSearchSourceTables: "Pesquisar tabelas de origem",
|
||||
copyColumnsSelectFields: "Selecione campos",
|
||||
copyColumnsSearchFields: "Pesquisar campos ou tipos",
|
||||
copyColumnsSelectAll: "Selecionar tudo",
|
||||
copyColumnsClearSelection: "Limpar seleção",
|
||||
copyColumnsNoSourceTables: "Não há outras tabelas disponíveis neste esquema.",
|
||||
copyColumnsNoMatchingSourceTables: "Nenhuma tabela de origem corresponde à pesquisa.",
|
||||
copyColumnsPreviousSourceTablePage: "Página anterior de tabelas de origem",
|
||||
copyColumnsNextSourceTablePage: "Próxima página de tabelas de origem",
|
||||
copyColumnsNoFields: "A tabela selecionada não tem campos.",
|
||||
copyColumnsNoMatchingFields: "Nenhum campo corresponde à pesquisa.",
|
||||
copyColumnsAlreadyExists: "Já existe",
|
||||
|
||||
@@ -3657,12 +3657,15 @@ export default withEnglishFallback({
|
||||
copyColumn: "复制字段",
|
||||
copyColumnsTitle: "从其他表复制字段",
|
||||
copyColumnsSourceTable: "源表",
|
||||
copyColumnsSelectSourceTable: "请选择源表",
|
||||
copyColumnsSearchSourceTables: "搜索源表",
|
||||
copyColumnsSelectFields: "选择字段",
|
||||
copyColumnsSearchFields: "搜索字段或类型",
|
||||
copyColumnsSelectAll: "全选",
|
||||
copyColumnsClearSelection: "取消全选",
|
||||
copyColumnsNoSourceTables: "当前模式下没有其他可选表。",
|
||||
copyColumnsNoMatchingSourceTables: "没有匹配的源表。",
|
||||
copyColumnsPreviousSourceTablePage: "上一页源表",
|
||||
copyColumnsNextSourceTablePage: "下一页源表",
|
||||
copyColumnsNoFields: "所选表没有字段。",
|
||||
copyColumnsNoMatchingFields: "没有匹配的字段。",
|
||||
copyColumnsAlreadyExists: "已存在",
|
||||
|
||||
@@ -3337,12 +3337,15 @@ export default withEnglishFallback({
|
||||
copyColumn: "複製欄位",
|
||||
copyColumnsTitle: "從其他資料表複製欄位",
|
||||
copyColumnsSourceTable: "來源資料表",
|
||||
copyColumnsSelectSourceTable: "請選擇來源資料表",
|
||||
copyColumnsSearchSourceTables: "搜尋來源資料表",
|
||||
copyColumnsSelectFields: "選擇欄位",
|
||||
copyColumnsSearchFields: "搜尋欄位或類型",
|
||||
copyColumnsSelectAll: "全選",
|
||||
copyColumnsClearSelection: "取消全選",
|
||||
copyColumnsNoSourceTables: "目前結構描述中沒有其他可選資料表。",
|
||||
copyColumnsNoMatchingSourceTables: "沒有符合搜尋的來源資料表。",
|
||||
copyColumnsPreviousSourceTablePage: "上一頁來源資料表",
|
||||
copyColumnsNextSourceTablePage: "下一頁來源資料表",
|
||||
copyColumnsNoFields: "所選資料表沒有欄位。",
|
||||
copyColumnsNoMatchingFields: "沒有符合搜尋的欄位。",
|
||||
copyColumnsAlreadyExists: "已存在",
|
||||
|
||||
Reference in new issue
Block a user