Skip to content

fix(plugin-grid,data-objectstack,types): the import wizard's Download template requests the server's import template - #11556

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-9600-import-template-endpoint
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-9600-import-template-endpoint

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #9600
Clause-②: yes

The import wizard's 「下载模板」 (Download template) button now downloads the server's import template (GET /api/v1/data/:object/export?template=true, an xlsx workbook) instead of building a CSV of every field it was handed. The CSV listed system and read-only columns that the server strips on import, so users filled in columns that were then dropped without an error. The server template lists only the columns this caller can import, marks required ones with *, and adds dropdowns and an instructions sheet. Net code in the wizard goes down: four helpers and their test file are deleted.

Published surface, for the contract review

Added

  • @object-ui/types: optional member DataSource.downloadImportTemplate(resource: string), returning a Promise that resolves to a Blob. Its doc says when to offer it (a user who can create records), that the server answers 405 / 403, and that an adapter must send it in the locale its importRecords uses. No client fallback: when it is absent, no template is offered.
  • @object-ui/data-objectstack: public method ObjectStackAdapter.downloadImportTemplate(resource). It reuses the export route's request path. A new private helper, fetchExportBlob, is now shared with exportDownload; it is not published, and exportDownload's wire is unchanged (exportDownload.test.ts passes as is).
  • @object-ui/i18n: keys grid.import.templateDownloadFailed and grid.import.templateNotPermitted, in all ten packs.

Changed

  • @object-ui/i18n: the value of grid.import.downloadTemplateHint in all ten packs. It said "CSV", which this change makes false.
  • @object-ui/plugin-grid: ImportWizard behaviour. It offers the button only when dataSource.downloadImportTemplate is a function and usePermissions().can(objectName, 'create') is true. Before, it offered the button whenever fields was non-empty. A failure shows a message inline on the upload step.
  • @object-ui/plugin-grid: the doc comment on ImportWizardProps.fields[].options no longer says that the template reads it. The member is kept; see Acceptance notes.

Deleted

  • @object-ui/plugin-grid, module-private (not exported from the package): buildImportTemplateCsv, exampleForField, firstOptionValue, downloadTextFile, and the __testables.buildImportTemplateCsv getter (__testables is internal). downloadTextFile had no other caller. A blob-saving downloadBlob takes its place for the server's response.

Unchanged: ImportWizardProps has no prop added or removed. The ImportWizard export and importTargetFields are unchanged, as the card requires, so the mapping step's targets, including the matchOnly ones, are untouched.

Route, and why (PM hypotheses, measured)

  1. Endpoint. Read in objectstack's rest server (readTemplateMode, answerImportTemplate). It refuses limit, page, filter, search, searchFields, orderby and header, and any format other than xlsx, on a template request. So the adapter sends template=true and nothing else.
  2. A flag or a method. The surface had to widen: the wizard cannot build URLs (adapters own fetch logic), and exportDownload writes only the query parameters it names. I chose a dedicated optional method over a template flag on ExportDownloadRequest. With a flag, the type would accept combinations the server refuses with a 400 (template plus filter, sort, limit or format: 'csv'). A method has no parameters to combine, and the wizard can feature-detect it.
  3. No @objectstack/client export used. Neither 17.5.0 nor 17.6.0 has a template request: 17.6.0's data.export has no template option. So the route did not change when main moved to 17.6.0 (objectui#11531 merged during this run). The request goes through the adapter's own fetchImpl, the same path exportDownload uses.
  4. Locale. The import request (importRecords, through client.data.import) gets its Accept-Language from the client's locale when one is set, and otherwise from the host's fetch. The console's createAuthenticatedFetch stamps Accept-Language from html lang. The adapter is built with that same fetch, and no objectui host sets a client locale. The template request goes through fetchImpl and does not pass through the client, so it now stamps client.getLocale() the way the client does. downloadImportTemplate.test.ts asserts that the two requests carry the same Accept-Language, with and without a client locale. Within one wizard session the two requests cannot differ: a language switch remounts the metadata subtree (MetadataProvider key={language}), and that unmounts the wizard. Across sessions they can differ; see the open question in the report.
  5. Offer gate. Who sees the button today: in ObjectView, the wizard opens only on objectCanImport (affordances.import and can(object, 'create')) or on the identity-import path. ExcelImportBar mounts it with no permission check. The gate now sits in the wizard, so every host gets it, through the same usePermissions().can that ObjectView reads. With no PermissionProvider, can() answers true and the server decides; this fail-open default matches every sibling gate. 405 shows grid.import.notAllowed, the message the import already shows for 405. 403 shows grid.import.templateNotPermitted. Anything else shows grid.import.templateDownloadFailed. In every case no file is saved.
  6. importTemplate.test.ts is deleted. Its autoMapColumns case tests live code: the server template also heads a required column with its label plus *. That case moves to autoMapColumns.test.ts, with a control case.
  7. No README or doc page names the CSV template (searched content/, docs/, the package READMEs and skills/). The only false sentence was the i18n hint, which is now updated.

Files beyond the claim's declared surface

  • packages/types/src/data.ts: the new DataSource member. The adapter method's contract lives on the interface the wizard feature-detects through; without it, the wizard would call an undeclared member.
  • packages/app-shell/src/views/identityImport.ts: one line, downloadImportTemplate: undefined, in the identity-import wrapper. The server's template describes the generic import door of the object, not the identity pipeline, whose targets are curated. The wrapper already nulls the job surfaces it must not offer. An ObjectStackAdapter base would not carry the method across the spread anyway, because the method is on the prototype. A plain-object base would carry it, and the new test case pins exactly that.
  • packages/app-shell/src/views/ObjectView.tsx is not touched. The gate moved into the wizard (point 5).

Deviations from the dispatch's suggested route

  • The filename is not taken from Content-Disposition. The method returns a Blob, the same shape as exportDownload. The wizard keeps its localized grid.import.templateFileName plus .xlsx, for example Contact-import-template.xlsx.
  • The failure is shown inline on the upload step, not as a toast. plugin-grid has no toast dependency, and the card says no new frontend dependencies.

Tests and gates (on 20a1b5b, the merge of origin/main 6158e4c93)

  • Build: turbo run build --filter='@object-ui/app-shell^...' exit 0, 28 tasks. The types dist carries downloadImportTemplate. plugin-grid type-checks code that reads the new member through the built @object-ui/types d.ts, so that d.ts is the rebuilt one.
  • Type-check exit 0: @object-ui/types, @object-ui/data-objectstack, @object-ui/plugin-grid, @object-ui/i18n, @object-ui/app-shell. --listFilesOnly shows the new test files in the type-check programs.
  • Tests: vitest run packages/data-objectstack/ packages/plugin-grid/ plus the three app-shell import files (identityImport.test.ts, identityImportSavedMapping.test.tsx, importTargetFields.test.ts): 262 files and 2774 tests passed. vitest run packages/i18n/ packages/types/: 425 files passed (10564 tests passed, 13 skipped). The full app-shell suite was not run locally and is declared to CI: the diff touches one app-shell line and its test.
  • One-time ablations through ablation-replace.mjs, each restored with blob equal to HEAD and git diff HEAD empty:
    • Removing the locale stamp turns exactly the locale-parity case red (1 failed, 6 passed).
    • Removing can(objectName, 'create') turns exactly the "not offered to a user who cannot create" case red (1 failed, 7 passed).
    • Removing the identity wrapper's downloadImportTemplate: undefined turns exactly its new case red (1 failed, 19 passed).
    • The first locale-stamp attempt was a no-op: the tool refused because the replacement text was not unique, nothing ran, and it was redone with a unique marker.
  • Gates exit 0: check:i18n-keys, check:i18n-drift (every changed en value followed by all nine packs), check:i18n-dead-keys (the two new keys are not candidates), check:unused-deps, check:control-bytes, check:new-line-citations (0 new), check:changeset-claims (two pending bodies name en.ts and data.ts; both paragraphs are about other keys and types and are still true), check:pending-changeset-literals, changeset:check, check-changeset-overwrite, check-changeset-presence, check-test-path-roots, check:phantom-deps, check:self-import, check:unreferenced-sources, the three vi-mock gates, check-type-check-coverage and check-lint-coverage.
  • Lint, a declared narrowing: eslint on the 18 changed .ts/.tsx files that still exist, with the same config pnpm lint uses. The JSON count is 18 files, 0 errors, 0 ignored. The config enables no type-aware linting (no parserOptions or projectService in eslint.config.js), so this diff cannot change a verdict on an untouched file.
  • NOT MEASURED:
    • check:readme-exports: prerequisite not met, because it needs every package's dist. No README or package barrel changed.
    • check-eager-locale-catalogues: prerequisite not met, because it needs the console build.
    • A live round trip against a 17.6.0 server: the server half was verified upstream, in objectstack#18386 acceptance 6.

Acceptance (card 验收 1–4)

  1. The columns the xlsx carries are the server's templateColumns, verified upstream. The wizard builds no columns.
  2. importTargetFields is untouched, so matchOnly targets stay mappable for upsert matching.
  3. The template request and the import request carry the same locale (asserted). A filled template's * headers map back to their fields (asserted). The live round trip is NOT MEASURED, as above.
  4. No new dependency: no package.json changed, and check:unused-deps and check:phantom-deps pass.

Acceptance notes (not filed)

  • ImportWizardProps.fields[].options, and the options that importTargetFields passes through, now have no reader. The template was their only reader, and the card keeps importTargetFields unchanged. Retiring both together is a follow-up. Carrier: none.
  • The adapter's raw-fetch export path (exportDownload, and now the template) does not send the client's X-Environment-Id, which client.data.import does when an environment is set. No objectui host sets one, so this is dormant. Carrier: none.

Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2


Generated by Claude Code

claude added 4 commits October 3, 2026 12:58
… button requests the server's import template

The upload step's "Download template" built a CSV of every field it was
handed, system and read-only columns included, which the server strips on
import. The template is now the server's own
(GET /data/:object/export?template=true), so the client-built copy is gone.

- types: DataSource gains the optional downloadImportTemplate(resource).
- data-objectstack: ObjectStackAdapter implements it on the export route's
  request path (shared with exportDownload) and stamps the client locale the
  way the import request does, so both carry the same Accept-Language.
- plugin-grid: buildImportTemplateCsv, exampleForField, firstOptionValue and
  downloadTextFile are deleted; the button is offered when the data source
  can fetch the template and the user can create records of the object, and
  a 403 / 405 / other failure shows its own message with no fallback file.
  importTemplate.test.ts goes; its autoMapColumns case moves to
  autoMapColumns.test.ts.
- app-shell: the identity-import wrapper withholds the template explicitly.
- i18n: the hint no longer says CSV; two failure messages added.

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
…ll sites

The dead-key census could not see keys returned through a helper; reading
them through t() literals puts both under check:i18n-keys.

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
…mport-template-endpoint

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 2 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6661-app-launcher-nav-menu-renderers.md

  • names en.ts → packages/i18n/src/locales/en.ts — edited by this change

    Three new strings — the launcher's and the menu's accessible names, and the menu's empty state — are declared under console.nav in en.ts and its nine sibling packs. An inline defaultValue alone is not a fix: it renders English at one call site and leaves the string untranslatable everywhere (objectui#3517).

.changeset/9309-object-gallery-filter-destination-typed.md

  • names data.ts → packages/types/src/data.ts — edited by this change

    It was filter?: unknown under the docblock "Query filter, forwarded verbatim as $filter", and $filter is RecordANGLE-BRACKETS(string, any) | FilterArray (QueryParams, data.ts). The declaration therefore named a destination it did not type: an author told to forward the value verbatim got a type error on the key the sentence had just told them to forward, and the way through was as — which un-checks the destination's real type at that call site too. The declaration is now an INDEXED ACCESS on QueryParams, not a copy of its arms, so the two cannot drift.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with 7121221fa (merge-base with origin/main): 19 file(s) changed outside .changeset/, read against 2077 pending declaration(s) that publish a body (2712 pending in total). · run

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3307.7 KB 3330.4 KB
Main entry chunk (gzip) 150.4 KB 350 KB
Entry file index-CG7y86UC.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
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) 570.92KB 136.80KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 232.57KB 64.51KB
fields (index.js) 261.51KB 66.22KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.49KB 9.23KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 39.47KB 11.25KB
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.75KB
mobile (index.js) 1.99KB 0.87KB
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) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 83.60KB 22.89KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 142.26KB 38.44KB
plugin-designer (index.js) 231.28KB 48.76KB
plugin-detail (index.js) 245.43KB 64.53KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.73KB 44.50KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 234.35KB 64.40KB
plugin-kanban (index.js) 49.59KB 15.58KB
plugin-list (index.js) 116.55KB 28.95KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.16KB 1.96KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.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 (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.07KB 2.39KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 20a1b5b1f2ae8e01e86cc1f129f7ee3ba5e1cb14
Local-runs: none

Read-only, at the tier: card objectui#9600 (body and all twelve comments, the seat ACCEPT 5970026643 treated as a claim), PR objectui#11556 (body, 20-file list, net diff), the 43 check-runs on the head, objectui origin/main through git show (the base 6158e4c93 is an ancestor), and the published @objectstack/client 17.6.0 tarball, which the lockfile at main resolves. Nothing built, run or re-run. Reviewed 2026-10-03T15:19Z.

① Derived judgments

Published surface, each change named and judged:

  1. @object-ui/types: DataSource.downloadImportTemplate?(resource): Promise of Blob, optional. RIGHT. Additive; it sits beside exportDownload?, importRecords? and createImportJob?, which are optional in exactly this shape, and every existing implementer still compiles (Type Check green). Its doc states the 405 / 403 contract and the locale rule the server imposes; both match the objectstack pointers 5909839793 and 5924464752.
  2. @object-ui/data-objectstack: public ObjectStackAdapter.downloadImportTemplate(resource). RIGHT. The wire is GET .../data/:object/export?template=true and nothing else, which is what the server accepts on a template request. The private fetchExportBlob is the old exportDownload tail moved verbatim: URL assembly, credentials: 'include', the ADR-0112 code + status on the thrown Error. The only delta is ...extraHeaders, which is undefined on the export path, so exportDownload's wire is byte-equal to main and its unchanged test holds. Not published: fetchExportBlob is private.
  3. The locale stamp. RIGHT, and the changeset's clause "the way the import request does" is verified against the published client, not the report: @objectstack/client 17.6.0 fetch() writes Accept-Language from this.locale when one is set and the caller did not, and data.import goes through that fetch(). The adapter's template request bypasses the client, so stamping client.getLocale() reproduces the rule. With no client locale, neither request carries the header and the host fetch (the console's createAuthenticatedFetch, seeded from html lang) stamps both alike. Parity is pinned in both states by downloadImportTemplate.test.ts.
  4. No @objectstack/client route exists for this. RIGHT. 17.6.0's data.export options are format | limit | filter | orderby | header; no template. The adapter's own fetchImpl path is the correct seam, and the 17.6.0 move on main did not change the route.
  5. @object-ui/plugin-grid: the offer gate moves into ImportWizard (typeof dataSource.downloadImportTemplate === 'function' and usePermissions().can(object, 'create')). RIGHT. @object-ui/permissions is already a plugin-grid dependency at main; usePermissions() without a provider returns the frozen NO_PROVIDER_PERMISSIONS whose can is () => true, so the fail-open default is the package's own, not new. Every mount gets the gate: ObjectView.tsx, console/ai/ExcelImportBar.tsx (which mounts the wizard with no check of its own) and the demo. ImportWizardProps gains no member; dataSource is any at main, so the Partial of DataSource read is type-sound. The hook call is unconditional at the component's top level.
  6. Behaviour change in ImportWizard, named because the PR body names it: a host whose data source lacks the method no longer sees a template button, where before it got the client CSV whenever fields was non-empty. RIGHT per the card: the CSV was wrong for every object, the card orders the client generation deleted, and a second template definition is what the card forbids. No prop or export is removed.
  7. Deletions are module-private. RIGHT. buildImportTemplateCsv, exampleForField, firstOptionValue, downloadTextFile and the __testables getter: packages/plugin-grid/src/index.tsx at main exports ObjectGrid, VirtualGrid, ImportWizard and the two types only; __testables and IMPORT_DEFAULT_TRANSLATIONS are not in the barrel. Nothing leaves a published surface.
  8. Error mapping. RIGHT. 405 reuses isImportNotAllowed (code OBJECT_API_METHOD_NOT_ALLOWED or status 405) and the import's own grid.import.notAllowed; 403 (PERMISSION_DENIED or status 403) gets templateNotPermitted; the residual gets templateDownloadFailed. The adapter throws with code and status set, so both branches are reachable and are pinned.
  9. @object-ui/app-shell: downloadImportTemplate: undefined in the identity-import wrapper. RIGHT. The wrapper's convention at main is to spread the base and null the import surfaces it must not offer; a class base would not carry the prototype method but a plain-object base would, and the new test pins that arm.
  10. @object-ui/i18n. RIGHT. downloadTemplateHint said CSV, now false, so changing it is required, not optional; the two new keys land in all ten packs and in IMPORT_DEFAULT_TRANSLATIONS. No tracker number in any runtime string.
  11. Card scope held. importTargetFields.ts and ObjectView.tsx are not in the file list; matchOnly targets stay mappable. No package.json changed, so 验收 4 (no new dependency) holds mechanically. No doc or README at main names the CSV template, so the i18n hint was the only false prose.
  12. Test migration. RIGHT. importTemplate.test.ts goes with the code it tested; its one live case ( * header maps back) moves to autoMapColumns.test.ts with a control.
  13. Check-runs on the head: 40 success, 3 skipped (the coverage matrix rows and dependabot, inert for a non-dependabot PR), 0 failure. Type Check, Lint, Build and E2E, 8 test shards plus Test and Test (dist pins), the five Changeset gates, README Export Check, Governed Surface Queue Guard (no governed path): all green.

② Semver level

.changeset/9600-import-template-from-server.md: @object-ui/types minor, @object-ui/data-objectstack minor, @object-ui/plugin-grid patch, @object-ui/i18n patch, @object-ui/app-shell patch. RIGHT. The two packages that gain surface take minor; the three whose published surface is unchanged take patch (behaviour fix, new i18n keys, one withheld member). All five sit in objectui's single fixed group, so the release moves every package to minor together, and the Changeset Bump Policy, Declaration, Fixed Group, Overwrite Report and Claim Re-read checks are green. Not breaking, so no ADR-0087 marker is owed and none is written. The changeset body carries no model identifier.

Clause-②: yes (PR body, line start, no arm). RIGHT. The diff widens two published surfaces (item ① 1 and ① 2) and narrows none (item ① 7), so yes without the (narrowing) arm is the true declaration and minor is its floor. The claim 5969227398 declared yes for exactly this possibility and the seat amended the claim's file surface in public (5970026643) to cover types/src/data.ts and identityImport.ts.

Changeset prose checked against the diff and the published client: "objectstack 17.6.0" (the lockfile at main resolves @objectstack/client 17.6.0; the import-door judgement 8f784959cf is in that release per 5945986604); "the way the import request does" (① 3); the 405 / 403 / other sentence (① 8); the sys_user sentence (① 9); the i18n sentence (① 10). All true. The Claim Re-read's two pending bodies (6661 on console.nav strings in en.ts; 9309 on QueryParams $filter typing in data.ts) describe paragraphs this diff does not touch; both remain true.

③ Boundary flags

Dev deviations (report 5970004502), each answered:

  • types/src/data.ts beyond the claim: ACCEPTED (① 1); the seat amended the claim in public.
  • identityImport.ts and its test beyond the claim: ACCEPTED (① 9).
  • Gate in ImportWizard, ObjectView.tsx untouched: RIGHT (① 5); strictly wider coverage than the claim's placement, with ExcelImportBar now gated too.
  • Inline message instead of a toast: RIGHT; plugin-grid carries no toast dependency and 验收 4 forbids adding one.
  • Filename from grid.import.templateFileName plus .xlsx, not Content-Disposition: RIGHT; same shape as exportDownload, and the localized filename the i18n packs already carry stays meaningful.
  • Dedicated method over an ExportDownloadRequest.template flag (also open question 2): RIGHT, judged here. A flag would let the type admit template beside format: 'csv', filter, sort, limit or includeHeaders, every one of which the server answers 400; the method has no parameter to combine, and it is feature-detectable like its optional siblings. Option A stands.
  • Local app-shell suite narrowed to three files: ANSWERED by the head's check-runs (eight shards, Test, dist pins), all green.
  • Model-free commit trailer pair: RIGHT; it is objectui AGENTS.md's own rule (the objectui#9441 ruling block), and the head commit carries exactly that pair.
  • Merge of origin/main rather than rebase: FINE; the base is an ancestor of origin/main and every check-run is on the merged head.

Open questions, each answered:

  1. Locale across sessions. Option A, as the seat decided, and I concur rather than escalate. Within one wizard session the two requests cannot differ (one locale channel, pinned; MetadataProvider key={language} in ConsoleShell.tsx remounts the wizard on a switch). Across sessions a mismatch answers invalid_option per row, loudly, with no silent loss. Closing that gap needs objectstack to write a locale marker into the workbook or accept labels in every locale, a producer-side feature with no named pull; a client-side guess (remembering a download locale) would be the lenient fallback the card does not ask for. Carried in the PR's Acceptance notes with carrier none; NOT ESCALATED.
  2. Method vs flag. Answered above: A.

Out-of-scope findings, acknowledged, not filed: ImportWizardProps.fields[].options and the importTargetFields passthrough lose their last reader (the member is kept, so no surface narrows here; a follow-up retirement); the raw-fetch export path sends no X-Environment-Id (pre-existing on exportDownload, inherited by the new method, dormant because no objectui host sets an environment).

Residual the record names so the seat does not read more into PASS than it says: card 验收 3 (a filled template re-imports clean) is NOT MEASURED live in this PR. What this head pins is each half it depends on: the * header maps back (autoMapColumns.test.ts), the template and import requests carry one locale (downloadImportTemplate.test.ts), and the server's own round trip was verified upstream (objectstack#18386 acceptance 6). A browser round trip against 17.6.0 is the seat's post-merge dogfood item, not a contract gap.

Implemented-by: claude/issue-9600-import-template-endpoint
Reviewed-by: session_01FjqrwXPfSMkSfkKYDSRkN2

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

needs:contract-review removed: an at-tier PASS is on record for this head · domain:ui seat 1 · session_01FjqrwXPfSMkSfkKYDSRkN2 · 2026-10-03T15:23Z.

  • Record: 5970513578 on this PR. ## Contract review, Served-tier: CONTRACT_REVIEW_TIER, Head-sha: 20a1b5b1f2ae8e01e86cc1f129f7ee3ba5e1cb14, Local-runs: none, Implemented-by: claude/issue-9600-import-template-endpoint / Reviewed-by: session_01FjqrwXPfSMkSfkKYDSRkN2, PASS.
  • The PR head at this act is 20a1b5b1f, the same head the record judged.
  • Pre-landing checks:
    • ① the at-tier PASS is on record (above);
    • ② every check on the head is success or an expected skip (40 / 3: the coverage matrix rows and dependabot);
    • check-governed-merges --pr objectstack-ai/objectui#11556: NOT governed, 694 changed lines ≤ 5000.
  • Landing in this act: ready, then auto-merge, so it enters the merge queue.
  • Residual the record names: the card's acceptance 3 (a filled template re-imports clean) is not measured live in this PR. The seat carries a browser round trip against 17.6.0 as a post-merge item.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 15:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 15:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit b253c4e Oct 3, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-9600-import-template-endpoint branch October 3, 2026 15:41
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.

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

2 participants