useDocyrusDataImportWizard
One-call wiring of a Docyrus data source to the DataImportWizard — handles upload, analyse, mapping, preview, and import in a single guided flow.
Installation
pnpm dlx @docyrus/cli add @docyrus/hooks-use-docyrus-data-import-wizardpnpm add @docyrus/app-utils @docyrus/api-client @tanstack/react-queryThis hook is distributed as source. It needs an authenticated RestApiClient from @docyrus/api-client and a QueryClientProvider from @tanstack/react-query somewhere above your component tree.
Overview
useDocyrusDataImportWizard is the one-call entry point that wires a Docyrus data source to <DataImportWizard> and returns a ready-to-render wizard element plus an openWizard() trigger. The hook owns:
- Upload — POSTs the picked file to
…/import/uploadas multipartFormData.RestApiClientauto-detectsFormDataso no extra headers are needed. PassuploadFileto replace this phase where the API has no upload route (see Deployments without an upload route). - Analyse — calls
…/import/details?fileName=…to get the parsed columns, sample rows, and unique-field hints. - Auto-mapping — pre-fills source columns ↔ target fields by scoring slug, name, and token-Jaccard similarity. Required-field slugs and the four reserved slugs (
name,description,created_on,autonumber_id) are surfaced in dedicated groups. - Preview — renders the first N rows using
useDocyrusFieldComponent(field.type, 'value-renderer')so every Docyrus field type (relation, money, phone, date, enum, multi-select, …) shows up exactly the way it does in<DataGrid>. - uniqueData pre-resolution — for enum-like columns the hook resolves
matchedIdclient-side from the field's enum options before posting; relation lookups are left to the server. - Import — POSTs
{ fileName, options: { fieldMapping, fieldOptions, uniqueData, upsertUniqueFields } }and returns the result summary (success count, warnings, duplicates, errors) for the wizard's final step.
Backend connection
The hook calls three endpoints on the Docyrus API by default. You can override any of them via endpoints:
| Phase | Method | Endpoint | Purpose |
|---|---|---|---|
| Upload | POST | /v1/apps/:appSlug/data-sources/:dataSourceSlug/import/upload | Multipart file upload. Server stores it in tenant storage and returns fileName. Not implemented on every deployment — see below. |
| Analyse | GET | /v1/apps/:appSlug/data-sources/:dataSourceSlug/import/details?fileName=… | Parses the file, returns sample rows, columns, unique-field hints. Capped at 10 000 rows. |
| Import | POST | /v1/apps/:appSlug/data-sources/:dataSourceSlug/import | Body: { fileName, options }. Returns the import result (totalSuccessfulRecords, errors, duplicates). |
Deployments without an upload route
The upload row above is a contract, not a guarantee: an API can ship the analyse and import routes without an upload route. There the raw file is expected to already sit in tenant storage at tenant-{tenantNo}/tmp/import/{fileName}, and the analyse phase rebuilds that path from the tenant in the caller's token plus the fileName you hand it.
Put the file there yourself and pass uploadFile; analyse and import stay untouched:
const { openWizard, wizard } = useDocyrusDataImportWizard({
client,
appSlug,
dataSourceSlug,
fields: dataSource?.fields,
uploadFile: async (file) => {
const fileName = /* slugified base + original extension */;
await putIntoTenantStorage(`tenant-${tenantNo}/tmp/import/${fileName}`, file);
return { fileName, originalName: file.name, size: file.size, mimeType: file.type };
},
onImported: reload,
});Two things have to line up or analyse will not find the file: the fileName you return must be the last segment of the path you wrote to, and the tenant in the path must be the tenant the analyse call authenticates as.
Required fields are sourced from one of:
fieldsoption — pass the sameDataSourceField[]you already use withuseDocyrusDataGridto skip an extra round-trip.- Auto-load — when
fieldsis omitted the hook composesuseDocyrusDataViewSelectunder the hood and readsdataSource.fields.
Usage
'use client';
import { useDocyrusAuth } from '@docyrus/signin';
import { Button } from '@docyrus/ui/primitives/ui/button';
import { useDocyrusDataImportWizard } from '@docyrus/ui/library/hooks/use-docyrus-data-import-wizard';
export function ContactsToolbar({ onImported }: { onImported: () => void }) {
const { client } = useDocyrusAuth();
if (!client) return null;
const { openWizard, wizard } = useDocyrusDataImportWizard({
client,
appSlug: 'crm',
dataSourceSlug: 'contact',
requiredFieldSlugs: ['email'],
onImported: () => onImported()
});
return (
<>
<Button onClick={openWizard}>Import contacts…</Button>
{wizard}
</>
);
}Side-by-side with useDocyrusDataGrid
Both hooks accept the same client + appSlug + dataSourceSlug. Wire onImported to the grid's reload() so a successful import refreshes the visible rows:
const grid = useDocyrusDataGrid({ client, appSlug, dataSourceSlug });
const importer = useDocyrusDataImportWizard({
client,
appSlug,
dataSourceSlug,
fields: grid.fields, // skip duplicate metadata fetch
onImported: () => grid.reload()
});
return (
<>
{grid.toolbar}
<DataGrid table={grid.table} {...grid.gridProps} />
<Button onClick={importer.openWizard}>Import…</Button>
{importer.wizard}
</>
);Imperative usage (skip the dialog UI)
const importer = useDocyrusDataImportWizard({ client, appSlug, dataSourceSlug });
async function silentImport(file: File) {
const uploaded = await importer.uploadAsync(file);
await importer.analyseAsync(uploaded.fileName);
// …mutate `importer.mapping` via your own UI…
return importer.importAsync();
}API Reference
Parameters
| Option | Type | Default | Description |
|---|---|---|---|
client | RestApiClient | — | Authenticated client from @docyrus/api-client. Required. |
appSlug | string | — | Target app slug. Required. |
dataSourceSlug | string | — | Target data source slug. Required. |
appId | string | — | Optional multi-tenant isolation key. |
fields | Array<DocyrusFieldLike> | — | Pre-resolved target fields. When provided, the hook skips its internal data source fetch. |
requiredFieldSlugs | Array<string> | [] | Slugs that must be mapped before the user can leave the mapping step. |
uniqueFieldSlugs | Array<string> | from analyse response | Slugs covered by unique indexes — controls upsert availability. |
endpoints | { upload?: string; analyse?: string; import?: string } | derived | Override the three endpoints (e.g. for tenant-specific routing). |
uploadFile | (file: File) => Promise<UploadedFileInfo> | — | Replace the upload phase. Called instead of POSTing to the upload endpoint; the fileName it returns is what analyse and import receive. |
previewRowCount | number | 10 | Number of rows shown in the preview step. |
maxFileSizeBytes | number | 20 * 1024 * 1024 | Client-side size limit. |
acceptedExtensions | Array<string> | ['xlsx','xls','csv'] | Allowed file extensions. |
initialMapping | WizardMappingMap | — | Seed mapping that overrides auto-mapping after the first analyse. |
enabled | boolean | true | When false the hook returns wizard: null (use as a feature flag). |
open | boolean | — | Controlled open state. When omitted the hook owns the open flag. |
onOpenChange | (open: boolean) => void | — | Fires whenever the open state changes (controlled or not). |
onImported | (result: ImportResultPayload) => void | — | Called after a successful import. |
onError | (error: Error, phase: 'upload' | 'analyse' | 'import') => void | — | Called whenever any of the three phases throws. |
title / description | ReactNode | translated default | Override the wizard header copy. |
Return Value
| Property | Type | Description |
|---|---|---|
wizard | ReactElement | null | Render this once next to the trigger button — already wired. null when enabled === false. |
open | boolean | Current open state of the dialog. |
openWizard / closeWizard / resetWizard | () => void | Imperative dialog controls. |
step | ImportWizardStep | Current step. |
setStep | (step) => void | Move to a step programmatically. |
uploadAsync | (file: File) => Promise<UploadedFileInfo> | POST to the upload endpoint. |
analyseAsync | (fileName: string) => Promise<AnalysedFile> | GET the analyse endpoint. |
importAsync | () => Promise<ImportResultPayload> | POST the import endpoint using the current mapping + options. |
uploadedFile / analysedFile / importResult | * | null | Latest payloads from each phase. |
fields | Array<DocyrusFieldLike> | Resolved target fields (from fields prop or useDocyrusDataViewSelect). |
isUploading / isAnalysing / isImporting | boolean | TanStack Query mutation pending flags. |
error | Error | null | First error from any of the three phases. |
Field-Type Awareness
The wizard uses field metadata in three places:
| Surface | Mechanism |
|---|---|
Mapping step — per-type config (relation reference_key, phone country code, date format, money/phone companion column) | Direct switch on field.type in the dumb <DataImportWizard> component. |
| Preview step — every cell rendered with the right Docyrus widget | useDocyrusFieldComponent(field.type, 'value-renderer'). |
uniqueData pre-resolution — slugs values to matchedId for enum-like fields | Reads field.enums / field.options client-side. Relation lookups are deferred to the server. |
Error Handling
- File-too-large, wrong-extension, and empty-file errors fire client-side and surface as inline banners on the upload step (no network round-trip).
- Network errors are stored on each mutation's
errorand shown in the same banner;onError(err, phase)fires for telemetry. - Partial server-side errors come back in
importResult.error[]— rendered as a per-row error list on the result step. - The wizard locks the close button (
preventOutsideClose) while any phase is in flight so the user can't accidentally drop the upload mid-stream.
useDocyrusDataGrid
One-call wiring of a Docyrus data source to a fully configured DataGrid + toolbar (DataGridViewSelect, search, filters, group, sort, row height, display) — including row fetching with view-derived query parameters.
useDocyrusDataSourceJsonSchema
Generate a JSON Schema (object schema, properties keyed by field slug) from a Docyrus data source field list.