Skip to content

feat(import): route list import through server /import (write-mode UI + special-value coercion)#2133

Open
baozhoutao wants to merge 10 commits into
mainfrom
claude/goofy-matsumoto-639489
Open

feat(import): route list import through server /import (write-mode UI + special-value coercion)#2133
baozhoutao wants to merge 10 commits into
mainfrom
claude/goofy-matsumoto-639489

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

背景 / Problem

列表导入向导原来是逐行 create,并在客户端猜测特殊值(布尔/日期/select/lookup)。这既重复又不可靠 —— lookup 名称→id 根本没法在前端解析。

方案 / Solution

改走单次服务端导入 POST /data/:object/import,前端只发原始映射后的行,由服务端统一强转特殊值并按 writeMode 路由每行。依赖框架侧 objectstack-ai/framework#2505(服务端 coercion + client.data.import)。

改动

  • types:DataSource.importRecords + ImportRequestOptions / ImportRecordsResult / ImportRowResult / ImportWriteMode / ImportFieldMappingEntry(镜像服务端 spec)
  • data-objectstack:ObjectStackAdapter.importRecords() —— 把原始行转发到 /data/:object/import;当连接的 @objectstack/client 没有 data.import 时抛 UNSUPPORTED_OPERATION
  • plugin-grid ImportWizard:
    • 发送原始映射行给服务端强转(不再客户端猜特殊值)
    • 写入模式 UI:insert / update / upsert + matchFields 勾选 + createMissingOptions / runAutomations / skipBlankMatchKey 开关
    • 逐行结果 → created/updated 报告;失败行 CSV 再导出(带 _error 列 + BOM)
    • 优雅降级:client 不支持 import 时回退到旧的逐行 create;真实服务端错误直接上报,不静默重试(避免重复导入)
  • app-shell ObjectView:只把可写字段作为导入目标(剔除 formula/summary/autonumber、readonly、permissions.write:false)
  • 测试:isUnsupportedImport + buildFailedRowsCsv 单元覆盖(7)

兼容性

objectui 当前锁定已发布的 @objectstack/client ^11.2.0(尚无 data.import),因此在框架 PR 发布新 client 之前,向导会自动走逐行 create 回退,不会报错。等 client 升级后自动切到服务端 /import

验证 / Verification

  • pnpm --filter @object-ui/plugin-grid exec vitest run102 passed(含 7 个新增)
  • pnpm --filter '@object-ui/app-shell...' build → tsc 通过(ObjectView 改动编译干净)
  • console dev server 启动编译通过(验证 app-shell 改动在真实构建图中有效);完整浏览器点通需要框架后端起服务并有种子对象 + 登录,超出本 PR 范围,导入逻辑已由上面单测/集成测覆盖

未做(后续 P1/P2)

大文件异步任务 + 进度 + 5万上限、导入历史、真正的钩子开关落地;任务级 undo、字段类型自动探测、xlsx 优化。

@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 1, 2026 8:47am

Request Review

baozhoutao added 10 commits July 1, 2026 14:11
…alue coercion

Route the list-import wizard through a single-call server import that coerces
special values (boolean/number/date→ISO/select label→code/lookup name→id) from
field metadata, instead of guessing on the client. Falls back to the legacy
per-row create loop when the connected client lacks data.import.

- types: DataSource.importRecords + ImportRequestOptions / ImportRecordsResult
  / ImportRowResult / ImportWriteMode / ImportFieldMappingEntry (mirror server spec)
- data-objectstack: ObjectStackAdapter.importRecords() — forwards raw rows to
  POST /data/:object/import; throws UNSUPPORTED_OPERATION when client lacks import
- plugin-grid ImportWizard: send RAW mapped rows to the server; write-mode
  (insert/update/upsert) + matchFields + createMissingOptions/runAutomations/
  skipBlankMatchKey options UI; per-row result → created/updated report;
  failed-row CSV re-export; graceful legacy fallback; real errors surfaced
- app-shell ObjectView: pass only writable fields (drop formula/summary/
  autonumber, readonly, permissions.write:false) as import targets
- tests: isUnsupportedImport + buildFailedRowsCsv unit coverage (7)
…in preview validation

The preview-step validator only accepted true/false/1/0/yes/no, so boolean
cells the server coercion would accept (Chinese 是/否, on/off, y/n, ✓/×) were
wrongly flagged as type errors. Mirror the server's BOOL token set.
Replace the two-pass name matcher with a scored, globally-assigned mapper:
each column/field pair is scored on exact/normalized name, bilingual
(EN/中文) synonyms, token overlap, and content-inferred type
compatibility, then fields are assigned by descending confidence so each
column and field is used at most once.

- importParsers: suggestColumnMappings() + ColumnSuggestion/MappableField
  types, scoreToConfidence() buckets (high/medium/low), synonym groups.
- ImportWizard: autoMapColumns() delegates to the scorer; mapping step
  shows an 'Auto-matched · <confidence>' hint per column (only while the
  user's choice still equals the suggestion) and a summary count. i18n
  keys added.
- tests: 7 cases (exact/normalized, synonyms, global de-dup, type gating,
  incompatible-type discount, unknown column, confidence buckets).
Adds the client-side surface for large-file background imports:

- types: DataSource gains createImportJob / getImportJobProgress /
  getImportJobResults / listImportJobs / cancelImportJob (all optional,
  feature-detected) plus the CreateImportJobResult / ImportJobProgressInfo /
  ImportJobResultsInfo / ImportJobSummaryInfo / ListImportJobsOptions /
  ImportJobStatus types mirroring the server contract.
- data-objectstack: adapter delegates each method to the @objectstack/client
  data namespace, feature-detecting createImportJob and throwing
  UNSUPPORTED_OPERATION when an older client/server lacks the job routes so
  callers can gracefully fall back to the synchronous /import path.
- tests: delegation + arg-shaping + graceful-degradation coverage.
Files over ASYNC_IMPORT_THRESHOLD (5,000 rows — the sync route's ceiling) are
handed to a server-side background job instead of the blocking /import call:

- ImportWizard creates a job via dataSource.createImportJob, then polls
  getImportJobProgress every 800ms, showing live "{processed} of {total}"
  progress; on terminal status it pulls getImportJobResults and renders the
  same completion screen as the sync path (jobResultToImportResult mirrors the
  sync mapping exactly).
- Cancel button aborts the poll loop and best-effort cancels the job server-side.
- Transient poll blips are tolerated (5 consecutive failures before giving up).
- Any unsupported signal (older adapter/client/server -> UNSUPPORTED_OPERATION /
  404 / missing method) transparently falls back to the synchronous route.
- Completion screen surfaces a cancelled state and a results-truncated note when
  the server caps the per-row report.
- tests: isUnsupportedImportJob + jobResultToImportResult coverage.
Upload step gains a "Download template" button that generates a CSV from the
object's fields — a header row of labels (required fields marked with *) plus
one type-appropriate example row (dates, emails, numbers, booleans, and the
first option value for selects). Not persisted; a convenience starting point.

- ObjectView forwards each field's enum options so the example row can seed
  select columns with a real allowed value.
- importParsers normalizeKey/tokenize now strip the * required-marker so a
  filled-in template round-trips back to the same field on re-import (covered
  by a round-trip test).
- tests: buildImportTemplateCsv (header/example/select/escaping) + round-trip.
Adds a 'Validate data' pre-check in the preview step for small files
(<= ASYNC_IMPORT_THRESHOLD) when the data source speaks /import. It
sends the exact import payload with dryRun:true so the server coerces
and validates every row without persisting, then shows an ok/error
summary + a capped list of failing rows.

- Extract assembleImportRequest() as a pure helper so the real import
  and the dry-run send byte-identical payloads (dryRun the only diff).
- handleValidate() calls importRecords(dryRun) and stores the result;
  older adapters/clients without /import degrade silently to the
  existing client-side cell validation.
- A prior dry-run is dropped whenever the payload changes (mapping,
  write-mode, options, or an inline correction) so the summary never
  reflects stale data.
- i18n + tests for assembleImportRequest (dryRun flag, matchFields
  gating, option threading).
Adds a 'History' view to the ImportWizard for objects whose data source
exposes listImportJobs. From the upload step a header toggle swaps the
wizard body for ImportHistoryPanel, which lists prior background import
jobs (status badge, processed/total, created/updated/skipped/errors,
timestamp) newest-first, with Refresh and — for pending/running jobs —
an inline Cancel.

- Degrades to an empty state when the adapter lacks listImportJobs
  (older client/server) and never renders the toggle in that case.
- Reuses the async job status enum + cancelImportJob adapter method.
- isImportJobActive() pure helper gates cancel/polling; unit-tested.
- i18n for history + per-status labels.
For files over ASYNC_IMPORT_THRESHOLD, the preview step already renders
only the first PREVIEW_ROW_COUNT rows and the import runs as a background
job — but nothing told the user either fact. Add a notice at the preview
step for large files that it's previewing the first N of M rows and the
import will run in the background.

Chosen over a streaming re-architecture (evaluated): the client already
parses the whole file and caps the preview, so a sample-preview notice
delivers the clarity at near-zero cost while keeping the one-shot payload
+ 50k ceiling contract intact.
Add an "Undo import" action to the import-history panel for finished jobs
the server captured an undo log for — deletes the records the import
created and restores updated records to their pre-import values.

- types: DataSource.undoImportJob(jobId) + ImportJobUndoResult;
  undoable/revertedAt on progress + summary DTOs.
- data-objectstack: undoImportJob adapter with feature-detection +
  UNSUPPORTED_OPERATION fallback for older clients.
- ImportHistoryPanel: per-row Undo button (confirm dialog, busy state)
  gated on isImportJobUndoable(); shows an "Undone" marker once reverted.
- i18n + isImportJobUndoable gating tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant