Une architecture en trois entrées
La racine @newvexa/flowora exporte Editor, createDemoDocument, NODE_CATALOG, les types JSON, la validation et la géométrie en ESM/CommonJS. Elle ne dépend ni d’Angular, ni de Rete, ni de Workify, ni d’API Node. Le renderer DOM standalone est isolé dans @newvexa/flowora/angular ; l’adaptateur ELK est isolé dans @newvexa/flowora/layout.
EditorDocument : des données versionnées
Le document porte schemaVersion: 1, id, name, nodes, connections et viewport. Chaque EditorNode a un identifiant, un type, un nom, une position absolue, des dimensions positives, des tableaux de ports, une configuration JSON et éventuellement un parentId. Une connexion référence une sortie et une entrée par leurs couples nodeId/portId.
Le snapshot contient document, selection, revision, canUndo et canRedo. La sélection est un état d’édition, pas un champ du JSON persisté. Les snapshots et leurs documents sont immuables : ne modifiez pas directement leurs tableaux, coordonnées ou configurations. La référence renvoyée par getSnapshot() reste stable entre deux notifications.
Observer l’état et les événements
subscribe(listener: () => void): () => void prévient d’un changement de snapshot ; son callback ne reçoit pas le document. Lisez getSnapshot() pour l’état initial et dans ce callback. onEvent(listener): () => void émet un EditorEvent avec seulement type et revision : document.changed, selection.changed ou viewport.changed. Plusieurs événements peuvent décrire une même révision.
Les abonnements ne constituent ni une sauvegarde automatique, ni un journal durable. Le store n’installe pas de timer ou de transport réseau. Gardez les deux fonctions de nettoyage, puis appelez dispose() quand vous possédez et abandonnez l’éditeur.
import { Editor } from '@newvexa/flowora';
import type { EditorDocument } from '@newvexa/flowora';
const document: EditorDocument = {
schemaVersion: 1,
id: 'atelier-local',
name: 'Mon document fictif',
nodes: [],
connections: [],
viewport: { x: 0, y: 0, zoom: 1 },
};
const editor = new Editor(document, { historyLimit: 64, allowCycles: false });
const offSnapshot = editor.subscribe(() => {
const snapshot = editor.getSnapshot();
console.log(snapshot.revision, snapshot.selection, snapshot.canUndo);
});
const offEvents = editor.onEvent((event) => {
if (event.type === 'document.changed') {
console.log('Document modifié', event.revision);
}
});
try {
const startId = editor.addFromCatalog('start', { x: 40, y: 80 });
const responseId = editor.addFromCatalog('response', { x: 360, y: 80 });
editor.updateNode(responseId, { name: 'Brouillon à relire' });
const connectionId = editor.connect(
{ nodeId: startId, portId: 'out' },
{ nodeId: responseId, portId: 'in' },
);
editor.select([responseId]);
console.log(editor.getNode(responseId), editor.getConnection(connectionId));
console.log(editor.toJSON());
} finally {
offEvents();
offSnapshot();
editor.dispose();
} Écrire uniquement par les commandes
addFromCatalog(type, position) et connect(source, target) renvoient les identifiants créés. addNode(node) accepte un nœud complet validé. updateNode(id, patch) modifie nom, position, dimensions, configuration, ports ou parent ; removeNode(id) et removeConnection(id) suppriment les éléments. Une commande invalide lève une erreur avant de remplacer l’état.
select(ids, additive?) accepte des identifiants de nœuds ou de connexions. moveNodes(ids, delta, record?) déplace des nœuds ; setViewport(viewport, record?) change la vue. Les commandes de duplication, suppression de sélection, groupes, transactions et annuler/rétablir sont détaillées dans le guide d’édition. toJSON(), loadJSON() et replaceDocument() sont décrits dans le guide de persistance.
Un patch config remplace la configuration complète : recopiez les clés conservées avec un spread comme dans l’exemple suivant. Changer des ports ne répare pas les liens existants ; une référence devenue invalide est refusée. Retirez d’abord les connexions concernées, au besoin dans une transaction. NodePatch ne renomme ni l’identifiant, ni le type du nœud.
Ports et connexions : des invariants, pas des styles
La source doit être un port de outputs et la cible un port de inputs d’un nœud existant. Les types doivent être identiques, sauf si l’un des ports a le type littéral any. Ce type de port ne désactive ni cardinalité, ni intégrité, ni contrôle de cycle ; il n’est pas une assertion TypeScript.
multiple: true autorise plusieurs connexions sur le port. Sans cette propriété, le maximum est une connexion, pour une entrée comme pour une sortie. Les mêmes extrémités ne peuvent pas être reliées deux fois. Les identifiants de ports sont uniques dans tout le nœud, y compris entre entrées et sorties. Les groupes n’ont pas de ports.
Les cycles de connexions sont rejetés par défaut. new Editor(document, { allowCycles: true }) peut les autoriser pour un besoin d’édition ; la hiérarchie de groupes reste toujours acyclique. Cette option n’apporte aucune sémantique d’exécution des boucles.
Ajouter un nœud métier data-only
Un NodePreset définit type, label, category, color, icon, inputs, outputs et defaultConfig. Passer catalog remplace le catalogue de l’instance : étendez NODE_CATALOG si vous souhaitez conserver les presets fournis. Les types de presets doivent être uniques.
import { Editor, NODE_CATALOG } from '@newvexa/flowora';
import type { NodePreset } from '@newvexa/flowora';
const editorialNote: NodePreset = {
type: 'editorial-note',
label: 'Note éditoriale',
category: 'Mon application',
color: '#3e7268',
icon: 'document',
inputs: [{ id: 'in', label: 'Entrée', type: 'flow', multiple: true }],
outputs: [{ id: 'out', label: 'Suite', type: 'flow', multiple: true }],
defaultConfig: { text: '', referenceId: '' },
};
const editor = new Editor(undefined, {
catalog: [...NODE_CATALOG, editorialNote],
});
try {
const source = editor.addFromCatalog('editorial-note', { x: 0, y: 0 });
const target = editor.addFromCatalog('editorial-note', { x: 320, y: 0 });
editor.connect(
{ nodeId: source, portId: 'out' },
{ nodeId: target, portId: 'in' },
);
const node = editor.getNode(target);
if (node) {
editor.updateNode(target, {
config: { ...node.config, text: 'Une note fictive à compléter' },
});
}
console.log(editor.getCatalog(), editor.toJSON());
} finally {
editor.dispose();
} Le catalogue fourni comprend départ, réponse, attente, email, RAG, variables, script, condition et groupe. Les nœuds ajoutés depuis le catalogue sont des cartes de 220 × 112 ; un nœud ajouté ou importé conserve ses dimensions explicites. Les presets ne sont pas des handlers : pas de fonction execute(), pas d’évaluation du script, pas d’email ou de RAG exécuté. Stockez une référence métier dans config, jamais une instance de classe ou un secret.
Le composant Angular fournit le canvas et les gestes. Les intégrations React et Vue observent le même store, sans transformer le renderer Angular en composant natif de ces frameworks.