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={dialPlanId} mode="view" />

// Full editor: node library, drag-and-drop, config panel, save
<DialPlan dialPlanId={dialPlanId} mode="edit" />

// Static thumbnail: no controls, no background, no interaction
<DialPlan dialPlanId={dialPlanId} 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={dialPlanId} />
</>
);
}
MethodReturnsDescription
save()Promise<void>Persist pending changes; resolves on success

Node types​

The editor builds the same nodes the API stores. For every node type, its config fields and its exits, see the Dial plans guide.

Editor tiles

The editor shows some nodes as their own tiles. Voicemail and Voice App are internal_dial nodes that target a shared voicemail box or a voice app. The stored node type stays internal_dial; only the canvas presentation differs.

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={dialPlanId}
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={dialPlanId}
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,
DialPlanNode,
DialPlanNodeType,
} 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​