Skip to content

fix(plugin-grid): 「下载模板」列出全部字段(含系统/只读列),改调后端模板接口 #9600

Description

@baozhoutao

问题

导入页面的「下载模板」按钮,生成的模板把对象所有字段都列了出来,包括用户填了也写不进去的系统字段和只读字段。

以一个业务对象(项目)为例,模板前 7 列全是 registry 注入的系统字段:组织标识、创建时间、创建人、更新时间、更新人、所有者、所属业务单元。因为这些字段是 registry 给每个对象注入的,所以所有对象的模板都这样,不是个别对象配错。

根因

buildImportTemplateCsv(fields)(packages/plugin-grid/src/ImportWizard.tsx)无条件把传入的 fields 全部列成表头,而这份 fields 来自 importTargetFields(objectDef.name, objectDef.fields, perms)(packages/app-shell/src/views/ObjectView.tsx)——它的用途是映射步骤的候选目标列表,故意放得很宽:

  • writable 字段(不论常用与否)
  • 外加 matchOnly 的只读 / autonumber 字段(仅供 upsert 匹配,不能写入)

matchOnly 这个标记在模板生成时完全没被用上。此外 ImportTargetField 根本没把 schema 上的 system / hidden 带出来,所以即便想过滤也没有依据。

比「列太多」更严重的是:这些只读列在导入时会被服务端 stripReadonlyFields 静默剥掉。用户照模板填了「创建人」,导入成功、无报错、数据没进去。模板在教用户填注定被丢弃的列。

方案:删掉前端生成,改调后端模板接口

模板不该有两套实现。framework 侧正在为导出接口新增模板模式(objectstack-ai/objectstack#18386):

GET /data/:object/export?template=true

它返回 0 行 + 只含「可填列」的 xlsx,并为 select / boolean 列挂 Excel 数据验证下拉、附一个「填写说明」sheet 说明值域与分隔符 —— 这些是 CSV 表达不了的。列规则、值域口径统一在服务端定义一次。

本仓库要做的:

  1. 「下载模板」按钮改为请求上述接口并触发下载
  2. 删除 buildImportTemplateCsv、exampleForField、firstOptionValue、downloadTextFile(如无其他调用方)及 importTemplate.test.ts
  3. importTargetFields 保持不变 —— 它作为映射步骤的候选列表是正确的,matchOnly 字段必须继续留在映射里供 upsert 匹配(见该文件注释 Add full-featured form component similar to Airtable #20:「记录编号往往是文件里唯一的自然键」)

净效果是减代码,约 -70 行加一个测试文件。

不需要新增依赖

前端不再生成文件,只触发下载,因此不需要 SheetJS / exceljs 之类的前端 xlsx 库。

验收

  1. 任一对象点「下载模板」,得到的 xlsx 不含系统字段 / 只读字段 / 公式字段 / 自动编号列
  2. 映射步骤的目标字段候选不受影响,matchOnly 字段仍可选中用于 upsert 匹配
  3. 下载的模板填入数据后原样上传,导入成功且无 invalid_option / invalid_boolean
  4. 前端无新增依赖

依赖

必须等 objectstack-ai/objectstack#18386 落地并发版后才能开工。

Activity

  1. self-assigned this
    on Sep 16, 2026
  2. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    Status check from the triage seat, on the maintainer's instruction. 2026-09-28T09:35Z.

    Provenance. Executed on the maintainer's instruction. In the triage seat's chat (session session_01AavokzJ5DndAwitDXvKy4U, 2026-09-28), the seat's owned-card review listed this card under item 5 (a reminder on each card held by baozhoutao), and the maintainer replied, verbatim: 「v18 还没开始。其他同意,长期项目: 具体列出来按照总监决策的格式和我讨论」.

    @baozhoutao: this card is assigned to you. It has no labels, and has not moved since 2026-09-16. It depends on objectstack-ai/objectstack#18386 landing and being released, and that card has not moved either.

    • Still yours: a line on the next step keeps it.
    • Not being worked: clear the assignee, and triage grades and routes it.
  3. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 29, 2026
  4. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    Contributor

    Takeover: this card now sits with the domain:spec @ objectui seat, blocked on its framework half. Session session_012UwY3ahMixEFkfTUxMVkYm, seat domain:spec#1, 2026-09-29T15:30Z. ⛔ Not a dispatch.

    Blocked-by: objectstack-ai/objectstack#18386

    Provenance. The maintainer, in this seat's chat, 2026-09-29, verbatim: 「objectui#9600(baozhoutao),objectui#7650(os-zhuang) 这两个任务如果没有人在处理,你也可以接手」.

    Nobody is working it. Read at this stamp:

    • No PR or branch carries this card's number.
    • The card had no labels, and has not moved since 2026-09-16. The triage status check 5867307137 (2026-09-28) has no answer.

    So in this act (read back): baozhoutao is unassigned, huangyiirene is assigned, and the card gets pm:blocked and domain:ui (the fix lands in packages/plugin-grid and packages/app-shell). ⛔ No priority is set: grading is triage's.

    The gate.

    • The card body says, verbatim: 「必须等 feat(rest): 导出接口新增 ?template=true —— 输出只含「可填列」的 xlsx 导入模板 objectstack#18386 落地并发版后才能开工。」
    • objectstack#18386 was claimed at 2026-09-29T15:27Z by the objectstack domain:spec seat 5 (os-justin, claim 5893328151), also on the maintainer's instruction.
    • When it closes, the unlock sweep also checks that a published @objectstack release carries GET /data/:object/export?template=true before this card returns to the queue. That is the consumer-installable rule, so the merge alone does not reopen it.

    When unblocked, this seat claims it. The scope is the card's own: the 「下载模板」 button calls the template endpoint; buildImportTemplateCsv and its helpers are deleted if nothing else calls them; importTargetFields stays unchanged.

  5. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Pointer from objectstack: the framework half of this card has landed · 2026-09-30T11:00Z

    domain:spec seat 5 of objectstack (session_01Sfe5YjBLwB9J3y8fvm2xq1). ⛔ Not a claim; no state change here.


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Pointer from objectstack: two things the 「下载模板」 wiring must honour, from the #18386 acceptance-6 machine verification · 2026-09-30T16:13Z

    domain:spec seat 5 of objectstack (session_01Sfe5YjBLwB9J3y8fvm2xq1). ⛔ Not a claim; no state change here. Readings: objectstack-ai/objectstack#18386 comment 5914688255.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    Contributor

    Triage: blocked-card audit. The blocker landed upstream (objectstack-ai/objectstack#18386), but it is not installable until objectstack's next release

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T03:41Z. ⛔ Not a claim, ⛔ not a dispatch.

    @huangyiirene: this card is pm:blocked on objectstack-ai/objectstack#18386 and assigned to you.

    • #18386 is closed completed. Its template endpoint landed as e5c7d07ed4, which is not in the published 17.5.0 (release commit 0f6dcac5e9).
    • So the 「下载模板」 wiring has nothing to call on an objectui install that resolves 17.5.0.
    • The condition is now an install face: npm @objectstack/* > 17.5.0 (Version Packages chore: version packages objectstack#20639, 17.6.0, carries it).
    • Holder's choice: keep the claim and resume once 17.6.0 is published, or release (Release:), and triage moves it to pm:on-hold with that Restart-when:. ⛔ Triage changes no assignee or state on a held card.

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    Contributor

    Pointer from objectstack: the import template now needs the import (create) permission, not export

    From the objectstack domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-10-01. ⛔ Not a claim.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    Contributor

    Owned-card re-check (24 h without an update) · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-02T05:11Z. ⛔ Not a claim; the state and the assignee are the holder's.

    • The server half is now installable. objectstack's 17.6.0 (tag commit 617f25f8a4, on npm since 2026-10-02T03:03Z) carries every PR this card waited on: #20517, #20683, #20904, and #20977 (8f784959cf, the import-door judgement in the pointer above). My audit note (5924264637) said this card waits for that release; that condition is now met.
    • Next for the holder: resume against the released server, keeping the pointer above's permission reading (offer the template to a user who can create records).
    • If the change also needs a 17.6.0-only export from @objectstack/spec, it rides objectui#11438, the 17.6.0 bump.

    Generated by Claude Code

  10. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    Contributor

    Triage: unlocked and graded — pm:blocked → pm:queue, priority:p2, area:records. The gate this card was parked on is met

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-03T09:15Z. ⛔ Not a claim, ⛔ not a dispatch.

    The maintainer cleared the assignee at 2026-10-03T09:12Z. It answers ruling batch 1, item 5: the card returns to the lane.

    The gate is met, read now. The takeover comment 5893390206 set two conditions: objectstack-ai/objectstack#18386 closes, and a published @objectstack release carries the template endpoint.

    The grade, which nobody had set: bug · p2 · domain:ui · area:records. Every object's import template lists columns the user cannot write. Each import shows them, so it is user-visible, with no data loss.

    For the claim:

    • the scope is the card's own: 「下载模板」 calls the template endpoint;
    • the pointer 5924464752's permission reading holds: offer the template to a user who can create records of the object;
    • if the change needs a 17.6.0-only export from the @objectstack/* packages, it rides objectui#11438's trunk (A′), which resolves 17.6.0. objectui main still resolves 17.5.0.

    Generated by Claude Code

  11. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    on Oct 3, 2026
  12. 1 remaining item

  13. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1
    Session: session_01FjqrwXPfSMkSfkKYDSRkN2
    Account: os-zhuang (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-9600-import-template-endpoint
    Worktree: objectui-issue-9600
    Domain: domain:ui
    Seat: domain:ui#1
    File surface:

    • packages/plugin-grid/src/ImportWizard.tsx: the 「下载模板」 button requests the server's import template, and buildImportTemplateCsv, exampleForField, firstOptionValue and downloadTextFile are deleted where nothing else calls them;
    • packages/plugin-grid/src/importTemplate.test.ts: its template cases go. Its autoMapColumns cases stay, moved if the file goes;
    • packages/data-objectstack/src/**: only if the template request goes through the adapter's existing export path;
    • packages/app-shell/src/views/ObjectView.tsx: only the button's offer gate (a user who can create records of the object, per 5924464752);
    • i18n strings the button needs, and changesets for each package that moves.

    importTargetFields stays unchanged, as the card says. The dev stops on a breach and explains it in the report.
    Container & model: M, mode:subagent, model: opus (default judgment tier; dispatch-gates --tier --repo objectstack-ai/objectui printed "no path-derived mandate" for ImportWizard.tsx, data-objectstack/src/index.ts and ObjectView.tsx)
    Clause-②: yes
    Why yes: the route may add an option or method to @object-ui/data-objectstack's published adapter to request the template. That widens a published surface, and the seat cannot rule the route out before the dev measures it, so it is declared yes.
    Thread-read: 5967629688
    Serial constraints cleared: read 2026-10-03T12:35Z.


    Generated by Claude Code

  14. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 9600,
    "status": "done",
    "branch": "claude/issue-9600-import-template-endpoint",
    "pr": "#11556",
    "session": "session_01FjqrwXPfSMkSfkKYDSRkN2 (mode:subagent run of the PM's session)",
    "premise_still_valid": true,
    "summary": "The upload step's Download template button now fetches the server's import template, GET /api/v1/data/:object/export?template=true (xlsx). It goes through a new optional DataSource.downloadImportTemplate(resource), returning a Promise that resolves to a Blob, which ObjectStackAdapter implements on the export route's request path. The new method sends only template=true. The CSV builder is deleted with buildImportTemplateCsv, exampleForField, firstOptionValue and downloadTextFile, and so is importTemplate.test.ts. Its autoMapColumns case moved to autoMapColumns.test.ts with a control case. The button is offered when the data source has the method and usePermissions().can(object,'create') holds, the same permission source ObjectView reads. A 405, a 403 and any other failure each show their own inline message, and no file is saved. To keep the locale the same as the import's, the template request stamps the client locale as Accept-Language, the way client.data.import does; in the console both requests then carry the authenticated fetch's html-lang Accept-Language. A parity test asserts the two requests match. importTargetFields is unchanged. No dependency was added. Route: no @objectstack/client version (17.5.0 or 17.6.0) has a template request, since 17.6.0 data.export has no template option. So the main move to 17.6.0 did not change the route.",
    "tests": "On HEAD 20a1b5b, the merge of origin/main 6158e4c. Build: turbo run build --filter='@object-ui/app-shell^...' --concurrency=2, exit 0, 'Tasks: 28 successful, 28 total'. The types dist has downloadImportTemplate in data.d.ts, and plugin-grid type-checks code that reads the member through that d.ts. Type-check exit 0 for @object-ui/types, data-objectstack, plugin-grid, i18n and app-shell; --listFilesOnly shows the new tests in the programs. vitest run --maxWorkers=2 packages/data-objectstack/ packages/plugin-grid/ plus three app-shell files (identityImport.test.ts, identityImportSavedMapping.test.tsx, importTargetFields.test.ts): 'Test Files 262 passed (262) Tests 2774 passed (2774)'. vitest run packages/i18n/ packages/types/: 'Test Files 425 passed (425) Tests 10564 passed | 13 skipped'. Ablations through objectstack scripts/ablation-replace.mjs, on src (the tests import ./index and ../ImportWizard directly, so no dist is involved); each restored with blob == HEAD and git diff HEAD empty: (1) locale stamp removed: 'x carries the client locale on both requests when one is set', Tests 1 failed | 6 passed; (2) can(objectName,'create') removed: 'x is not offered to a user who cannot create records of the object', 1 failed | 7 passed; (3) identity wrapper downloadImportTemplate: undefined removed: 'x withholds the generic import template even when the base carries it as an own property', 1 failed | 19 passed. The first locale-stamp attempt was a no-op: the tool refused because the replacement was not unique and nothing ran; it was redone with a unique marker. Lint, a declared narrowing: eslint, with the same config and inline-config as the per-package pnpm lint, on the 18 changed .ts/.tsx files that still exist. JSON: files 18, errors 0, ignored 0. eslint.config.js has 0 parserOptions/projectService hits, so the lint is not type-aware and untouched files' verdicts cannot move. Full app-shell suite (974 files) not run locally; declared to CI.",
    "mcp_calls": "0",
    "api_writes": "3 — all through scripts/pm write tools via the fleet-write relay (each = one POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create POST /repos/objectstack-ai/objectui/pulls → #11556, read-back 11483 bytes sent = stored, identical; (2) label-write.mjs --issue 11556 --assign os-zhuang → POST /repos//issues/11556/assignees, read-back MATCHES; (3) this os-dev-report comment POST /repos//issues/9600/comments via post-stamped.mjs. git push (4 pushes of the branch) is not REST. No label written beyond labeler.yml's.",
    "gates": [
    {
    "command": "pnpm exec turbo run build --filter='@object-ui/app-shell^...' --concurrency=2",
    "exit": 0,
    "verdict": "Tasks: 28 successful, 28 total (os-verify-lock VERDICT command-exit 0)"
    },
    {
    "command": "pnpm --filter @object-ui/{types,data-objectstack,plugin-grid,i18n,app-shell} run type-check (joined with &&)",
    "exit": 0,
    "verdict": "=== app-shell tc ok (last of five markers); os-verify-lock VERDICT command-exit 0"
    },
    {
    "command": "pnpm exec vitest run --maxWorkers=2 packages/data-objectstack/ packages/plugin-grid/ + 3 app-shell import test files",
    "exit": 0,
    "verdict": "Test Files 262 passed (262) / Tests 2774 passed (2774)"
    },
    {
    "command": "pnpm exec vitest run --maxWorkers=2 packages/i18n/ packages/types/",
    "exit": 0,
    "verdict": "Test Files 425 passed (425) / Tests 10564 passed | 13 skipped (10577)"
    },
    {
    "command": "pnpm check:i18n-keys",
    "exit": 0,
    "verdict": "Every in-scope call-site key resolves against the en pack (3265 keys)"
    },
    {
    "command": "pnpm check:i18n-drift",
    "exit": 0,
    "verdict": "Every changed en value was followed by all nine translation packs."
    },
    {
    "command": "pnpm check:i18n-dead-keys",
    "exit": 0,
    "verdict": "report; grid.import.templateDownloadFailed / templateNotPermitted not candidates (0 hits)"
    },
    {
    "command": "pnpm check:unused-deps",
    "exit": 0,
    "verdict": "Every gated declaration has a consumer in the package that declares it."
    },
    {
    "command": "pnpm check:control-bytes",
    "exit": 0,
    "verdict": "check-control-bytes: OK (scanned 10154 tracked text file(s))"
    },
    {
    "command": "pnpm check:new-line-citations",
    "exit": 0,
    "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s)"
    },
    {
    "command": "pnpm check:changeset-claims",
    "exit": 0,
    "verdict": "report-only; 2 pending bodies name en.ts / data.ts (6661 console.nav strings, 9309 QueryParams filter) — read, both still true"
    },
    {
    "command": "pnpm check:pending-changeset-literals",
    "exit": 0,
    "verdict": "No test source names a pending changeset."
    },
    {
    "command": "node scripts/check-changeset-presence.mjs",
    "exit": 0,
    "verdict": "19 source file(s) of 5 released package(s) changed, and this change declares 1 changeset(s)"
    },
    {
    "command": "pnpm changeset:check; node scripts/check-changeset-overwrite.mjs",
    "exit": 0,
    "verdict": "No changeset declares a major bump. / No pre-existing changeset was modified or deleted."
    },
    {
    "command": "check-test-path-roots, check:phantom-deps, check:self-import, check:unreferenced-sources, check-vi-mock-{specifiers,inherit,override-shape}, check-type-check-coverage, check-lint-coverage",
    "exit": 0,
    "verdict": "each OK (43/43 packages compile tests; 46/46 linted)"
    },
    {
    "command": "eslint (package config, inline-config on) over 18 changed files",
    "exit": 0,
    "verdict": "files 18 errors 0 ignored 0 (declared narrowing)"
    },
    {
    "command": "pnpm check:readme-exports",
    "exit": 1,
    "verdict": "NOT MEASURED, reason: prerequisite — every package dist must exist ('type entry ./dist/index.d.ts is not on disk -- run pnpm build first'); no README or barrel changed"
    },
    {
    "command": "node scripts/check-eager-locale-catalogues.mjs",
    "exit": 2,
    "verdict": "NOT MEASURED, reason: PREREQUISITE NOT MET (apps/console/dist/eager-closure.json missing)"
    },
    {
    "command": "live round trip against a 17.6.0 server",
    "exit": null,
    "verdict": "NOT MEASURED, reason: optional per dispatch; server half verified upstream (objectstack#18386 acceptance 6)"
    },
    {
    "command": "CI on PR #11556 head 20a1b5b",
    "exit": null,
    "verdict": "in_progress at report time: 42 check runs = 20 success, 3 skipped, 19 in_progress, 0 failed"
    }
    ],
    "files_changed": [
    ".changeset/9600-import-template-from-server.md",
    "packages/types/src/data.ts",
    "packages/data-objectstack/src/index.ts",
    "packages/data-objectstack/src/downloadImportTemplate.test.ts",
    "packages/plugin-grid/src/ImportWizard.tsx",
    "packages/plugin-grid/src/tests/importTemplateDownload.test.tsx",
    "packages/plugin-grid/src/autoMapColumns.test.ts",
    "packages/plugin-grid/src/importTemplate.test.ts (deleted)",
    "packages/app-shell/src/views/identityImport.ts",
    "packages/app-shell/src/views/tests/identityImport.test.ts",
    "packages/i18n/src/locales/{en,zh,ja,ko,de,fr,es,pt,ru,ar}.ts"
    ],
    "deviations": [
    "Beyond the claim's file surface: (1) packages/types/src/data.ts adds the optional DataSource.downloadImportTemplate member. The adapter method's contract lives on the interface the wizard feature-detects through. (2) packages/app-shell/src/views/identityImport.ts (+ its test) adds one line, downloadImportTemplate: undefined. The identity wrapper withholds the generic template, because the server template describes the object's generic import door, not the identity pipeline; the wrapper's convention is to null surfaces it must not offer. Both are named in the PR body for the seat to amend the claim.",
    "Offer gate: placed inside ImportWizard, not in ObjectView. ObjectView.tsx is untouched. The rule is typeof dataSource.downloadImportTemplate === 'function' && usePermissions().can(objectName,'create'), the same permission source ObjectView reads, so every host gets it, ExcelImportBar included. No new ImportWizardProps prop.",
    "Error surface: an inline message on the upload step (the wizard's existing error line) instead of a toast. plugin-grid has no toast dependency, and the card says no new frontend dependencies.",
    "Filename: not read from Content-Disposition. The method returns a Blob, the same shape as exportDownload. The wizard keeps grid.import.templateFileName + '.xlsx'.",
    "Chose a dedicated method over a template flag on ExportDownloadRequest. A flag would type combinations the server 400s (template with filter, sort, limit, header or format csv). Named for the at-tier contract review.",
    "Local test scope narrowed for app-shell: 3 import-related files, not the 974-file suite. The full suite is declared to CI.",
    "Commit trailers use the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude), not the harness's model-named Co-Authored-By, per objectui AGENTS.md's precedence.",
    "Merged origin/main (6158e4c) per the PM's mid-task update, with a merge commit; no rebase or force. All gates were re-run on the merged head 20a1b5b."
    ],
    "line_budget": "n/a (no skills/** touched)",
    "open_questions": [
    {
    "question": "Locale across sessions. The pointer 5915156263 says the wizard must send the locale the template was downloaded in, or the current locale if the two cannot differ. Within one wizard session they cannot differ: both requests carry the same Accept-Language, and a language switch remounts MetadataProvider (key={language}), which unmounts the wizard. Across sessions they can: download in zh, switch the UI to en, import later. The server's workbook carries no machine-readable locale marker (buildImportTemplateWorkbook writes none), so the wizard cannot learn the download locale from the file. The upstream measurement says that case answers invalid_option.",
    "options": [
    "A: accept the current-locale reading as implemented. Both requests share one locale channel, asserted by a parity test.",
    "B: objectstack writes the locale into the template (a workbook property or defined name). objectui reads it on upload and sends it on the import. This is a cross-repo feature, plus a new import-request locale override in the adapter.",
    "C: objectstack's import door accepts option labels in every configured locale, not only the request's."
    ],
    "recommendation": "A for this card. Real business need: no named user has hit the cross-session case, and it was measured only upstream. Long-term: B or C belong in objectstack, the producer, not in a client-side guess such as remembering a download locale in localStorage. AI-proofing: A adds no lenient fallback. Startup scope: no expansion without named pull. Revisit only with evidence."
    },
    {
    "question": "For the at-tier contract review: should the template stay a dedicated optional DataSource.downloadImportTemplate(resource), or move to a template flag on ExportDownloadRequest?",
    "options": [
    "A: dedicated method (implemented). No illegal combinations, and it can be feature-detected.",
    "B: ExportDownloadRequest.template?: boolean. It mirrors the wire 1:1, but the type then admits combinations the server refuses."
    ],
    "recommendation": "A, because contract tightening beats a loud server 400: the illegal states cannot be written."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed — ImportWizardProps.fields[].options and the options passthrough in importTargetFields have no reader after this PR. The client template was the only one, and the card kept importTargetFields unchanged. Retire both together in a follow-up. Dedupe words: ImportWizardProps options, importTargetFields options, dead prop. In PR Acceptance notes.",
    "carrier: 承接者:无 · noted, not filed — the adapter's raw-fetch export path (exportDownload, and now downloadImportTemplate) does not send the client's X-Environment-Id, which client.data.import sends when an environment is set. No objectui host sets environmentId, so it is dormant. Dedupe words: X-Environment-Id, exportDownload, fetchImpl, environment header. In PR Acceptance notes."
    ]
    }


    Generated by Claude Code

  15. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    Contributor

    ✅ Seat ACCEPT of PR objectui#11556 at head 20a1b5b1f (dev report 5970004502). The at-tier contract review is owed before ready (Clause-②: yes)

    domain:ui seat 1 · session_01FjqrwXPfSMkSfkKYDSRkN2 · 2026-10-03T14:19Z. Reviewed against GitHub, not the report.

    Shape and surface

    • Draft, base main. The first line is Fixes #9600, with no other closing keyword. Clause-②: yes is at line start. The PR assignee is os-zhuang.
    • 20 files, +557 / −137, no governed path, no content/docs/releases/.
    • Claim 5969227398 amended here, in public. Two paths are beyond its declared surface, and both are accepted:
      • packages/types/src/data.ts: the optional DataSource.downloadImportTemplate(resource): Promise<Blob> the wizard feature-detects through. It is the published-surface widening the claim declared Clause-②: yes for.
      • packages/app-shell/src/views/identityImport.ts (and its test): downloadImportTemplate: undefined, so the identity wrapper withholds the generic template, as it nulls the other import surfaces it must not offer.
      • ObjectView.tsx is untouched: the offer gate sits in ImportWizard.

    The diff, read

    • Gone: buildImportTemplateCsv, exampleForField, firstOptionValue and downloadTextFile, as the card orders, and importTemplate.test.ts. Its autoMapColumns case moved to autoMapColumns.test.ts, so the coverage stays.
    • The request: ObjectStackAdapter.downloadImportTemplate sends only template=true on the export route through the shared fetchExportBlob, stamping Accept-Language from client.getLocale() when one is set.
    • The offer: the button shows when the data source has the method and usePermissions().can(object, 'create') holds, per the pointer 5924464752. A 405 shows the import's own not-allowed message, a 403 templateNotPermitted, and anything else templateDownloadFailed. No file is saved and there is no CSV fallback.
    • importTargetFields is unchanged. No dependency is added (@object-ui/permissions was already a plugin-grid dependency; check:phantom-deps OK).

    Changeset prose, checked sentence by sentence against the diff

    • minor for @object-ui/types and @object-ui/data-objectstack, which add surface; patch for the other three. ✓ (objectui's fixed group never takes major).
    • "lists only the columns this caller can import … dropdowns for select and boolean columns, and includes an instructions sheet": the server's behaviour per objectstack's pointer 5909839793 ✓.
    • "When the client has a locale set (getClient().setLocale), the request carries it as Accept-Language": the downloadImportTemplate body ✓. The clause "the way the import request does" is left for the contract review to test against @objectstack/client.
    • "A 405 … a 403 … and any other failure each show a message on the upload step, and no file is saved": templateDownloadErrorMessage ✓.
    • "the identity-import data source (sys_user) withholds downloadImportTemplate" ✓.

    The dev's open questions

    1. Locale across sessions: A, decided here (a validation-strategy call, not escalated).
      • Within one wizard session the two requests cannot differ: both stamp the same locale, and a language switch remounts the wizard.
      • Across sessions (download in zh, switch to en, import later) the import answers invalid_option: loudly, with no data loss.
      • Making it work needs objectstack to record the locale in the workbook, or to accept labels in every locale. That is a producer-side feature with no named user pulling it.
      • Under the four axes, A for this card. It goes to Acceptance notes, ⛔ not filed.
    2. A dedicated method vs a template flag on ExportDownloadRequest: for the contract review to judge.

    Out-of-scope (both already in the PR's Acceptance notes, ⛔ not filed)

    • ImportWizardProps.fields[].options and importTargetFields' options passthrough lose their last reader. Dead-prop cleanup with no reach.
    • The raw-fetch export path does not send X-Environment-Id. It is dormant: no objectui host sets an environment.

    Still owed before ready

    • The at-tier contract review on this head (dispatched now, isolated).
    • CI: 13 checks in progress at this read, 0 failed.

    Generated by Claude Code

  16. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    Contributor

    Contract review not run: the at-tier reviewer died on the tier's quota (HTTP 429) before reading anything · domain:ui seat 1 · session_01FjqrwXPfSMkSfkKYDSRkN2 · 2026-10-03T14:21Z.

    • PR objectui#11556 (head 20a1b5b1f) stays draft, outside the queue, carrying needs:contract-review. That is the safe state, ⛔ not a failure.
    • ⛔ The quota downgrade does not reach review: 「豁免对象是派发,复核 ⛔ 不随派发档位免除」. So no lower-tier review stands in for it, and the seat does not self-review.
    • Next: the seat re-dispatches the isolated at-tier reviewer on its next fire once the tier answers. The only bypass is the maintainer reviewing it in person.
    • No reset time came with the refusal. The seat's hourly Routine is the retry clock.

    Generated by Claude Code

  17. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    Contributor

    Landed and verified by content: closed completed by PR objectui#11556 · domain:ui seat 1 · session_01FjqrwXPfSMkSfkKYDSRkN2 · 2026-10-03T15:41Z.

    • The merge: PR objectui#11556 went through the merge queue to main as b253c4e28 at 2026-10-03T15:41Z, after the at-tier PASS 5970513578 on its head.

    • Content check on main b253c4e28 against its first parent d0c0c7fe9:

      main parent
      downloadImportTemplate in packages/types/src/data.ts 2 0
      async downloadImportTemplate in packages/data-objectstack/src/index.ts 1 0
      buildImportTemplateCsv in packages/plugin-grid/src/ImportWizard.tsx 0 3
      the control export const ImportWizard 1 1
    • No other card closed in that window.

    • Carried by this seat: the card's acceptance 3 (a downloaded template, filled in, re-imports clean) was not measured live. The contract record names it as a post-merge browser check against 17.6.0. It is on the seat post, ⛔ not a reason to reopen.

    • pm:dispatched is removed in the same act. domain:ui, area:records, the type and the priority stay.


    Generated by Claude Code

  18. added 2 commits that reference this issue on Oct 7, 2026
    b253c4e
    e5c7d07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions