diff --git a/frontend/src/lib/terms.ts b/frontend/src/lib/terms.ts index cd3bc739..1f33dd6a 100644 --- a/frontend/src/lib/terms.ts +++ b/frontend/src/lib/terms.ts @@ -3287,9 +3287,23 @@ export const CATALOG_TRANSFER_TEXT = { importing: '正在导入…', nothing: '没有可以导入的内容:文件中的表在这个数据源里都对不上', done: '导入完成', - doneTotals: (t: { tables: number; add: number; update: number; conflicts: number }) => - `写入 ${formatNumber(t.tables)} 张表:新增 ${formatNumber(t.add)} 项、更新 ${formatNumber(t.update)} 项;冲突 ${formatNumber(t.conflicts)} 项以这里为准`, + /** + * 导入结果的合计。written 只数真正写进去的表(有新增或更新的项、版本前进了),没有变化的表不算(服务端的 totals.tables + * 是对上的表数);一张都没写进去时说清没有需要写入的内容。failed 是写入出错、没有导入的表;guidance 为真时起草要求写进去了 + */ + doneTotals: (written: number, t: { add: number; update: number; conflicts: number }, failed: number, guidance: boolean) => { + const head = written + ? `写入 ${formatNumber(written)} 张表:新增 ${formatNumber(t.add)} 项、更新 ${formatNumber(t.update)} 项` + : failed ? '没有写入任何表' : guidance ? '目录里没有需要写入的内容,只写入了起草要求' : '没有需要写入的内容' + const tail = [ + t.conflicts ? `冲突 ${formatNumber(t.conflicts)} 项以这里为准` : '', + failed ? `${formatNumber(failed)} 张表没有导入,原因见下` : '', + ].filter(Boolean) + return [head, ...tail].join(';') + }, version: (v: number) => `第 ${formatNumber(v)} 版`, + /** 导入结果里没有写进任何内容的表 */ + unchanged: '没有变化', tableFailed: (reason: string) => `这张表未导入:${reason}`, back: '重新选择文件', cancel: '取消', diff --git a/frontend/src/pages/catalog/TransferDialog.tsx b/frontend/src/pages/catalog/TransferDialog.tsx index f60eabdb..8f7e0c79 100644 --- a/frontend/src/pages/catalog/TransferDialog.tsx +++ b/frontend/src/pages/catalog/TransferDialog.tsx @@ -36,6 +36,20 @@ const pathText = (path: string) => { return t && t.kind !== 'relation' ? patchWhere({ path, before: null, after: null }) : path } +/** + * 导入真正写进去的表(文件中的表名):没出错、有新增或更新的项、版本比预览时前进了。服务端合计里的 tables 是对上的表数, + * 文件中的项这里都有了的表也算在里面,不能拿它说「写入 N 张表」。preview 是确认导入之前那次预览(版本是导入前的); + * 拿不到某张表导入前的版本、或者服务端没给版本时只看新增和更新 + */ +export function writtenTables(result: CatalogImportReport, preview: CatalogImportReport | null): Set { + const before = new Map((preview?.tables ?? []).map((t) => [t.table_name, t.version])) + return new Set(result.tables.filter((t) => { + if (t.error || t.add + t.update <= 0) return false + const was = before.get(t.table_name) + return typeof t.version !== 'number' || typeof was !== 'number' || t.version > was + }).map((t) => t.table_name)) +} + export function ImportDialog({ sourceId, onClose, onImported }: { sourceId: string onClose: () => void @@ -102,7 +116,8 @@ export function ImportDialog({ sourceId, onClose, onImported }: { const r = await api.dataCatalog.importCatalog(sourceId, { content: file.content, include_guidance: includeGuidance }) setResult(r) setStep('done') - onImported(r.tables.filter((t) => !t.error).map((t) => t.matched_as ?? t.table_name)) + const written = writtenTables(r, report) + onImported(r.tables.filter((t) => written.has(t.table_name)).map((t) => t.matched_as ?? t.table_name)) } catch (e) { // 有起草任务在进行(409)、别的原因:写服务端那句话,留在预览 setError(errorMessage(e)) @@ -179,15 +194,7 @@ export function ImportDialog({ sourceId, onClose, onImported }: { {step === 'preview' && report && } - {step === 'done' && result && ( - <> - -

{T.done}

-

{T.doneTotals(result.totals)}

-
- - - )} + {step === 'done' && result && } {error && {error}} @@ -195,8 +202,26 @@ export function ImportDialog({ sourceId, onClose, onImported }: { ) } -/** 预览、导入结果:合计、逐表的计数(导入后写第几版)、对不上的表、冲突逐条、跳过的原因、起草要求怎么处理 */ -function ReportView({ report, done = false }: { report: CatalogImportReport; done?: boolean }) { +/** 导入结果:合计只数真正写进去的表,一张都没写进去时说清没有需要写入的内容;逐表写第几版或没有变化 */ +function DoneView({ result, preview }: { result: CatalogImportReport; preview: CatalogImportReport | null }) { + const written = writtenTables(result, preview) + const failed = result.tables.filter((t) => !!t.error).length + return ( + <> + +

{T.done}

+

{T.doneTotals(written.size, result.totals, failed, result.guidance.action === 'replace')}

+
+ + + ) +} + +/** + * 预览、导入结果:合计、逐表的计数(导入后写第几版,没写进去的写没有变化)、对不上的表、冲突逐条、跳过的原因、起草要求 + * 怎么处理。written:导入结果里真正写进去的表 + */ +function ReportView({ report, done = false, written }: { report: CatalogImportReport; done?: boolean; written?: ReadonlySet }) { const conflicts = report.tables.flatMap((t) => t.conflicts.map((c) => ({ t, c }))) const skipped = report.tables.flatMap((t) => t.skipped.map((s) => ({ t, s }))) return ( @@ -213,7 +238,7 @@ function ReportView({ report, done = false }: { report: CatalogImportReport; don - {report.tables.map((t) => )} + {report.tables.map((t) => )} @@ -262,16 +287,18 @@ function ReportView({ report, done = false }: { report: CatalogImportReport; don ) } -function TableRow({ t, done }: { t: CatalogImportTable; done: boolean }) { +function TableRow({ t, done, written }: { t: CatalogImportTable; done: boolean; written: boolean }) { const renamed = t.matched_as && t.matched_as !== t.table_name ? t.matched_as : null const n = (k: typeof COUNT_KEYS[number]) => (k === 'conflicts' || k === 'skipped' ? t[k].length : t[k]) return ( - + {t.table_name} {renamed && {T.matchedAs(renamed)}} {done && t.error && {T.tableFailed(t.error)}} - {done && !t.error && typeof t.version === 'number' && {T.version(t.version)}} + {done && !t.error && (written + ? typeof t.version === 'number' && {T.version(t.version)} + : {T.unchanged})} {COUNT_KEYS.map((k) => ( 0 ? 'text-[var(--warn)]' : 'text-dim')} diff --git a/scripts/check-catalog-loop.mjs b/scripts/check-catalog-loop.mjs index d66fedfb..d9058d87 100644 --- a/scripts/check-catalog-loop.mjs +++ b/scripts/check-catalog-loop.mjs @@ -1429,6 +1429,59 @@ await section('导出导入 · 导出文件、导入先预览再写入', async ( await close() }) +await section('导出导入 · 导入结果只数真正写进去的表', async () => { + // 文件里的两张表都对得上:visits 有一项新增(写进去,第 2 版 → 第 3 版);ORDERS 文件里的项这里都有了(没有变化,还是第 2 版)。 + // 第二次导入两张表都没有变化。合计按服务端的写法:tables 是对上的表数,不是写进去的表数 + const report = (visitsAdd, after) => ({ + ...clone(IMPORT_PREVIEW), + tables: [ + { ...clone(IMPORT_PREVIEW.tables[0]), add: visitsAdd, version: after && visitsAdd ? 3 : 2 }, + { ...clone(IMPORT_PREVIEW.tables[1]), update: 0, keep: 2, version: 2 }, + ], + totals: { tables: 2, add: visitsAdd, update: 0, keep: 6, conflicts: 1, skipped: 1 }, + }) + const replies = { + importPreview: [json(report(1, false)), json(report(0, false))], + import: [json(report(1, true)), json(report(0, true))], + } + const { page, sent, errors, natives, close } = await open(`/data/catalog/${S1}`, { handlers: loopHandlers(freshState(), replies) }) + await page.locator('[data-catalog-row]').first().waitFor() + const dlg = page.locator('[data-import-dialog]') + const importOnce = async () => { + await page.locator('[data-catalog-import]').click() + await until(async () => (await dlg.getAttribute('data-step').catch(() => '')) === 'choose') + await dlg.locator('[data-import-file]').setInputFiles({ name: 'other.json', mimeType: 'application/json', buffer: Buffer.from(JSON.stringify(IMPORT_FILE)) }) + await until(async () => (await textOf(dlg.locator('[data-import-file-meta]'))).includes('zzloopother')) + await page.locator('[data-import-preview]').click() + await until(async () => (await dlg.getAttribute('data-step')) === 'preview') + await page.locator('[data-import-confirm]').click() + return until(async () => (await dlg.getAttribute('data-step')) === 'done') + } + check('导入完成', await importOnce()) + const doneText = await textOf(dlg.locator('[data-import-done]')) + check('合计只数真正写进去的表:「写入 1 张表」,没有变化的 ORDERS 不算', doneText.includes('写入 1 张表:新增 1 项、更新 0 项') + && !doneText.includes('写入 2 张表') && doneText.includes('冲突 1 项以这里为准'), doneText) + await shot(page, 'catalog-import-written') + check('……逐表:写进去的写第几版,没有变化的写「没有变化」', (await textOf(dlg.locator('[data-import-table="visits"]'))).includes('第 3 版') + && (await textOf(dlg.locator('[data-import-table="ORDERS"]'))).includes('没有变化') + && !(await textOf(dlg.locator('[data-import-table="ORDERS"]'))).includes('第 2 版'), + `${await textOf(dlg.locator('[data-import-table="visits"]'))} · ${await textOf(dlg.locator('[data-import-table="ORDERS"]'))}`) + await page.locator('[data-import-close]').click() + await until(async () => (await dlg.count()) === 0) + check('再导入一次,两张表都没有变化', await importOnce()) + const nothing = await textOf(dlg.locator('[data-import-done]')) + await shot(page, 'catalog-import-nothing') + check('……一张都没写进去:说清「没有需要写入的内容」,不写「写入 2 张表」', nothing.includes('没有需要写入的内容') && !nothing.includes('写入 2 张表') + && !nothing.includes('新增 0 项'), nothing) + check('……两张表都写「没有变化」', (await textOf(dlg.locator('[data-import-table="visits"]'))).includes('没有变化') + && (await textOf(dlg.locator('[data-import-table="ORDERS"]'))).includes('没有变化')) + await cleanCopy(page, '导入结果') + check('只发过导入的写请求(预览、导入)', writes(sent).every((s) => /catalog\/import(\/preview)?$/.test(s.key)), JSON.stringify(writes(sent).map((s) => s.key))) + check('没有原生对话框', natives.length === 0, natives.join(' | ')) + check('没有运行时报错', errors.length === 0, errors[0] ?? '') + await close() +}) + await section('表详情 · 人工确认的项的说明', async () => { // 人工确认的项带着备注(保存提案时的依据、数据剖析的说明):表级的项写在值下面(同有效记录条件的说明),列的一格、 // 关联关系悬停看得到。推断的项照旧只有有效记录条件、去重规则在值下面写说明