# CAIRO-4230 — Import CSV: map a column to a *new* field (create-on-import)

> High-level implementation summary. Not a line-by-line plan — see the tickets for
> the full spec/design.
>
> - **CAIRO-4230** — Import CSV: Support "Add New Field" flow (this task)
> - **CAIRO-4131** — Suggest field type from column data (the design/visual source;
>   we only borrow the "create new field" mechanic from it, not the type detection)
> - Design: https://www.figma.com/design/RpB9iQ6a43xcvKiovmJaA1/Import?node-id=81-110873

## Goal

Today, an unmapped CSV column defaults to **"Don't import"** (`mappingByHeader[header] = null`).
We want to optionally let the user map a column to a **brand-new collection field that
is created as part of the import flow itself** — shown in the mapping dropdown as
`<Column Title> (New)` with the helper text *"This will be a new text field. Edit Field"*
(see image in CAIRO-4131).

## Scope (what we are building now vs. not)

**In scope**
- Ability to create a new field from an unmapped column during the mapping step.
- **Data-driven field-type detection** (CAIRO-4131 — done): the `(New)` suggestion's
  type is inferred from the column's data (number / date / boolean / url / text).
  See "Field type is detected from the column data" below.
- A way to **choose the default behavior** for unmapped columns (create new field vs.
  don't import). The **presence of the `updateSchema` callback prop itself is the
  opt-in** — no separate flag.

**Out of scope (deferred)**
- The "Edit Field" entry point that opens a type selector pre-populated with the
  detected type ([CAIRO-4398](https://wix.atlassian.net/browse/CAIRO-4398)), and the
  "+ Create New Field" dropdown footer ([CAIRO-4399](https://wix.atlassian.net/browse/CAIRO-4399)).

> Note: the **default-behavior prop is the deliverable the tickets call out explicitly**
> ("some entry points want this as default, some want 'don't import' as a default").

## Key design point — configurability via the callback prop

Field creation is exposed as a new **callback prop, a sibling to `bulkUploadItems`**:
`updateSchema`. The **presence of that callback is itself the opt-in**:

- `updateSchema` provided → unmapped columns default to **create new (text) field**.
- `updateSchema` absent → current behavior, unmapped columns default to **don't import**.

This avoids a separate `unmappedColumnDefault` flag: a consumer can only create fields
if it gives us the means to (the callback), so wanting the behavior and providing the
capability are the same decision. This is what `getDefaultMapping` keys off when
assigning the default for a column that didn't auto-match an existing field.

> If a consumer later needs "provide `updateSchema` but still default to don't import",
> we can add an explicit override flag then — but start with the implicit form.

## Data model changes (`packages/core/src/state/ImportState/ImportState.ts`)

Today a mapping value is `string | null` (existing field id, or skip). To represent
"create a new text field for this column" we need a third state. Options (pick during
implementation):

- Extend the mapping value to a discriminated shape, e.g.
  `{ kind: 'existing'; fieldId } | { kind: 'new'; fieldKey; fieldType: 'text' } | null`, or
- Keep `mappingByHeader` for existing-field mappings and add a parallel
  `newFieldByHeader` set/record for columns flagged as "create new".

Whichever is chosen, the following must be updated accordingly:
- `getDefaultMapping` — honor the new default-behavior prop for unmatched columns.
- `hasMappings` / `hasNoMappedData` / `selectedFieldIds` — a "create new" column
  counts as a real mapping.
- `mappedRows` / `toMappedRows` — rows must write column data under the new field's
  key/id so the values get uploaded.
- `setMapping` — needs a way to set a column to "create new field".

## New-field creation on import — single `updateSchema` call

New fields must exist in the collection before the rows upload, otherwise the
uploaded values have nowhere to land. Following the cm-import `beforeImport` flow
(`wix-data-client/cm-import/.../progress/actions.js`), **all new fields are added
in a single call** — not one call per field. The callback is modeled after
`bulkUploadItems` (consumer supplies the actual mutation; Cairo orchestrates):

```ts
interface NewFieldDefinition { key: string; type: 'text'; displayName: string }
type UpdateSchemaFn = (fields: NewFieldDefinition[]) => Promise<void>;
```

The consumer maps these to one schema mutation, e.g.
`updateSchema(collectionId, ...fields.map(f => addField(f.key, f)))`.

**Keys are decided by Cairo, not returned by the consumer.** `_resolveFieldMapping`
derives each new field's `key` from the column header (`uniqueFieldKey`, camelCase +
numeric suffix, deduped against existing field ids and other new keys), passes the
list to `updateSchema`, and uses that same key as the upload payload key. So no id
round-trip is needed and there's no per-field parallelism/collision concern.

In `startImport`, after backup and before `_uploadBatches`:
1. Collect all "create new" columns and derive their keys.
2. Call `updateSchema(newFields)` once.
3. Build the upload payload keyed by those keys (+ existing field ids).
4. Proceed with the existing batch upload.

If `updateSchema` throws, the import enters the **error state** with a "Try again"
CTA (no rows uploaded). Open questions: whether created fields should be rolled
back on a later failure/cancel (currently not — restore covers row data only).

## UI changes (`packages/cairo/src/components/ImportButton/ImportStepConfiguration.tsx`)

- Add a `<Column Title> (New)` option (or per-column toggle) to the mapping `Dropdown`.
- When a column is set to "create new", render the helper text
  *"This will be a new text field."* under the dropdown (the "Edit Field" link is
  out of scope for now — omit or render disabled).
- Match the design's warning/`(New)` visual treatment from CAIRO-4131 (yellow accent).

## Wiring (props path)

The new prop (`updateSchema`) follows the existing chain, exactly like `bulkUploadItems`:

`ImportButtonProps` / `ImportModalProps` (`ImportProps.tsx`)
→ `ImportButtonModalLayout.tsx`
→ `useCreateImport({ ... })`
→ `ImportStateProps` → `ImportState` constructor.

All new props should be **optional** and additive to keep the change backward-compatible
(core is widely consumed — see `packages/core/AGENTS.md`).

## Translations

New keys (route through the Wix Translation Center — core `en.json` and the generated
`translations.ts` are gitignored and rebuilt from TC, so the local additions made
during implementation are **not** committed and must be registered in TC):
- `cairo.import.newFieldSuffix` (core) → `"(New)"` — appended in code to the CSV
  column header to form the option label (e.g. `Title (New)`). The header is built
  in code (not via an interpolated key) so the column name always shows.
- `cairo.import.newFieldHelper` (core) → `"This will be a new {typeName} field."` —
  the `{typeName}` param is the field-type name, injected lowercased (matching the
  patterns-fields `type_mismatch_*` convention).
- `auto-patterns.fields.field_type_text_title` (**patterns-fields**) → `"Text"` —
  the shared field-type name reused for the type label. Future types reuse the
  existing `auto-patterns.fields.field_type_<type>_title` keys.

(`cairo.import.newFieldOption` = `"{column} (New)"` and `cairo.import.fieldType.text`
were earlier forms; superseded.)

### Field type is detected from the column data (CAIRO-4131 — done)

Detection is **split by layer and injected** (`detectColumnType` is a DI prop on
`ImportState`), because media detection needs a media-URI parser that core must
not depend on (`utils → core → cairo/fields`):

- **core** (`detectFieldType.ts`) owns the `NewFieldType` union
  (`text | number | date | boolean | url | image | video | document | audio`), the
  sampling + ⅔-majority vote (`detectColumnType(values, detectValue?)`), and the
  **primitive** per-cell detector (`detectValueType`: number / date / boolean /
  url, else text — ported from cm-import `detectFieldConfig` /
  `guessFieldConfigFromSamples`). This is the default when nothing is injected.
- **fields** (`utils/detect-field-type.ts`, exported as
  `@wix/patterns-fields/detect-field-type`) owns the **media-aware** detector:
  it composes a per-cell detector that recognizes Wix media URIs (image / video /
  document / audio, via the local `parseMediaItemUri` port) and external image
  URLs *before* delegating to core's primitive detector, then reuses core's
  `detectColumnType` sampling. No new runtime dep — `parseMediaItemUri` is the
  existing zero-dep port; the heavy `@wix/cm-media-utils` bundle is not pulled in.
- **cairo** (`ImportButtonModalLayout`) injects the fields detector into
  `ImportState` via `useCreateImport({ detectColumnType })`. So `@wix/patterns`
  gets full media-aware detection; bare-core consumers get primitive detection.
- The mapping carries `fieldType` end-to-end: `getDefaultMapping` /
  `setNewFieldMapping` set it from the injected `detectColumnType`, and
  `_resolveFieldMapping` passes it to `updateSchema`.
- The helper text resolves the type name from the **shared patterns-fields**
  field-type translations via `NEW_FIELD_TYPE_TITLE_KEY`, a
  `Record<NewFieldType, …>` in `ImportStepConfiguration` (keys
  `auto-patterns.fields.field_type_<type>_title`). Adding a `NewFieldType` makes
  the missing map entry a **compile error** — the one place to extend.
- Those translations live in the patterns-fields artifact (fields'
  `useTranslations()`), so the import modal is wrapped in `PatternsFieldsProvider`
  **only when field creation is enabled** (`ImportButtonModalLayout`).
- **Consumer follow-up:** the `updateSchema` consumer must map each
  `NewFieldType` to its source field type (auto-patterns `useImport` currently
  hardcodes `SHORT_TEXT`; the CMS reverse map needs `URL` / `IMAGE` / `VIDEO` /
  `DOCUMENT` / `AUDIO`). Gated on the new `@wix/patterns` preview being published.

## Testing

- Core/cairo: `Table.importButton.spec` — default mapping under updateSchema
  present/absent, the single `updateSchema` call with all new fields, upload payload
  keyed by derived keys, helper text, and schema-update failure → error state.
- Follow existing Import test patterns (`InMemoryBackend`, `flush()`).

## Decisions made during implementation
- **Mapping model:** `mappingByHeader` value is now a discriminated `ColumnMapping`
  = `{kind:'existing', fieldId} | {kind:'new', fieldType:'text'} | null`.
- **Field creation:** runs in `startImport` **after** backup and **before** the batch
  upload, via `_resolveFieldMapping()`. All "new" columns are added in **one**
  `updateSchema(fields)` call; their keys are Cairo-derived (`uniqueFieldKey`) and
  reused as the upload payload keys — no id round-trip.
- **Failure handling:** if `updateSchema` throws, the import enters the **error state**
  with a "Try again" CTA (same path as a first-batch upload failure); no rows upload.
- **UI:** a synthetic `(New)` option is appended to each column dropdown when
  `canCreateNewField`; selecting it shows the helper text. Clearing still means
  "don't import" (`null`).

## Still open (consumer/CMS side)
- Whether created fields should be rolled back on cancel/restore (currently they are
  not — restore only covers row data).
- **Consumer wiring:** `auto-patterns` `useImport` must implement `updateSchema`
  (one batched schema mutation, e.g. `updateSchema(collectionId, ...addField(...))`),
  replacing the earlier per-field `createField`/`addField` path — pending a new
  `@wix/patterns` publish.

## AGENTS.md impact

- Status: not needed (feature work inside an existing component/state pattern).
