Skip to content

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

Merged
os-zhuang merged 15 commits into
mainfrom
claude/goofy-matsumoto-639489
Jul 1, 2026
Merged

feat(import): route list import through server /import (write-mode UI + special-value coercion)#2133
os-zhuang merged 15 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 2:07pm

Request Review

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 59.1 KB 350 KB
Entry file index-BdcaYhsz.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 7.28KB 2.63KB
app-shell (runtime-config.js) 4.72KB 1.69KB
app-shell (types.js) 0.01KB 0.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 17.90KB 3.67KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 4.79KB 1.88KB
auth (LoginForm.js) 9.55KB 3.36KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.63KB 2.15KB
auth (SocialSignInButtons.js) 8.89KB 3.61KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 26.07KB 6.30KB
auth (createAuthenticatedFetch.js) 3.93KB 1.55KB
auth (index.js) 1.75KB 0.76KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.29KB 0.82KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.42KB 0.96KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 434.75KB 93.32KB
core (index.js) 1.65KB 0.59KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 107.48KB 26.47KB
fields (index.js) 166.29KB 40.47KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.31KB 0.67KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 21.15KB 4.68KB
i18n (useSafeTranslation.js) 2.68KB 0.98KB
layout (index.js) 36.30KB 10.04KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 4.42KB 1.27KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 1.77KB 0.77KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 5.09KB 1.84KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.46KB 1.03KB
permissions (evaluator.js) 4.00KB 1.23KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.50KB 0.70KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.10KB 12.33KB
plugin-charts (index.js) 46.11KB 12.99KB
plugin-chatbot (index.js) 172.75KB 41.13KB
plugin-dashboard (index.js) 108.28KB 26.86KB
plugin-designer (index.js) 213.48KB 42.95KB
plugin-detail (index.js) 200.33KB 48.19KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 97.03KB 23.51KB
plugin-gantt (index.js) 136.67KB 33.88KB
plugin-grid (index.js) 156.38KB 41.33KB
plugin-kanban (index.js) 48.15KB 12.94KB
plugin-list (index.js) 97.93KB 23.09KB
plugin-map (index.js) 16.02KB 4.98KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 37.83KB 9.97KB
plugin-timeline (index.js) 25.37KB 7.20KB
plugin-tree (index.js) 8.21KB 2.76KB
plugin-view (index.js) 84.50KB 20.37KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.55KB 0.67KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 3.19KB 1.38KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 18.23KB 5.97KB
react (index.js) 0.76KB 0.42KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 2.16KB 0.94KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
tenant (TenantContext.js) 0.31KB 0.25KB
tenant (TenantGuard.js) 1.04KB 0.43KB
tenant (TenantProvider.js) 2.76KB 0.98KB
tenant (TenantScopedQuery.js) 0.77KB 0.44KB
tenant (index.js) 0.75KB 0.38KB
tenant (resolver.js) 2.64KB 0.76KB
tenant (useTenant.js) 0.50KB 0.32KB
tenant (useTenantBranding.js) 0.62KB 0.39KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 0.77KB 0.41KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 1.54KB 0.68KB
types (layout.js) 0.20KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.26KB 1.96KB
types (tenant.js) 0.20KB 0.18KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 0.75KB 0.46KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-zhuang
os-zhuang merged commit 2b86379 into main Jul 1, 2026
10 checks passed
@os-zhuang
os-zhuang deleted the claude/goofy-matsumoto-639489 branch July 1, 2026 14:17
baozhoutao and others added 15 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.
The import wizard resolves grid.import.* keys from @object-ui/i18n and falls
back to an embedded English map when a key is missing. The zh locale only
carried the original ~54 keys, so the newer features (download template,
server dry-run validate, write-mode/match options, large-file/async import,
import history, and job-level undo) rendered in English under zh.

- Add the 57 missing grid.import.* keys to zh.ts, reaching full parity with
  the embedded English fallback.
- Rename two colliding sub-namespaces so they can be expressed as nested
  i18next objects (a key cannot be both a string and a parent):
    grid.import.status.<s>    -> grid.import.jobStatus.<s>   (job status labels)
    grid.import.writeMode.<m>  -> grid.import.writeModeOpt.<m> (write-mode options)
  The bare grid.import.status (column header) and grid.import.writeMode
  (option-group label) keep their names.

Verified in the live app-crm harness under ?lang=zh: history panel, status
badges, created/updated counts, and the undo button all render in Chinese.
The wizard only ran a background import job when rows > ASYNC_IMPORT_THRESHOLD
(5000), but the server only captures undo state when rows <= 5000. The two
ranges never overlapped, so an undoable job was unreachable through the UI.

Add a 'Import in the background' checkbox (shown when the data source supports
import jobs and the file is under the async threshold) that routes a small
import through the job path, producing an undoable job that appears in History
and can be rolled back. English fallback + zh translations included.

Verified live (UI + app-crm backend): 3-row import with the toggle created an
undoable job; Undo via History deleted the 3 records (42 -> 39).
Playwright spec driving the real Import Wizard UI against a real backend end to
end: upload a 3-row CSV → enable the background-import toggle → run → open
History → Undo, asserting backend record counts move baseline → +3 → baseline
and the job row flips to reverted.

Regression guard for the undo-reachability gap fixed in the prior commit. Uses
the standalone import harness (packages/plugin-grid/demo) via IMPORT_HARNESS_ORIGIN;
the single spec skips (does not fail) when that origin isn't serving /live.html,
so it's CI-safe. Run: pnpm test:e2e:import-harness.
Drives the actual product path a user takes — log in, open the crm_lead
list view (ObjectView), click the real toolbar Import button, upload a
CSV, opt into a background import, run it, then Undo from History —
asserting backend record counts on both sides (baseline → +3 → baseline).

- ImportWizard: stable testids on the Next / Import / Close footer buttons
  so the flow is language-agnostic (the real console renders zh labels).
- apps/console/vite.config.ts: opt-in OBJECTSTACK_CLIENT_DIST override that
  aliases @objectstack/client to a locally built, import-job-capable client
  (the published 11.2.0 predates data.createImportJob), widening server.fs
  .allow so Vite can serve it. Inert unless the env var is set.
- playwright.import-console.config.ts + e2e/import-console spec, gated on
  IMPORT_CONSOLE_LIVE=1 and skipped when the backend lacks import-job
  routes, so an unconfigured CI run reports skipped, not failed.
- package.json: test:e2e:import-console script.
…o-639489

# Conflicts:
#	packages/data-objectstack/src/index.ts
#	packages/types/src/data.ts
#	packages/types/src/index.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants