@esfaenza/flow-builder 20.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,354 @@
1
+ # flow-builder
2
+
3
+ Editor visuale per i flow descritti in [`FRONTEND.md`](../../FRONTEND.md): un componente
4
+ **standalone Angular** che disegna il grafo, edita ogni tipo di elemento, valida, versiona e
5
+ prova un flow.
6
+
7
+ Requisiti: **Angular ≥ 18** (sviluppato e verificato su 19.2), TypeScript.
8
+
9
+ ---
10
+
11
+ ## Indice
12
+
13
+ 1. [Integrazione in tre passi](#integrazione-in-tre-passi)
14
+ 2. [L'interfaccia verso il backend](#linterfaccia-verso-il-backend)
15
+ 3. [Cosa fa il builder](#cosa-fa-il-builder)
16
+ 4. [Architettura](#architettura)
17
+ 5. [Dipendenze esterne](#dipendenze-esterne)
18
+ 6. [Tema e personalizzazione](#tema-e-personalizzazione)
19
+ 7. [Le trappole della §13, una per una](#le-trappole-della-13-una-per-una)
20
+ 8. [Limiti noti](#limiti-noti)
21
+
22
+ ---
23
+
24
+ ## Integrazione in tre passi
25
+
26
+ ### 1. Fornire l'implementazione dell'interfaccia
27
+
28
+ `FlowBuilderApi` e' l'unica porta verso il backend: **tutto** il traffico di dati passa da
29
+ lì, e nessun componente della libreria conosce HTTP.
30
+
31
+ ```ts
32
+ import { FlowBuilderApi, HttpFlowBuilderApi, FLOW_BUILDER_HTTP_CONFIG } from 'flow-builder';
33
+
34
+ bootstrapApplication(App, {
35
+ providers: [
36
+ provideHttpClient(),
37
+ { provide: FLOW_BUILDER_HTTP_CONFIG, useValue: { baseUrl: '/api' } },
38
+ { provide: FlowBuilderApi, useClass: HttpFlowBuilderApi },
39
+ ],
40
+ });
41
+ ```
42
+
43
+ `HttpFlowBuilderApi` usa le rotte **proposte** dalla §6. Se le vostre differiscono, ci sono
44
+ due strade e nessuna delle due tocca l'editor:
45
+
46
+ ```ts
47
+ // 1. estendere e sovrascrivere i soli metodi con rotta diversa
48
+ class NostraApi extends HttpFlowBuilderApi {
49
+ override loadFlow(flowName: string, version?: number) { /* … */ }
50
+ }
51
+
52
+ // 2. implementare `FlowBuilderApi` da zero: e' una classe astratta, quindi il compilatore
53
+ // elenca esattamente i metodi che mancano
54
+ ```
55
+
56
+ I metodi con implementazione di default rifiutano con `MissingService`: un ambiente che non
57
+ espone `parseFlow`, `exportFlow` o le primitive di esecuzione resta usabile, con il comando
58
+ corrispondente degradato invece di rotto.
59
+
60
+ ### 2. Includere i due fogli di stile
61
+
62
+ ```json
63
+ // angular.json → architect.build.options.styles
64
+ "styles": [
65
+ "node_modules/@foblex/flow/styles/default.scss",
66
+ "node_modules/flow-builder/styles/flow-builder.css",
67
+ "src/styles.css"
68
+ ]
69
+ ```
70
+
71
+ Servono entrambi. `flow-builder.css` contiene le variabili del tema, i controlli di form e
72
+ gli stili degli archi: i path SVG delle connessioni sono creati a runtime e non portano
73
+ l'attributo di scope di Angular, quindi un CSS incapsulato non li raggiunge.
74
+
75
+ ### 3. Montare il componente
76
+
77
+ ```html
78
+ <fb-flow-builder
79
+ [flowName]="'ApprovazioneOrdine'"
80
+ [version]="null"
81
+ [author]="utenteCorrente"
82
+ (saved)="onSaved($event)"
83
+ (activated)="onActivated($event)"
84
+ />
85
+ ```
86
+
87
+ | Input | Significato |
88
+ |---|---|
89
+ | `flowName` | il flow da aprire; `null` → si parte da un flow nuovo |
90
+ | `version` | la versione da aprire; assente → l'attiva se c'e', altrimenti l'ultima (§6.1) |
91
+ | `author` | registrato sulla versione e mostrato nell'elenco |
92
+ | `defaultProcessType` | `processType` iniziale di un flow nuovo |
93
+ | `inspectorMode` | `'dialog'` (predefinito) apre il dettaglio dell'elemento in una finestra sopra il canvas, come il flow builder di Salesforce; `'panel'` lo tiene nel pannello laterale |
94
+
95
+ | Output | Quando |
96
+ |---|---|
97
+ | `saved` | dopo un salvataggio riuscito, con `FlowSaveResult` |
98
+ | `activated` | dopo un'attivazione riuscita |
99
+
100
+ Il componente vuole un'altezza: `<fb-flow-builder style="height: 100vh">`, oppure un
101
+ contenitore flex. Gli store sono provider **del componente**, quindi due builder sulla stessa
102
+ pagina editano due flow indipendenti.
103
+
104
+ ---
105
+
106
+ ## L'interfaccia verso il backend
107
+
108
+ Metodi raggruppati come nella specifica. Gli **astratti** sono obbligatori; i **concreti**
109
+ sono opzionali e rifiutano con `MissingService` se non sovrascritti.
110
+
111
+ | §6.1 Lettura | |
112
+ |---|---|
113
+ | `listFlows(query?)` | astratto |
114
+ | `listVersions(flowName)` | astratto |
115
+ | `loadFlow(flowName, version?)` | astratto |
116
+ | `exportFlow(flowName, query?)` | opzionale |
117
+ | `parseFlow(definition)` | opzionale — serve all'import di un file |
118
+
119
+ | §6.2 Scrittura | |
120
+ |---|---|
121
+ | `createFlow(request)` | astratto |
122
+ | `saveFlow(flowName, request)` | astratto |
123
+ | `createVersion(flowName, request?)` | astratto |
124
+ | `activateVersion(flowName, version)` | astratto |
125
+ | `deactivateFlow(flowName)` | astratto |
126
+ | `cloneFlow`, `deleteVersion`, `deleteFlow` | opzionali |
127
+
128
+ | §6.3 Validazione | |
129
+ |---|---|
130
+ | `validateDefinition(definition)` | astratto — e' il pannello dei problemi |
131
+ | `validateVersion(flowName, version)` | opzionale |
132
+
133
+ | §6.4 Dizionari e cataloghi | |
134
+ |---|---|
135
+ | `getDictionaries()` | astratto |
136
+ | `getReferences(query)` / `getWritableReferences(query)` | astratti |
137
+ | `getConnectorTargets(definition, excluding?)` | astratto |
138
+ | `getOutline(definition)` | astratto |
139
+ | `listObjects()`, `listFields(object, usage?)` | astratti |
140
+ | `listActionTypes` / `listActions` / `listActionParameters` | astratti |
141
+ | `listScripts` / `listScriptParameters` | astratti |
142
+ | `listForms` / `listFormParameters` | astratti |
143
+ | `listEnumTypes`, `listEvents`, `listSubflowCandidates` | astratti |
144
+ | `describeObject`, `listFieldValues` | opzionali |
145
+
146
+ | §6.5 Esecuzione | |
147
+ |---|---|
148
+ | `startInterview`, `respondToScreen`, `resumeInterview`, `inspectInterview`, `abandonInterview` | opzionali — senza, il pannello «Prova» segnala che la funzione non c'e' |
149
+
150
+ **Errori.** Ogni metodo, in caso di rifiuto, deve fallire con un `FlowApiError`
151
+ categorizzato (§10). L'editor si comporta in base alla `category`, mai leggendo il
152
+ messaggio: `NotEditable` fa comparire «Nuova versione» al posto di «Salva»,
153
+ `VersionConflict` apre il banner con «ricarica» / «salva come nuova versione»,
154
+ `ValidationFailed` apre il pannello dei problemi con il payload.
155
+
156
+ ```ts
157
+ throw new FlowApiError('VersionConflict', 'Ha salvato mrossi.', undefined, 'mrossi');
158
+ ```
159
+
160
+ Un'implementazione completa e commentata sta in
161
+ [`projects/demo/src/app/backend/in-memory-flow-builder-api.ts`](../demo/src/app/backend/in-memory-flow-builder-api.ts):
162
+ copre tutte le primitive, il ciclo di vita, la concorrenza ottimistica e un esecutore finto,
163
+ senza una sola richiesta di rete.
164
+
165
+ ---
166
+
167
+ ## Cosa fa il builder
168
+
169
+ **Canvas.** Node draggabili con l'ingresso sul bordo superiore e **un'uscita per ramo sul
170
+ bordo inferiore**, in fila e nell'ordine del modello, secondo la mappa della §3.5:
171
+ `connector`, `faultConnector`, `timeoutConnector`,
172
+ `rules[].connector`, `nextValueConnector` / `noMoreValuesConnector`,
173
+ `waitEvents[].connector`, `scheduledPaths[].connector`. Archi colorati per `kind` (Fault
174
+ rosso, Rule viola ed etichettato, Default tratteggiato, LoopNext verde), `isGoTo` reso come
175
+ salto, pan/zoom, minimap, selezione, creazione e riassegnazione di archi trascinando,
176
+ drag-and-drop dalla palette, comando «Riordina» che ricalcola e **scrive** le coordinate nel
177
+ documento. I node non raggiungibili e i rami dichiarati senza destinazione sono marcati
178
+ sul canvas.
179
+
180
+ Il lato da cui gli archi entrano ed escono e' dichiarato (`fConnectorConnectableSide`:
181
+ `TOP` sull'ingresso, `BOTTOM` sulle uscite), non calcolato: lasciandolo calcolare a
182
+ @foblex/flow, un ramo che risale — il ritorno di un Loop, per esempio — uscirebbe di fianco e
183
+ il grafo perderebbe la lettura dall'alto verso il basso. La larghezza del node cresce a
184
+ scaglioni col numero di rami, perche' le etichette delle uscite stanno in fila: arriva da una
185
+ **classe** (`.fb-node--outlets-N`) e non da uno stile inline, perche' @foblex/flow riscrive
186
+ l'attributo `style` del node a ogni spostamento e uno `[style.width]` verrebbe cancellato. La
187
+ tabella delle larghezze e' la stessa che il comando «Riordina» passa a dagre: se divergessero,
188
+ i node larghi resterebbero sovrapposti.
189
+
190
+ **Dettaglio dell'elemento.** Con `inspectorMode="dialog"` (predefinito) il form si apre in una
191
+ finestra sopra il canvas: doppio click sul node, bottone «✎» sul node, comando «Dettaglio»
192
+ nella top bar, click su un rilievo nel pannello dei problemi, o creazione di un elemento dalla
193
+ palette. Il click singolo **seleziona soltanto**: un click capita anche solo per spostare un
194
+ node. La dialog non ha «Annulla» — le modifiche entrano nel documento mentre si digita,
195
+ esattamente come nel pannello, e tornare indietro e' compito dell'annulla dell'editor. Vive
196
+ dentro il componente e non in fondo al `body`: la libreria e' innestabile in una pagina
197
+ qualsiasi.
198
+
199
+ **Palette.** Costruita da `elementTypes`: nessun tipo e' cablato, e i tre non supportati
200
+ (`Step`, `Experiment`, `OrchestratedStage`) sono nascosti tramite `isSupported`.
201
+
202
+ **Inspector.** Un form per ogni tipo supportato: Start, Screen, Assignment, Decision, Loop,
203
+ Collection Processor, Get / Create / Update / Delete / Rollback Records, Action, Script,
204
+ Subflow, Wait, Custom Error, Transform. Più l'intestazione comune con la rinomina, che
205
+ **riscrive tutti i riferimenti** all'elemento.
206
+
207
+ **Risorse.** I sette tipi, con i vincoli visibili: `dataType` obbligatorio, `objectType` per
208
+ `Object` / `Enum`, `scale` solo sui numerici, costanti che non possono referenziare risorse,
209
+ `stageOrder` distinti.
210
+
211
+ **Problemi.** Pannello con filtro per gravita', navigazione all'elemento con un clic,
212
+ validazione su pausa di digitazione (debounce 400 ms) e l'avvertenza che l'assenza di rilievi
213
+ non e' una garanzia di correttezza.
214
+
215
+ **Versioni.** Elenco con stato, «Nuova versione» al posto di «Salva» sulle versioni chiuse,
216
+ attivazione bloccata finche' ci sono errori, eliminazione dietro una conferma esplicita che
217
+ spiega il rischio per le esecuzioni sospese.
218
+
219
+ **Prova.** Interview con form generato dalle variabili di input, form generico per gli screen
220
+ (valori in ingresso più i campi da restituire), `canGoBack` / `canFinish` / `canPause` come
221
+ verita' runtime, traccia navigabile e tabella delle risorse — con l'avviso in chiaro che la
222
+ traccia riporta anche i dati personali.
223
+
224
+ **Import/export.** Export JSON indentato; import via `POST /flows/parse`, che normalizza il
225
+ documento o lo rifiuta con `InvalidDefinition`.
226
+
227
+ ---
228
+
229
+ ## Architettura
230
+
231
+ ```
232
+ src/lib/
233
+ model/ i tipi: documento Flow, dizionari, payload API, errori categorizzati
234
+ api/ FlowBuilderApi (astratta = token DI) + HttpFlowBuilderApi
235
+ core/
236
+ element-outlets.ts la mappa "tipo di elemento → uscite" (§3.5), in un posto solo
237
+ flow-document.store.ts il documento come fonte di verita': flatten, CRUD, undo/redo
238
+ flow-dictionary.store.ts cache dei dizionari e memoizzazione dei cataloghi
239
+ flow-validation.store.ts debounce, indicizzazione dei rilievi per elemento e campo
240
+ flow-editor-session.ts versioni, salvataggio, concorrenza ottimistica
241
+ flow-layout.service.ts auto-layout con dagre
242
+ flow-name.util.ts nomi: regexp, namespace unico, slug dalla label
243
+ condition-logic.util.ts riscrittura di conditionLogic su cancella / riordina
244
+ ui/
245
+ flow-builder.component.ts il componente da montare
246
+ canvas/ palette/ inspector/ resources/ problems/ versions/ debug/ shared/
247
+ styles/
248
+ flow-builder.css variabili del tema, controlli di form, stili degli archi
249
+ ```
250
+
251
+ Due scelte di progetto che vale la pena conoscere.
252
+
253
+ **Il documento e' la fonte di verita' unica.** Canvas, inspector e pannelli sono funzioni del
254
+ `FlowDefinition` in memoria, e ogni gesto e' una mutazione del documento. Le primitive del
255
+ backend non vengono mai chiamate dallo store, così l'editing resta sincrono e reattivo anche
256
+ mentre una validazione e' in volo.
257
+
258
+ **Local-first per il disegno, backend per la verita'.** Archi e raggiungibilita' sono
259
+ calcolati anche in locale, perche' trascinare un arco deve riflettersi subito;
260
+ `POST /flows/outline` viene richiamato con debounce e, quando arriva, il suo `isReachable`
261
+ sovrascrive il calcolo locale. Chiedere l'outline a ogni movimento del mouse renderebbe il
262
+ canvas dipendente dalla latenza.
263
+
264
+ ---
265
+
266
+ ## Dipendenze esterne
267
+
268
+ Oltre ad Angular:
269
+
270
+ | Pacchetto | Perche' |
271
+ |---|---|
272
+ | `@foblex/flow` (+ `@foblex/platform`, `@foblex/mediator`, `@foblex/2d`, `@foblex/utils`) | rendering del grafo, pan/zoom, gesti su node e connessioni. Usata in *classic mode*: la libreria disegna e riconosce i gesti, lo stato resta nostro |
273
+ | `dagre` | auto-layout gerarchico del comando «Riordina» |
274
+
275
+ Nessun design system: i controlli sono input nativi con binding espliciti e CSS custom, così
276
+ il builder si integra nel tema dell'app ospite senza imporre il proprio.
277
+
278
+ ---
279
+
280
+ ## Tema e personalizzazione
281
+
282
+ Le variabili si sovrascrivono sul contenitore:
283
+
284
+ ```css
285
+ .mio-contenitore {
286
+ --fb-accent: #7048c4;
287
+ --fb-surface: #ffffff;
288
+ --fb-border: #dcdfe4;
289
+ --fb-error: #b3261e;
290
+ --fb-edge-fault: #b3261e;
291
+ }
292
+ ```
293
+
294
+ Elenco completo in `styles/flow-builder.css`. Per la variante scura basta
295
+ `data-fb-theme="dark"` su un antenato.
296
+
297
+ Nota sugli archi: il colore si imposta valorizzando le custom properties del tema di
298
+ @foblex/flow (`--ff-connection-color`, `--ff-marker-color`) invece di sovrascrivere le sue
299
+ regole, perche' i selettori hanno la stessa specificita' e chi vince dipenderebbe dall'ordine
300
+ con cui l'app ospite carica i fogli di stile. Lo stesso vale per il resto del disegno che fa
301
+ la libreria: `flow-builder.css` mappa i propri colori sui token `--ff-*` (sfondo del canvas,
302
+ pattern, minimappa, spessore degli archi) e ne annulla due — le maniglie di riassegnazione
303
+ (`--ff-connection-drag-handle-*`), che disegnate formavano una macchia sopra la punta della
304
+ freccia. Il colore dell'uscita di un ramo passa da `--ff-connector-connected-color`, unico
305
+ modo di sopravvivere alla regola di foblex per i connettori collegati, che ha specificita'
306
+ (0,4,1) e non si batte con una classe.
307
+
308
+ L'aspetto dei node segue l'esempio *call-center* di @foblex/flow: card con badge dell'icona
309
+ colorato per categoria, titolo, sottotitolo e uscite sul bordo inferiore. I glifi sono
310
+ caratteri Unicode in `core/element-icons.ts`, condivisi fra palette e canvas: nessun asset da
311
+ distribuire e nessuna dipendenza da un font di icone dell'applicazione ospite.
312
+
313
+ ---
314
+
315
+ ## Le trappole della §13, una per una
316
+
317
+ | # | Trappola | Dove e' gestita |
318
+ |---|---|---|
319
+ | 1 | **Operatori unari**: `rightValue` e' l'esito atteso, non il termine di confronto | `ConditionEditorComponent`: per gli operatori con `isUnary` il campo «confronta con» non esiste; c'e' un selettore «è / NON è» e la frase «questa condizione e' vera quando: *Esito NON è vuoto*». Il flag arriva dal dizionario, non da una lista cablata |
320
+ | 2 | `assignNextValueToReference` di un Collection Processor **non** e' la destinazione | `CollectionProcessorInspectorComponent`: il campo si chiama «Elemento in esame», e un callout dice che il risultato e' l'output automatico dell'elemento |
321
+ | 3 | Gli **output automatici** sono riferimenti validi anche se non dichiarati | `ReferencePickerComponent` chiede l'elenco a `POST /flows/references`, che li include; non ricostruisce nulla lato client |
322
+ | 4 | Cancellare una condizione richiede di **riscrivere `conditionLogic`** | `condition-logic.util.ts`: `removeCondition` e `moveCondition` rimappano gli indici 1-based; un termine rimasto orfano diventa `?` e viene segnalato |
323
+ | 5 | Node e risorse condividono **un unico spazio di nomi** | `FlowDocumentStore.usedNames` unisce i due insiemi e `checkFlowName` confronta case-insensitive; pannello risorse e inspector usano lo stesso controllo |
324
+ | 6 | `processType` non e' decorativo | Banner in cima all'editor per `AutoLaunched` + screen, `Orchestration` non eseguibile, `Screen` senza screen; la palette segnala lo Screen incompatibile |
325
+ | 7 | Le **regole di una Decision sono ordinate** | `DecisionInspectorComponent`: regole numerate, frecce di riordino, callout «valutate dall'alto verso il basso, si ferma alla prima vera» |
326
+ | 8 | **Rinominare non aggiorna i riferimenti** | `FlowDocumentStore.renameNode` cammina il documento e riscrive ogni campo di riferimento, incluso il caso navigato (`Vecchio.Campo` → `Nuovo.Campo`); l'inspector dice quante occorrenze verranno toccate |
327
+ | 9 | `Step`, `Experiment`, `OrchestratedStage` non sono supportati | Filtrati dalla palette via `isSupported`; se arrivano da un import, l'inspector li mostra in sola lettura spiegando perche' |
328
+ | 10 | Un `Delete` senza filtri e' un **errore**, un `Update` senza filtri un **avviso** | `RecordWriteInspectorComponent`: severita' diverse, e l'update di massa chiede una conferma esplicita |
329
+ | 11 | Non cablare enum e operatori | Ogni tendina legge da `getDictionaries()`; le mappe locali intervengono solo come fallback quando il dizionario non e' disponibile |
330
+ | 12 | Il **token di continuazione e' opaco** | `DebugPanelComponent` lo rimanda tale e quale, non lo legge e non lo mette in nessun URL |
331
+
332
+ Altre regole del contratto rispettate: liste vuote omesse invece di scritte come `[]` (§2),
333
+ `filterLogic` mostrato **solo** dove il modello lo prevede (§4.4), `fullName` / `status` mai
334
+ usati per rinominare o attivare (§2), un solo campo valorizzato in
335
+ `FlowElementReferenceOrValue` con segnalazione e normalizzazione dei documenti ambigui (§4.2),
336
+ `Transform.connector` e `start` trattati come array per fedelta' storica (§3.5), salvataggio
337
+ possibile anche con errori e attivazione bloccata dagli errori (§8).
338
+
339
+ ---
340
+
341
+ ## Limiti noti
342
+
343
+ - **Le uscite indicizzate cambiano chiave se si riordinano le regole**: la chiave di un
344
+ connettore contiene l'indice (`rule:0`), quindi riordinare le regole di una Decision
345
+ riassegna gli id dei connettori sul canvas. Le destinazioni seguono la regola, perche'
346
+ vivono dentro `rules[i].connector`, ma un arco selezionato perde la selezione.
347
+ - **`relatedRecords` di Get Records** e' esposto in sola lettura con un avviso: e' modellato
348
+ ma non tradotto in query.
349
+ - **L'esecutore della demo non valuta le condizioni**: prende il primo ramo disponibile e lo
350
+ scrive nella traccia. È un mock per esercitare il pannello, non il motore.
351
+ - **Nessun test automatico**: la libreria e' stata verificata a mano sull'app demo (canvas,
352
+ inspector, salvataggio, attivazione, interview completa). Casi su `condition-logic.util`,
353
+ `flow-name.util`, `element-outlets` e `FlowDocumentStore` sarebbero il primo investimento
354
+ sensato.