Skip to content

Commit b253c4e

Browse files
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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/data-objectstack': minor
4+
'@object-ui/plugin-grid': patch
5+
'@object-ui/i18n': patch
6+
'@object-ui/app-shell': patch
7+
---
8+
9+
The import wizard's "Download template" button now downloads the server's import template, an Excel workbook, instead of building a CSV of every field (objectui#9600).
10+
11+
The old CSV listed every field the wizard was handed, including the system and read-only columns every object carries (created by, owner, and so on). The server strips those columns on import without an error, so a user who filled them in lost that data silently. The server's template (`GET /api/v1/data/:object/export?template=true`, objectstack 17.6.0) lists only the columns this caller can import. It marks required ones with `*`, adds dropdowns for select and boolean columns, and includes an instructions sheet.
12+
13+
- **`@object-ui/types`: new optional `DataSource.downloadImportTemplate(resource)`**, resolving to the template file as a `Blob`. Additive. When a data source lacks it, no template is offered: there is no client-side fallback.
14+
- **`@object-ui/data-objectstack`: `ObjectStackAdapter.downloadImportTemplate(resource)`** sends `template=true` on the export route, on the same request path as `exportDownload`. When the client has a locale set (`getClient().setLocale`), the request carries it as `Accept-Language`, the way the import request does. The template's labels are written in the request's locale, and the import accepts a translated label only in its own request's locale, so the two requests must match.
15+
- **`@object-ui/plugin-grid`: `ImportWizard` offers the template** when its data source has `downloadImportTemplate` and the user can create records of the object (`usePermissions().can(object, 'create')`, the server's own gate). A 405 (object not open for import), a 403 (no create permission) and any other failure each show a message on the upload step, and no file is saved. The client-built CSV is deleted, with its helpers (`buildImportTemplateCsv`, `exampleForField`, `firstOptionValue`, `downloadTextFile`). The column mapping step is unchanged.
16+
- **`@object-ui/app-shell`**: the identity-import data source (`sys_user`) withholds `downloadImportTemplate`, because the server's template describes the generic import door, not the identity pipeline.
17+
- **`@object-ui/i18n`**: `grid.import.downloadTemplateHint` describes the Excel template in all ten packs, and `grid.import.templateDownloadFailed` and `grid.import.templateNotPermitted` are new.

‎packages/app-shell/src/views/__tests__/identityImport.test.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,14 @@ describe('createIdentityImportDataSource — saved mappings are expressed, not i
255255
expect(typeof ds[withheld]).not.toBe('function');
256256
}
257257
});
258+
259+
it('withholds the generic import template even when the base carries it as an own property (objectui#9600)', () => {
260+
// A plain-object base: the spread WOULD copy an own method across, so this
261+
// arm is the one that fails without the explicit `undefined`. The template
262+
// describes the object's generic import door, not the identity pipeline.
263+
const ds = wrap({ downloadImportTemplate: async () => new Blob(['x']) });
264+
expect(typeof ds.downloadImportTemplate).not.toBe('function');
265+
});
258266
});
259267

260268
/**

‎packages/app-shell/src/views/identityImport.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ export function createIdentityImportDataSource(opts: IdentityImportDataSourceOpt
245245
listImportJobs: undefined,
246246
cancelImportJob: undefined,
247247
undoImportJob: undefined,
248+
// The server's import template describes the GENERIC import door of the
249+
// object (objectui#9600), not the identity pipeline this wrapper writes
250+
// through, whose target set is curated (`identityImportFields`). The
251+
// wizard offers the template only when this method is a function.
252+
downloadImportTemplate: undefined,
248253
// Saved mappings ARE offered for identity import — director-seat ruling,
249254
// decision batch #68 (objectui#7740, comment 5565349507; ledger on
250255
// objectstack#12708). Importing users benefits from saved field mappings
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* `ObjectStackAdapter.downloadImportTemplate` (objectui#9600) — the import
11+
* wizard's 「下载模板」 request: `GET /api/v1/data/:object/export?template=true`.
12+
*
13+
* Three things are pinned:
14+
* - the wire: the export route, `template=true` and nothing else (the server
15+
* refuses the export's row parameters on a template request);
16+
* - the locale: the server writes the template's labels in the request's
17+
* locale, and the import accepts a translated label only in its OWN
18+
* request's locale. So the template request must carry the same
19+
* `Accept-Language` the import request does. Asserted as parity against the
20+
* real `importRecords` request, through the same `fetch`, in both states of
21+
* the client locale;
22+
* - the refusals the server answers a template with: 403 `PERMISSION_DENIED`
23+
* (no create permission, ADR-0112 nested envelope) and 405
24+
* `OBJECT_API_METHOD_NOT_ALLOWED` (object not open for import, flat
25+
* dialect). Both reach the caller as `code` + `status`.
26+
*/
27+
28+
import { describe, it, expect, vi } from 'vitest';
29+
import { ObjectStackAdapter } from './index';
30+
31+
type FetchSpy = ReturnType<typeof vi.fn<(url: string, init: RequestInit) => Promise<Response>>>;
32+
33+
function makeDS(fetchImpl: FetchSpy, baseUrl = 'http://test.local') {
34+
const ds: any = new ObjectStackAdapter({ baseUrl, fetch: fetchImpl as any });
35+
ds.connected = true;
36+
ds.connectionState = 'connected';
37+
return ds;
38+
}
39+
40+
const xlsxResponse = () =>
41+
new Response('xlsx-bytes', {
42+
status: 200,
43+
headers: { 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
44+
});
45+
46+
/** Read one header off a recorded `init`, whatever shape its headers took. */
47+
function headerOf(init: RequestInit | undefined, name: string): string | null {
48+
return new Headers(init?.headers as HeadersInit | undefined).get(name);
49+
}
50+
51+
describe('ObjectStackAdapter.downloadImportTemplate — the wire', () => {
52+
it('GETs the export route with template=true and no other parameter', async () => {
53+
const fetchImpl: FetchSpy = vi.fn(async () => xlsxResponse());
54+
const ds = makeDS(fetchImpl);
55+
56+
const blob = await ds.downloadImportTemplate('task');
57+
58+
expect(blob).toBeInstanceOf(Blob);
59+
expect(await blob.text()).toBe('xlsx-bytes');
60+
expect(fetchImpl).toHaveBeenCalledTimes(1);
61+
const [url, init] = fetchImpl.mock.calls[0];
62+
const parsed = new URL(url);
63+
expect(parsed.pathname).toBe('/api/v1/data/task/export');
64+
expect([...parsed.searchParams.entries()]).toEqual([['template', 'true']]);
65+
expect(init.method).toBe('GET');
66+
expect(init.credentials).toBe('include');
67+
});
68+
69+
it('does not double the version segment when the base URL already ends in /api/v1', async () => {
70+
const fetchImpl: FetchSpy = vi.fn(async () => xlsxResponse());
71+
const ds = makeDS(fetchImpl, 'http://test.local/api/v1');
72+
73+
await ds.downloadImportTemplate('task');
74+
75+
expect(new URL(fetchImpl.mock.calls[0][0]).pathname).toBe('/api/v1/data/task/export');
76+
});
77+
78+
it('sends the bearer token like the export does', async () => {
79+
const fetchImpl: FetchSpy = vi.fn(async () => xlsxResponse());
80+
const ds: any = new ObjectStackAdapter({ baseUrl: 'http://test.local', token: 'tok', fetch: fetchImpl as any });
81+
ds.connected = true;
82+
83+
await ds.downloadImportTemplate('task');
84+
85+
expect(headerOf(fetchImpl.mock.calls[0][1], 'Authorization')).toBe('Bearer tok');
86+
});
87+
});
88+
89+
describe('ObjectStackAdapter.downloadImportTemplate — same locale as the import request', () => {
90+
/**
91+
* A fetch that answers both routes: the import's JSON result and the
92+
* template's bytes. Every call is recorded, so the two requests are read off
93+
* the same spy.
94+
*/
95+
function bothRoutes(): FetchSpy {
96+
return vi.fn(async (url: string) =>
97+
/\/import(\?|$)/.test(new URL(url).pathname)
98+
? new Response(JSON.stringify({ success: true, data: { total: 0, created: 0, updated: 0, skipped: 0, errors: 0, results: [] } }), {
99+
status: 200,
100+
headers: { 'Content-Type': 'application/json' },
101+
})
102+
: xlsxResponse(),
103+
);
104+
}
105+
106+
const callTo = (spy: FetchSpy, route: RegExp) => {
107+
const call = spy.mock.calls.find(([u]) => route.test(new URL(u).pathname));
108+
expect(call, `no request to ${route}`).toBeDefined();
109+
return call![1];
110+
};
111+
112+
it('carries the client locale on both requests when one is set', async () => {
113+
const fetchImpl = bothRoutes();
114+
const ds = makeDS(fetchImpl);
115+
ds.getClient().setLocale('zh-CN');
116+
117+
await ds.importRecords('task', { format: 'json', rows: [] });
118+
await ds.downloadImportTemplate('task');
119+
120+
const importLang = headerOf(callTo(fetchImpl, /\/task\/import$/), 'Accept-Language');
121+
const templateLang = headerOf(callTo(fetchImpl, /\/task\/export$/), 'Accept-Language');
122+
expect(importLang).toBe('zh-CN');
123+
expect(templateLang).toBe(importLang);
124+
});
125+
126+
it('carries none on either when no client locale is set, so the host fetch stamps both alike', async () => {
127+
const fetchImpl = bothRoutes();
128+
const ds = makeDS(fetchImpl);
129+
130+
await ds.importRecords('task', { format: 'json', rows: [] });
131+
await ds.downloadImportTemplate('task');
132+
133+
expect(headerOf(callTo(fetchImpl, /\/task\/import$/), 'Accept-Language')).toBeNull();
134+
expect(headerOf(callTo(fetchImpl, /\/task\/export$/), 'Accept-Language')).toBeNull();
135+
});
136+
});
137+
138+
describe('ObjectStackAdapter.downloadImportTemplate — refusals', () => {
139+
const failWith = (status: number, statusText: string, body: unknown) =>
140+
makeDS(
141+
vi.fn(
142+
async () =>
143+
new Response(JSON.stringify(body), {
144+
status,
145+
statusText,
146+
headers: { 'Content-Type': 'application/json' },
147+
}),
148+
),
149+
);
150+
151+
it('403: a caller without create gets PERMISSION_DENIED with its sentence', async () => {
152+
const ds = failWith(403, 'Forbidden', {
153+
success: false,
154+
error: {
155+
code: 'PERMISSION_DENIED',
156+
message: "Creating records on object 'task' is not permitted for this user, so its import template is not served",
157+
details: { object: 'task' },
158+
},
159+
});
160+
161+
await expect(ds.downloadImportTemplate('task')).rejects.toMatchObject({
162+
code: 'PERMISSION_DENIED',
163+
status: 403,
164+
message: "Creating records on object 'task' is not permitted for this user, so its import template is not served",
165+
});
166+
});
167+
168+
it('405: an object not open for import gets OBJECT_API_METHOD_NOT_ALLOWED', async () => {
169+
const ds = failWith(405, 'Method Not Allowed', {
170+
error: "API operation 'import' is not allowed on object 'task'",
171+
code: 'OBJECT_API_METHOD_NOT_ALLOWED',
172+
object: 'task',
173+
allowed: ['read'],
174+
});
175+
176+
await expect(ds.downloadImportTemplate('task')).rejects.toMatchObject({
177+
code: 'OBJECT_API_METHOD_NOT_ALLOWED',
178+
status: 405,
179+
message: "API operation 'import' is not allowed on object 'task'",
180+
});
181+
});
182+
});

‎packages/data-objectstack/src/index.ts‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4913,13 +4913,52 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
49134913
}
49144914
}
49154915

4916+
return this.fetchExportBlob(resource, queryParams);
4917+
}
4918+
4919+
/**
4920+
* Download the server's import template:
4921+
* `GET /api/v1/data/:object/export?template=true` (objectui#9600).
4922+
*
4923+
* Same route as {@link exportDownload}, with the server's template switch. It
4924+
* sends only `template=true`: the server refuses the export's row parameters
4925+
* on a template request, and its answer is always an xlsx workbook.
4926+
*
4927+
* The server writes the template's labels in the request's locale, and the
4928+
* import accepts a translated label only in its own request's locale. The
4929+
* import request goes through `@objectstack/client`, which stamps
4930+
* `Accept-Language` with the client's locale when one is set (and the host's
4931+
* `fetch` stamps its own otherwise). This request bypasses the client, so it
4932+
* stamps the client's locale the same way. Both requests then carry the same
4933+
* locale.
4934+
*/
4935+
async downloadImportTemplate(resource: string): Promise<Blob> {
4936+
const locale = this.client.getLocale();
4937+
return this.fetchExportBlob(
4938+
resource,
4939+
new URLSearchParams({ template: 'true' }),
4940+
locale ? { 'Accept-Language': locale } : undefined,
4941+
);
4942+
}
4943+
4944+
/**
4945+
* GET `/api/v1/data/:object/export` with the given query and answer the body
4946+
* as a Blob. A failure throws an `Error` that carries the server's `status`
4947+
* and its ADR-0112 `code`. The one request path {@link exportDownload} and
4948+
* {@link downloadImportTemplate} share.
4949+
*/
4950+
private async fetchExportBlob(
4951+
resource: string,
4952+
queryParams: URLSearchParams,
4953+
extraHeaders?: Record<string, string>,
4954+
): Promise<Blob> {
49164955
const baseUrl = this.baseUrl.replace(/\/$/, '');
49174956
// Avoid doubling /api/v1 if baseUrl already includes the version suffix.
49184957
const hasApiVersionSuffix = /\/api\/v\d+$/i.test(baseUrl);
49194958
const dataPath = hasApiVersionSuffix ? '/data' : '/api/v1/data';
49204959
const url = `${baseUrl}${dataPath}/${encodeURIComponent(resource)}/export?${queryParams.toString()}`;
49214960

4922-
const headers: Record<string, string> = { ...this.getAuthHeaders() };
4961+
const headers: Record<string, string> = { ...this.getAuthHeaders(), ...extraHeaders };
49234962
// `credentials: 'include'` carries the session cookie for the browser
49244963
// console (which authenticates by cookie, not a bearer token).
49254964
const res = await this.fetchImpl(url, { method: 'GET', headers, credentials: 'include' });

‎packages/i18n/src/locales/ar.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,8 +566,10 @@ const ar = {
566566
},
567567
import: {
568568
downloadTemplate: "تنزيل القالب",
569-
downloadTemplateHint: "احصل على ملف CSV بالأعمدة الصحيحة (الحقول المطلوبة معلَّمة بـ *).",
569+
downloadTemplateHint: "احصل على ملف Excel بالأعمدة التي يمكنك استيرادها (الحقول المطلوبة معلَّمة بـ *) مع تعليمات التعبئة.",
570570
templateFileName: "{{object}}-import-template",
571+
templateDownloadFailed: "تعذّر تنزيل القالب. يرجى المحاولة مرة أخرى.",
572+
templateNotPermitted: "ليست لديك صلاحية إنشاء سجلات لهذا الكائن، لذا فإن قالب الاستيراد غير متاح.",
571573
autoMatched: "تمت المطابقة تلقائيًا",
572574
autoMatchedSummary: "تمت مطابقة {{count}} عمودًا تلقائيًا — راجعها وعدّلها أدناه.",
573575
autoMatchedSummary_zero: "تمت مطابقة {{count}} عمود تلقائيًا — راجِع وعدِّل أدناه.",

‎packages/i18n/src/locales/de.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,8 +502,10 @@ const de = {
502502
},
503503
import: {
504504
downloadTemplate: "Vorlage herunterladen",
505-
downloadTemplateHint: "Laden Sie eine CSV mit den richtigen Spalten herunter (Pflichtfelder sind mit * markiert).",
505+
downloadTemplateHint: "Laden Sie eine Excel-Datei mit den importierbaren Spalten herunter (Pflichtfelder sind mit * markiert), samt Ausfüllhinweisen.",
506506
templateFileName: "{{object}}-import-template",
507+
templateDownloadFailed: "Die Vorlage konnte nicht heruntergeladen werden. Bitte versuchen Sie es erneut.",
508+
templateNotPermitted: "Sie sind nicht berechtigt, Datensätze dieses Objekts zu erstellen, daher ist die Importvorlage nicht verfügbar.",
507509
autoMatched: "Automatisch zugeordnet",
508510
autoMatchedSummary: "{{count}} Spalte(n) automatisch zugeordnet — unten prüfen und anpassen.",
509511
autoMatchedSummary_one: "{{count}} Spalte automatisch zugeordnet — unten prüfen und anpassen.",

‎packages/i18n/src/locales/en.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,8 +708,10 @@ const en = {
708708
dragDrop: 'Drag & drop a CSV or Excel file here, or click to browse',
709709
browseFiles: 'Browse Files',
710710
downloadTemplate: 'Download template',
711-
downloadTemplateHint: 'Get a CSV with the right columns (required fields marked *).',
711+
downloadTemplateHint: 'Get an Excel file of the columns you can import (required fields marked *), with instructions for filling it in.',
712712
templateFileName: '{{object}}-import-template',
713+
templateDownloadFailed: 'Could not download the template. Please try again.',
714+
templateNotPermitted: 'You do not have permission to create records of this object, so its import template is not available.',
713715
parsing: 'Parsing…',
714716
pasteHint: 'or paste (Ctrl/⌘+V) rows copied from Excel or Google Sheets',
715717
legacyXls: 'Legacy .xls files aren\'t supported — please re-save as .xlsx.',

‎packages/i18n/src/locales/es.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,8 +519,10 @@ const es = {
519519
},
520520
import: {
521521
downloadTemplate: "Descargar plantilla",
522-
downloadTemplateHint: "Obtenga un CSV con las columnas correctas (los campos obligatorios llevan *).",
522+
downloadTemplateHint: "Obtenga un archivo de Excel con las columnas que puede importar (los campos obligatorios llevan *) e instrucciones para rellenarlo.",
523523
templateFileName: "{{object}}-import-template",
524+
templateDownloadFailed: "No se pudo descargar la plantilla. Inténtelo de nuevo.",
525+
templateNotPermitted: "No tiene permiso para crear registros de este objeto, por lo que su plantilla de importación no está disponible.",
524526
autoMatched: "Emparejado automáticamente",
525527
autoMatchedSummary: "{{count}} columna(s) emparejada(s) automáticamente: revise y ajuste abajo.",
526528
autoMatchedSummary_one: "{{count}} columna emparejada automáticamente: revise y ajuste abajo.",

‎packages/i18n/src/locales/fr.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,8 +515,10 @@ const fr = {
515515
},
516516
import: {
517517
downloadTemplate: "Télécharger le modèle",
518-
downloadTemplateHint: "Obtenez un CSV avec les bonnes colonnes (les champs obligatoires sont marqués d'un *).",
518+
downloadTemplateHint: "Obtenez un fichier Excel des colonnes importables (les champs obligatoires sont marqués d'un *), avec des instructions de saisie.",
519519
templateFileName: "{{object}}-import-template",
520+
templateDownloadFailed: "Impossible de télécharger le modèle. Veuillez réessayer.",
521+
templateNotPermitted: "Vous n'êtes pas autorisé à créer des enregistrements de cet objet, son modèle d'import n'est donc pas disponible.",
520522
autoMatched: "Associé automatiquement",
521523
autoMatchedSummary: "{{count}} colonne(s) associée(s) automatiquement — vérifiez et ajustez ci-dessous.",
522524
autoMatchedSummary_one: "{{count}} colonne associée automatiquement — vérifiez et ajustez ci-dessous.",

0 commit comments

Comments
 (0)