Contact Channels Panel
A Docyrus-backed panel for managing a record's contact channels, the append-only consent ledger and the validation engine.
ContactChannelsPanel wires the full Contact Channels & Consent API to a single component, laid
out as a Contact Points & Consent detail view: a preferred email / phone summary, grouped
Emails / Phones (and messaging / social / web when present) cards showing each channel's
validation status, validating provider and source, per-brand consent breakdowns, and a merged
consent + validation history trail.
Give it an authenticated client and the owning (appSlug, dataSourceSlug, recordId) and it lists
the record's channels, lets you add / edit / archive / restore / delete them, promote a primary per
kind, append to the consent ledger (scoped by brand → medium → purpose), record a validation
result and run the validation engine — all through useDocyrusContactChannels, which
you can also use standalone to build a custom UI.
This is a Docyrus-connection component. It requires an authenticated RestApiClient,
so the preview above is informational only — wire it against your tenant (or the playground page
on base.contact) to see it live. For a backend-free editor, use the
Contact Channels Field.
Installation
pnpm dlx @docyrus/cli add @docyrus/ui-contact-channels-panelpnpm add @docyrus/api-client @tanstack/react-queryUsage
import { useDocyrusAuth } from '@docyrus/signin';
import { ContactChannelsPanel } from '@docyrus/ui/components/contact-channels-panel';
function ContactChannels({ recordId }: { recordId: string }) {
const { client } = useDocyrusAuth();
if (!client) return null;
return (
<ContactChannelsPanel
client={client}
appSlug="base"
dataSourceSlug="contact"
recordId={recordId}
/>
);
}As a side drawer
Pass open / onOpenChange to render the panel inside an
AwesomeDialog — a right-side sheet by default. The
typical pairing is the compact ContactChannelsField
summary whose Details link opens this panel, with the contact / organization name as the
subtitle:
const [open, setOpen] = useState(false);
<ContactChannelsField value={channels} brands={brands} readOnly onDetailsClick={() => setOpen(true)} />
<ContactChannelsPanel
open={open}
onOpenChange={setOpen}
client={client}
appSlug="base"
dataSourceSlug="contact"
recordId={recordId}
title="Contact Points & Consent"
subtitle="Emre Terzi · Emre Company"
container="sheet" // 'sheet' (default) · 'drawer' · 'modal'
side="right"
size="xl"
/>Endpoints
The panel (via useDocyrusContactChannels) targets, under the record base path
/v1/apps/{appSlug}/data-sources/{dataSourceSlug}/items/{recordId}:
| Group | Operations |
|---|---|
| Channels | GET /channels · POST /channels · PATCH /channels/{id} · POST /channels/{id}/make-primary · POST /channels/{id}/archive · POST /channels/{id}/unarchive · DELETE /channels/{id} |
| Consent | GET /consent (record-wide) · GET /channels/{id}/consent · POST /channels/{id}/consent · POST /channels/{id}/consent/bulk · POST /channels/consent/bulk |
| Validation | GET /validations (record-wide) · GET /channels/{id}/validations · POST /channels/{id}/validations · POST /channels/{id}/validate |
| Brands | GET /v1/tenant/brands (for brand-aware consent labels) |
Read endpoints require the DS.Read.All scope; write endpoints require DS.ReadWrite.All. The
record-wide consent / validation queries avoid a per-channel N+1 in the panel. The consent and
validation ledgers are append-only; the channel's consent / validation_status caches are
maintained server-side by DB triggers and treated as read-only.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
client | ContactChannelsClient | — | Authenticated REST client (@docyrus/api-client's RestApiClient works). |
appSlug | string | — | App slug of the owning record. |
dataSourceSlug | string | — | Data source slug of the owning record. |
recordId | string | — | Owning record id (uuid). |
title | string | 'Contact Points & Consent' | Dialog heading. |
subtitle | string | — | Sub-heading under the title (e.g. the contact and organization names). |
includeArchived | boolean | false | Initial state of the "Show archived" toggle. |
enabled | boolean | true | Defer queries until ready (e.g. auth resolved). |
loadBrands | boolean | true | Fetch tenant brands for brand-aware consent. |
readOnly | boolean | false | Hide every write affordance. |
maxHeight | string | '40rem' | Max height when rendered inline (the dialog body scrolls). |
onChange | (channels: ContactChannel[]) => void | — | Fired with the refreshed channel list after mutations. |
className | string | — | Additional class on the root. |
open | boolean | — | Dialog open state. Providing open/onOpenChange renders the panel in an AwesomeDialog. |
onOpenChange | (open: boolean) => void | — | Dialog open-change handler. |
container | 'sheet' | 'drawer' | 'modal' | 'sheet' | Dialog container when shown as a dialog. |
side | 'left' | 'right' | 'top' | 'bottom' | 'right' | Side for the sheet / drawer container. |
size | 'sm' | 'default' | 'lg' | 'xl' | 'full' | 'xl' | Dialog size preset. |
Hook
useDocyrusContactChannels({ client, appSlug, dataSourceSlug, recordId }) returns the data and a
typed mutation for every endpoint:
const channels = useDocyrusContactChannels({ client, appSlug, dataSourceSlug, recordId });
// data
channels.channels; // ContactChannel[]
channels.brands; // ContactBrand[]
channels.consentHistory; // ConsentRecord[] (record-wide)
channels.validationHistory; // ValidationRecord[] (record-wide)
channels.isLoading;
channels.error;
// mutations (all return promises)
await channels.createChannel({ channelKind: 'email', channelType: 'email', value: 'a@b.com' });
await channels.updateChannel(id, { label: 'work' });
await channels.makePrimary(id);
await channels.archiveChannel(id, 'old work email');
await channels.unarchiveChannel(id);
await channels.deleteChannel(id);
await channels.recordConsent(id, { purpose: 'marketing', action: 'opt_in', consentStatus: 'opted_in', consentChannel: 'sms' });
await channels.bulkRecordConsent(id, items);
await channels.bulkRecordRecordConsent(items); // spans many channels
await channels.recordValidation(id, { method: 'manual', status: 'valid' });
await channels.runValidation(id); // POST /channels/{id}/validateComponents
| Component | Description |
|---|---|
ContactChannelsPanel | Full Docyrus-backed panel (inline or as an AwesomeDialog drawer). |
PanelSummary | Preferred email / phone summary cards. |
ChannelCard | Per-channel read card with status grid (validation, provider, source) and the action menu. |
BrandConsentCard | One brand's consent breakdown (EMAIL / PHONE mediums + capture metadata). |
ConsentHistory | Merged, collapsible consent + validation audit trail. |
ChannelFormDialog | Add / edit dialog (wraps the backend-agnostic ChannelEditor). |
ConsentDialog | Records a single consent action (channel · purpose · action · status · medium · brand). |
ValidationDialog | Records an externally-supplied validation result (POST /channels/{id}/validations). |
useDocyrusContactChannels | The data + mutation hook. |
Type Exports
| Type | Description |
|---|---|
ContactChannelsPanelProps | Props for ContactChannelsPanel. |
ContactChannelsClient | Minimal { get, post, patch, delete } client contract. |
UseDocyrusContactChannelsOptions / UseDocyrusContactChannelsResult | Hook options + return shape. |
ConsentEntryWithChannel | A consent entry plus its target channelId (record-wide bulk). |
ContactChannel / ConsentRecord / ValidationRecord / ContactBrand | Re-exported from the field package. |