# Import review

Check rows before they land: how many are ready, which need fixing and why, grouped by kind of problem. A suggested fix can be accepted per row or for the whole group. Loading stays locked while problems remain, unless the person chooses to leave those rows out — and it says how many that is.

- Registry name: `import-review` (`@candor/import-review`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/import-review

## Install

```bash
npx candor-ui add import-review
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/import-review`.
It brings in: button, collapsible, data, locale, locked, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { ImportReview } from "@/components/import-review"
import type { ImportIssue } from "@/lib/data"

const found: ImportIssue[] = [
  { row: 14, field: "Repository", kind: "unknown-value", message: "Not one of acme/api, acme/web, acme/workers", value: "acme/website", suggestion: "acme/web" },
  { row: 22, field: "Repository", kind: "unknown-value", message: "Not one of acme/api, acme/web, acme/workers", value: "API", suggestion: "acme/api" },
  { row: 3, field: "Owner", kind: "invalid", message: "No active user with this email", value: "jonas@acme", suggestion: "jonas@acme.example" },
  { row: 3, field: "Rollout", kind: "invalid", message: "Not a number", value: "n/a" },
  { row: 88, field: "Flag key", kind: "duplicate", message: "Same key as an existing flag", value: "dark-mode" },
  { row: 140, field: "Flag key", kind: "missing", message: "Required", value: "" },
]

export default function Example() {
  const [issues, setIssues] = React.useState(found)
  const [loading, setLoading] = React.useState(false)
  return (
    <ImportReview
      className="max-w-2xl"
      total={214}
      issues={issues}
      target="flags in Staging"
      loading={loading}
      onFix={(fixes) => setIssues((all) => all.filter((i) => !fixes.includes(i)))}
      onLoad={() => setLoading(true)}
    />
  )
}
```

## <ImportReview>

Props:

- `total: number`: How many rows the file has.
- `issues: ImportIssue[]`: Problems found, one per row and field, with a suggested fix where there is one.
- `target: string`: Where rows go, e.g. "Customers in Shop staging".
- `onFix: (fixes: ImportIssue[]) => void`: Called with the issues whose suggested fixes a person accepted, one row or a whole group.
- `onLoad: (skipRows: number[]) => void`: Called on Load with the row numbers to leave out; empty unless the person chose to skip rows with problems.
- `loading?: boolean | undefined` (default `false`): Rows are being loaded; the Load button is disabled.
- `className?: string | undefined`

## Strings (`importReview`)

The fixed words, English by default. Override any of them under `importReview` in `<LocaleProvider strings={{ importReview: { … } }}>`; the table is exported as `importReviewStrings`.

- `reviewRows`: Review rows
- `ready`: Ready
- `needFixing`: Need fixing
- `total`: Total
- `allGood`: Every row checks out.
- `kindInvalid`: Not valid
- `kindMissing`: Missing a required value
- `kindDuplicate`: Duplicates
- `kindTooLong`: Too long
- `kindUnknownValue`: Not an allowed value
- `applyFixes`: { one: "Apply {count} fix", other: "Apply {count} fixes" }
- `row`: row {row}
- `blank`: (blank)
- `apply`: Apply
- `more`: and {count} more
- `locked`: { one: "{count} row needs fixing, or choose to leave them out.", other: "{count} rows need fixing, or choose to leave them out." }
- `leaveOut`: { one: "Leave out the {count} row with problems", other: "Leave out the {count} rows with problems" }
- `load`: { one: "Load {count} row into {target}", other: "Load {count} rows into {target}" }

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
