Hooks

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-wizard
Required Packages(3 packages)
pnpm add @docyrus/app-utils @docyrus/api-client @tanstack/react-query

This 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/upload as multipart FormData. RestApiClient auto-detects FormData so no extra headers are needed. Pass uploadFile to 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 matchedId client-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:

PhaseMethodEndpointPurpose
UploadPOST/v1/apps/:appSlug/data-sources/:dataSourceSlug/import/uploadMultipart file upload. Server stores it in tenant storage and returns fileName. Not implemented on every deployment — see below.
AnalyseGET/v1/apps/:appSlug/data-sources/:dataSourceSlug/import/details?fileName=…Parses the file, returns sample rows, columns, unique-field hints. Capped at 10 000 rows.
ImportPOST/v1/apps/:appSlug/data-sources/:dataSourceSlug/importBody: { 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:

  1. fields option — pass the same DataSourceField[] you already use with useDocyrusDataGrid to skip an extra round-trip.
  2. Auto-load — when fields is omitted the hook composes useDocyrusDataViewSelect under the hood and reads dataSource.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

OptionTypeDefaultDescription
clientRestApiClient—Authenticated client from @docyrus/api-client. Required.
appSlugstring—Target app slug. Required.
dataSourceSlugstring—Target data source slug. Required.
appIdstring—Optional multi-tenant isolation key.
fieldsArray<DocyrusFieldLike>—Pre-resolved target fields. When provided, the hook skips its internal data source fetch.
requiredFieldSlugsArray<string>[]Slugs that must be mapped before the user can leave the mapping step.
uniqueFieldSlugsArray<string>from analyse responseSlugs covered by unique indexes — controls upsert availability.
endpoints{ upload?: string; analyse?: string; import?: string }derivedOverride 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.
previewRowCountnumber10Number of rows shown in the preview step.
maxFileSizeBytesnumber20 * 1024 * 1024Client-side size limit.
acceptedExtensionsArray<string>['xlsx','xls','csv']Allowed file extensions.
initialMappingWizardMappingMap—Seed mapping that overrides auto-mapping after the first analyse.
enabledbooleantrueWhen false the hook returns wizard: null (use as a feature flag).
openboolean—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 / descriptionReactNodetranslated defaultOverride the wizard header copy.

Return Value

PropertyTypeDescription
wizardReactElement | nullRender this once next to the trigger button — already wired. null when enabled === false.
openbooleanCurrent open state of the dialog.
openWizard / closeWizard / resetWizard() => voidImperative dialog controls.
stepImportWizardStepCurrent step.
setStep(step) => voidMove 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* | nullLatest payloads from each phase.
fieldsArray<DocyrusFieldLike>Resolved target fields (from fields prop or useDocyrusDataViewSelect).
isUploading / isAnalysing / isImportingbooleanTanStack Query mutation pending flags.
errorError | nullFirst error from any of the three phases.

Field-Type Awareness

The wizard uses field metadata in three places:

SurfaceMechanism
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 widgetuseDocyrusFieldComponent(field.type, 'value-renderer').
uniqueData pre-resolution — slugs values to matchedId for enum-like fieldsReads 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 error and 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.

On this page