ContactMerge
Merge duplicate contacts by choosing a survivor, then choosing which value wins for every field that differs, with a live result summary and a confirmation step.
Code
<ContactMerge records={[ { id: "a", name: "Sara Ali", values: { email: "[email protected]", phone: "0501" } }, { id: "b", name: "Sara A.", values: { email: "[email protected]", phone: "0501" } }, ]} fields={[{ id: "email", label: "Email", ltr: true }, { id: "phone", label: "Phone", ltr: true }]} onMerge={async (outcome) => merge(outcome)}/>CRM · beta
Live examples and controls: ContactMerge in the lab.
Install
npx shadcn@latest add https://docs.nasaqui.com/r/contact-merge.jsonTwo or more records for the same person. Pick the one that survives, then settle each field where they disagree. Fields that hold a list (tags, emails) can be combined. A sticky summary shows what the merged contact will be and how much moves over, and nothing happens until the person confirms.
When to use
- A "merge these contacts" flow opened from a duplicate finder or a selection in a list.
When not to use
- Deleting a duplicate outright: use a confirm dialog.
- Editing one contact: use a form.
Import
import { ContactMerge } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"Quick start
<ContactMerge
records={[
{ id: "a", name: "Sara Ali", values: { email: "[email protected]", phone: "0501" } },
{ id: "b", name: "Sara A.", values: { email: "[email protected]", phone: "0501" } },
]}
fields={[{ id: "email", label: "Email", ltr: true }, { id: "phone", label: "Phone", ltr: true }]}
onMerge={async (outcome) => merge(outcome)}
/>Anatomy
- Survivor choice, one card per record.
- One radio group per field that differs (list fields add "Combine all").
- A collapsible list of fields that are the same everywhere.
- Result panel: final values, identities and activity that move over, consent (the most restrictive wins).
- Confirmation dialog.
API
| Prop | Type | Description |
|---|---|---|
records | ContactMergeRecord[] | { id, name, avatar?, values, createdAt?, stats?, identities?, consent? }. At least two. |
fields | ContactMergeField[] | { id, label, multi?, ltr? }. Default: name, email, phone, company, job title, owner, tags. |
defaultSurvivorId | string | Default: the first record. |
onMerge | (outcome) => Promise<void or { error? }> | outcome holds the survivor id, absorbed ids and resolved values. |
onCancel | () => void | Shows a cancel button. |
labels | ContactMergeLabelOverrides | Override any string. |
Pure helpers: resolveContactMerge, defaultContactMergeChoices, resolveContactMergeValue, contactMergeConflict, combineMergeLists, rebaseContactMergeChoices, isEmptyMergeValue.
Examples
Changing the survivor keeps every choice you already made and only re-defaults fields that no longer apply.
Accessibility
Radio groups are named by field. The confirmation is an alert dialog. Errors from onMerge show in an alert.
RTL & i18n
English and Arabic ship. Values marked ltr (emails, phones) stay left-to-right.
Styling & tokens
Semantic tokens only. Extend with className.
Do / Don't
- Do show what will be lost before the person confirms.
- Don't auto-merge on similarity alone.
Related
Lab
https://docs.nasaqui.com/?path=/docs/components-crm-contact-merge--docs
ContactList
A list of contacts as a table or cards with avatar, company, stage, tags, owner and last activity, plus search, stage and tag and owner filters, bulk select, loading skeletons and an empty state.
LeadsInbox
Inquiries from your forms with where each came from (UTM, referrer, Google click id), a stage pipeline with counts, a detail panel with reply and canned replies, and conversion into a CRM contact.