Repository navigation
Commit b253c4e
fix(plugin-grid,data-objectstack,types): the import wizard's Download template requests the server's import template (#11556)
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](https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d0c0c7f commit b253c4e
20 files changed
Lines changed: 557 additions & 137 deletions
File tree
- .changeset
- packages
- app-shell/src/views
- __tests__
- data-objectstack/src
- i18n/src/locales
- plugin-grid/src
- __tests__
- types/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
255 | 255 | | |
256 | 256 | | |
257 | 257 | | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
258 | 266 | | |
259 | 267 | | |
260 | 268 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
245 | 245 | | |
246 | 246 | | |
247 | 247 | | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
248 | 253 | | |
249 | 254 | | |
250 | 255 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4913 | 4913 | | |
4914 | 4914 | | |
4915 | 4915 | | |
| 4916 | + | |
| 4917 | + | |
| 4918 | + | |
| 4919 | + | |
| 4920 | + | |
| 4921 | + | |
| 4922 | + | |
| 4923 | + | |
| 4924 | + | |
| 4925 | + | |
| 4926 | + | |
| 4927 | + | |
| 4928 | + | |
| 4929 | + | |
| 4930 | + | |
| 4931 | + | |
| 4932 | + | |
| 4933 | + | |
| 4934 | + | |
| 4935 | + | |
| 4936 | + | |
| 4937 | + | |
| 4938 | + | |
| 4939 | + | |
| 4940 | + | |
| 4941 | + | |
| 4942 | + | |
| 4943 | + | |
| 4944 | + | |
| 4945 | + | |
| 4946 | + | |
| 4947 | + | |
| 4948 | + | |
| 4949 | + | |
| 4950 | + | |
| 4951 | + | |
| 4952 | + | |
| 4953 | + | |
| 4954 | + | |
4916 | 4955 | | |
4917 | 4956 | | |
4918 | 4957 | | |
4919 | 4958 | | |
4920 | 4959 | | |
4921 | 4960 | | |
4922 | | - | |
| 4961 | + | |
4923 | 4962 | | |
4924 | 4963 | | |
4925 | 4964 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
566 | 566 | | |
567 | 567 | | |
568 | 568 | | |
569 | | - | |
| 569 | + | |
570 | 570 | | |
| 571 | + | |
| 572 | + | |
571 | 573 | | |
572 | 574 | | |
573 | 575 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
502 | 502 | | |
503 | 503 | | |
504 | 504 | | |
505 | | - | |
| 505 | + | |
506 | 506 | | |
| 507 | + | |
| 508 | + | |
507 | 509 | | |
508 | 510 | | |
509 | 511 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
708 | 708 | | |
709 | 709 | | |
710 | 710 | | |
711 | | - | |
| 711 | + | |
712 | 712 | | |
| 713 | + | |
| 714 | + | |
713 | 715 | | |
714 | 716 | | |
715 | 717 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
519 | 519 | | |
520 | 520 | | |
521 | 521 | | |
522 | | - | |
| 522 | + | |
523 | 523 | | |
| 524 | + | |
| 525 | + | |
524 | 526 | | |
525 | 527 | | |
526 | 528 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
515 | 515 | | |
516 | 516 | | |
517 | 517 | | |
518 | | - | |
| 518 | + | |
519 | 519 | | |
| 520 | + | |
| 521 | + | |
520 | 522 | | |
521 | 523 | | |
522 | 524 | | |
| |||
0 commit comments