mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:31:58 +00:00
导入目录完成的提示只数真正写进去的表(有新增或更新的项、版本前进了),文件中的项这里都有了的表不算、逐表写「没有变化」;一张都没写进去时说清「没有需要写入的内容」,写入出错的表另外说明;导入之后只重新载入写进去的表;check-catalog-loop 补导入结果的检查
This commit is contained in:
@@ -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: '取消',
|
||||
|
||||
@@ -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<string> {
|
||||
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 && <ReportView report={report} />}
|
||||
|
||||
{step === 'done' && result && (
|
||||
<>
|
||||
<Notice tone="info" attr={{ 'data-import-done': '' }}>
|
||||
<p className="font-medium">{T.done}</p>
|
||||
<p className="mt-0.5 text-dim">{T.doneTotals(result.totals)}</p>
|
||||
</Notice>
|
||||
<ReportView report={result} done />
|
||||
</>
|
||||
)}
|
||||
{step === 'done' && result && <DoneView result={result} preview={report} />}
|
||||
|
||||
{error && <Notice tone="err" attr={{ 'data-import-error': '' }}>{error}</Notice>}
|
||||
</div>
|
||||
@@ -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 (
|
||||
<>
|
||||
<Notice tone="info" attr={{ 'data-import-done': '', 'data-import-written': String(written.size) }}>
|
||||
<p className="font-medium">{T.done}</p>
|
||||
<p className="mt-0.5 text-dim">{T.doneTotals(written.size, result.totals, failed, result.guidance.action === 'replace')}</p>
|
||||
</Notice>
|
||||
<ReportView report={result} done written={written} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览、导入结果:合计、逐表的计数(导入后写第几版,没写进去的写没有变化)、对不上的表、冲突逐条、跳过的原因、起草要求
|
||||
* 怎么处理。written:导入结果里真正写进去的表
|
||||
*/
|
||||
function ReportView({ report, done = false, written }: { report: CatalogImportReport; done?: boolean; written?: ReadonlySet<string> }) {
|
||||
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
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{report.tables.map((t) => <TableRow key={t.table_name} t={t} done={done} />)}
|
||||
{report.tables.map((t) => <TableRow key={t.table_name} t={t} done={done} written={!!written?.has(t.table_name)} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -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 (
|
||||
<tr className="border-b border-hairline last:border-b-0" data-import-table={t.table_name}>
|
||||
<tr className="border-b border-hairline last:border-b-0" data-import-table={t.table_name} data-import-written={done ? String(written) : undefined}>
|
||||
<th scope="row" className="px-2.5 py-1.5 text-left align-top font-normal">
|
||||
<span className="mono break-all text-fg">{t.table_name}</span>
|
||||
{renamed && <span className="ml-1.5 text-faint">{T.matchedAs(renamed)}</span>}
|
||||
{done && t.error && <span className="block text-[var(--err)]">{T.tableFailed(t.error)}</span>}
|
||||
{done && !t.error && typeof t.version === 'number' && <span className="block text-faint">{T.version(t.version)}</span>}
|
||||
{done && !t.error && (written
|
||||
? typeof t.version === 'number' && <span className="block text-faint">{T.version(t.version)}</span>
|
||||
: <span className="block text-faint">{T.unchanged}</span>)}
|
||||
</th>
|
||||
{COUNT_KEYS.map((k) => (
|
||||
<td key={k} className={clsx('tnum px-2 py-1.5 text-right align-top', k === 'conflicts' && n(k) > 0 ? 'text-[var(--warn)]' : 'text-dim')}
|
||||
|
||||
@@ -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 () => {
|
||||
// 人工确认的项带着备注(保存提案时的依据、数据剖析的说明):表级的项写在值下面(同有效记录条件的说明),列的一格、
|
||||
// 关联关系悬停看得到。推断的项照旧只有有效记录条件、去重规则在值下面写说明
|
||||
|
||||
Reference in New Issue
Block a user