@esfaenza/flow-builder
v20.3.67
Published
Editor visuale di flow: componente standalone Angular, con tutto il traffico verso il backend dietro un'unica interfaccia.
Readme
flow-builder
Editor visuale per i flow descritti in FRONTEND.md: un componente
standalone Angular che disegna il grafo, edita ogni tipo di elemento, valida, versiona un
flow e ne consegna l'esecuzione all'applicazione ospite.
Requisiti: Angular ≥ 18 (sviluppato e verificato su 19.2), TypeScript.
Indice
- Integrazione in tre passi
- L'interfaccia verso il backend
- Cosa fa il builder
- Architettura
- Dipendenze esterne
- Tema e personalizzazione
- Le trappole della §13, una per una
- Limiti noti
Integrazione in tre passi
1. Fornire l'implementazione dell'interfaccia
FlowBuilderApi e' l'unica porta verso il backend: tutto il traffico di dati passa da
lì, e nessun componente della libreria conosce HTTP.
import { FlowBuilderApi, HttpFlowBuilderApi, FLOW_BUILDER_HTTP_CONFIG } from 'flow-builder';
bootstrapApplication(App, {
providers: [
provideHttpClient(),
{ provide: FLOW_BUILDER_HTTP_CONFIG, useValue: { baseUrl: '/api' } },
{ provide: FlowBuilderApi, useClass: HttpFlowBuilderApi },
],
});HttpFlowBuilderApi usa le rotte proposte dalla §6. Se le vostre differiscono, ci sono
due strade e nessuna delle due tocca l'editor:
// 1. estendere e sovrascrivere i soli metodi con rotta diversa
class NostraApi extends HttpFlowBuilderApi {
override loadFlow(flowName: string, version?: number) { /* … */ }
}
// 2. implementare `FlowBuilderApi` da zero: e' una classe astratta, quindi il compilatore
// elenca esattamente i metodi che mancanoLe primitive di authoring e i cataloghi stanno sotto /flows/{prefix}/, e prefix si
configura: editor (il default) per i flow base, tenant per quelli del tenant. I due prefissi
espongono lo stesso contratto — stessi percorsi relativi, parametri e risposte — quindi lo stesso
editor lavora sull'uno o sull'altro cambiando solo la configurazione. Oggetti, action e tipi di
action restano senza prefisso (/flows/objects, /flows/actions, /flows/action-types), e
l'esecuzione pure.
{ provide: FLOW_BUILDER_HTTP_CONFIG, useValue: { baseUrl: `/api/${idTenant}`, prefix: 'tenant' } }Un override in una sottoclasse costruisce il percorso con this.prefixed('/…') e non scrivendo
editor/: altrimenti quel metodo resta sui flow base mentre il resto dell'editor lavora su quelli
del tenant, e nessun errore lo dice.
I metodi con implementazione di default rifiutano con MissingService: un ambiente che non
espone parseFlow, exportFlow o le primitive di esecuzione resta usabile, con il comando
corrispondente degradato invece di rotto.
2. Includere i due fogli di stile
// angular.json → architect.build.options.styles
"styles": [
"node_modules/@foblex/flow/styles/default.scss",
"node_modules/flow-builder/styles/flow-builder.css",
"src/styles.css"
]Servono entrambi. flow-builder.css contiene le variabili del tema, i controlli di form e
gli stili degli archi: i path SVG delle connessioni sono creati a runtime e non portano
l'attributo di scope di Angular, quindi un CSS incapsulato non li raggiunge.
3. Montare il componente
<fb-flow-builder
[flowName]="'ApprovazioneOrdine'"
[version]="null"
[author]="utenteCorrente"
[runOutcome]="esitoUltimaEsecuzione()"
[isRunning]="esecuzioneInCorso()"
(saved)="onSaved($event)"
(activated)="onActivated($event)"
(runRequested)="esegui($event)"
/>| Input | Significato |
|---|---|
| flowName | il flow da aprire; null → si parte da un flow nuovo |
| version | la versione da aprire; assente → l'attiva se c'e', altrimenti l'ultima (§6.1) |
| author | registrato sulla versione e mostrato nell'elenco |
| defaultProcessType | processType iniziale di un flow nuovo |
| inspectorMode | 'dialog' (predefinito) apre il dettaglio dell'elemento in una finestra sopra il canvas; 'panel' lo tiene nel pannello laterale |
| runOutcome | l'esito che l'host vuole far vedere nel pannello «Debug» dopo un runRequested (FlowRunOutcome); null → il pannello dice che non c'e' niente. Facoltativo: chi mostra l'esecuzione in una sua interfaccia non passa niente |
| isRunning | l'esecuzione annunciata e' ancora in corso. Solo l'host lo sa: accende il puntino sul tab «Debug» e tiene spento il bottone della finestra degli ingressi |
| Output | Quando |
|---|---|
| saved | dopo un salvataggio riuscito, con FlowSaveResult. Lo emette anche «Duplica», e in quel caso flowName e' quello della copia: la sessione si e' spostata su di essa, quindi l'host che tiene il nome da parte deve adottarlo |
| activated | dopo un'attivazione riuscita |
| runRequested | l'utente ha premuto Esegui o Debug, con FlowRunRequest: debug, flowName, version, inputs già convertiti, processType, isDirty e la definition in mano all'editor. L'editor non esegue e non chiama niente: cosa significhi eseguire — una navigazione, delle API dell'host, una dialog con l'interview dentro — lo decide l'host. Un host che non ascolta questo output fa sì che i due comandi non facciano niente: non c'e' nessuna primitiva che possa mancare, quindi nessun errore da mostrare |
Il componente vuole un'altezza, e non e' un dettaglio di stile: <fb-flow-builder
style="height: 100vh">, oppure un contenitore flex che gliene dia una. L'editor e' un guscio
d'applicazione, e ogni suo pannello — l'elenco delle risorse, i problemi, l'inspector, la traccia —
scorre per conto proprio dentro l'altezza che riceve.
Se non la riceve, height: 100% si risolve su auto e la catena si rovescia: ogni regione diventa
alta quanto il suo contenuto, nessuna scorre piu', e la pagina cresce all'infinito. È un difetto che
non si vede finche' il flow e' piccolo — con dieci risorse la differenza non si nota, con quaranta la
pagina non finisce piu'. Da lì max-height: 100vh su .fb-builder, che e' un freno e non il
modo in cui l'editor prende la sua altezza: rimette in piedi lo scorrimento dove l'altezza manca, e
dove c'e' non morde. Il freno non sostituisce l'altezza: sotto un'intestazione di pagina il
viewport intero e' comunque troppo, e a fare la cosa giusta e' l'ospite.
Gli store sono provider del componente, quindi due builder sulla stessa pagina editano due flow indipendenti.
L'interfaccia verso il backend
Metodi raggruppati come nella specifica. Gli astratti sono obbligatori; i concreti
sono opzionali e rifiutano con MissingService se non sovrascritti.
| §6.1 Lettura | |
|---|---|
| listFlows(query?) | astratto |
| listVersions(flowName) | astratto — l'editor la chiama prima di loadFlow e ne ricava il numero di versione da aprire: senza, quel numero resterebbe da indovinare (il documento porta status, non il numero) |
| loadFlow(flowName, version?) | astratto — l'editor passa sempre la versione, tranne quando listVersions non risponde: lì ripiega su «senza versione» e deduce |
| exportFlow(flowName, query?) | opzionale |
| parseFlow(definition) | opzionale — serve all'import di un file |
| §6.2 Scrittura | |
|---|---|
| createFlow(request) | astratto |
| saveFlow(flowName, request) | astratto |
| createVersion(flowName, request?) | astratto — senza body: from e author vanno in query come fromVersion e author (§6.2). Nel body il backend non li legge e copia la versione predefinita invece di quella aperta |
| activateVersion(flowName, version) | astratto |
| deactivateFlow(flowName) | astratto |
| cloneFlow, deleteVersion, deleteFlow | opzionali — senza cloneFlow «Duplica» resta disponibile e ripiega su createFlow. Anche cloneFlow e' senza body: FlowCloneRequest sono gli argomenti, e in query diventano newFlowName, newLabel, fromVersion, author (§6.2); deleteVersion e' il comando del pannello versioni, mentre deleteFlow non la chiama nessun componente: eliminare un flow e' dell'ospite (vedi «Versioni») |
| §6.3 Validazione | |
|---|---|
| validateDefinition(definition) | astratto — e' il pannello dei problemi |
| validateVersion(flowName, version) | opzionale |
| validateFormula(request) | opzionale — verifica una espressione mentre si digita, ed e' cio' che alimenta <fb-formula-editor>. Unica primitiva di analisi con un involucro: il documento sta sotto flow, insieme a expression, usage e expectedDataType (§6.3, §6.4). I rilievi portano position/length, che la validazione dell'intero documento non ha. Tre esiti da tenere distinti e nessuno dei tre e' «formula valida»: rifiuto MissingService (primitiva assente), isVerified: false (primitiva sì, motore non configurato), isValid (verificata davvero) |
| §6.4 Dizionari e cataloghi | |
|---|---|
| getDictionaries(processType?) | astratto — processType filtra le globali (§4.1) |
| getReferences(definition) / getWritableReferences(definition) | astratti — prendono il documento nudo e restituiscono tutti i riferimenti: nel body non c'e' nessun filtro di tipo, e la compatibilita' col campo la decide l'editor (§6.4, §13.17) |
| getConnectorTargets(definition, excluding?) | astratto |
| getOutline(definition) | astratto |
| listObjects(), listFields(object, usage?) | astratti |
| listActionTypes / listActions / listActionParameters | astratti |
| listScripts / listScriptParameters | astratti |
| listForms(kind?) / listFormParameters | astratti — un catalogo, due ruoli (§5.1, §5.2): le schermate intere di uno Screen (formName) e i componenti di uno screen dinamico (extensionName) stanno nello stesso elenco e si distinguono per formKind. kind vale Form, Component o assente per tutto; una voce senza formKind vale Any e resta proponibile in entrambi i posti, perche' un catalogo che il ruolo non lo dichiara non deve vietare tutto. Usare l'uno al posto dell'altro e' FORM_KIND_MISMATCH, che e' un errore diverso da «il nome non esiste». I parametri si chiedono per nome, uguali per i due ruoli |
| listEnumTypes, listEvents | astratti |
| listSubflowCandidates(excluding?) | astratto — restituisce FlowSubflowCandidate[], col nome del flow in flowName. Il backend risponde voci di catalogo (name, label, description, group), non FlowSummary (§6.4): chi implementa il trasporto porta name in flowName, altrimenti la tendina del Subflow si riempie di righe senza nome. HttpFlowBuilderApi lo fa, e accetta anche chi manda gia' flowName |
| describeObject | opzionale |
| listFieldValues(object, field) | opzionale — i valori ammessi di un campo a scelta chiusa (hasClosedValueSet, §6.4), cioe' l'altra meta' degli insiemi chiusi: quelli di un Enum sono del tipo (listEnumValues), questi della colonna — Ordine.Stato e Attivita.Stato possono ammettere valori diversi pur essendo due String, ed e' perche' serve la coppia. Lista vuota = "non lo so": il valore si scrive a mano |
| listFormulaFunctions(usage?) | opzionale — le funzioni (e gli operatori, se il motore li dichiara) scrivibili in un'espressione: e' la palette di <fb-formula-editor>, la terza gamba accanto ai nomi (getReferences) e al giudizio (validateFormula). usage e' lo stesso di validateFormula e un motore che non filtra risponde comunque tutto, che e' legittimo. Due regole: lista vuota = «il motore non le dichiara» e la palette non compare (§4.6), e l'elenco non e' autoritativo — accusare una funzione fuori catalogo sarebbe un rilievo falso su una formula valida. requiresPriorValues marca le funzioni sui valori precedenti del record: la palette le toglie dove un «prima» non c'e' (§6.3) |
| listEnumValues(enumType) | opzionale — i valori di un tipo di enumerazione, cioe' i nomi scrivibili in enumValue (§4.2, §4.6). listEnumTypes e' l'altra meta' e non basta: quella dice quali tipi esistono, questa quali valori ha un tipo. enumType e' esattamente l'objectType della risorsa, del parametro o del membro di una classe (§4.7.1): la risposta e' del tipo, non del punto di uso, e la cache e' per nome del tipo. Lista vuota = "non lo so" — il valore si scrive a mano, e un tipo sconosciuto risponde vuoto, non 404 |
| listStructures, describeStructure(className) | opzionali — le classi e i loro membri per le risorse Structure (§4.7). listStructures e' l'elenco delle classi senza membri; describeStructure restituisce la descrizione di una classe (className, label, members), non un array nudo (§4.7.1). Senza, le classi non sono verificabili e il nome si scrive a mano: "non lo so", non "non esiste". I tre esiti sono distinti: members popolato = elenco autorevole, members: [] = membri non dichiarati (si digita, nessuna accusa), rifiuto StructureNotFound = classe non registrata (si segnala la classe) |
| Esecuzione dall'editor | |
|---|---|
| startInterview, respondToScreen, resumeInterview, inspectInterview, abandonInterview | opzionali — le primitive §6.5 dell'interview. L'editor non le usa: servono a chi risponde a runRequested con un runner proprio (la demo lo fa) |
| completeStageStep(request) | opzionale — conclude uno step di orchestrazione (§5.14) |
Errori. Ogni metodo, in caso di rifiuto, deve fallire con un FlowApiError
categorizzato (§10). L'editor si comporta in base alla category, mai leggendo il
messaggio: NotEditable (la versione e' diventata quella attiva) rilegge l'elenco versioni,
e da lì compare «Nuova versione» al posto di «Salva»,
VersionConflict apre il banner con «ricarica» / «salva come nuova versione»,
ValidationFailed apre il pannello dei problemi con il payload.
throw new FlowApiError('VersionConflict', 'Ha salvato mrossi.', undefined, 'mrossi');Un'implementazione completa e commentata sta in
projects/flow-builder-example/src/app/backend/in-memory-flow-builder-api.ts:
copre tutte le primitive, il ciclo di vita, la concorrenza ottimistica e un esecutore finto,
senza una sola richiesta di rete.
Cosa fa il builder
Canvas. Node draggabili con l'ingresso sul bordo superiore e un'uscita per ramo sul
bordo inferiore, in fila e nell'ordine del modello, secondo la mappa della §3.5:
connector, faultConnector, timeoutConnector,
rules[].connector, nextValueConnector / noMoreValuesConnector,
waitEvents[].connector, scheduledPaths[].connector. Archi colorati per kind (Fault
rosso, Rule viola ed etichettato, Default tratteggiato, LoopNext verde), isGoTo reso come
salto, pan/zoom, minimap, selezione, creazione e riassegnazione di archi trascinando,
drag-and-drop dalla palette, comando «Riordina» che ricalcola e scrive le coordinate nel
documento. I node non raggiungibili e i rami dichiarati senza destinazione sono marcati
sul canvas.
Il lato da cui gli archi entrano ed escono e' dichiarato (fConnectorConnectableSide:
TOP sull'ingresso, BOTTOM sulle uscite), non calcolato: lasciandolo calcolare a
@foblex/flow, un ramo che risale — il ritorno di un Loop, per esempio — uscirebbe di fianco e
il grafo perderebbe la lettura dall'alto verso il basso. La larghezza del node cresce a
scaglioni col numero di rami, perche' le etichette delle uscite stanno in fila: arriva da una
classe (.fb-node--outlets-N) e non da uno stile inline, perche' @foblex/flow riscrive
l'attributo style del node a ogni spostamento e uno [style.width] verrebbe cancellato. La
tabella delle larghezze e' la stessa che il comando «Riordina» passa a dagre: se divergessero,
i node larghi resterebbero sovrapposti.
Lavorare su un flow grande. Un flow di produzione arriva a quaranta elementi distribuiti su settemila pixel di canvas, e a quella scala quasi tutto quello che serve e' trovare e orientarsi. Quattro cose lo rendono possibile.
Inquadratura leggibile. «Inquadra» non scende sotto il 60%: fitToScreen non ha un minimo
e su un flow del genere chiede lo 8%, cioe' quaranta rettangoli grigi senza una etichetta
leggibile. Sotto la soglia si rinuncia a mostrare tutto e si inquadra l'inizio del flow.
Accanto ci sono i comandi di zoom con la percentuale, e il numero e' un bottone: riporta al 100%
sull'elemento selezionato.
Ricerca nel documento. La casella in alto (Ctrl+F) cerca fra tutto cio' che ha un nome
nello spazio dei nomi comune (§3.3): elementi, risorse, campi di screen dinamico, screen action,
step di orchestrazione. Scegliere un risultato porta la vista sull'elemento e apre il suo form —
e se il risultato e' un campo di uno screen dinamico, apre il form con quel campo
selezionato. La casella della palette e' un'altra cosa e lo dice: filtra i tipi che si
possono aggiungere.
Innesto su un arco. Trascinando un elemento dalla palette sopra un arco, l'arco si accende
e il rilascio lo innesta nel percorso: chi precedeva punta al nuovo elemento, il nuovo elemento
riprende da dove l'arco arrivava, e cio' che sta sotto scende per fargli spazio — solo se serve,
e in un solo passo di annulla. Quale uscita «riprende il percorso» e' una scelta semantica e
non «la prima»: una Decision riprende dal ramo default (le regole sono cio' che l'autore deve
ancora scrivere), un Loop da loopEnd, e un Custom Error non riprende affatto perche'
interrompe l'esecuzione (§5.12) — in quel caso l'editor dice che il percorso a valle e' rimasto
scollegato.
Spostare un blocco di elementi. Su un flow di produzione riordinare a mano un elemento alla
volta non e' lavoro, e' penitenza. I gesti ci sono — Ctrl+clic aggiunge o toglie un elemento
dalla selezione, Shift+trascina disegna un rettangolo, Ctrl+A prende tutto, e trascinando
uno degli elementi selezionati si spostano tutti in un solo passo di annulla — ma nessuno
di questi si vede. Per questo accanto ai comandi di zoom c'e' un interruttore «Seleziona»: acceso,
il trascinamento sul canvas vuoto disegna il rettangolo di selezione invece di spostare la vista
(che nel frattempo si muove col tasto centrale del mouse). E quando la selezione contiene piu' di
un elemento, il canvas lo dice e dice cosa farne: «trascinane uno per spostarli tutti». Un
gesto che nessuno annuncia non esiste.
Risorse. Il pannello «Risorse» e' un elenco: nome, tipo — con la classe o l'entita' accanto, perche' «Structure» da solo non dice quale, e su ventun variabili la classe si leggeva solo aprendole una per una — quante volte e' usata, i rilievi della riga. Aggiungerne una o cliccarne il nome apre una finestra dedicata con il form dentro, come per il dettaglio di un elemento — e per la stessa ragione: il form di un dynamic choice set (sorgente, due campi, ordinamento, limite, filtri) dentro la colonna laterale spingeva l'elenco fuori dallo schermo, cioe' faceva sparire il contesto per cui l'elenco esiste. La finestra si sposta prendendola per l'intestazione, non ha «Annulla» — le modifiche sono già nel documento, e a tornare indietro e' l'annulla dell'editor — e si chiude da se' se la risorsa viene rimossa.
Quante volte e' usata. Ogni risorsa porta il numero dei suoi usi; a zero
occorrenze lo dice, ed e' l'unico modo di sapere quali delle venti variabili sono morte. Il
conteggio comprende tre modi di citare un nome, e il title della riga dice quanti ce n'e'
per ognuno, perche' sono tre posti diversi in cui andare a cercare: i campi di riferimento, le
espressioni delle formule (Importo * 1.22 usa Importo) e i merge field {!…} nei
testi che il contratto dichiara — interviewLabel, il testo di un text template, fieldText e
helpText, choiceText, errorMessage (§5.12). Senza i merge field una variabile citata da tre
messaggi risultava «non usata», che e' esattamente l'informazione su cui si decide di
cancellarla.
Rimuovere una risorsa usata chiede conferma e dice quanti riferimenti resterebbero orfani:
cancellare una variabile non fa sparire nessun arco dal canvas, e senza il numero il danno si
scopriva alla validazione successiva.
Il filtro. Sopra l'elenco c'e' una casella e due interruttori, e sono per la scala: un flow
di produzione ha ventun variabili e dodici dynamic choice set, e a quella misura il problema
dell'elenco non e' leggerlo ma trovarci dentro una riga. La casella cerca nel nome,
nell'etichetta, nel tipo (objectType compreso: «address» trova le tre variabili AddressDTO) e
nelle parole dei flag (input, output, collection) — cioe' in cio' che la riga mostra, ed
e' anche il motivo per cui la riga mostra la classe — e non dentro i valori e le formule: «chi cita Totale» e' un'altra
domanda, e la risponde il conteggio degli usi. I due interruttori sono invece le domande che un
nome non sa fare: non usate e con rilievi; a zero si disabilitano, perche' un filtro che
darebbe certamente un elenco vuoto non restringe l'elenco, lo nasconde.
Il filtro non e' la ricerca del flow: quella (la casella in cima all'editor) sa dov'e' un nome che si conosce già, e arrivando su una risorsa spegne il filtro — altrimenti aprirebbe la finestra su una riga che il filtro tiene fuori dall'elenco. Qui invece si restringe un elenco che si sta guardando, e per questo il filtro resta acceso cambiando scheda: il numero accanto a ogni tipo diventa quello delle corrispondenze, ed e' cio' che dice dove sta la risorsa cercata — che il piu' delle volte e' in un'altra collection. Un elenco vuoto distingue i tre casi, perche' i rimedi sono diversi: la collection e' vuota, il filtro non trova niente qui ma trova altrove, il filtro non trova niente da nessuna parte. Aggiungere una risorsa spegne il filtro: si chiama «variabile_1» e non corrisponderebbe a niente di cio' che si stava cercando.
Copia, incolla, duplica. Ctrl+C copia gli elementi selezionati — la selezione del canvas
puo' contenerne piu' d'uno (rettangolo, Ctrl+A) — Ctrl+V li incolla, Ctrl+D li duplica in
loco; gli stessi comandi stanno nella top bar («⧉» e «⎘») e sul node selezionato («⧉»). I tasti
ascoltano sull'host del builder e si ignorano quando il fuoco e' in un campo di testo, dove
Ctrl+C deve copiare il testo.
Cosa viaggia con il blocco: la configurazione intera dell'elemento, i collegamenti interni al
blocco (quelli verso l'esterno no — sarebbero CONNECTOR_TARGET_UNKNOWN) e le risorse
referenziate, che e' cio' che rende il gesto utile su un elemento complesso. Gli appunti stanno in
localStorage, quindi valgono anche fra flow diversi e fra schede: non e' la clipboard di sistema,
che per essere letta chiede un permesso che in un iframe puo' non arrivare mai.
L'incolla passa da una conferma perche' non e' mai una copia identica, e le tre cose che cambiano le decide il contratto:
- i nomi già usati vengono rinominati, e non solo quello dell'elemento: uno spazio di nomi unico
(§3.3) comprende gli step di orchestrazione, i campi di uno screen dinamico e le screen action, e
duplicare uno screen dinamico senza rinominare i campi produceva un
NAME_DUPLICATEDper campo; - i riferimenti interni al blocco seguono i nomi nuovi (§13.8: nessuna primitiva lo fa), condizioni,
visibilityRule, trigger e output automatici compresi; - le risorse che nella destinazione non esistono si creano (con una casella per non farlo), quelle
omonime si riusano, e cio' che resta irrisolvibile — un elemento rimasto nel flow di origine —
viene elencato invece di sparire in un
REFERENCE_UNKNOWNda scoprire dopo.
Tutto l'incolla e' un solo passo di annulla.
Dettaglio dell'elemento. Con inspectorMode="dialog" (predefinito) il form si apre in una
finestra sopra il canvas: doppio click sul node, bottone «✎» sul node, comando «Dettaglio»
nella top bar, bottone «✎» (o doppio click) su un rilievo nel pannello dei problemi, un risultato
della ricerca, o creazione di un elemento dalla palette. Da qualunque di queste strade la vista si porta
sull'elemento: aprire il form giusto lasciando il grafo dov'era, su un flow grande, lascia
«dove sono» senza risposta. Il click singolo seleziona soltanto: un click capita anche solo per spostare un
node. La dialog non ha «Annulla» — le modifiche entrano nel documento mentre si digita,
esattamente come nel pannello, e tornare indietro e' compito dell'annulla dell'editor. Vive
dentro il componente e non in fondo al body: la libreria e' innestabile in una pagina
qualsiasi. Si sposta prendendola per l'intestazione: configurare un elemento richiede spesso
di guardare il grafo — quale ramo esce da dove, come si chiama l'elemento accanto — e una finestra
larga 1400px inchiodata al centro copre esattamente cio' che si deve leggere. Cio' che resta dentro
il builder e' la maniglia, non tutta la finestra: si puo' spingere quasi fuori per sbirciare
sotto, e riportarla indietro e' sempre possibile. La posizione non si azzera cambiando elemento —
solo alla riapertura.
Nelle opzioni di un campo choice il selettore e' un comando e non un campo: si sceglie
dall'elenco, oppure si scrive il nome e si conferma con Invio (commitOn="enter" del name
picker). A emettere per tasto ogni lettera diventerebbe un'opzione a se'.
Le tendine dei picker si chiudono cliccando fuori, e Esc chiude la tendina senza portarsi
via la dialog. Sul selettore di riferimento, entrare in un record o in un'istanza di classe (il
›) porta i suoi campi in cima all'elenco: sotto le radici, e in fondo a una tendina che
scorre, sembrava che il comando non facesse niente.
Palette. Costruita da elementTypes: nessun tipo e' cablato, e i due non supportati
(Step, Experiment) sono nascosti tramite isSupported. L'etichetta del ramo di fault arriva
dalla mappa delle uscite invece di essere «ramo di errore» per tutti: su uno stage di
orchestrazione quel ramo e' lo step rifiutato, non un guasto.
La stessa mappa distingue i rami di recupero da errore (isFaultRecovery: Record*,
ActionCall, ScriptCall, Wait) dagli altri, perche' lasciarne uno vuoto non significa «il
percorso finisce qui» ma «se questo fallisce l'interview termina Failed e le modifiche
vengono annullate» (§3.5). L'editor delle uscite lo dice come promemoria — non col colore
d'avviso, perche' rinunciare al recupero e' la scelta piu' comune — e il backend lo segnala
con FAULT_CONNECTOR_MISSING, che e' un Warning e non blocca niente. Lo stage di
orchestrazione ne resta fuori: la §7 non lo comprende.
Un tipo con un discriminatore compare come piu' voci, una per valore: il Collection
Processor e' «Ordina» e «Filtra», non «ordina o filtra». Sono due gesti diversi — un Sort
vuole sortOptions, un Filter vuole condizioni (§5.6) — e sceglierli dalla palette evita di
sceglierli dopo, dentro il form. L'elemento nasce con il discriminatore già scritto, con il
glifo della variante — lo stesso che avra' sul canvas — e con un nome tecnico provvisorio preso
dalla variante. core/element-variants.ts tiene la mappa tipo → campo del
discriminatore; i valori restano nel dizionario del backend (collectionProcessorTypes), e
senza quel dizionario il tipo torna a essere una voce sola.
Inspector. Un form per ogni tipo supportato: Start, Screen, Screen dinamico, Assignment, Decision, Loop, Collection Processor, Get / Create / Update / Delete / Rollback Records, Action, Script, Subflow, Wait, Custom Error, Transform, Orchestrated Stage. Più l'intestazione comune con la rinomina, che riscrive tutti i riferimenti all'elemento.
Nominare un elemento nuovo. Un elemento appena creato nasce senza etichetta, e il suo form
si apre con il cursore lì: il nome tecnico parte vuoto e si costruisce dall'etichetta mentre
la si digita, con spazi e caratteri non ammessi sostituiti da _ (§3.3). Nel documento un nome
provvisorio c'e' comunque, perché il nome e' la chiave con cui tutto raggiunge l'elemento — e' anche
cio' che il canvas mostra al posto del titolo finché l'etichetta manca — ma non e' quello che si
legge nel campo. Scrivere il nome a mano lo sgancia dall'etichetta; svuotare il campo lo rimette a
seguirla. Su un elemento che un'etichetta ce l'ha già il nome torna in sola lettura dietro il
comando «Rinomina»: da lì in poi cambiarlo riscrive i riferimenti di tutto il documento (§13.8) e
non e' un gesto da fare per sbaglio. Corollario: l'annulla riporta indietro etichetta e nome
insieme, in un passo solo, e la selezione dell'editor — che e' il nome — lo segue.
Il tipo di flow (§3.4). È la prima scelta, e nel documento non e' un campo: il tipo e' la
coppia processType + start.triggerType, e i tipi sono sei — con schermate, su richiesta,
innescato da un record, pianificato, innescato da un evento, orchestrazione innescata da un record.
La tabella con i flag arriva nei dizionari come flowTypes e non si ricopia: allowsScreens,
requiresObject, requiresSchedule, requiresEvent, requiresRegistration sono gli stessi che
applica il backend.
Tre conseguenze nell'editor:
- si sceglie alla creazione e poi resta un'etichetta, come prima; cio' che cambia e' che la tendina elenca i sei tipi e scrive due campi in una sola mutazione. Il contratto ammette il cambio di tipo (riscrivere la coppia); l'editor non lo offre, per la ragione di sempre — un documento già scritto si riempirebbe di riferimenti a globali che da quel momento non esistono;
- la coppia si legge in un posto solo,
FlowDictionaryStore.flowTypeOf. Tre dei sei tipi sonoAutoLaunched, quindi ogni domanda posta al soloprocessTypeda qui in avanti risponde male: e' il motivo per cui la palette decide di offrire gli screen daallowsScreense non piu' dalprocessType, e per cui lo Start propone i solitriggerTypesdel tipo scelto — e su un flow innescato da un record nemmeno quelli, vedi qui sotto; flowTypecompare derivato nell'elenco dei flow, nelle versioni e nell'outline, e valenullper cio' che non e' uno dei sei (unEvaluation, un'orchestrazione senza trigger): lì si mostra ilprocessType, e non e' un difetto da correggere.
Quando parte un flow innescato da un record (§3.4). I campi sono due — recordTriggerType è
l'operazione, triggerType il momento rispetto alla scrittura — ma non sono due assi liberi,
perché l'eliminazione ha un momento solo: le coppie che significano qualcosa sono sei. Da due tendine
indipendenti si dichiara «prima dell'eliminazione» insieme a «alla creazione», e il runtime la legge
come un'eliminazione — con RecordBeforeDelete l'operazione la dice il trigger e recordTriggerType
non viene guardato — cioè il flow parte su un'operazione che nessuno ha scelto.
Quindi su questi flow l'inspector dello Start non ha la tendina «quando parte»: chiede
l'oggetto, poi l'operazione, poi i criteri, poi il momento fra i soli che l'operazione
ammette. Quali siano lo dice la voce di recordTriggerTypes nei dizionari — triggerTypes,
defaultTriggerType, allowsTimingChoice, usesPriorRecord — e sono gli stessi flag che applica il
backend: proporre i triggerTypes di primo livello significa proporre coppie che la validazione
rifiuta. Quattro conseguenze:
- i due campi si scrivono insieme, in una sola mutazione: con due, l'annulla riporterebbe indietro il momento lasciando l'operazione nuova, cioè una coppia che non è nessuna delle sei;
- dove il momento è unico la domanda non si pone e il campo si scrive comunque, perché il contratto chiede entrambi;
doesRequireRecordChangedToMeetCriteriacompare solo dove l'operazione portausesPriorRecord: alla creazione e all'eliminazione non c'è un «prima» da confrontare, e il flag resterebbe inerte;START_TRIGGER_MISMATCHda questa sequenza non è esprimibile. Lo si incontra solo aprendo un documento scritto altrove, e lì l'editor lo dice e offre il comando per rimettere il momento fra quelli ammessi, invece di correggerlo da sé — quale metà della coppia sia quella voluta non lo sa. Gli altri due rilievi sono avvisi: operazione assente su un trigger di salvataggio vale «ogni salvataggio» (START_RECORD_TRIGGER_MISSING), operazione su un flow che non è innescato da un record è inerte (START_RECORD_TRIGGER_IGNORED).
I valori precedenti del record non ci sono sempre (§7). Chi li legge — l'operatore IsChanged,
un riferimento radicato su $Record__Prior, il flag doesRequireRecordChangedToMeetCriteria — vale
quanto l'operazione che innesca il flow, non quanto il momento. È l'unico codice con tre
gravità: sull'aggiornamento i valori ci sono a ogni esecuzione e non c'è niente da dire; su
«creazione e aggiornamento» sulla creazione non ci sono, e lì la condizione non è valutabile — il
flow funziona a metà, quindi Warning; sulla creazione e sull'eliminazione non ci sono mai e a
runtime l'interview fallisce davanti a chi stava salvando, quindi Error. Su un flow che non è
innescato da un record resta un avviso: i valori può passarli chi avvia l'interview.
La regola sta in core/record-trigger.ts — priorValuesOfDefinition, priorValuesSeverity,
priorCriteriaSeverity — ed è esportata perché la applicano i due capi: l'editor la dice mentre
si scrive la condizione (condition editor, editor dei filtri, reference picker), il backend la
emette come PRIOR_VALUES_NOT_AVAILABLE. Tre conseguenze:
- il flag dei criteri di ingresso è l'eccezione: il runtime lo ignora invece di fallire, quindi non è mai un errore, e su «creazione e aggiornamento» non è nemmeno un rilievo;
- nel reference picker
$Record__Priorha uno stato suo, guardato prima degli altri: un host che smette di proporre quello scope — ed è giusto che lo faccia dove i valori non ci sono mai — lo farebbe altrimenti cadere in «questo nome non esiste», che manda a cercare un nome sbagliato; - le formule restano fuori: un'espressione che cita
$Record__Priornon è analizzabile lato client, perché la sintassi la conosce solo il motore (§6.3).
Con quali permessi accede ai dati (§3.1). È runInMode del radice, il «How to Run the Flow» di
Flow Builder, e sta nelle avanzate dell'inspector dello Start: è lì che si legge il tipo di flow, e
il tipo decide se il contesto utente ha senso. Le voci arrivano dai dizionari (runInModes), con la
description sotto la tendina e come tooltip, perché dal nome la differenza fra le quattro non si
capisce. Tre cose da sapere:
- il backend dei flow non ne applica nessuna: le inoltra all'ospite con ogni lettura e scrittura di record, ed è l'ospite a tradurle — il form lo dice, invece di promettere un controllo che non c'è. Action e script restano fuori, e un subflow che non sceglie eredita la modalità del chiamante;
DefaultModesi scrive togliendo il campo, che è ciò che il contratto legge come predefinito: chi torna al predefinito salva lo stesso documento di chi non ha mai toccato la tendina;UserModeha senso solo dove qualcuno avvia il flow, e lo dice il flagallowsUserModediflowTypes: dove è falso la voce non si propone. Dal form quindiRUN_IN_MODE_NOT_SUPPORTEDnon nasce; su un documento scritto altrove la voce resta visibile — unselectbianco nasconderebbe il valore di cui l'avviso parla — e la ripulitura è un comando. La regola èisRunInModeUnsupportedincore/run-in-mode.ts, esportata perché la applicano i due capi.
Il lato destro di una condizione (§4.3). Lo decide rightSide dell'operatore, non isUnary.
I quattro controlli sono un valore (Value), l'esito atteso degli unari (ExpectedOutcome, il
selettore «è / NON è»), niente (None) e un intero N (Count). I ventidue operatori sulle date sono
il motivo: IsToday e HasValue non sono unari e non leggono nessun esito atteso, quindi il selettore
di IsNull scriverebbe lì un false che il motore ignora e che non nega niente. Tre conseguenze:
- cambiando operatore il lato destro si tiene solo se il nuovo lo legge allo stesso modo — da
«negli ultimi N giorni» a «nei prossimi N mesi» N resta, da
IsOnaIsTodayla data se ne va — e suNonetogliere è anche ciò che evita di produrre, con un gesto dell'editor,CONDITION_RIGHT_VALUE_IGNORED. Quell'avviso quindi arriva da documenti scritti altrove: il form lo dice e offre «Togli il valore»; - N è un intero non negativo: un letterale negativo è
CONDITION_COUNT_INVALID, un testoCONDITION_TYPE_MISMATCH, un decimale conta per la sola parte intera e si dice senza colore. La regola ècountValueIssue, erightSideOf— con il ripiego suisUnarydove il dizionario non dichiararightSide— sta accanto incore/condition-types.util.ts: la usano editor e validatore; - gli operatori sulle date hanno
appliesTo: ["Date"]e il menu li filtra come gli altri, cioè compaiono anche dove il tipo del lato sinistro non si conosce (un output automatico): non sapere non è sapere che è sbagliato. Nei filtri sui record non esistono (§4.4), e lì non arrivano: quel menu leggerecordFilterOperators.
L'attivazione, e la sua seconda meta' (§8). I tre tipi che non partono da soli vanno presi in
carico dall'infrastruttura dell'ospite — uno scheduler, un bus, un intercettore delle scritture — e
il backend li registra prima di attivare. Da lì due esiti che il pannello dei problemi non spiega,
perche' non parlano della definizione: MissingService (su questa installazione nessuno prende in
carico quel tipo di flow: la definizione e' valida, «riprova» non ha senso) e RegistrationFailed
(l'host ha rifiutato: «riprova» ce l'ha). In entrambi i casi la versione non e' attiva, e la
sessione ricarica l'elenco versioni invece di lasciar credere all'esito; su un salvataggio con
activate: true la bozza resta salvata e non attivata. Le due frasi le compone
describeActivationError, in un posto solo perche' ad attivare sono in due.
Screen dinamico (§5.2). È un compositore, non un form, e per questo la dialog e' larga il
doppio di com'era: tre colonne. A sinistra la palette (due schede — i componenti, e i campi di
un'entita' che portano tipo ed etichetta dallo schema), al centro la schermata disegnata (si
clicca il campo da configurare, lo si trascina dove deve stare, la larghezza in dodicesimi si
vede), a destra le proprieta' di cio' che e' selezionato — o della schermata, se non c'e'
selezione, che e' anche cio' che si ottiene cliccando l'anteprima fuori dai campi. L'anteprima e'
inerte di proposito: i controlli non ricevono il puntatore, perche' un input che accetta testo in
un editor di metadati fa credere di star compilando la schermata. Quali proprieta' mostrare lo
decide il dizionario
(screenFieldTypes e i suoi flag storesValue, isCollection, acceptsChoices, isContainer,
isRepeatingSection, requiresDataType), non il codice: sono gli stessi flag che applica il
runtime, e reimplementarli porta a proporre configurazioni che il motore rifiuta.
Tre cose che il modello non mostra e il form dice:
- un campo che raccoglie un valore e' una risorsa del flow, referenziabile per nome ovunque
e di sola lettura — il suo nome vive nello spazio dei nomi comune, e rinominarlo riscrive i
riferimenti (
FlowDocumentStore.renameScreenField, come per i node: nessuna primitiva del backend lo fa); - le regole di visibilita' si rivalutano sui valori appena inviati, i campi nascosti vengono azzerati e solo i visibili vengono validati;
choiceReferencesaccetta soloChoiceeDynamicChoiceSet, e gli output automatici di unComponentInstancesono esclusivi con i suoioutputParameters;autoSelectSingleChoicefa prendere al campo l'unica opzione risolta, quando e' una sola: e' la tendina che dipende da un'altra tendina — i CAP di un comune — dove chiedere di scegliere non decide niente. La spunta compare solo dove il tipoacceptsChoices, e altrove e'SCREEN_FIELD_CONFIGURATION_INVALID(avviso);formatdice che cosa un campo di testo raccoglie —Email,Url,Phone,MobilePhone,LandlinePhone— e serve a due cose insieme: il frontend ne ricava il tipo di input, il runtime rifiuta l'invio sbagliato (FORMAT_INVALID). È l'unico dizionario dichiaratamente aperto (screenFieldFormats): i pattern non stanno nel metadata — che cosa sia un cellulare valido e' una regola del paese in cui il sistema gira — e l'host ne registra dei propri. Da lì le due gravita', che sono l'eccezione alla regola di tutti gli altri cataloghi: fuori elenco e' un'osservazione (SCREEN_FIELD_FORMAT_UNKNOWN), scritta senza il colore d'avviso, perche' bloccare l'attivazione per un nome che la libreria non conosce sarebbe un falso positivo; su un campo che non raccoglie un solo testo e' l'avvisoSCREEN_FIELD_FORMAT_NOT_APPLICABLE, e il controllo non compare affatto — la condizione e' il tipo di dato piu' la cardinalita', ed e' scritta una volta sola (formatApplies, la stessa che applica il validatore della demo). Due cose che il formato non e': non e' un'etichetta che il flow possa leggere — se a valle serve sapere che un numero e' un cellulare, quella distinzione sta nel dato — e non riscrive il valore, che e' una trasformazione e la fa una screen action.
Il riordino e l'annidamento si fanno trascinando (@angular/cdk/drag-drop, senza drop list: il
bersaglio si calcola dal DOM) oppure con i comandi «su / giu' / porta fuori».
La sezione ripetibile (§5.2). RepeatingSection e' N copie della stessa struttura di campi
sulla stessa schermata — N punti di fornitura, N referenti, N righe di un ordine — e un tasto con
cui l'utente decide quante. Il flag che l'accende e' isRepeatingSection, ed e' distinto da
isContainer di proposito: contiene campi, ma quelli che contiene sono il modello di una riga,
non campi della schermata.
Da quella distinzione discende tutto cio' che l'editor fa di diverso lì dentro, e sono le quattro cose che un editor ingenuo sbaglia lasciandole comporre per poi accusarle:
i campi di una riga non sono risorse. Esistono in una copia per ogni elemento della collection, quindi il loro nome non individua alcun valore: non compaiono fra i riferimenti proponibili (
resourceFieldsOfli esclude, e la demo li toglie anche daPOST /flows/references) e nominarli e'SCREEN_FIELD_IN_REPEATER— un codice diverso da «il nome non esiste», perche' quel nome nel documento c'e' e il rimedio e' un altro. Il nome resta comunque unico, perche' e' l'indirizzo con cui il frontend rende il campo: un omonimo di una variabile e'NAME_DUPLICATEDcome sempre, escreenFieldNamesli comprende tutti;dentro una riga si nomina il cursore.
collectionReferencee' dove vivono le righe (una variabile collection diStructure),itemReferencee' il cursore su cui il runtime appoggia la riga corrente — lo stesso ruolo diassignNextValueToReferencein un Loop. Il pannello lo dice, e i selettori di riferimento del campo lo propongono per primo (preferredRoots, che riordina e non aggiunge niente). Ogni incoerenza fra i due — mancante, costante invece di variabile, collection scambiata col cursore, classi diverse — e'REPEATING_SECTION_INVALID, e l'editor la dice prima della validazione;due cose che fuori sono lecite qui non lo sono, e non si propongono affatto: la tendina dei
fieldTypedentro una riga non offre la sezione ripetibile (non si annidano: il cursore e' una variabile sola per sezione) e la spunta degli output automatici di unComponentInstancesparisce — si esporrebbero come<NomeCampo>.<NomeOutput>, cioe' una risorsa sola per tutte le righe. Un campo di riga compare invece fra gli inneschi di una screen action, che allora gira per riga (vedi Screen action, sotto);memberNamee il riferimento sono la stessa domanda posta due volte.memberName: "PodId"ePuntoCorrente.PodIdsono lo stesso membro, e la classe da cui vengono e' l'objectTypedella variabile incollectionReference: il campo si sceglie da<fb-structure-member-picker>conusage="writable", perche' un membro calcolato e'TARGET_NOT_WRITABLE.le righe minime e le righe iniziali non sono la stessa cosa, ed e' la distinzione che serve piu' spesso.
minInstancese' un vincolo: sotto quel numero la sezione non scende, e con1il runtime non lascia togliere l'ultima riga.initialInstancese' una comodita': la schermata si apre con qualche riga pronta, e chi non la vuole la elimina. Un modulo con un punto luce e un punto gas gia' aperti, dove chi ha una sola commodity toglie quello che non gli serve, e'initialInstances: 1conminInstances: 0— col minimo a 1 sarebbe un modulo che obbliga a entrambe. Le righe iniziali si applicano una volta sola, e solo finche' nessuno ha scritto la collection: righe arrivate da un ciclo o da una schermata precedente sono le righe iniziali e non vengono diluite con righe vuote. Fuori dai limiti il runtime le riporta dentro, quindi l'editor lo dice come avviso e non come errore: il difetto non e' che qualcosa si rompa, e' che il numero scritto non e' quello che si vedra'.
Nell'anteprima si disegna una riga, ed e' il modello: a runtime la schermata ne rende una copia per elemento. Disegnarne due sarebbe finto — quante siano lo decide chi compila.
Transform (§5.13). Anche questo e' un compositore e non un form: tre colonne — le sorgenti, la destinazione, il dettaglio della riga scelta. Si mappa trascinando una sorgente su una destinazione, oppure selezionando la destinazione e cliccando la sorgente; il bersaglio del rilascio si calcola dal DOM, come nello screen dinamico.
Le due cose che il modello non mostra e la colonna centrale dice:
- le righe sono le destinazioni, non le azioni. Vengono dal catalogo del risultato — i membri
della classe (§4.7.1) o i campi dell'entita' (§4.4) — quindi si vede anche cio' che non e'
ancora mappato, che nell'elenco delle
transformValueActionsnon compariva. Una riga esiste prima dell'azione: la prima modifica la crea. Un membro calcolato non c'e' affatto, perche' assegnarlo e'TARGET_NOT_WRITABLE. Dove il catalogo non e' autorevole — una classe che non dichiara i membri, un risultato scalare — le righe tornano a essere le azioni e il nome si digita, senza che nessuno venga accusato di non esistere; - le sorgenti si navigano. Sono i riferimenti di
POST /flows/references, e dentro un record o una classe si entra per proporne campi e membri, con la regola del reference picker: gli output automatici si propongono e non si giudicano (§4.5).aggregationFielddi unSumresta il nome di un campo (stringValue), ma dove il tipo della collection aggregata e' noto lo si sceglie da un elenco invece di ricordarlo.
Le regole della schermata (§5.2). validationRules sono le verifiche che non appartengono a
nessun campo: «almeno un punto fra le due sezioni», «la somma delle percentuali fa cento». Si
modellano nelle proprieta' della schermata, accanto alle action, e a runtime arrivano come
SCREEN_RULE — l'unico codice di validazione che puo' presentarsi senza fieldName.
Perche' esistono: la validationRule di un campo giudica un valore, e su una sezione ripetibile
e' addirittura inerte (una sezione non raccoglie un valore proprio). L'alternativa storica era un
node a valle dello screen, che pero' non blocca la schermata — l'interview avanza e poi torna
indietro — e mette una validazione dentro il disegno del flow.
Tre cose che il form mette a posto:
errorMessagee' obbligatorio (SCREEN_RULE_INVALID): senza, la schermata si rifiuta di avanzare senza dire perche', e un messaggio non si puo' costruire — non c'e' un campo da nominare;- si scrivono con le condizioni, non con la formula, ed e' il consiglio che il pannello ripete:
IsEmptysu una collection e' un operatore che il runtime valuta, mentre la stessa domanda in una formula chiede al motore una funzione che potrebbe non avere.Formularesta unconditionLogicammesso — qui sì, a differenza di unfilterLogic— quindi l'editor la offre lo stesso; - il
fieldNamefacoltativo pesca dai campi della schermata senza quelli dentro una riga: un campo che esiste in una copia per elemento non individua un controllo a cui agganciare il messaggio. Non e' l'elenco degli inneschi, che i campi di riga li comprende.
Screen action (§5.2). Un campo che l'utente compila puo' innescare un'action i cui risultati
finiscono negli altri campi della stessa schermata: e' il caso «scrivi il codice fiscale e nome e
cognome compaiono da soli», e senza di essa lo si farebbe spezzando lo screen in due con un elemento
Action in mezzo. Si modellano nelle proprieta' della schermata (cliccare l'anteprima fuori dai
campi), in due elenchi che il contratto tiene separati — actions e triggers — perche' piu'
trigger possono invocare la stessa action. Identificazione e parametri sono quelli di un elemento
Action, ACTION_UNKNOWN e PARAMETER_UNKNOWN compresi.
Le cose che riguardano l'editor, e non il runtime:
- il picker della destinazione e' allargato qui e solo qui. L'
assignToReferencedi un output accetta anche i campi di questa schermata, che ovunque altrove sono di sola lettura:POST /flows/references/writablecontinua a escluderli — e giustamente, un Assignment che ci scrive restaTARGET_NOT_WRITABLE. L'allargamento e' quindi un parametro del chiamante (<fb-reference-picker [extraReferences]>), non un comportamento del picker; - il nome dell'action sta nello spazio dei nomi del flow (§3.3): con l'output automatico e' la
radice di
Cerca.nome, quindi un'omonimia con una variabile — o con unaStructurechiamata come lei — renderebbe ambiguo quel riferimento.usedNameslo comprende, e il form lo dice conNAME_DUPLICATEDcome per il nome di uno step; - un'action senza output e' un avviso (
SCREEN_ACTION_WITHOUT_OUTPUTS): viene invocata e il suo risultato si perde. L'output automatico non e' la cura per tutti i casi — rende il risultato leggibile comeCerca.nomema non scrive in nessun campo: e' la forma per una condizione, non per precompilare; - un trigger senza campo e senza
initBehaviornon scattera' mai (SCREEN_TRIGGER_WITHOUT_CAUSE), e l'unicoinitBehaviorammesso e'runOnLoad— ilrunOnRevisitdella specifica Salesforce e'SCREEN_TRIGGER_INIT_BEHAVIOR_UNKNOWN, e vale la pena saperlo perche' chi arriva da quella specifica lo scrive per abitudine. Dove il campo osservato ha undefaultValueil form dice cherunOnLoadnon serve: un campo precompilato dal flow conta come un campo che ha preso un valore, e l'action gira comunque all'arrivo; - un trigger puo' osservare un campo di riga, e allora l'action gira per ogni riga in cui
quel campo e' cambiato (Una screen action per ogni riga).
triggerFieldNamee' l'unico posto in cui il nome di un campo di riga si scrive, perche' nomina un campo e non un riferimento: condizioni, input e output nominano la riga attraverso il cursore (PuntoLuce.ProductId), e i selettori lo propongono per primo. Da quel momento le destinazioni dipendono da chi invoca l'action, e le sbagliate sonoSCREEN_ACTION_ROW_TARGET_INVALID: da un trigger di riga il cursore intero, la collection delle righe, le righe di un'altra sezione e l'output automatico (che l'editor non propone nemmeno); da uno di primo livello il cursore, che fuori dalla riga non porta nessuna riga. Il rilievo sta sull'output e nomina il trigger, perche' con due trigger — uno di riga e uno no — la stessa destinazione e' giusta per l'uno e sbagliata per l'altro. La regola e'screenActionRowTargetIssues(core/screen-field.util.ts), e la applica anche il validatore della demo; - anche un
ComponentInstancepuo' innescare, in una riga o fuori. Non e' una risorsa e non ha un valore proprio, quindi scatta quando uno dei suoi output cambia rispetto a cio' che la destinazione conteneva. Il form lo dice sotto il trigger, e avverte quando il componente non dichiara output — ne' parametri di uscita con una destinazione, ne' l'output automatico — perche' allora non c'e' niente che possa cambiare e il trigger non scatta mai. La §7 non ha un codice per questo caso, quindi resta un avviso del form e il validatore non lo emette.
Le action si aprono una alla volta: i cataloghi delle action e dei loro parametri dipendono da
actionType/actionName, e tenerne N in volo sarebbe N volte la corsa fra risposte che l'inspector
dell'Action risolve scartando quelle superate.
Sullo stage di orchestrazione il form dice le tre cose che il modello non mostra: gli step
non sono una sequenza (le frecce riordinano l'esame, non l'esecuzione), ingresso e uscita
non sono simmetriche (ingresso falso = step saltato, uscita falsa = stage in stallo), e
faultConnector e' il ramo dello step rifiutato, non un ramo di guasto — senza, un rifiuto
fa fallire l'interview. Una condizione che referenzia l'output di un altro step viene segnalata
con la via d'uscita: scrivere quel risultato in una variabile.
Risorse. I sette tipi, con i vincoli visibili: dataType obbligatorio, objectType per
Object / Enum / Structure, scale solo sui numerici, costanti che non possono referenziare
risorse, stageOrder distinti.
Dynamic choice set. Le sorgenti sono tre e si escludono a vicenda (§5.2): una collection
del flow, una query su un'entita', i valori di un tipo enum. Nel form sono tre pulsanti e non
tre campi da riempire a caso: sceglierne una cancella le altre — dichiararne due e'
CHOICE_SET_SOURCE_AMBIGUOUS — e cancella anche i campi citati, perche' Label non e' un campo
di un'entita' e Ragione_Sociale non e' una proprieta' di un valore di enum. Sulla sorgente enum
cambiano due cose: il tipo si sceglie da listEnumTypes e fuori catalogo e' un errore
(ENUM_TYPE_UNKNOWN, l'elenco qui e' autorevole quando non e' vuoto), e i campi citabili sono le
tre proprieta' di un valore — Name, Label, NumericValue — che arrivano dal dizionario
(enumChoiceSetFields) e non da un catalogo di campi. Le sa core/enum-choice-set.util.ts,
compreso il tipo di ciascuna: senza, il valore di un filtro su NumericValue sarebbe una
casella di testo. displayField e valueField hanno un default (Label e Name) e il form li
mostra come tali, cosi' non finiscono nel documento senza motivo.
E la sorgente decide anche quali campi esistono: limit, displayField e valueField
valgono su tutt'e tre, ma filters, filterLogic, sortField e sortOrder valgono sulle sole
object — dove diventano la query — ed enumType, dove il runtime li valuta in memoria. Su una
collectionReference le opzioni sono gli elementi della collection nell'ordine in cui ci
sono: il form non offre quei controlli, e se il documento li porta lo dice con
CHOICE_SET_FILTERS_IGNORED — un avviso, perche' il flow gira e il campo funziona, solo
propone tutto; il difetto e' che chi l'ha scritto crede di aver selezionato qualcosa. La
ripulitura e' un comando («Togli»), non un effetto dell'apertura del form: cancellare i filtri di
un flow scritto altrove farebbe sparire l'unico indizio di cosa doveva selezionare. Il rilievo si
vede in tutti e due i posti, elenco e form, come gli altri controlli locali.
Da dove vengono i due campi su una collection. Su collectionReference non c'e' nessun object a cui
chiedere i campi: i nomi citabili sono quelli del tipo degli elementi, che il form legge da
getReferences (dataType + objectType della collection scelta, output automatici compresi) — un
Object propone i campi dell'entita', una Structure i membri della classe, la stessa strada del
campo da sommare di un Transform. Su questa sorgente nessuno dei due e' obbligatorio (su una
collection di scalari vanno lasciati vuoti) e la validazione non li verifica: un nome sbagliato
ricade in silenzio sull'elemento intero. Per questo l'elenco si propone davvero, e un nome fuori
elenco si dice come avviso senza codice (unverified dello structure member picker): accusarlo
con STRUCTURE_MEMBER_UNKNOWN prometterebbe un rilievo che il backend lì non emette. object resta
vuoto: valorizzarlo per far funzionare il picker sarebbe CHOICE_SET_SOURCE_AMBIGUOUS.
La logica dei filtri. filterLogic esiste anche sui choice set, con le stesse forme degli
elementi che interrogano il database: AND (o assente), OR, oppure l'espressione sugli indici
1-based dei filtri (1 AND (2 OR 3)), che l'editor verifica mentre si scrive e riscrive quando
si cancella un filtro — senza, l'espressione resterebbe valida puntando al filtro sbagliato, che
e' il peggiore dei due esiti. Il valore Formula invece non e' supportato da nessun elemento:
non lo scrive nessun gesto dell'editor — la modalita' «Formula» scrive filterFormula, che e' un
campo suo — e se arriva da un documento scritto altrove si vede un rilievo che dice cosa fare, non
una modalita' che sembra buona.
Classi (Structure). L'ottavo tipo di dato non e' un doppione di Object (§4.7): di
un'istanza di classe il flow legge e scrive i membri. Cambia quindi il form: la classe si
scoglie con <fb-structure-picker> e la sua assenza e' un errore — senza classe il runtime
non ha nulla da istanziare — mentre il campo «valore iniziale» non c'e' affatto, perche' non
esiste un structureValue e la variabile parte con un'istanza vuota. I membri si scelgono con
<fb-structure-member-picker>, e nelle destinazioni si propongono solo quelli scrivibili: un
membro calcolato si legge in una condizione ma assegnarlo e' TARGET_NOT_WRITABLE. Lo stesso
vale dentro <fb-reference-picker>, dove Richiesta.Ragione si propone e si verifica
(STRUCTURE_MEMBER_UNKNOWN). Un Transform con target Structure compone l'istanza intera in un
elemento solo: lì outputFieldApiName e' il nome di un membro, e il form lo dice invece di far
indovinare (§5.13).
Percorsi annidati. I percorsi con il punto non sono piu' un buco nero: la validazione li segue,
e quindi li segue l'editor. core/path-navigation.ts naviga la catena una tappa alla volta —
objectType di un membro Structure porta la classe annidata, quello di un membro Object porta
un'entita' dello schema, e da lì si continua con i campi (§4.7.1) — ed e' lo stesso codice per
i membri di una classe e per i percorsi di relazione di un record (Cliente.Citta, §4.4). Tre
conseguenze visibili: i pickers propongono il livello in cui si sta scrivendo e hanno un «›»
per scendere di uno; un refuso in mezzo (Richiesta.Clente.Nome) e' segnalato subito, con l'elenco
di cio' che esiste in quella tappa; e dove la catena si interrompe — un membro senza
objectType, una classe annidata non descritta, una collection — il rilievo e' l'avviso
PATH_NOT_VERIFIABLE con il motivo, che vuol dire "non lo so" e non impedisce di scrivere.
Restano fuori i percorsi radicati sul risultato automatico di un elemento
(Leggi_Contatti.Owner.Name): la validazione non li verifica, e l'editor non accusa cio' che il
backend non verifica.
Il filtro di tipo di un campo (§6.4) lo applica l'editor: la primitiva prende il documento nudo
e restituisce tutti i riferimenti, così la stessa risposta serve tutte le tendine di una schermata
(§13.17). La regola sta in core/reference-filter.util.ts, ed e' la stessa per le radici e per i
segmenti di un percorso. Il filtro guarda il tipo della radice, e quindi non basta: in un
parametro String una variabile Structure non e' compatibile, ma Richiesta.Ragione sì. Per
questo <fb-reference-picker> tiene l'elenco completo accanto a quello filtrato, e ne ricava
due cose: i contenitori compaiono come righe in cui si entra — cliccarle apre il percorso
invece di scegliere un valore del tipo sbagliato — e le radici si riconoscono tutte, così un
Leggi_Ordine.Numero già scritto in un campo String non viene accusato di non esistere. Nelle
proposte non entra comunque nessun valore del tipo sbagliato: i segmenti li filtra la stessa
regola, tenendo solo quelli da cui si scende. In un campo di destinazione le due liste sono due
richieste — getWritableReferences per le proposte, getReferences per le radici — perche' la
scrivibilita' e' del percorso e non della radice: un $User.Anagrafica.Citta scritto a mano e'
TARGET_NOT_WRITABLE, non un nome che non esiste.
C'e' un posto in cui alle proposte si aggiunge qualcosa che la primitiva non restituisce, e
extraReferences esiste solo per quello: l'output di una screen action, che si assegna a un campo
della schermata che si sta modificando (§5.2). Non e' una scorciatoia per riferimenti che il backend
non conosce — quei campi restano esclusi da references/writable, e altrove assegnarli e'
TARGET_NOT_WRITABLE: e' l'unico punto in cui la regola del contratto e' diversa, e per questo
l'elenco lo passa il chiamante.
Le globali si navigano come le risorse. Un percorso dichiarato dall'host puo' portare un
contenitore, e allora objectType dice di che tipo: $User.Anagrafica.Citta si propone e si
verifica come Richiesta.Ragione. La regola da non sbagliare e' che vince il prefisso dichiarato
piu' lungo — non tutto cio' che segue lo scope — perche' altrimenti un percorso giusto viene
segnalato come rotto (§4.1). Dove il catalogo dichiara il contenitore ma non il tipo il rilievo e'
PATH_NOT_VERIFIABLE, e la correzione sta nel catalogo dell'host; navigare uno scalare
($User.Email.Dominio) e' invece GLOBAL_UNKNOWN. Sulle destinazioni non cambia niente da
sapere: le globali sono di sola lettura, membri compresi — le assegnabili sono $Flow.CurrentStage,
$Flow.ActiveStages e i campi di $Record — e il picker lo dice con TARGET_NOT_WRITABLE invece
di far sembrare il nome inesistente (§5.2).
Uno scope puo' nominarsi da solo. $Profile senza percorso e' un riferimento valido se — e solo
se — l'host lo dichiara così, con una voce senza punto che porta il suo dataType e il suo
objectType (§4.1): e' come un host espone l'utente corrente come un'unica istanza invece di
elencarne i campi, e da lì la navigazione riparte, quindi $Profile.Livello si verifica sui membri
della classe senza che il catalogo dichiari un percorso per ognuno. Il prefisso dichiarato piu' lungo
e' la stessa regola di prima: la voce senza punto e' il prefisso vuoto, cioe' quello che vince
solo quando nessun altro corrisponde. Dove lo scope dichiara percorsi ma non la voce senza punto,
$Profile da solo resta GLOBAL_UNKNOWN: nella demo e' la differenza fra $Profile (dichiarato
così) e $User (che dichiara i suoi campi).
Su una destinazione la colonna che decide non e' la stessa nei due mondi: un membro porta il proprio
isWritable, un campo no. Fuori da un Create o da un Update il contesto non c'e' — un Assignment su
Cliente.Codice non sa se quel record verra' creato o aggiornato — quindi lì basta isCreateable
oppure isUpdateable, e un campo che non e' ne' l'uno ne' l'altro e' TARGET_NOT_WRITABLE in
qualunque contesto (§5.8).
Dei tre esiti di describeStructure il picker mostra tre messaggi diversi, perche' portano a
gesti opposti: con l'elenco autorevole un nome fuori elenco e' un errore; con members: [] la
classe e' valida ma i membri non sono dichiarati e il nome si digita, senza accuse; con un
404 (StructureNotFound) e' la classe a essere sbagliata, e il messaggio manda a correggere
quella invece del membro. L'intestazione dell'elenco usa la label della classe, che e' la
ragione per cui la risposta ha un involucro invece di essere un array.
Oggetti e campi. Ogni punto che chiede un'entita' o un suo campo usa lo stesso controllo:
<fb-object-picker> e <fb-field-picker>, una casella con autocomplete che filtra il catalogo
mentre si scrive. Un <select> andava bene con dieci entita', non con trecento; e dove il
catalogo non arriva il controllo resta lo stesso, con un avviso, invece di degradare in un
campo di testo cieco. La scrittura libera e' voluta
