+
DB Migration Logs
{/*
diff --git a/ui/src/proxy/admin/wizard/models.ts b/ui/src/proxy/admin/wizard/models.ts
index 126c0f52..85959322 100644
--- a/ui/src/proxy/admin/wizard/models.ts
+++ b/ui/src/proxy/admin/wizard/models.ts
@@ -211,14 +211,70 @@ export interface WizardImportAnalysisDto {
scopeFolder: string
entries: WizardImportEntryDto[]
warnings: string[]
+ /** Zip icerigi ile hedef ortam karsilastirildiginda bulunan tutarsizliklar. */
+ issues: WizardImportIssueDto[]
+ /** Engelleyici bulgu varsa import baslatilamaz; sunucu da yazma ucunu reddeder. */
+ hasBlockingIssues: boolean
+ /** Sema islerinin (script, kolon ekleme, tablo tasarimcisi) hedef aldigi veri kaynagi. */
+ dataSourceCode: string
+}
+
+/** Import analizinde bulunan bir tutarsizligin agirligi; backend enum'u ile birebir. */
+export enum WizardImportIssueSeverityEnum {
+ Error = 0,
+ Warning = 1,
+}
+
+/** Import analizinde bulunan tek bir tutarsizlik. */
+export interface WizardImportIssueDto {
+ severity: WizardImportIssueSeverityEnum
+ /** Bulgunun ilgili oldugu kapsam-goreli yol; genel bir bulguda bostur. */
+ path?: string
+ message: string
+ /**
+ * Bulgunun karsiligi olan SQL cumlesi. Eksik kolon uyarilarinda kapanista calistirilacak
+ * cumledir; engelleyici bulgularda kullanicinin elle uygulayacagi cumledir.
+ */
+ statement?: string
+ /** Bulgu bir tablo semasiyla ilgiliyse tablonun adi; tablo tasarimcisi bununla acilir. */
+ tableName?: string
+ /** Tablo semasi; script'te sema oneki yoksa bostur. */
+ schemaName?: string
+ /** Tablo hedefte hic yoksa true; tasarimci olusturma modunda acilir. */
+ tableMissing?: boolean
+ /**
+ * Bulgu arsivin kendi eksikliginden/bozulmasindan kaynaklaniyorsa true. Hedef ortamda
+ * giderilemez; tek cozum kaynak ortamda export'u tekrarlamaktir.
+ */
+ requiresReexport?: boolean
}
/**
- * Import kapanisinin sonucu. Yazilan `data/*.json` dosyalari ayni islemde veritabanina da
- * uygulanir; her dosyanin sonucu burada raporlanir.
+ * Import kapanisinin sonucu. Yazilan dosyalar bagimlilik sirasiyla veritabanina uygulanir:
+ * once SQL nesneleri, sonra CRUD tanimlari, en son veri satirlari.
*/
export interface WizardImportCompleteResultDto {
+ /** Calistirilan SQL nesne scriptleri; veri satirlarindan once uygulanir. */
+ appliedSql: WizardImportStepResultDto[]
+ /**
+ * Hedefte eksik olup eklenen kolonlar. Script'in CREATE blogu var olan bir tabloda calismadigi
+ * icin yeni kolonlar buradan gelir; var olan bir kolon degistirilmez, hicbir kolon dusurulmez.
+ */
+ appliedSchema: WizardImportStepResultDto[]
+ /** Veritabanina uygulanan CRUD endpoint dosyalari. */
+ appliedCrud: WizardImportStepResultDto[]
appliedData: WizardImportSeedApplyResultDto[]
+ /** Kurulan ekran tanimlari; import bitince ekran calisir, migrate beklemez. */
+ appliedWizards: WizardImportStepResultDto[]
+}
+
+/** Import kapanisindaki tek bir adimin sonucu (SQL scripti ya da CRUD dosyasi). */
+export interface WizardImportStepResultDto {
+ path: string
+ /** Adim basariliysa yapilan isi ozetleyen kisa metin. */
+ detail?: string
+ /** Adim basarisizsa nedeni. */
+ error?: string
}
/** Tek bir veri seed dosyasinin veritabanina uygulanma sonucu. */
@@ -240,3 +296,21 @@ export interface WizardImportApplyResultDto {
overwritten: boolean
size: number
}
+
+/** Export zip'ine girecek tek bir seed dosyasi. */
+export interface WizardExportEntryDto {
+ /** Kapsam klasorune goreli yol, ornegin `sql/object/Prj_T_Task.sql`. */
+ path: string
+ /** Dosyanin arsive neden girdigi (wizard'in kendisi, alt form, lookup, veri...). */
+ reason: string
+}
+
+/** Indirmeden once gosterilen export onizlemesi. */
+export interface WizardExportAnalysisDto {
+ fileName: string
+ archiveName: string
+ scopeFolder: string
+ entries: WizardExportEntryDto[]
+ /** Bagimlilik olarak bulunan ama diskte karsiligi olmayan dosyalar. */
+ warnings: string[]
+}
diff --git a/ui/src/services/wizard.service.ts b/ui/src/services/wizard.service.ts
index 6951fa22..fba9259b 100644
--- a/ui/src/services/wizard.service.ts
+++ b/ui/src/services/wizard.service.ts
@@ -1,5 +1,6 @@
import {
ListFormWizardDto,
+ WizardExportAnalysisDto,
WizardFileInfoDto,
WizardImportAnalysisDto,
WizardImportApplyResultDto,
@@ -44,8 +45,20 @@ export const deleteWizardFile = (fileName: string) =>
})
/**
- * Wizard dosyasini ve bagli seed dosyalarini (data/crud/sql) tek bir zip olarak indirir.
- * Zip icindeki klasor duzeni seed kapsam klasoru ile aynidir; baska bir ortamda dogrudan acilabilir.
+ * Indirmeden once arsivin tam icerigini ve cozulemeyen bagimliliklarini getirir; export ile
+ * ayni toplamayi kullanir, dolayisiyla onizlemedeki liste indirilen zip ile birebir aynidir.
+ */
+export const getWizardExportAnalysis = (fileName: string) =>
+ apiService.fetchData
({
+ method: 'GET',
+ url: `/api/app/list-form-wizard/export-analysis`,
+ params: { fileName },
+ })
+
+/**
+ * Wizard dosyasini ve bagimlilik kapanisindaki tum seed dosyalarini (data/crud/sql, alt formlar)
+ * tek bir zip olarak indirir. Zip icindeki klasor duzeni seed kapsam klasoru ile aynidir; baska bir
+ * ortamda dogrudan acilabilir.
*/
export const exportWizardFile = (fileName: string) =>
apiService.fetchData({
diff --git a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx
index 7f3ee851..600ea096 100644
--- a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx
+++ b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx
@@ -28,6 +28,7 @@ import {
deleteWizardFile,
downloadBlob,
exportWizardFile,
+ getWizardExportAnalysis,
getWizardFiles,
} from '@/services/wizard.service'
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
@@ -35,7 +36,11 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
import { useStoreActions, useStoreState } from '@/store/store'
import type { ListViewLayoutType } from '../edit/types'
import { ROUTES_ENUM } from '@/routes/route.constant'
-import { WizardComponentKindEnum, WizardFileInfoDto } from '@/proxy/admin/wizard/models'
+import {
+ WizardComponentKindEnum,
+ WizardExportAnalysisDto,
+ WizardFileInfoDto,
+} from '@/proxy/admin/wizard/models'
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
import { usePermission } from '@/utils/hooks/usePermission'
import { WIZARD_PERMISSION } from '@/constants/permission.constant'
@@ -61,6 +66,11 @@ const WizardFileManager = () => {
const [loading, setLoading] = useState(false)
const [deletingFile, setDeletingFile] = useState(null)
const [exportingFile, setExportingFile] = useState(null)
+ // Eksik bagimlilik bulundugunda indirmeden once gosterilen onizleme.
+ const [exportPreview, setExportPreview] = useState<{
+ file: WizardFileInfoDto
+ analysis: WizardExportAnalysisDto
+ } | null>(null)
const [confirm, setConfirm] = useState(null)
const [search, setSearch] = useState('')
const [importFile, setImportFile] = useState(null)
@@ -114,14 +124,51 @@ const WizardFileManager = () => {
// Zip'i sunucu uretir; istemci yalnizca indirmeyi tetikler. Dosya adi Content-Disposition
// basligindan okunur, okunamazsa wizard adina duser.
+ /** Arsivi indirir; dosya adi sunucunun verdigi addir, yoksa wizard adina duser. */
+ const downloadExport = async (file: WizardFileInfoDto) => {
+ const res = await exportWizardFile(file.fileName)
+ const disposition = String(res.headers?.['content-disposition'] ?? '')
+ const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition)
+ const fallback = `${file.wizardName || file.fileName.replace(/\.json$/i, '')}.zip`
+ downloadBlob(res.data, match ? decodeURIComponent(match[1]) : fallback)
+ }
+
+ /**
+ * Once arsivin icerigi hesaplanir. Cozulemeyen bir bagimlilik varsa indirme dogrudan yapilmaz:
+ * eksik bir arsiv baska bir ortamda yarim kurulum uretir, bu yuzden kullaniciya once ne
+ * eksildigi gosterilir.
+ */
const handleExport = async (file: WizardFileInfoDto) => {
setExportingFile(file.fileName)
try {
- const res = await exportWizardFile(file.fileName)
- const disposition = String(res.headers?.['content-disposition'] ?? '')
- const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition)
- const fallback = `${file.wizardName || file.fileName.replace(/\.json$/i, '')}.zip`
- downloadBlob(res.data, match ? decodeURIComponent(match[1]) : fallback)
+ const analysis = await getWizardExportAnalysis(file.fileName)
+ if (analysis.data.warnings.length > 0) {
+ setExportPreview({ file, analysis: analysis.data })
+ return
+ }
+
+ await downloadExport(file)
+ } catch (err: any) {
+ toast.push(
+
+ {translate('::App.ListForm.WizardFileExportError')}: {err?.message ?? 'Unknown error'}
+ ,
+ { placement: 'bottom-end' },
+ )
+ } finally {
+ setExportingFile(null)
+ }
+ }
+
+ /** Onizlemedeki eksiklere ragmen indirmeyi surdurur. */
+ const handleExportAnyway = async () => {
+ if (!exportPreview) return
+
+ const { file } = exportPreview
+ setExportPreview(null)
+ setExportingFile(file.fileName)
+ try {
+ await downloadExport(file)
} catch (err: any) {
toast.push(
@@ -552,6 +599,55 @@ const WizardFileManager = () => {
+
+ {/* Eksik bagimlilikli export onizlemesi */}
+ setExportPreview(null)}
+ onRequestClose={() => setExportPreview(null)}
+ >
+
+
+
+
+
+ {translate('::App.ListForm.WizardExportIncomplete')}
+
+
+ {exportPreview?.analysis.warnings.map((warning) => (
+ {warning}
+ ))}
+
+
+ {translate('::App.ListForm.WizardExportContents')} (
+ {exportPreview?.analysis.entries.length})
+
+
+ {exportPreview?.analysis.entries.map((entry) => (
+
+
+ {entry.path}
+
+ {entry.reason}
+
+ ))}
+
+
+
+
+
+ setExportPreview(null)}>
+ {translate('::App.Platform.Cancel')}
+
+
+ {translate('::App.ListForm.WizardExportDownloadAnyway')}
+
+
+
)
}
diff --git a/ui/src/views/admin/listForm/wizard/WizardImportDialog.tsx b/ui/src/views/admin/listForm/wizard/WizardImportDialog.tsx
index 1fbd9af6..c88e53c7 100644
--- a/ui/src/views/admin/listForm/wizard/WizardImportDialog.tsx
+++ b/ui/src/views/admin/listForm/wizard/WizardImportDialog.tsx
@@ -2,15 +2,17 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { ReactNode } from 'react'
import classNames from 'classnames'
import { DiffEditor } from '@monaco-editor/react'
-import { Button, Dialog, Progress } from '@/components/ui'
+import { Button, Dialog, Progress, Steps } from '@/components/ui'
import {
FaCheck,
FaCheckCircle,
FaCodeBranch,
+ FaCopy,
FaExclamationTriangle,
FaFileAlt,
FaFileImport,
FaPlusCircle,
+ FaTable,
FaTimes,
FaTimesCircle,
FaUndo,
@@ -22,14 +24,18 @@ import {
rollbackWizardImport,
} from '@/services/wizard.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
-import { useStoreState } from '@/store/store'
+import { useStoreActions, useStoreState } from '@/store/store'
import Input from '@/components/ui/Input'
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
+import SqlTableDesignerDialog from '@/views/developerKit/SqlTableDesignerDialog'
import {
WizardImportAnalysisDto,
WizardImportEntryDto,
WizardImportEntryStatusEnum,
+ WizardImportIssueDto,
+ WizardImportIssueSeverityEnum,
WizardImportSeedApplyResultDto,
+ WizardImportStepResultDto,
} from '@/proxy/admin/wizard/models'
interface WizardImportDialogProps {
@@ -49,7 +55,19 @@ interface EntryDecision {
content?: string
}
-type ImportPhase = 'analyzing' | 'review' | 'running' | 'finished' | 'rolledBack'
+/**
+ * Dialogun durumu. Adim gezinmesi `step` ile yapilir; bu tur yalnizca uygulamanin nerede oldugunu
+ * soyler — `ready` iken kullanici adimlar arasinda serbestce dolasir, `running` sonrasi adim sabitlenir.
+ */
+type ImportPhase = 'analyzing' | 'ready' | 'running' | 'finished' | 'rolledBack'
+
+/** Dialog adimlari; sira bagimlilik sirasidir, geriye donulebilir ama ileri atlanmaz. */
+const STEP = {
+ files: 0,
+ checks: 1,
+ plan: 2,
+ apply: 3,
+} as const
interface AppliedRow {
path: string
@@ -102,9 +120,18 @@ const ImportDialogBody = ({ children }: { children: ReactNode }) => {
*/
const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogProps) => {
const { translate } = useLocalization()
+ const { getConfig } = useStoreActions((a) => a.abpConfig)
const mode = useStoreState((state) => state.theme.mode)
const [phase, setPhase] = useState('analyzing')
+ const [step, setStep] = useState(STEP.files)
+ // Bulgudan acilan tablo tasarimcisi; kapaninca analiz yenilenir. `missing` ise tablo hedefte hic
+ // yoktur ve tasarimci olusturma modunda acilir.
+ const [designTable, setDesignTable] = useState<{
+ schemaName: string
+ tableName: string
+ missing: boolean
+ } | null>(null)
const [analysis, setAnalysis] = useState(null)
const [error, setError] = useState(null)
const [decisions, setDecisions] = useState>({})
@@ -112,6 +139,13 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
const [applied, setApplied] = useState([])
// Yazilan data/*.json dosyalarinin veritabanina uygulanma sonucu; oturum kapanisinda doner.
const [seedApplied, setSeedApplied] = useState([])
+ // Kapanistaki bagimlilik sirasinin ilk iki adimi: SQL nesneleri ve CRUD tanimlari.
+ const [sqlApplied, setSqlApplied] = useState([])
+ const [crudApplied, setCrudApplied] = useState([])
+ // Hedefte eksik olup kapanista eklenen kolonlar.
+ const [schemaApplied, setSchemaApplied] = useState([])
+ // Kurulan ekran tanimlari; import bitince ekran calisir durumdadir, migrate beklemez.
+ const [wizardsApplied, setWizardsApplied] = useState([])
const [currentPath, setCurrentPath] = useState(null)
const [showIdentical, setShowIdentical] = useState(false)
const [editorEpoch, setEditorEpoch] = useState(0)
@@ -158,18 +192,66 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
[entries, selectedPath],
)
- // Analiz: zip sunucuda gecici bir oturuma acilir, hedef klasore henuz dokunulmaz.
- useEffect(() => {
- let cancelled = false
+ /**
+ * Kapanışta veritabanına gerçekten uygulanacak girdiler. Sunucu kuralıyla aynı: yazılan dosyalar
+ * ve hedefle **zaten aynı** olanlar uygulanır — ikincisinde yazacak bir şey yoktur ama
+ * veritabanındaki karşılığı eksik olabilir. Kullanıcının "mevcudu koru" dediği dosya dışarıdadır.
+ */
+ const effectiveEntries = useMemo(
+ () =>
+ entries.filter(
+ (e) => e.status === WizardImportEntryStatusEnum.Identical || decisions[e.path]?.apply,
+ ),
+ [entries, decisions],
+ )
- const run = async () => {
+ const effectiveByFolder = useCallback(
+ (...folders: string[]) =>
+ effectiveEntries.filter((e) => folders.includes(e.folder.toLowerCase())),
+ [effectiveEntries],
+ )
+
+ const blockingIssues = useMemo(
+ () =>
+ (analysis?.issues ?? []).filter((i) => i.severity === WizardImportIssueSeverityEnum.Error),
+ [analysis],
+ )
+ const advisoryIssues = useMemo(
+ () =>
+ (analysis?.issues ?? []).filter((i) => i.severity === WizardImportIssueSeverityEnum.Warning),
+ [analysis],
+ )
+
+ // Engelleyici bulgular iki gruba ayrilir: arsivin kendi eksikligi (burada giderilemez, export
+ // tekrarlanmali) ve hedef ortamin eksikligi (tablo tasarimcisiyla bu ekrandan giderilir).
+ const reexportIssues = useMemo(
+ () => blockingIssues.filter((i) => i.requiresReexport),
+ [blockingIssues],
+ )
+ const repairableIssues = useMemo(
+ () => blockingIssues.filter((i) => !i.requiresReexport),
+ [blockingIssues],
+ )
+
+ /**
+ * Zip'i sunucuda gecici bir oturuma acar ve hedefle karsilastirir; hedef klasore dokunmaz.
+ * Kullanici sema farkini tablo tasarimcisindan giderdiginde de yeniden calisir — o durumda
+ * onceki oturum birakilir, cunku bulgular artik bayattir.
+ */
+ const runAnalysis = useCallback(
+ async (previousImportId?: string) => {
if (!file) return
+
setPhase('analyzing')
setError(null)
+ setSelectedPath(null)
+
+ if (previousImportId) {
+ await rollbackWizardImport(previousImportId).catch(() => undefined)
+ }
+
try {
const res = await analyzeWizardImport(file)
- if (cancelled) return
-
const data = res.data
const initial: Record = {}
data.entries.forEach((entry) => {
@@ -186,24 +268,24 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
setAnalysis(data)
setDecisions(initial)
- setPhase('review')
+ setPhase('ready')
+ setStep(STEP.files)
const firstConflict = data.entries.find(
(e) => e.status === WizardImportEntryStatusEnum.Conflict,
)
if (firstConflict) setSelectedPath(firstConflict.path)
} catch (err: any) {
- if (cancelled) return
setError(err?.response?.data?.error?.message ?? err?.message ?? 'Unknown error')
- setPhase('review')
+ setPhase('ready')
}
- }
+ },
+ [file],
+ )
- run()
- return () => {
- cancelled = true
- }
- }, [file])
+ useEffect(() => {
+ runAnalysis()
+ }, [runAnalysis])
// Diff editorunun tasidigi taslak, secim degistikce ilgili dosyanin son haline doner.
useEffect(() => {
@@ -255,8 +337,13 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
if (!analysis) return
setPhase('running')
+ setStep(STEP.apply)
setApplied([])
setSeedApplied([])
+ setSqlApplied([])
+ setSchemaApplied([])
+ setCrudApplied([])
+ setWizardsApplied([])
setError(null)
const rows: AppliedRow[] = []
@@ -297,7 +384,16 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
setCurrentPath(null)
try {
const res = await completeWizardImport(analysis.importId)
+ setSqlApplied(res.data.appliedSql ?? [])
+ setSchemaApplied(res.data.appliedSchema ?? [])
+ setCrudApplied(res.data.appliedCrud ?? [])
setSeedApplied(res.data.appliedData ?? [])
+ const installed = res.data.appliedWizards ?? []
+ setWizardsApplied(installed)
+
+ // Kurulan ekranin menusu ve yetkileri ABP config'inden gelir; yenilenmezse kullanici
+ // ekrani ancak sayfayi bastan yukledikten sonra gorurdu.
+ if (installed.some((row) => !row.error)) await getConfig(true)
} catch (err: any) {
// Dosyalar hedefe yazildi; yalnizca veritabanina uygulama ya da temizlik basarisiz oldu.
setError(err?.response?.data?.error?.message ?? err?.message ?? null)
@@ -309,7 +405,7 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
/** Import baslamadan kapatilirsa sunucudaki staging klasoru birakilmaz. */
const handleClose = () => {
if (phase === 'running') return
- if (analysis && phase === 'review') {
+ if (analysis && phase === 'ready') {
rollbackWizardImport(analysis.importId).catch(() => undefined)
}
onClose()
@@ -361,6 +457,128 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
)
}
+ /**
+ * Tek bir bulgu. `statement` dolu olduğunda cümle kopyalanabilir bir blokta gösterilir; şema
+ * bulgularında ayrıca tablo tasarımcısı açılabilir. Bir kolon eklemek kayıpsız olduğu için
+ * kapanışta kendiliğinden uygulanır, var olan bir kolonu değiştirmek kullanıcının kararıdır —
+ * mesaj hangisinin geçerli olduğunu söyler.
+ */
+ const renderIssue = (issue: WizardImportIssueDto, key: string) => (
+
+ {issue.path && {issue.path}: }
+ {issue.message}
+ {(issue.statement || issue.tableName) && (
+
+ {issue.statement && (
+
+ {issue.statement}
+
+ )}
+ {issue.statement && (
+
}
+ title={translate('::App.Platform.Copy')}
+ onClick={() => navigator.clipboard?.writeText(issue.statement ?? '')}
+ />
+ )}
+ {issue.tableName && (
+
}
+ onClick={() =>
+ setDesignTable({
+ schemaName: issue.schemaName || 'dbo',
+ tableName: issue.tableName as string,
+ missing: !!issue.tableMissing,
+ })
+ }
+ >
+ {translate(
+ issue.tableMissing
+ ? '::App.ListForm.WizardImportCreateTable'
+ : '::App.ListForm.WizardImportEditTable',
+ )}
+
+ )}
+
+ )}
+
+ )
+
+ /** Zip acilirken atlanan girdiler; bulgularla ayni adimda gosterilir. */
+ const renderZipWarnings = () =>
+ analysis && analysis.warnings.length > 0 ? (
+
+
+
+ {translate('::App.ListForm.WizardImportWarnings')} ({analysis.warnings.length})
+
+
+ {analysis.warnings.map((warning) => (
+ {warning}
+ ))}
+
+
+ ) : null
+
+ const renderIssues = () => {
+ const hasZipWarnings = (analysis?.warnings.length ?? 0) > 0
+
+ if (blockingIssues.length === 0 && advisoryIssues.length === 0 && !hasZipWarnings) {
+ return (
+
+
+ {translate('::App.ListForm.WizardImportNoIssues')}
+
+ )
+ }
+
+ return (
+
+ {renderZipWarnings()}
+ {reexportIssues.length > 0 && (
+
+
+
+ {translate('::App.ListForm.WizardImportReexportIssues')} ({reexportIssues.length})
+
+
{translate('::App.ListForm.WizardImportReexportHint')}
+
+ {reexportIssues.map((issue, index) => renderIssue(issue, `${issue.path ?? ''}#${index}`))}
+
+
+ )}
+ {repairableIssues.length > 0 && (
+
+
+
+ {translate('::App.ListForm.WizardImportBlockingIssues')} ({repairableIssues.length})
+
+
+ {repairableIssues.map((issue, index) => renderIssue(issue, `${issue.path ?? ''}#${index}`))}
+
+
+ )}
+ {advisoryIssues.length > 0 && (
+
+
+
+ {translate('::App.ListForm.WizardImportAdvisoryIssues')} ({advisoryIssues.length})
+
+
+ {advisoryIssues.map((issue, index) => renderIssue(issue, `${issue.path ?? ''}#${index}`))}
+
+
+ )}
+
+ )
+ }
+
const renderReview = () => (
{/* Dosya listesi */}
@@ -524,25 +742,82 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
) : (
{translate('::App.ListForm.WizardImportReady')}
)}
- {analysis && analysis.warnings.length > 0 && (
-
-
-
- {translate('::App.ListForm.WizardImportWarnings')}
-
-
- {analysis.warnings.map((warning) => (
- {warning}
- ))}
-
-
- )}
)}
)
+ /** Plan bölümü; boş bölüm hiç çizilmez, kullanıcı yalnızca gerçekten olacakları görür. */
+ const renderPlanSection = (title: string, items: string[], hint?: string) => {
+ if (items.length === 0) return null
+
+ return (
+
+
+
+ {title}
+ ({items.length})
+
+ {hint &&
{hint}
}
+
+ {items.map((item) => (
+
+ {item}
+
+ ))}
+
+
+ )
+ }
+
+ /**
+ * Onaydan önceki son adım: onaylandığında ne olacağının tam listesi. Sıra, sunucudaki kapanış
+ * sırasıyla aynıdır — dosya yazımı, SQL nesneleri, eksik kolonlar, CRUD, veri; ekran tanımları
+ * bu adımda uygulanmaz ve ayrıca belirtilir.
+ */
+ const renderPlan = () => {
+ const files = applyList.map((e) => e.path)
+ const sql = effectiveByFolder('sql', 'postgres').map((e) => e.path)
+ const columns = advisoryIssues
+ .filter((i) => i.statement && i.tableName)
+ .map((i) => i.statement as string)
+ const crud = effectiveByFolder('crud').map((e) => e.path)
+ const data = effectiveByFolder('data').map((e) => e.path)
+ const wizards = effectiveByFolder('wizard').map((e) => e.path)
+
+ const isEmpty =
+ files.length === 0 &&
+ sql.length === 0 &&
+ columns.length === 0 &&
+ crud.length === 0 &&
+ data.length === 0 &&
+ wizards.length === 0
+
+ return (
+
+
+ {translate('::App.ListForm.WizardImportPlanTitle')}
+
+ {isEmpty && (
+
+ {translate('::App.ListForm.WizardImportPlanNothing')}
+
+ )}
+ {renderPlanSection(
+ `1. ${translate('::App.ListForm.WizardImportPlanFiles')}`,
+ files,
+ analysis?.scopeFolder,
+ )}
+ {renderPlanSection(`2. ${translate('::App.ListForm.WizardImportPlanSql')}`, sql)}
+ {renderPlanSection(`3. ${translate('::App.ListForm.WizardImportPlanColumns')}`, columns)}
+ {renderPlanSection(`4. ${translate('::App.ListForm.WizardImportPlanCrud')}`, crud)}
+ {renderPlanSection(`5. ${translate('::App.ListForm.WizardImportPlanData')}`, data)}
+ {renderPlanSection(`6. ${translate('::App.ListForm.WizardImportPlanWizards')}`, wizards)}
+
+ )
+ }
+
const renderProgress = () => (
{translate('::App.ListForm.WizardImportSuccess')}
+ {sqlApplied.length > 0 && (
+
+
{translate('::App.ListForm.WizardImportSqlApplied')}
+
+ {sqlApplied.map((row) => (
+
+ {row.path}
+ {row.error ?? row.detail}
+
+ ))}
+
+
+ )}
+ {schemaApplied.length > 0 && (
+
+
+ {translate('::App.ListForm.WizardImportSchemaApplied')}
+
+
+ {schemaApplied.map((row) => (
+
+ {row.path}
+ {row.error ?? row.detail}
+
+ ))}
+
+
+ )}
+ {crudApplied.length > 0 && (
+
+
+ {translate('::App.ListForm.WizardImportCrudApplied')}
+
+
+ {crudApplied.map((row) => (
+
+ {row.path}
+ {row.error ?? row.detail}
+
+ ))}
+
+
+ )}
{seedApplied.length > 0 && (
@@ -618,7 +936,21 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
)}
-
{translate('::App.ListForm.WizardImportRunMigrateHint')}
+ {wizardsApplied.length > 0 && (
+
+
+ {translate('::App.ListForm.WizardImportWizardsApplied')}
+
+
+ {wizardsApplied.map((row) => (
+
+ {row.path}
+ {row.error ?? row.detail}
+
+ ))}
+
+
+ )}
{error && (
@@ -632,13 +964,30 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
)
- return (
+ const isBusy = phase === 'running'
+ const isDone = phase === 'finished' || phase === 'rolledBack'
+
+ // Adimlar bagimlilik sirasindadir: catisma cozulmeden kontrole, engelleyici bulgu giderilmeden
+ // plana gecilmez. Geriye donus her zaman serbesttir.
+ const canLeaveFiles = unresolvedCount === 0
+ const canLeaveChecks = blockingIssues.length === 0
+
+ const canGoTo = (target: number) => {
+ if (isBusy || isDone || error) return false
+ if (target <= step) return true
+ if (target > STEP.files && !canLeaveFiles) return false
+ if (target > STEP.checks && !canLeaveChecks) return false
+
+ return target <= STEP.plan
+ }
+
+ const dialog = (
@@ -647,7 +996,7 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
{translate('::App.ListForm.WizardImportTitle')}
{file?.name}
- {phase === 'review' && analysis && (
+ {phase === 'ready' && analysis && (
{entries.filter((e) => e.status === WizardImportEntryStatusEnum.New).length}{' '}
@@ -664,6 +1013,21 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
)}
+ {!error && (
+ {
+ if (canGoTo(index)) setStep(index)
+ }}
+ >
+
+
+
+
+
+ )}
+
{phase === 'analyzing' && (
@@ -672,54 +1036,103 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
)}
- {phase === 'review' && error && (
+ {phase !== 'analyzing' && error && (
{error}
)}
- {phase === 'review' && !error && analysis && renderReview()}
+ {phase === 'ready' && !error && analysis && (
+ <>
+ {step === STEP.files && renderReview()}
+ {step === STEP.checks && renderIssues()}
+ {step === STEP.plan && renderPlan()}
+ >
+ )}
- {(phase === 'running' || phase === 'finished' || phase === 'rolledBack') &&
- renderProgress()}
+ {(isBusy || isDone) && renderProgress()}
- {phase === 'review' && !error && unresolvedCount > 0 && (
+ {phase === 'ready' && !error && step === STEP.files && unresolvedCount > 0 && (
{unresolvedCount} {translate('::App.ListForm.WizardImportUnresolved')}
)}
+ {phase === 'ready' && !error && step === STEP.checks && blockingIssues.length > 0 && (
+
+ {translate('::App.ListForm.WizardImportBlocked')}
+
+ )}
+
}
- disabled={phase === 'running'}
+ disabled={isBusy}
onClick={handleClose}
>
- {translate(
- phase === 'finished' || phase === 'rolledBack'
- ? '::App.Platform.Close'
- : '::App.Platform.Cancel',
- )}
+ {translate(isDone ? '::App.Platform.Close' : '::App.Platform.Cancel')}
- {phase === 'review' && !error && (
+
+ {phase === 'ready' && !error && step > STEP.files && (
+ setStep(step - 1)}>
+ {translate('::App.Platform.Back')}
+
+ )}
+
+ {phase === 'ready' && !error && step < STEP.plan && (
+ setStep(step + 1)}
+ >
+ {translate('::App.Platform.Next')}
+
+ )}
+
+ {phase === 'ready' && !error && step === STEP.plan && (
}
- disabled={unresolvedCount > 0 || applyList.length === 0}
+ disabled={applyList.length === 0 || blockingIssues.length > 0}
onClick={handleStart}
>
- {translate('::App.ListForm.WizardImportStart')} ({applyList.length})
+ {translate('::App.ListForm.WizardImportConfirmStart')} ({applyList.length})
)}
)
+
+ return (
+ <>
+ {dialog}
+
+ {/* Sema farkini gidermek icin mevcut tablo tasarimcisi acilir; deploy sonrasi analiz
+ yenilenir. Import dialogunun icine degil kardesi olarak cizilir: ic ice dialog odak ve
+ katman sirasini bozar. */}
+ {designTable && analysis && (
+
setDesignTable(null)}
+ onDeployed={async () => {
+ setDesignTable(null)
+ await runAnalysis(analysis.importId)
+ }}
+ />
+ )}
+ >
+ )
}
export default WizardImportDialog
diff --git a/ui/src/views/developerKit/SqlTableDesignerDialog.tsx b/ui/src/views/developerKit/SqlTableDesignerDialog.tsx
index 57b3792c..5db26d4e 100644
--- a/ui/src/views/developerKit/SqlTableDesignerDialog.tsx
+++ b/ui/src/views/developerKit/SqlTableDesignerDialog.tsx
@@ -89,6 +89,11 @@ interface TableDesignerDialogProps {
dataSource: string | null
onDeployed?: (table: { schemaName: string; tableName: string }) => void | Promise
initialTableData?: { schemaName: string; tableName: string } | null
+ /**
+ * Henuz var olmayan bir tablonun adi. Olusturma modu korunur; yalnizca ad, modul ve entity
+ * alanlari on doldurulur. `initialTableData` verildiginde yok sayilir.
+ */
+ initialTableName?: string | null
}
type IndexType = 'PrimaryKey' | 'UniqueKey' | 'Index'
@@ -1297,6 +1302,7 @@ const SqlTableDesignerDialog = ({
dataSource,
onDeployed,
initialTableData,
+ initialTableName,
}: TableDesignerDialogProps) => {
const { translate } = useLocalization()
const lastCreateTableScript = useStoreState(
@@ -1403,16 +1409,29 @@ const SqlTableDesignerDialog = ({
useEffect(() => {
if (!isOpen) return
+ const knownTableName = initialTableData?.tableName ?? initialTableName
reloadMenus((items) => {
- // In edit mode, auto-select the matching menu code by moduleId
- if (initialTableData) {
- const parts = initialTableData.tableName.split('_')
- const derivedModuleId = parts[0] ?? ''
+ // Adi bilinen tabloda (duzenleme ya da eksik tablo onarimi) modul menusu kendiliginden secilir.
+ if (knownTableName) {
+ const derivedModuleId = knownTableName.split('_')[0] ?? ''
const match = items.find((m) => m.moduleId === derivedModuleId)
if (match?.code) setSelectedMenuCode(match.code)
}
})
+ // Eksik tablo onarimi: tablo hedefte yok, olusturma modu korunur; yalnizca ad alanlari dolar.
+ if (!initialTableData && initialTableName) {
+ const parts = initialTableName.split('_')
+ setSettings((s) => ({
+ ...s,
+ tableName: initialTableName,
+ menuValue: parts[0] ?? '',
+ menuPrefix: parts[0] ?? '',
+ entityName: parts[parts.length - 1] ?? initialTableName,
+ displayName: parts[parts.length - 1] ?? initialTableName,
+ }))
+ }
+
if (dataSource) {
sqlObjectManagerService
.getAllObjects(dataSource)
@@ -1560,7 +1579,7 @@ const SqlTableDesignerDialog = ({
.catch(() => {})
.finally(() => setIndexesLoading(false))
}
- }, [isOpen, dataSource, initialTableData, canManageCrudEndpoints])
+ }, [isOpen, dataSource, initialTableData, initialTableName, canManageCrudEndpoints])
const generatedSql = useMemo(
() =>