DialPlan
Embed the DialStack visual dial-plan editor directly in your app. One component, three display modes: read-only viewer, full editor, or static thumbnail.
Basic Usage
import { DialPlan } from '@dialstack/sdk/react';
function Routing({ dialPlanId }: { dialPlanId: string }) {
return <DialPlan dialPlanId={dialPlanId} />;
}
The default mode is view — a read-only flow diagram with pan/zoom and controls.
Modes
// Read-only diagram (default)
<DialPlan dialPlanId="dp_..." mode="view" />
// Full editor: node library, drag-and-drop, config panel, save
<DialPlan dialPlanId="dp_..." mode="edit" />
// Static thumbnail: no controls, no background, no interaction
<DialPlan dialPlanId="dp_..." mode="preview" />
// Create mode: no dialPlanId + mode="edit" starts a blank dial plan
<DialPlan mode="edit" onSave={(dp) => console.log('Created', dp.id)} />
| Mode | Editable | Pan/zoom | Controls | Use case |
|---|---|---|---|---|
view | No | Yes | Yes | Inspect an existing dial plan |
edit | Yes | Yes | Yes | Build or modify a dial plan |
preview | No | No | No | Thumbnail in a list or card |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
dialPlanId | string | - | Dial plan to load. Omit in edit mode to create one |
mode | 'view' | 'edit' | 'preview' | 'view' | Display mode |
locale | DialPlanLocale | English | i18n strings (labels, exits, descriptions) |
theme | 'light' | 'dark' | 'light' | Color theme |
className | string | - | Container class |
style | React.CSSProperties | - | Container inline style |
onNodeClick | (nodeId, node) => void | - | Node click callback (view mode) |
onLoaderStart | () => void | - | Fired when loading starts |
onLoaderEnd | (dialPlan) => void | - | Fired when loading finishes |
onLoadError | (error) => void | - | Fired on load failure |
onSave | (dialPlan) => void | - | Fired after a successful save (edit mode) |
onDirtyChange | (isDirty) => void | - | Fired when the editor's dirty state changes |
onError | (error) => void | - | Alias for onLoadError, used in edit mode |
onCreateResource | (type) => Promise<{ id, name, ... }> | - | Host hook to open a "create new …" modal |
onOpenResource | (resourceId) => void | - | Host hook to navigate to a resource detail page |
Imperative Handle
Pass a ref to trigger actions programmatically:
import { useRef } from 'react';
import { DialPlan, type DialPlanHandle } from '@dialstack/sdk/react';
function Editor() {
const ref = useRef<DialPlanHandle>(null);
return (
<>
<button onClick={() => ref.current?.save()}>Save</button>
<DialPlan ref={ref} mode="edit" dialPlanId="dp_..." />
</>
);
}
| Method | Returns | Description |
|---|---|---|
save() | Promise<void> | Persist pending changes; resolves on success |
Node Types
The editor supports four node types. The config payload is type-specific — the shape is determined by type.
| Type | Purpose | Exits |
|---|---|---|
schedule | Route by schedule (open or closed; holidays count as closed) | open, closed |
internal_dial | Ring an internal target — user, ring group, nested dial plan, voice app, or shared voicemail | next |
ring_all_users | Ring every user in the account at the same time | next |
external_dial | Dial an external phone number over the PSTN | next |
The editor renders some internal_dial nodes as distinct visual tiles — Voicemail (for shared-voicemail targets or timeout: 0) and Voice App (for voice-app targets). The serialized node type stays internal_dial; only the on-canvas presentation differs.
ScheduleNodeConfig
| Field | Type | Description |
|---|---|---|
schedule_id | string | ID of the schedule to check (required) |
open | string | null | Node to route to when the schedule is open |
closed | string | null | Node to route to when the schedule is closed or on holiday |
InternalDialNodeConfig
| Field | Type | Description |
|---|---|---|
target_id | string | ID of the target to dial — user, ring group, dial plan, voice app, or shared voicemail |
timeout | number | Ring timeout in seconds (0–300). 0 routes straight to voicemail |
next | string | null | Node to route to on timeout or busy |
RingAllUsersNodeConfig
| Field | Type | Description |
|---|---|---|
timeout | number | Ring timeout in seconds (1–300) |
next | string | null | Node to route to on timeout or busy |
ExternalDialNodeConfig
| Field | Type | Description |
|---|---|---|
phone_number | string | Destination in E.164 format (+14155551234) |
timeout | number | Ring timeout in seconds (1–120) |
next | string | null | Node to route to on timeout or busy |
Host Integration (edit mode)
The editor lets users pick targets from your directory. Provide two host hooks so the UI can list resources and create new ones on the fly:
<DialPlan
mode="edit"
dialPlanId={id}
onCreateResource={async (type) => {
// type: 'schedule' | 'user' | 'ring_group' | 'dial_plan' | 'voice_app' | 'shared_voicemail'
const created = await openCreateModal(type);
return created; // { id, name, extension_number? }
}}
onOpenResource={(resourceId) => {
router.push(`/resources/${resourceId}`);
}}
/>
onCreateResource(type)— called when the user selects "Create new …" in a target combobox. Return the new resource's{ id, name }to auto-select it.onOpenResource(id)— called when the user clicks the "Open target details" link next to a selected target.
Resource lists themselves come from the DialStack API; nothing extra to wire.
Events
<DialPlan
dialPlanId="dp_..."
mode="edit"
onLoaderStart={() => setLoading(true)}
onLoaderEnd={(dp) => {
setLoading(false);
setName(dp.name);
}}
onDirtyChange={setUnsaved}
onSave={(dp) => toast.success(`Saved ${dp.name}`)}
onError={(err) => toast.error(err.message)}
/>
Internationalization
Supply a locale object to override every UI string. All fields are required on the type — see DialPlanLocale for the full shape.
<DialPlan
dialPlanId="dp_..."
locale={{
nodeTypes: {
start: 'Démarrer',
schedule: 'Horaire',
internalDial: 'Poste interne',
voicemail: 'Messagerie',
externalDial: 'Numéro externe',
ringAllUsers: 'Sonner tous',
voiceApp: 'Voice App',
},
// exits, nodeDescriptions, targetTypes, resourceGroups, configLabels,
// toolbar, panel, combobox, status — see DialPlanLocale
}}
/>
TypeScript
import type {
DialPlanProps,
DialPlanMode,
DialPlanHandle,
DialPlanData,
DialPlanNode,
DialPlanNodeType,
ScheduleNode,
InternalDialNode,
RingAllUsersNode,
ExternalDialNode,
ScheduleNodeConfig,
InternalDialNodeConfig,
RingAllUsersNodeConfig,
ExternalDialNodeConfig,
ResourceType,
} from '@dialstack/sdk/react';
DialPlanNode is a discriminated union — narrow on node.type to get the typed config.
Complete Example
import { useRef, useState } from 'react';
import { DialPlan, type DialPlanHandle } from '@dialstack/sdk/react';
export function DialPlanEditorPage({ id }: { id: string }) {
const ref = useRef<DialPlanHandle>(null);
const [dirty, setDirty] = useState(false);
const [saving, setSaving] = useState(false);
async function handleSave() {
setSaving(true);
try {
await ref.current?.save();
} finally {
setSaving(false);
}
}
return (
<div className="h-screen flex flex-col">
<header className="flex items-center justify-between p-4 border-b">
<h1>Edit dial plan</h1>
<button disabled={!dirty || saving} onClick={handleSave}>
{saving ? 'Saving…' : 'Save'}
</button>
</header>
<div className="flex-1">
<DialPlan
ref={ref}
dialPlanId={id}
mode="edit"
theme="light"
onDirtyChange={setDirty}
onSave={() => setDirty(false)}
onError={(err) => console.error(err)}
onCreateResource={async (type) => openCreateModal(type)}
onOpenResource={(rid) => router.push(`/resources/${rid}`)}
/>
</div>
</div>
);
}
Next Steps
- CallLogs — Call history table
- Voicemails — Voicemail component
- Theming — Customize appearance
- i18n — Internationalization