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

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)} />
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:

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_..." />
</>
);
}
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:

<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