Skip to main content

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​

TypeScript
import { DialPlan } from '@dialstack/sdk-react/dial-plan';

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​

TypeScript
// 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)} />
ModeEditablePan/zoomControlsUse case
viewNoYesYesInspect an existing dial plan
editYesYesYesBuild or modify a dial plan
previewNoNoNoThumbnail in a list or card

Props​

PropTypeDefaultDescription
dialPlanIdstring-Dial plan to load. Omit in edit mode to create one
mode'view' | 'edit' | 'preview''view'Display mode
localeDialPlanLocaleEnglishi18n strings (labels, exits, descriptions)
theme'light' | 'dark''light'Color theme
classNamestring-Container class
styleReact.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:

TypeScript
import { useRef } from 'react';
import { DialPlan, type DialPlanHandle } from '@dialstack/sdk-react/dial-plan';

function Editor() {
const ref = useRef<DialPlanHandle>(null);

return (
<>
<button onClick={() => ref.current?.save()}>Save</button>
<DialPlan ref={ref} mode="edit" dialPlanId="dp_..." />
</>
);
}
MethodReturnsDescription
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.

TypePurposeExits
scheduleRoute by schedule (open or closed; holidays count as closed)open, closed
internal_dialRing an internal target — user, ring group, nested dial plan, voice app, or shared voicemailnext
ring_all_usersRing every user in the account at the same timenext
external_dialDial an external phone number over the PSTNnext
Editor aliases

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​

FieldTypeDescription
schedule_idstringID of the schedule to check (required)
openstring | nullNode to route to when the schedule is open
closedstring | nullNode to route to when the schedule is closed or on holiday

InternalDialNodeConfig​

FieldTypeDescription
target_idstringID of the target to dial — user, ring group, dial plan, voice app, or shared voicemail
timeoutnumberRing timeout in seconds (0–300). 0 routes straight to voicemail
nextstring | nullNode to route to on timeout or busy

RingAllUsersNodeConfig​

FieldTypeDescription
timeoutnumberRing timeout in seconds (1–300)
nextstring | nullNode to route to on timeout or busy

ExternalDialNodeConfig​

FieldTypeDescription
phone_numberstringDestination in E.164 format (+14155551234)
timeoutnumberRing timeout in seconds (1–120)
nextstring | nullNode 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:

TypeScript
<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​

TypeScript
<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.

TypeScript
<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​

TypeScript
import type {
DialPlanProps,
DialPlanMode,
DialPlanHandle,
DialPlanData,
DialPlanNode,
DialPlanNodeType,
ScheduleNode,
InternalDialNode,
RingAllUsersNode,
ExternalDialNode,
ScheduleNodeConfig,
InternalDialNodeConfig,
RingAllUsersNodeConfig,
ExternalDialNodeConfig,
ResourceType,
} from '@dialstack/sdk-react/dial-plan';

DialPlanNode is a discriminated union — narrow on node.type to get the typed config.

Complete Example​

TypeScript
import { useRef, useState } from 'react';
import { DialPlan, type DialPlanHandle } from '@dialstack/sdk-react/dial-plan';

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​