npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

  1. Integrazione in tre passi
  2. L'interfaccia verso il backend
  3. Cosa fa il builder
  4. Architettura
  5. Dipendenze esterne
  6. Tema e personalizzazione
  7. Le trappole della §13, una per una
  8. 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 mancano

Le 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_DUPLICATED per 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_UNKNOWN da 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 sono AutoLaunched, quindi ogni domanda posta al solo processType da qui in avanti risponde male: e' il motivo per cui la palette decide di offrire gli screen da allowsScreens e non piu' dal processType, e per cui lo Start propone i soli triggerTypes del tipo scelto — e su un flow innescato da un record nemmeno quelli, vedi qui sotto;
  • flowType compare derivato nell'elenco dei flow, nelle versioni e nell'outline, e vale null per cio' che non e' uno dei sei (un Evaluation, un'orchestrazione senza trigger): lì si mostra il processType, 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;
  • doesRequireRecordChangedToMeetCriteria compare solo dove l'operazione porta usesPriorRecord: alla creazione e all'eliminazione non c'è un «prima» da confrontare, e il flag resterebbe inerte;
  • START_TRIGGER_MISMATCH da 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__Prior ha 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__Prior non è 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;
  • DefaultMode si 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;
  • UserMode ha senso solo dove qualcuno avvia il flow, e lo dice il flag allowsUserMode di flowTypes: dove è falso la voce non si propone. Dal form quindi RUN_IN_MODE_NOT_SUPPORTED non nasce; su un documento scritto altrove la voce resta visibile — un select bianco nasconderebbe il valore di cui l'avviso parla — e la ripulitura è un comando. La regola è isRunInModeUnsupported in core/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 IsOn a IsToday la data se ne va — e su None togliere è 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 testo CONDITION_TYPE_MISMATCH, un decimale conta per la sola parte intera e si dice senza colore. La regola è countValueIssue, e rightSideOf — con il ripiego su isUnary dove il dizionario non dichiara rightSide — sta accanto in core/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 legge recordFilterOperators.

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;
  • choiceReferences accetta solo Choice e DynamicChoiceSet, e gli output automatici di un ComponentInstance sono esclusivi con i suoi outputParameters;
  • autoSelectSingleChoice fa 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 tipo acceptsChoices, e altrove e' SCREEN_FIELD_CONFIGURATION_INVALID (avviso);
  • format dice 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'avviso SCREEN_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 (resourceFieldsOf li esclude, e la demo li toglie anche da POST /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_DUPLICATED come sempre, e screenFieldNames li comprende tutti;

  • dentro una riga si nomina il cursore. collectionReference e' dove vivono le righe (una variabile collection di Structure), itemReference e' il cursore su cui il runtime appoggia la riga corrente — lo stesso ruolo di assignNextValueToReference in 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 fieldType dentro 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 un ComponentInstance sparisce — 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);

  • memberName e il riferimento sono la stessa domanda posta due volte. memberName: "PodId" e PuntoCorrente.PodId sono lo stesso membro, e la classe da cui vengono e' l'objectType della variabile in collectionReference: il campo si sceglie da <fb-structure-member-picker> con usage="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. minInstances e' un vincolo: sotto quel numero la sezione non scende, e con 1 il runtime non lascia togliere l'ultima riga. initialInstances e' 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: 1 con minInstances: 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 transformValueActions non 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). aggregationField di un Sum resta 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:

  • errorMessage e' 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: IsEmpty su 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. Formula resta un conditionLogic ammesso — qui sì, a differenza di un filterLogic — quindi l'editor la offre lo stesso;
  • il fieldName facoltativo 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'assignToReference di un output accetta anche i campi di questa schermata, che ovunque altrove sono di sola lettura: POST /flows/references/writable continua a escluderli — e giustamente, un Assignment che ci scrive resta TARGET_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 una Structure chiamata come lei — renderebbe ambiguo quel riferimento. usedNames lo comprende, e il form lo dice con NAME_DUPLICATED come 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 come Cerca.nome ma non scrive in nessun campo: e' la forma per una condizione, non per precompilare;
  • un trigger senza campo e senza initBehavior non scattera' mai (SCREEN_TRIGGER_WITHOUT_CAUSE), e l'unico initBehavior ammesso e' runOnLoad — il runOnRevisit della 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 un defaultValue il form dice che runOnLoad non 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). triggerFieldName e' 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 sono SCREEN_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 ComponentInstance puo' 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