@esfaenza/flow-builder 20.3.34 → 20.3.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3488,14 +3488,25 @@ class FlowDocumentStore {
3488
3488
  *
3489
3489
  * `silent` per il trascinamento: il chiamante registra un solo passo alla fine.
3490
3490
  */
3491
+ /**
3492
+ * `from` e' l'origine da cui misurare lo spostamento, e serve per i riquadri **auto-dimensionati**
3493
+ * (§3.6): lì il rettangolo disegnato e' l'ingombro dei membri, che dopo che qualcuno ha spostato un
3494
+ * membro non coincide piu' con `locationX`/`locationY`. Misurando dal documento, lo spostamento
3495
+ * della cornice portava i membri di un delta che nessuno aveva chiesto. Chi disegna il rettangolo
3496
+ * lo conosce: e' il canvas, e lo passa. Assente, si misura dal documento come prima.
3497
+ */
3491
3498
  moveGroup(name, x, y, options) {
3492
3499
  const reference = this.groups().find((candidate) => candidate.name === name);
3493
3500
  if (!reference) {
3494
3501
  return;
3495
3502
  }
3496
3503
  const rounded = { x: Math.round(x), y: Math.round(y) };
3497
- const deltaX = rounded.x - Math.round(reference.group.locationX ?? 0);
3498
- const deltaY = rounded.y - Math.round(reference.group.locationY ?? 0);
3504
+ const origin = options?.from ?? {
3505
+ x: reference.group.locationX ?? 0,
3506
+ y: reference.group.locationY ?? 0,
3507
+ };
3508
+ const deltaX = rounded.x - Math.round(origin.x);
3509
+ const deltaY = rounded.y - Math.round(origin.y);
3499
3510
  const members = new Set(reference.group.members ?? []);
3500
3511
  this.update((draft) => {
3501
3512
  const group = draft.groups?.[reference.index];
@@ -4961,6 +4972,42 @@ function canvasGroupId(groupName) {
4961
4972
  function parseCanvasGroupId(id) {
4962
4973
  return id.startsWith('group:') ? id.slice(6) : null;
4963
4974
  }
4975
+ /**
4976
+ * Il **node segnaposto** di un riquadro chiuso (§3.6).
4977
+ *
4978
+ * Terzo prefisso, accanto a `node:` e `group:`, e per la stessa ragione: gli eventi della libreria
4979
+ * portano tutto nello stesso elenco, e un riquadro chiuso non e' ne' un elemento del documento ne'
4980
+ * una cornice — spostarlo sposta i **membri**, cancellarlo cancella il riquadro.
4981
+ */
4982
+ function canvasFoldId(groupName) {
4983
+ return `fold:${groupName}`;
4984
+ }
4985
+ function parseCanvasFoldId(id) {
4986
+ return id.startsWith('fold:') ? id.slice(5) : null;
4987
+ }
4988
+ /**
4989
+ * I connettori del segnaposto: uno in ingresso e **uno solo** in uscita.
4990
+ *
4991
+ * Uno solo perche' il segnaposto non ha rami: gli archi che escono dal riquadro appartengono a
4992
+ * elementi diversi che stanno dentro, e disegnarne un connettore per ciascuno rimetterebbe sul
4993
+ * canvas la complessita' che chiudere il riquadro serviva a togliere. L'arco porta nell'etichetta
4994
+ * il nome dell'elemento interno, che e' l'informazione che serve.
4995
+ *
4996
+ * Il nome dietro il prefisso e' quello del **riquadro**, quindi `parseSourceConnectorId` risponde
4997
+ * `fold:<riquadro>`, che non e' il nome di nessun elemento: chi gestisce i gesti degli archi lo
4998
+ * riconosce con {@link isFoldReference} e rifiuta, invece di scrivere un connector su un node che
4999
+ * non esiste.
5000
+ */
5001
+ function foldTargetConnectorId(groupName) {
5002
+ return targetConnectorId(canvasFoldId(groupName));
5003
+ }
5004
+ function foldSourceConnectorId(groupName) {
5005
+ return sourceConnectorId(canvasFoldId(groupName), 'fold');
5006
+ }
5007
+ /** `true` se il nome estratto da un connettore e' un segnaposto e non un elemento del documento. */
5008
+ function isFoldReference(name) {
5009
+ return !!name && name.startsWith('fold:');
5010
+ }
4964
5011
 
4965
5012
  /**
4966
5013
  * Il canvas del grafo.
@@ -4985,6 +5032,14 @@ function parseCanvasGroupId(id) {
4985
5032
  */
4986
5033
  const FLOW_NODE_WIDTH = 240;
4987
5034
  const FLOW_NODE_HEIGHT = 116;
5035
+ /**
5036
+ * L'ingombro della pastiglia di un riquadro chiuso (§3.6). Gli stessi valori stanno nel CSS
5037
+ * (`.fb-fold`): la misura serve **due volte** — a disegnarla e a stimarla per l'inquadratura — e la
5038
+ * larghezza non puo' essere uno stile inline, perche' @foblex/flow riscrive l'attributo `style` del
5039
+ * node a ogni riposizionamento.
5040
+ */
5041
+ const FLOW_FOLD_WIDTH = 260;
5042
+ const FLOW_FOLD_HEIGHT = 64;
4988
5043
  /**
4989
5044
  * La larghezza del node per numero di rami: le uscite stanno in fila sul bordo inferiore,
4990
5045
  * quindi una Decision con quattro regole ha bisogno di piu' spazio di un Assignment.
@@ -4996,6 +5051,20 @@ const FLOW_NODE_HEIGHT = 116;
4996
5051
  * restare uguali: la stessa misura serve a disegnare il node e a stimarlo per dagre.
4997
5052
  */
4998
5053
  const OUTLET_WIDTHS = [240, 240, 240, 304, 380, 456, 520];
5054
+ /**
5055
+ * La peggiore di due gravita' (§7). Serve al segnaposto di un riquadro chiuso, che riassume i
5056
+ * rilievi di cio' che nasconde: `SEVERITY_BUCKETS` e' ordinato per gravita' decrescente, quindi
5057
+ * vince chi ha l'indice piu' basso.
5058
+ */
5059
+ function worstSeverity(left, right) {
5060
+ if (!left) {
5061
+ return right;
5062
+ }
5063
+ if (!right) {
5064
+ return left;
5065
+ }
5066
+ return SEVERITY_BUCKETS.indexOf(left) <= SEVERITY_BUCKETS.indexOf(right) ? left : right;
5067
+ }
4999
5068
  /**
5000
5069
  * Margine attorno al flow quando lo si inquadra: senza, i node di bordo toccano il bordo
5001
5070
  * della viewport e sembrano tagliati. In basso a destra ci sono minimappa e pulsante, ma il
@@ -5121,7 +5190,17 @@ class FlowCanvasComponent {
5121
5190
  * Con un solo node (un flow appena creato ha solo lo Start) non si inquadra niente: la
5122
5191
  * vista si spalancherebbe su una card sola.
5123
5192
  */
5193
+ /**
5194
+ * L'elemento su cui andare appena i node sono stati disegnati: lo scrive `bringIntoView` quando
5195
+ * per mostrarlo ha dovuto aprire un riquadro chiuso.
5196
+ */
5197
+ pendingFocus = null;
5124
5198
  onNodesRendered() {
5199
+ const pending = this.pendingFocus;
5200
+ if (pending) {
5201
+ this.pendingFocus = null;
5202
+ this.bringIntoView(pending);
5203
+ }
5125
5204
  if (this.hasFramedOnce || this.nodes().length < 2) {
5126
5205
  return;
5127
5206
  }
@@ -5134,6 +5213,19 @@ class FlowCanvasComponent {
5134
5213
  * 100% non vuole essere rimpicciolito per aver cercato un nome.
5135
5214
  */
5136
5215
  bringIntoView(name) {
5216
+ /**
5217
+ * L'elemento e' dentro un riquadro **chiuso**: non c'e' niente da inquadrare finche' non lo si
5218
+ * apre. Aprirlo e' l'unica cosa sensata — la ricerca e il clic su un problema promettono di
5219
+ * portare *sull'elemento*, e una vista centrata su una pastiglia non mostra cio' che si cercava.
5220
+ * Il centramento aspetta il render: `centerGroupOrNode` non fa niente finche' la libreria non ha
5221
+ * misurato i node, e avviserebbe con FF1009 senza spostare niente.
5222
+ */
5223
+ const folded = this.foldedByMember().get(name);
5224
+ if (folded) {
5225
+ this.pendingFocus = name;
5226
+ this.openGroup(folded);
5227
+ return;
5228
+ }
5137
5229
  /**
5138
5230
  * Anche i **riquadri** (§3.6) sono un bersaglio: un rilievo della loro famiglia porta il
5139
5231
  * nome di un riquadro, e senza questo ramo il clic sul problema selezionava una cornice che
@@ -5287,6 +5379,14 @@ class FlowCanvasComponent {
5287
5379
  right = Math.max(right, node.position.x + flowNodeWidth(node.outlets.length));
5288
5380
  bottom = Math.max(bottom, node.position.y + FLOW_NODE_HEIGHT);
5289
5381
  }
5382
+ // Le pastiglie dei riquadri chiusi sono disegnate come i node: fuori dal conto, un flow tutto
5383
+ // chiuso si inquadrerebbe su cio' che resta aperto.
5384
+ for (const fold of this.folds()) {
5385
+ left = Math.min(left, fold.position.x);
5386
+ top = Math.min(top, fold.position.y);
5387
+ right = Math.max(right, fold.position.x + FLOW_FOLD_WIDTH);
5388
+ bottom = Math.max(bottom, fold.position.y + FLOW_FOLD_HEIGHT);
5389
+ }
5290
5390
  const width = right - left;
5291
5391
  const height = bottom - top;
5292
5392
  if (width <= 0 || height <= 0) {
@@ -5324,7 +5424,12 @@ class FlowCanvasComponent {
5324
5424
  isReachable: true,
5325
5425
  subtitle: this.startSubtitle(),
5326
5426
  }));
5427
+ const hidden = this.foldedByMember();
5327
5428
  for (const reference of this.store.nodes()) {
5429
+ // Membro di un riquadro **chiuso**: al suo posto si disegna la pastiglia del riquadro.
5430
+ if (hidden.has(reference.name)) {
5431
+ continue;
5432
+ }
5328
5433
  result.push(this.toCanvasNode({
5329
5434
  name: reference.name,
5330
5435
  type: reference.type,
@@ -5371,21 +5476,75 @@ class FlowCanvasComponent {
5371
5476
  }),
5372
5477
  };
5373
5478
  }
5374
- /** Gli archi con destinazione: solo questi diventano `<f-connection>`. */
5479
+ /**
5480
+ * Gli archi con destinazione: solo questi diventano `<f-connection>`.
5481
+ *
5482
+ * Un riquadro **chiuso** (§3.6) cambia tre cose, e sono la ragione per cui questo non e' piu' una
5483
+ * `map` sulle uscite del documento. Un arco fra due membri dello stesso riquadro chiuso **non si
5484
+ * disegna**: entrambi i capi sono nascosti, e un arco fra due punti che non ci sono e' un arco che
5485
+ * parte dall'angolo del canvas. Un arco che attraversa il confine si riattacca al **segnaposto**,
5486
+ * che e' l'unico modo di nascondere un node senza rompere l'arco: un connettore con `display:
5487
+ * none` ha geometria 0×0 (regola 4 di @foblex/flow), quindi il node si nasconde solo se l'arco
5488
+ * trova un connettore vero altrove. E due archi che, riattaccati, avrebbero gli stessi capi
5489
+ * diventano **uno**: sarebbero due tratti sovrapposti, indistinguibili e cliccabili a caso.
5490
+ */
5375
5491
  edges = computed(() => {
5376
- const known = new Set(this.nodes().map((node) => node.name));
5377
- return this.store
5378
- .edges()
5379
- .filter((edge) => !!edge.to && known.has(edge.to))
5380
- .map((edge) => ({
5381
- id: edge.id,
5382
- sourceId: sourceConnectorId(edge.from, edge.outletKey),
5383
- targetId: targetConnectorId(edge.to),
5384
- kind: edge.kind,
5385
- label: edge.label,
5386
- isGoTo: !!edge.isGoTo,
5387
- }));
5492
+ const hidden = this.foldedByMember();
5493
+ const visible = new Set(this.nodes().map((node) => node.name));
5494
+ const result = [];
5495
+ const drawn = new Set();
5496
+ for (const edge of this.store.edges()) {
5497
+ const to = edge.to;
5498
+ if (!to) {
5499
+ continue;
5500
+ }
5501
+ const fromFold = hidden.get(edge.from);
5502
+ const toFold = hidden.get(to);
5503
+ if ((!fromFold && !visible.has(edge.from)) || (!toFold && !visible.has(to))) {
5504
+ continue;
5505
+ }
5506
+ if (fromFold && fromFold === toFold) {
5507
+ // Interno al riquadro chiuso: non c'e' niente da disegnare fra due elementi nascosti.
5508
+ continue;
5509
+ }
5510
+ const sourceId = fromFold
5511
+ ? foldSourceConnectorId(fromFold)
5512
+ : sourceConnectorId(edge.from, edge.outletKey);
5513
+ const targetId = toFold ? foldTargetConnectorId(toFold) : targetConnectorId(to);
5514
+ const key = `${sourceId}>${targetId}`;
5515
+ if (drawn.has(key)) {
5516
+ continue;
5517
+ }
5518
+ drawn.add(key);
5519
+ result.push({
5520
+ id: edge.id,
5521
+ sourceId,
5522
+ targetId,
5523
+ kind: edge.kind,
5524
+ label: this.edgeLabel(edge.from, to, edge.label, !!fromFold, !!toFold),
5525
+ isGoTo: !!edge.isGoTo,
5526
+ isFolded: !!fromFold || !!toFold,
5527
+ });
5528
+ }
5529
+ return result;
5388
5530
  }, ...(ngDevMode ? [{ debugName: "edges" }] : []));
5531
+ /**
5532
+ * L'etichetta di un arco. Sugli archi normali e' quella del ramo; su un arco riattaccato a un
5533
+ * segnaposto e' il **nome dell'elemento nascosto**, perche' quella e' l'unica informazione che
5534
+ * chiudere il riquadro ha portato via: «da dove esce» e «dove entra» non si leggono piu' dai capi.
5535
+ */
5536
+ edgeLabel(from, to, label, fromFold, toFold) {
5537
+ if (fromFold && toFold) {
5538
+ return `${from} → ${to}`;
5539
+ }
5540
+ if (fromFold) {
5541
+ return label ? `${from} · ${label}` : from;
5542
+ }
5543
+ if (toFold) {
5544
+ return label ? `${label} · ${to}` : to;
5545
+ }
5546
+ return label ?? null;
5547
+ }
5389
5548
  // -------------------------------------------------------------------------
5390
5549
  // Riquadri di raggruppamento (§3.6)
5391
5550
  // -------------------------------------------------------------------------
@@ -5416,7 +5575,11 @@ class FlowCanvasComponent {
5416
5575
  */
5417
5576
  groups = computed(() => {
5418
5577
  const known = new Set(this.nodeRects().map((entry) => entry.name));
5419
- return this.store.groups().map((reference) => {
5578
+ return this.store.groups()
5579
+ // Un riquadro chiuso e' disegnato da `folds()`: cornice e pastiglia sono **alternative**,
5580
+ // e disegnarle entrambe metterebbe due titoli nello stesso punto.
5581
+ .filter((reference) => !reference.group.isCollapsed)
5582
+ .map((reference) => {
5420
5583
  const group = reference.group;
5421
5584
  const rect = groupRect(group, this.rectsOf(group.members));
5422
5585
  const members = group.members ?? [];
@@ -5426,18 +5589,87 @@ class FlowCanvasComponent {
5426
5589
  label: group.label || reference.name || 'Riquadro',
5427
5590
  description: group.description ?? null,
5428
5591
  position: { x: rect.x, y: rect.y },
5429
- // Chiuso = resta la sola barra del titolo. I node **non** si nascondono: un connettore
5430
- // con `display: none` ha geometria 0×0 e l'arco si attaccherebbe nel punto sbagliato,
5431
- // quindi chiudere il riquadro toglie di mezzo la cornice, non il contenuto.
5432
- size: { width: rect.width, height: group.isCollapsed ? GROUP_HEADER_HEIGHT : rect.height },
5592
+ size: { width: rect.width, height: rect.height },
5433
5593
  colorClass: groupColorClass(group.color),
5434
- isCollapsed: !!group.isCollapsed,
5594
+ isCollapsed: false,
5435
5595
  memberCount: members.filter((name) => known.has(name)).length,
5436
5596
  unknownMembers: members.filter((name) => !known.has(name)),
5437
5597
  isAutoSized: hasAutoSize(group),
5438
5598
  };
5439
5599
  });
5440
5600
  }, ...(ngDevMode ? [{ debugName: "groups" }] : []));
5601
+ /**
5602
+ * Chi nasconde chi: nome del membro → nome del riquadro chiuso che lo copre.
5603
+ *
5604
+ * Un elemento in **due** riquadri chiusi finisce nel primo in ordine di documento: sarebbe
5605
+ * disegnato due volte, e due pastiglie che si prendono lo stesso arco fanno sparire un arco senza
5606
+ * dire niente. La sovrapposizione e' legittima (i riquadri sono commenti, §3.6), quindi la regola
5607
+ * deve essere deterministica, non impedita.
5608
+ */
5609
+ foldedByMember = computed(() => {
5610
+ const map = new Map();
5611
+ for (const reference of this.store.groups()) {
5612
+ if (!reference.group.isCollapsed) {
5613
+ continue;
5614
+ }
5615
+ for (const member of reference.group.members ?? []) {
5616
+ if (!map.has(member)) {
5617
+ map.set(member, reference.name);
5618
+ }
5619
+ }
5620
+ }
5621
+ return map;
5622
+ }, ...(ngDevMode ? [{ debugName: "foldedByMember" }] : []));
5623
+ /**
5624
+ * I riquadri chiusi, disegnati come node segnaposto.
5625
+ *
5626
+ * La pastiglia sta nell'angolo in alto a sinistra del rettangolo che la cornice occupava: e' il
5627
+ * punto da cui il riquadro si e' chiuso, quindi riaprendolo il contenuto ricompare dove era. Il
5628
+ * rettangolo lo da' `groupRect`, non `width`/`height` del documento: a 0 significano «calcolala
5629
+ * sui membri» (§3.6), e i membri esistono anche mentre sono nascosti.
5630
+ */
5631
+ folds = computed(() => {
5632
+ const known = new Set(this.nodeRects().map((entry) => entry.name));
5633
+ return this.store.groups()
5634
+ .filter((reference) => !!reference.group.isCollapsed)
5635
+ .map((reference) => {
5636
+ const group = reference.group;
5637
+ const rect = groupRect(group, this.rectsOf(group.members));
5638
+ const members = group.members ?? [];
5639
+ const inside = members.filter((name) => known.has(name));
5640
+ // I rilievi degli elementi nascosti, sommati: chiudere un riquadro non deve nascondere un
5641
+ // errore, altrimenti il badge del pannello dei problemi conterebbe cose invisibili.
5642
+ let issueCount = 0;
5643
+ let severity = null;
5644
+ for (const name of inside) {
5645
+ issueCount += (this.validation.issuesByElement().get(name) ?? []).length;
5646
+ severity = worstSeverity(severity, this.validation.severityOf(name));
5647
+ }
5648
+ return {
5649
+ name: reference.name,
5650
+ id: canvasFoldId(reference.name),
5651
+ label: group.label || reference.name || 'Riquadro',
5652
+ description: group.description ?? null,
5653
+ position: { x: rect.x, y: rect.y },
5654
+ colorClass: groupColorClass(group.color),
5655
+ memberCount: inside.length,
5656
+ issueCount,
5657
+ severity,
5658
+ unknownMembers: members.filter((name) => !known.has(name)),
5659
+ };
5660
+ });
5661
+ }, ...(ngDevMode ? [{ debugName: "folds" }] : []));
5662
+ isFoldSelected(fold) {
5663
+ return this.selectedGroupName() === fold.name;
5664
+ }
5665
+ /** L'angolo da cui il riquadro e' stato disegnato, aperto o chiuso: e' l'origine dello spostamento. */
5666
+ drawnOrigin(name) {
5667
+ const group = this.groups().find((candidate) => candidate.name === name);
5668
+ if (group) {
5669
+ return group.position;
5670
+ }
5671
+ return this.folds().find((candidate) => candidate.name === name)?.position;
5672
+ }
5441
5673
  /**
5442
5674
  * Ricalcola l'appartenenza dai rettangoli: e' il gesto «trascino un node dentro o fuori dal
5443
5675
  * riquadro» della §3.6, regola 2 — la geometria non **e'** l'appartenenza, la **calcola**, e
@@ -5456,6 +5688,15 @@ class FlowCanvasComponent {
5456
5688
  const known = new Set(all.map((entry) => entry.name));
5457
5689
  const movedSet = new Set(moved);
5458
5690
  for (const reference of this.store.groups()) {
5691
+ /**
5692
+ * Un riquadro **chiuso** non cambia i suoi membri per un trascinamento: i suoi elementi non
5693
+ * sono sullo schermo, e il rettangolo che occupava e' coperto dalla pastiglia. Senza questo
5694
+ * salto, trascinare un elemento sopra la pastiglia lo faceva **sparire** — entrava fra i
5695
+ * membri di un riquadro chiuso, e il solo modo di ritrovarlo era riaprirlo.
5696
+ */
5697
+ if (reference.group.isCollapsed) {
5698
+ continue;
5699
+ }
5459
5700
  const auto = hasAutoSize(reference.group);
5460
5701
  const memberNames = (reference.group.members ?? []).filter((name) => !(auto && movedSet.has(name)));
5461
5702
  const rect = groupRect(reference.group, this.rectsOf(memberNames));
@@ -5463,7 +5704,20 @@ class FlowCanvasComponent {
5463
5704
  this.store.setGroupMembers(reference.name, members, { silent: true });
5464
5705
  }
5465
5706
  }
5466
- /** Apre e chiude il riquadro. Non tocca i membri: chiuso e' un fatto del disegno (§3.6). */
5707
+ /** Apre un riquadro chiuso. Passo di storico normale, come la chiusura a mano: e' la stessa modifica. */
5708
+ openGroup(name) {
5709
+ const reference = this.store.groups().find((candidate) => candidate.name === name);
5710
+ if (!reference?.group.isCollapsed) {
5711
+ return;
5712
+ }
5713
+ this.store.updateGroup(reference.index, (group) => {
5714
+ group.isCollapsed = undefined;
5715
+ });
5716
+ }
5717
+ /**
5718
+ * Apre e chiude il riquadro. Non tocca i membri: chiuso e aperto sono due **disegni** dello
5719
+ * stesso riquadro (§3.6) — chiuso, i membri non si disegnano e al loro posto c'e' la pastiglia.
5720
+ */
5467
5721
  toggleGroupCollapsed(event, name) {
5468
5722
  event.stopPropagation();
5469
5723
  const reference = this.store.groups().find((candidate) => candidate.name === name);
@@ -5575,8 +5829,15 @@ class FlowCanvasComponent {
5575
5829
  */
5576
5830
  _insertionEdgeId = signal(null, ...(ngDevMode ? [{ debugName: "_insertionEdgeId" }] : []));
5577
5831
  insertionEdgeId = this._insertionEdgeId.asReadonly();
5578
- /** Gli id degli archi disegnati: il hit test accetta solo questi. */
5579
- edgeIds = computed(() => new Set(this.edges().map((edge) => edge.id)), ...(ngDevMode ? [{ debugName: "edgeIds" }] : []));
5832
+ /**
5833
+ * Gli id degli archi su cui si puo' innestare: il hit test accetta solo questi.
5834
+ *
5835
+ * Gli archi di un riquadro chiuso restano fuori di proposito. Innestare vuol dire mettersi **in
5836
+ * mezzo** a due elementi, e di quell'arco uno dei due non si vede: il flow si ricollegherebbe
5837
+ * attorno a un elemento che non e' sullo schermo, che non e' una scelta ma una sorpresa. Per
5838
+ * innestare lì si apre il riquadro.
5839
+ */
5840
+ edgeIds = computed(() => new Set(this.edges().filter((edge) => !edge.isFolded).map((edge) => edge.id)), ...(ngDevMode ? [{ debugName: "edgeIds" }] : []));
5580
5841
  /** L'ascoltatore attivo solo durante un trascinamento dalla palette. */
5581
5842
  pointerListener = null;
5582
5843
  /**
@@ -5670,6 +5931,14 @@ class FlowCanvasComponent {
5670
5931
  return;
5671
5932
  }
5672
5933
  const target = event.targetId ? parseTargetConnectorId(event.targetId) : null;
5934
+ /**
5935
+ * I connettori di una pastiglia non sono connettori di nessun elemento: dietro ce ne stanno
5936
+ * diversi, e non c'e' modo di sapere quale l'utente intendeva. Il gesto si rifiuta invece di
5937
+ * scrivere sul primo che capita — per collegare un elemento nascosto si apre il riquadro.
5938
+ */
5939
+ if (isFoldReference(source.nodeName) || isFoldReference(target)) {
5940
+ return;
5941
+ }
5673
5942
  if (target === source.nodeName) {
5674
5943
  // `CONNECTOR_SELF_LOOP`: un node che punta a se stesso non e' mai voluto.
5675
5944
  return;
@@ -5682,15 +5951,22 @@ class FlowCanvasComponent {
5682
5951
  if (!previousSource) {
5683
5952
  return;
5684
5953
  }
5954
+ // Un capo su una pastiglia: vedi `onCreateConnection`, dietro non c'e' un solo elemento.
5955
+ if (isFoldReference(previousSource.nodeName)) {
5956
+ return;
5957
+ }
5685
5958
  if (event.endpoint === 'target') {
5686
5959
  const nextTarget = event.nextTargetId ? parseTargetConnectorId(event.nextTargetId) : null;
5960
+ if (isFoldReference(nextTarget)) {
5961
+ return;
5962
+ }
5687
5963
  this.store.setConnector(previousSource.nodeName, previousSource.outletKey, nextTarget ?? undefined);
5688
5964
  return;
5689
5965
  }
5690
5966
  // Capo sorgente spostato: l'arco cambia uscita, la destinazione resta.
5691
5967
  const nextSource = event.nextSourceId ? parseSourceConnectorId(event.nextSourceId) : null;
5692
5968
  const target = parseTargetConnectorId(event.previousTargetId);
5693
- if (!nextSource || !target) {
5969
+ if (!nextSource || !target || isFoldReference(nextSource.nodeName) || isFoldReference(target)) {
5694
5970
  return;
5695
5971
  }
5696
5972
  this.store.removeConnector(previousSource.nodeName, previousSource.outletKey);
@@ -5716,6 +5992,17 @@ class FlowCanvasComponent {
5716
5992
  const groupName = parseCanvasGroupId(moved.id);
5717
5993
  if (groupName) {
5718
5994
  moves.push({ name: groupName, position: moved.position, isGroup: true });
5995
+ continue;
5996
+ }
5997
+ /**
5998
+ * La pastiglia di un riquadro chiuso e' un node per la libreria, ma spostarla e' spostare il
5999
+ * **riquadro**: i suoi membri vengono dietro, come con la cornice aperta. Senza questo ramo il
6000
+ * gesto non faceva niente e la pastiglia tornava al suo posto — il riquadro chiuso sarebbe
6001
+ * stato l'unica cosa del canvas che non si puo' spostare.
6002
+ */
6003
+ const foldName = parseCanvasFoldId(moved.id);
6004
+ if (foldName) {
6005
+ moves.push({ name: foldName, position: moved.position, isGroup: true });
5719
6006
  }
5720
6007
  }
5721
6008
  if (!moves.length) {
@@ -5724,8 +6011,16 @@ class FlowCanvasComponent {
5724
6011
  moves.forEach((moved, index) => {
5725
6012
  const options = { silent: index > 0 };
5726
6013
  if (moved.isGroup) {
5727
- // Il riquadro si porta dietro i suoi membri: e' cio' per cui `members` esiste (§3.6).
5728
- this.store.moveGroup(moved.name, moved.position.x, moved.position.y, options);
6014
+ /**
6015
+ * Il riquadro si porta dietro i suoi membri: e' cio' per cui `members` esiste (§3.6).
6016
+ * L'origine da cui misurare e' quella **disegnata** e non quella del documento: su un
6017
+ * riquadro auto-dimensionato le due cose divergono appena qualcuno sposta un membro, e
6018
+ * misurare dal documento spostava i membri di un delta sbagliato.
6019
+ */
6020
+ this.store.moveGroup(moved.name, moved.position.x, moved.position.y, {
6021
+ ...options,
6022
+ from: this.drawnOrigin(moved.name),
6023
+ });
5729
6024
  }
5730
6025
  else {
5731
6026
  this.store.moveNode(moved.name, moved.position.x, moved.position.y, options);
@@ -5753,7 +6048,7 @@ class FlowCanvasComponent {
5753
6048
  * porterebbe via il form che si stava guardando.
5754
6049
  */
5755
6050
  const groups = event.nodeIds
5756
- .map((id) => parseCanvasGroupId(id))
6051
+ .map((id) => parseCanvasGroupId(id) ?? parseCanvasFoldId(id))
5757
6052
  .filter((name) => !!name);
5758
6053
  if (groups.length === 1 && !names.length) {
5759
6054
  this.groupSelected.emit(groups[0]);
@@ -5770,8 +6065,9 @@ class FlowCanvasComponent {
5770
6065
  this.nodeRemoveRequested.emit(name);
5771
6066
  continue;
5772
6067
  }
5773
- // Cancellare un riquadro non cancella cio' che contiene: e' un commento (§3.6).
5774
- const groupName = parseCanvasGroupId(id);
6068
+ // Cancellare un riquadro non cancella cio' che contiene: e' un commento (§3.6). Vale anche
6069
+ // per un riquadro **chiuso**: gli elementi tornano visibili, non spariscono con la pastiglia.
6070
+ const groupName = parseCanvasGroupId(id) ?? parseCanvasFoldId(id);
5775
6071
  if (groupName) {
5776
6072
  this.groupRemoveRequested.emit(groupName);
5777
6073
  }
@@ -5842,6 +6138,13 @@ class FlowCanvasComponent {
5842
6138
  targetIdOf(node) {
5843
6139
  return targetConnectorId(node.name);
5844
6140
  }
6141
+ /** I connettori della pastiglia di un riquadro chiuso: gli id li costruisce il modello, non il template. */
6142
+ foldTargetIdOf(fold) {
6143
+ return foldTargetConnectorId(fold.name);
6144
+ }
6145
+ foldSourceIdOf(fold) {
6146
+ return foldSourceConnectorId(fold.name);
6147
+ }
5845
6148
  /**
5846
6149
  * Selezionato = corrente **oppure** parte del blocco selezionato. Senza il secondo caso, un
5847
6150
  * rettangolo che prende cinque elementi ne evidenziava uno, e i comandi che agiscono su tutti
@@ -5887,7 +6190,7 @@ class FlowCanvasComponent {
5887
6190
  * esisterebbe. Registrarlo col preset di default non cambia niente di per se'.
5888
6191
  */
5889
6192
  withControlScheme(F_DEFAULT_CONTROL_SCHEME)),
5890
- ], viewQueries: [{ propertyName: "canvas", first: true, predicate: FCanvasComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!--\r\n Gerarchia obbligatoria: f-flow > f-canvas > fNode / f-connection.\r\n I `@for` sono direttamente dentro <f-canvas>: nessun wrapper, quindi non serve\r\n `ngProjectAs` (che sarebbe indispensabile con blocchi annidati).\r\n-->\r\n<f-flow\r\n fDraggable\r\n (fCreateConnection)=\"onCreateConnection($event)\"\r\n (fReassignConnection)=\"onReassignConnection($event)\"\r\n (fMoveNodes)=\"onMoveNodes($event)\"\r\n (fSelectionChange)=\"onSelectionChange($event)\"\r\n (fDeleteSelected)=\"onDeleteSelected($event)\"\r\n (fCreateNode)=\"onCreateNode($event)\"\r\n (fNodesRendered)=\"onNodesRendered()\"\r\n (fDragStarted)=\"onDragStarted($event)\"\r\n (fDragEnded)=\"onDragEnded()\"\r\n>\r\n <!--\r\n `#canvas` + `(fCanvasChange)`: il primo serve per chiamare `fitToScreen()`, il secondo per\r\n sapere che l'utente ha mosso la vista. `[debounceTime]` non si imposta: l'evento serve solo\r\n ad accendere un flag, non a ricalcolare niente.\r\n -->\r\n <f-canvas fZoom #canvas (fCanvasChange)=\"onCanvasChange()\">\r\n <f-background>\r\n <f-circle-pattern />\r\n </f-background>\r\n\r\n <!--\r\n \u00A73.6 \u2014 i riquadri di raggruppamento. Vanno **sotto** i node, e non c'e' niente da fare per\r\n ottenerlo: `[fGroup]` viene proiettato nel layer dei gruppi, che nell'ordine della libreria\r\n sta sotto connessioni e node.\r\n\r\n Sono commenti: nessun connettore, nessun arco, nessuna raggiungibilita'. Il corpo del\r\n riquadro ha `pointer-events: none` (nel CSS) e solo la barra del titolo risponde: senza,\r\n una cornice grande quanto mezzo canvas si mangerebbe i click destinati alla panoramica e\r\n alla selezione a rettangolo, e i node dentro non si potrebbero piu' prendere.\r\n -->\r\n @for (group of groups(); track group.name) {\r\n <div\r\n fGroup\r\n [fGroupId]=\"group.id\"\r\n [fGroupPosition]=\"group.position\"\r\n [fGroupSize]=\"group.size\"\r\n [fGroupDraggingDisabled]=\"!isEditable()\"\r\n [class]=\"'fb-group ' + group.colorClass\"\r\n [class.fb-group--collapsed]=\"group.isCollapsed\"\r\n [class.fb-group--selected]=\"isGroupSelected(group)\"\r\n (fGroupSizeChange)=\"onGroupResized(group.name, $event)\"\r\n >\r\n <!--\r\n `fDragHandle` sulla sola barra: il riquadro si sposta dal titolo, come una finestra.\r\n Trascinandolo da dentro si sposterebbe la cornice ogni volta che si prova a prendere\r\n un node che le sta sopra.\r\n -->\r\n <div\r\n class=\"fb-group__bar\"\r\n fDragHandle\r\n [title]=\"group.description || group.label\"\r\n (click)=\"onGroupClick($event, group.name)\"\r\n >\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-group__fold\"\r\n [attr.aria-label]=\"group.isCollapsed ? 'Apri il riquadro' : 'Chiudi il riquadro'\"\r\n [title]=\"\r\n group.isCollapsed\r\n ? 'Apri il riquadro'\r\n : 'Chiudi il riquadro: resta la barra del titolo, gli elementi restano dove sono'\r\n \"\r\n (click)=\"toggleGroupCollapsed($event, group.name)\"\r\n >\r\n {{ group.isCollapsed ? '\u25B8' : '\u25BE' }}\r\n </button>\r\n <span class=\"fb-group__title\">{{ group.label }}</span>\r\n <span class=\"fb-group__count\" [title]=\"group.memberCount + ' elementi nel riquadro'\">\r\n {{ group.memberCount }}\r\n </span>\r\n @if (group.unknownMembers.length > 0) {\r\n <!--\r\n Membri che nominano un elemento che non c'e' piu'. Non e' un difetto (\u00A73.6): e' cio'\r\n che resta dopo una cancellazione, ed e' un avviso \u2014 l'attivazione non si blocca.\r\n -->\r\n <span\r\n class=\"fb-group__stale\"\r\n [title]=\"'Elementi non piu\u2019 esistenti: ' + group.unknownMembers.join(', ')\"\r\n >\r\n {{ group.unknownMembers.length }} da ripulire\r\n </span>\r\n }\r\n @if (isEditable()) {\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-group__remove\"\r\n [attr.aria-label]=\"'Elimina il riquadro ' + group.label\"\r\n title=\"Elimina il riquadro: gli elementi che contiene restano\"\r\n (click)=\"onGroupRemoveClick($event, group.name)\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (!group.isCollapsed && group.description) {\r\n <p class=\"fb-group__note\">{{ group.description }}</p>\r\n }\r\n\r\n @if (isEditable() && !group.isCollapsed) {\r\n <!--\r\n Una sola maniglia, in basso a destra: quattro bersagli su una cornice che sta sotto\r\n i node si prendono per sbaglio. Ridimensionare **dichiara** la geometria, che e' il\r\n modo di uscire dal \u00ABcalcolala sui membri\u00BB (\u00A73.6).\r\n -->\r\n <div\r\n fResizeHandle\r\n [fResizeHandleType]=\"resizeHandles.RIGHT_BOTTOM\"\r\n class=\"fb-group__resize\"\r\n title=\"Ridimensiona il riquadro\"\r\n ></div>\r\n }\r\n </div>\r\n }\r\n\r\n @for (edge of edges(); track edge.id) {\r\n <f-connection\r\n [fConnectionId]=\"edge.id\"\r\n [fSourceId]=\"edge.sourceId\"\r\n [fTargetId]=\"edge.targetId\"\r\n fBehavior=\"floating\"\r\n [fType]=\"edge.isGoTo ? 'segment' : 'bezier'\"\r\n [class]=\"'fb-edge fb-edge--' + edge.kind + (edge.isGoTo ? ' fb-edge--goto' : '')\"\r\n [class.fb-edge--insert]=\"edge.id === insertionEdgeId()\"\r\n >\r\n <f-connection-marker-arrow [type]=\"markerEnd\" />\r\n @if (edge.label) {\r\n <div fConnectionContent class=\"fb-edge-label\">{{ edge.label }}</div>\r\n }\r\n </f-connection>\r\n }\r\n\r\n @for (node of nodes(); track node.id) {\r\n <div\r\n fNode\r\n fDragHandle\r\n [fNodeId]=\"node.id\"\r\n [fNodePosition]=\"node.position\"\r\n [class]=\"'fb-node ' + categoryClass(node) + ' ' + node.widthClass\"\r\n [class.fb-node--start]=\"node.isStart\"\r\n [class.fb-node--selected]=\"isSelected(node)\"\r\n [class.fb-node--unreachable]=\"!node.isReachable\"\r\n [class.fb-node--error]=\"node.severity === 'Error'\"\r\n [class.fb-node--warning]=\"node.severity === 'Warning'\"\r\n (dblclick)=\"onNodeDoubleClick(node.name)\"\r\n >\r\n <!--\r\n Lo Start non ha ingresso: e' il punto di partenza (\u00A73.4).\r\n `fConnectorConnectableSide` e' cio' che fa entrare l'arco dall'alto: senza, foblex\r\n calcola il lato e un arco che scende dal node sopra potrebbe agganciarsi di fianco.\r\n -->\r\n @if (!node.isStart) {\r\n <div\r\n fConnector\r\n fConnectorType=\"target\"\r\n [fConnectorId]=\"targetIdOf(node)\"\r\n [fConnectorConnectableSide]=\"sides.TOP\"\r\n fConnectorMultiple=\"true\"\r\n class=\"fb-connector fb-connector--in\"\r\n title=\"Ingresso\"\r\n ></div>\r\n }\r\n\r\n <div class=\"fb-node__head\">\r\n <span class=\"fb-node__icon\" aria-hidden=\"true\">{{ node.icon }}</span>\r\n <div class=\"fb-node__text\">\r\n <span class=\"fb-node__title\" [title]=\"node.description || node.label\">{{ node.label }}</span>\r\n <span class=\"fb-node__sub\">\r\n {{ node.typeLabel }}\r\n @if (node.subtitle) {\r\n <span class=\"fb-node__sub-dot\">\u00B7</span>{{ node.subtitle }}\r\n }\r\n </span>\r\n </div>\r\n @if (node.hasAutomaticOutput) {\r\n <!-- L'elemento espone il proprio risultato sotto il proprio nome (\u00A74.5). -->\r\n <span class=\"fb-node__auto\" title=\"Espone un output automatico referenziabile come \u00AB{{ node.name }}\u00BB\">\r\n \u0192\r\n </span>\r\n }\r\n <!--\r\n `fDragBlocker` impedisce che il pointerdown sul bottone diventi un trascinamento\r\n del node: senza, aprire il dettaglio sposterebbe l'elemento di qualche pixel.\r\n -->\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-node__edit\"\r\n [attr.aria-label]=\"'Apri il dettaglio di ' + node.label\"\r\n title=\"Apri il dettaglio\"\r\n (click)=\"onEditClick($event, node.name)\"\r\n >\r\n \u270E\r\n </button>\r\n @if (!node.isStart && isEditable()) {\r\n <!--\r\n Duplica: sta accanto a \u270E e non solo nell'inspector perche' e' il gesto con cui si\r\n riusa un elemento gi\u00E0 configurato, e cercarlo dentro il form dell'elemento da\r\n copiare e' il posto in cui non lo si cerca. Compare come \u00AB\u00D7\u00BB, sul solo node\r\n selezionato: aggiunge un elemento al documento, quindi non deve trovarsi sotto il\r\n puntatore di chi sta solo attraversando il grafo.\r\n -->\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-node__duplicate\"\r\n [attr.aria-label]=\"'Duplica ' + node.label\"\r\n title=\"Duplica questo elemento (Ctrl+D)\"\r\n (click)=\"onDuplicateClick($event, node.name)\"\r\n >\r\n \u29C9\r\n </button>\r\n <!--\r\n Si mostra solo sul node **selezionato**, non al passaggio del mouse come \u270E:\r\n cancellare non e' reversibile con un altro clic, e un comando distruttivo che\r\n appare sotto il puntatore mentre si attraversa il grafo si preme per sbaglio.\r\n Lo Start non lo espone: un flow senza ingresso non esisterebbe.\r\n -->\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-node__remove\"\r\n [attr.aria-label]=\"'Elimina ' + node.label\"\r\n title=\"Elimina questo elemento (si annulla con \u21B6)\"\r\n (click)=\"onRemoveClick($event, node.name)\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n <div class=\"fb-node__meta\">\r\n @if (!node.isStart) {\r\n <span class=\"fb-node__name\" [title]=\"'Nome tecnico: ' + node.name\">{{ node.name }}</span>\r\n }\r\n @if (!node.isReachable) {\r\n <span class=\"fb-badge fb-badge--unreachable\" title=\"Nessun percorso raggiunge questo elemento dallo Start\">\r\n scollegato\r\n </span>\r\n }\r\n @if (node.issueCount > 0) {\r\n <span\r\n class=\"fb-badge\"\r\n [class.fb-badge--error]=\"node.severity === 'Error'\"\r\n [class.fb-badge--warning]=\"node.severity === 'Warning'\"\r\n [class.fb-badge--info]=\"node.severity === 'Info'\"\r\n [title]=\"node.issueCount + ' rilievi di validazione'\"\r\n >\r\n {{ node.issueCount }}\r\n </span>\r\n }\r\n @if (node.danglingOutlets.length > 0) {\r\n <span\r\n class=\"fb-badge fb-badge--dangling\"\r\n [title]=\"'Rami dichiarati senza destinazione: ' + danglingLabels(node)\"\r\n >\r\n ramo incompleto\r\n </span>\r\n }\r\n </div>\r\n\r\n <!--\r\n Le uscite stanno sul bordo **inferiore**, una per ramo, nell'ordine in cui il modello\r\n le dichiara: per una Decision e' l'ordine di valutazione delle regole, che e'\r\n semantico (\u00A75.4), e da sinistra a destra si legge come la lista nell'inspector.\r\n L'etichetta si mostra solo quando i rami sono piu' di uno: su un `next` unico\r\n direbbe soltanto \u00ABSuccessivo\u00BB.\r\n -->\r\n <div class=\"fb-node__outlets\" [class.fb-node__outlets--labelled]=\"node.showsOutletLabels\">\r\n @for (outlet of node.outlets; track outlet.key) {\r\n <div class=\"fb-outlet\">\r\n @if (node.showsOutletLabels) {\r\n <span class=\"fb-outlet__label\" [title]=\"outlet.label\">{{ outlet.label }}</span>\r\n }\r\n <div\r\n fConnector\r\n fConnectorType=\"source\"\r\n [fConnectorId]=\"connectorIdOf(node, outlet.key)\"\r\n [fConnectorConnectableSide]=\"sides.BOTTOM\"\r\n [class]=\"'fb-connector fb-connector--out fb-connector--' + outlet.kind\"\r\n [title]=\"outlet.label\"\r\n ></div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Anteprima dell'arco durante il trascinamento. -->\r\n <f-connection-for-create fBehavior=\"floating\" fType=\"bezier\" class=\"fb-edge fb-edge--creating\">\r\n <f-connection-marker-arrow [type]=\"markerEnd\" />\r\n </f-connection-for-create>\r\n\r\n <f-selection-area />\r\n </f-canvas>\r\n\r\n <!--\r\n Fuori da <f-canvas> come la minimappa: dentro, la trasformazione del canvas se lo\r\n porterebbe via insieme ai node. `fDragBlocker` perche' il pointerdown non inizi una\r\n panoramica invece di premere il bottone.\r\n -->\r\n <!--\r\n Sempre nel DOM, nascosto con una classe e non con `@if`: dentro una proiezione di contenuto\r\n un blocco di controllo e' una complicazione gratuita, e `visibility: hidden` lo toglie\r\n anche dall'ordine di tabulazione. Il costo e' un bottone in piu' nel DOM, non un rischio.\r\n -->\r\n <div class=\"fb-viewport\" fDragBlocker role=\"group\" aria-label=\"Vista\">\r\n <!--\r\n Lo zoom ha bisogno di **comandi e di un numero**: con la sola rotella si finisce all'8% su\r\n un flow grande e sembra che il canvas si sia svuotato, senza un modo evidente di tornare.\r\n Il numero e' un bottone: riporta al 100% sull'elemento corrente.\r\n -->\r\n <button type=\"button\" class=\"fb-btn fb-viewport__btn\" title=\"Riduci (rotella indietro)\" (click)=\"zoomOut()\">\r\n \u2212\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-viewport__level\"\r\n title=\"Torna al 100% sull\u2019elemento selezionato\"\r\n (click)=\"zoomToActual()\"\r\n >\r\n {{ zoomPercent() }}%\r\n </button>\r\n <button type=\"button\" class=\"fb-btn fb-viewport__btn\" title=\"Ingrandisci (rotella avanti)\" (click)=\"zoomIn()\">\r\n +\r\n </button>\r\n <!--\r\n La modalita\u2019 selezione. Esiste perche\u2019 i due gesti che la libreria offre di serie \u2014\r\n `Ctrl`+clic per aggiungere, `Shift`+trascina per il rettangolo \u2014 non si vedono: senza un\r\n interruttore, su un flow grande si finisce a spostare un elemento alla volta. Accesa, il\r\n trascinamento sul canvas vuoto **seleziona** e la vista si sposta col tasto centrale.\r\n -->\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-viewport__select\"\r\n [class.fb-viewport__select--on]=\"isSelecting()\"\r\n [attr.aria-pressed]=\"isSelecting()\"\r\n [title]=\"\r\n isSelecting()\r\n ? 'Modalita\u2019 selezione attiva: trascina sul canvas per selezionare piu\u2019 elementi. La vista si sposta col tasto centrale del mouse'\r\n : 'Attiva la selezione a rettangolo: trascinando sul canvas selezioni piu\u2019 elementi invece di spostare la vista (Shift+trascina fa lo stesso, sempre)'\r\n \"\r\n (click)=\"toggleSelectionMode()\"\r\n >\r\n <span class=\"fb-viewport__icon\" aria-hidden=\"true\">\u25A4</span>\r\n Seleziona\r\n </button>\r\n <!--\r\n Sempre nel DOM, nascosto con una classe e non con `@if`: dentro una proiezione di contenuto\r\n un blocco di controllo e' una complicazione gratuita, e `visibility: hidden` lo toglie\r\n anche dall'ordine di tabulazione. Il costo e' un bottone in piu' nel DOM, non un rischio.\r\n -->\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-viewport__fit\"\r\n [class.fb-viewport__fit--hidden]=\"!hasMovedViewport()\"\r\n title=\"Rimetti il flow in vista. Su un flow molto grande inquadra l\u2019inizio, perche\u2019 l\u2019intero non sarebbe leggibile\"\r\n (click)=\"resetViewport()\"\r\n >\r\n <span class=\"fb-viewport__icon\" aria-hidden=\"true\">\u2922</span>\r\n Inquadra\r\n </button>\r\n </div>\r\n\r\n @if (selectionCount() > 1) {\r\n <!--\r\n Il dato che rende utile la selezione: che si spostano **insieme**. Senza dirlo, dopo aver\r\n selezionato cinque elementi si continua a trascinarne uno per volta.\r\n -->\r\n <p class=\"fb-select-hint\" role=\"status\">\r\n {{ selectionCount() }} elementi selezionati: trascinane uno per spostarli tutti \u00B7 Ctrl+clic\r\n aggiunge o toglie \u00B7 Canc li elimina\r\n </p>\r\n } @else if (isSelecting()) {\r\n <p class=\"fb-select-hint\" role=\"status\">\r\n Trascina sul canvas per selezionare \u00B7 la vista si sposta col tasto centrale\r\n </p>\r\n }\r\n\r\n @if (insertionEdgeId()) {\r\n <!-- Il rilascio su un arco non e' un gesto che si indovina: mentre e' possibile, si dice. -->\r\n <p class=\"fb-insert-hint\" role=\"status\">Rilascia qui per inserire l\u2019elemento in questo percorso</p>\r\n }\r\n\r\n <f-minimap [fMinSize]=\"1200\" class=\"fb-minimap\" />\r\n</f-flow>\r\n", styles: [":host{display:block;position:relative;width:100%;height:100%;overflow:hidden;background:var(--fb-canvas-bg, #f4f5f7)}f-flow{display:block;width:100%;height:100%}.fb-minimap{position:absolute;right:14px;bottom:14px;width:150px;height:300px;max-height:40%;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius, 10px);background:var(--fb-surface, #fff);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));overflow:hidden}.fb-viewport{position:absolute;right:14px;bottom:322px;z-index:1;display:flex;align-items:center;gap:4px;flex-wrap:wrap;justify-content:flex-end;max-width:200px}.fb-viewport__btn,.fb-viewport__level,.fb-viewport__fit{border-radius:var(--fb-radius-xs, 6px);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));font-size:11px}.fb-viewport__btn{width:24px;padding:2px 0;font-size:14px;line-height:1}.fb-viewport__level{min-width:52px;font-variant-numeric:tabular-nums}.fb-viewport__fit--hidden{opacity:0;visibility:hidden}.fb-viewport__fit{transition:opacity .15s ease,visibility .15s ease}.fb-viewport__icon{font-size:13px;line-height:1;color:var(--fb-text-muted, #667085)}.fb-insert-hint{position:absolute;bottom:18px;left:50%;transform:translate(-50%);z-index:1;margin:0;padding:6px 12px;border-radius:999px;background:var(--fb-accent, #2f6feb);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));color:#fff;font-size:11px;font-weight:600;pointer-events:none}.fb-select-hint{position:absolute;bottom:18px;left:50%;transform:translate(-50%);z-index:1;margin:0;padding:6px 12px;border-radius:999px;border:1px solid var(--fb-border, #e2e5eb);background:var(--fb-surface, #fff);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));color:var(--fb-text-muted, #667085);font-size:11px;font-weight:600;pointer-events:none}.fb-viewport__select{border-radius:var(--fb-radius-xs, 6px);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));font-size:11px}.fb-viewport__select--on{border-color:var(--fb-accent, #2f6feb);background:var(--fb-accent, #2f6feb);color:#fff}.fb-node{position:absolute;display:flex;flex-direction:column;box-sizing:border-box;width:240px;padding:0;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius-lg, 12px);background:var(--fb-surface, #fff);box-shadow:var(--fb-shadow-sm, 0 1px 2px rgb(16 24 40 / 6%));font:inherit;cursor:grab;-webkit-user-select:none;user-select:none;transition:box-shadow .12s ease,border-color .12s ease}.fb-node--outlets-3{width:304px}.fb-node--outlets-4{width:380px}.fb-node--outlets-5{width:456px}.fb-node--outlets-6{width:520px}.fb-node:hover{box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%))}.fb-node--selected{border-color:var(--fb-accent, #4f6ef7);box-shadow:0 0 0 3px color-mix(in srgb,var(--fb-accent, #4f6ef7) 22%,transparent)}.fb-node--unreachable{border-style:dashed;opacity:.8}.fb-node--error{border-color:var(--fb-error, #c9372c)}.fb-node--warning{border-color:var(--fb-warning, #b7791f)}.fb-node__head{display:flex;align-items:center;gap:8px;padding:10px 10px 6px 12px}.fb-node__icon{display:grid;place-items:center;flex:0 0 auto;width:28px;height:28px;border-radius:var(--fb-radius-sm, 8px);background:var(--fb-node-accent, #667085);color:#fff;font-size:14px;line-height:1}.cat-start{--fb-node-accent: #22a06b}.cat-screen{--fb-node-accent: #3b82f6}.cat-logic{--fb-node-accent: #8b5cf6}.cat-data{--fb-node-accent: #06b6d4}.cat-action{--fb-node-accent: #f59e0b}.cat-flow{--fb-node-accent: #14b8a6}.cat-other{--fb-node-accent: #667085}.fb-node__text{flex:1;min-width:0}.fb-node__title{display:block;font-size:13px;font-weight:600;line-height:17px;color:var(--fb-text, #1a1c23);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-node__sub{display:block;margin-top:1px;font-size:11px;line-height:14px;color:var(--fb-text-muted, #6b7086);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-node__sub-dot{margin:0 4px;color:var(--fb-text-subtle, #98a2b3)}.fb-node__auto{flex:0 0 auto;font-size:12px;font-weight:700;color:var(--fb-accent, #4f6ef7);cursor:help}.fb-node__edit{display:grid;place-items:center;flex:0 0 auto;width:22px;height:22px;padding:0;border:0;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text-subtle, #98a2b3);font:inherit;font-size:12px;cursor:pointer;opacity:0;transition:opacity .12s ease,background .12s ease}.fb-node:hover .fb-node__edit,.fb-node--selected .fb-node__edit,.fb-node__edit:focus-visible{opacity:1}.fb-node__edit:hover{background:var(--fb-surface-alt, #f7f8fa);color:var(--fb-text, #1a1c23)}.fb-node__duplicate,.fb-node__remove{display:grid;place-items:center;flex:0 0 auto;width:22px;height:22px;padding:0;border:0;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text-subtle, #98a2b3);font:inherit;font-size:15px;line-height:1;cursor:pointer;opacity:0;transition:opacity .12s ease,background .12s ease,color .12s ease}.fb-node--selected .fb-node__duplicate,.fb-node--selected .fb-node__remove,.fb-node__duplicate:focus-visible,.fb-node__remove:focus-visible{opacity:1}.fb-node__remove:hover{background:color-mix(in srgb,var(--fb-error, #c9372c) 12%,transparent);color:var(--fb-error, #c9372c)}.fb-node__duplicate:hover{background:color-mix(in srgb,var(--fb-accent, #3b6ef2) 12%,transparent);color:var(--fb-accent, #3b6ef2)}.fb-node__meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px;min-height:14px;padding:0 12px 6px}.fb-node__name{flex:0 1 auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;color:var(--fb-text-subtle, #98a2b3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-badge{padding:1px 6px;border-radius:10px;background:var(--fb-badge-bg, #eef0f4);font-size:10px;font-weight:600;color:var(--fb-text-muted, #6b7086);white-space:nowrap;cursor:help}.fb-badge--error{background:color-mix(in srgb,var(--fb-error, #c9372c) 14%,transparent);color:var(--fb-error, #c9372c)}.fb-badge--warning{background:color-mix(in srgb,var(--fb-warning, #b7791f) 16%,transparent);color:var(--fb-warning, #b7791f)}.fb-badge--info{background:color-mix(in srgb,var(--fb-accent, #4f6ef7) 12%,transparent);color:var(--fb-accent, #4f6ef7)}.fb-badge--unreachable,.fb-badge--dangling{background:color-mix(in srgb,var(--fb-warning, #b7791f) 12%,transparent);color:var(--fb-warning, #b7791f)}.fb-node__outlets{display:flex;align-items:flex-end;justify-content:space-evenly;gap:4px;padding:0 8px 4px}.fb-node__outlets--labelled{padding-top:5px;border-top:1px solid var(--fb-border-subtle, #eef0f4)}.fb-outlet{display:flex;flex:1 1 0;flex-direction:column;align-items:center;gap:2px;min-width:0}.fb-outlet__label{max-width:100%;font-size:10px;line-height:13px;color:var(--fb-text-muted, #6b7086);text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-connector{box-sizing:border-box;width:12px;height:12px;flex:0 0 auto;border:2px solid var(--fb-surface, #fff);border-radius:50%;background:var(--fb-connector, #98a2b3);cursor:crosshair;transition:transform .12s ease,box-shadow .12s ease}f-flow .fb-connector--out{position:relative;inset:auto;margin-bottom:-12px}.fb-connector--out:hover{transform:scale(1.2)}f-flow .fb-connector--in{position:absolute;inset:-2px auto auto 50%;width:24px;height:4px;border:0;border-radius:2px;transform:translate(-50%);background:var(--fb-border-strong, #cfd4de)}.fb-connector--out{--ff-connector-connected-color: var(--fb-connector, #98a2b3)}.fb-connector--Next,.fb-connector--Start,.fb-connector--LoopNext{--fb-connector: var(--fb-edge-next, #98a2b3)}.fb-connector--Rule,.fb-connector--WaitEvent{--fb-connector: var(--fb-edge-rule, #8b5cf6)}.fb-connector--Default,.fb-connector--LoopEnd{--fb-connector: var(--fb-edge-default, #06b6d4)}.fb-connector--Fault{--fb-connector: var(--fb-edge-fault, #dc2626)}.fb-connector--Timeout,.fb-connector--ScheduledPath{--fb-connector: var(--fb-edge-timeout, #f59e0b)}.fb-connector.f-connector-connectable{box-shadow:0 0 0 4px color-mix(in srgb,var(--fb-accent, #4f6ef7) 28%,transparent)}.fb-group{position:absolute;box-sizing:border-box;border:1px solid var(--fb-group-border, #d6dae1);border-radius:var(--fb-radius, 10px);background:var(--fb-group-fill, rgb(148 163 184 / 10%));pointer-events:none;overflow:hidden}.fb-group--selected{border-color:var(--fb-accent, #2f6feb);box-shadow:0 0 0 2px color-mix(in srgb,var(--fb-accent, #2f6feb) 25%,transparent)}.fb-group__bar{display:flex;align-items:center;gap:6px;height:30px;padding:0 6px;border-bottom:1px solid var(--fb-group-border, #d6dae1);background:var(--fb-group-bar, rgb(148 163 184 / 22%));color:var(--fb-text, #1d2939);font-size:12px;font-weight:600;pointer-events:auto;cursor:move;-webkit-user-select:none;user-select:none}.fb-group--collapsed .fb-group__bar{border-bottom:none}.fb-group__fold,.fb-group__remove{flex:none;width:18px;height:18px;padding:0;border:none;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:inherit;font:inherit;font-size:12px;line-height:1;cursor:pointer}.fb-group__fold:hover,.fb-group__remove:hover{background:#10182814}.fb-group__remove{margin-left:auto;font-size:14px}.fb-group__title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fb-group__count{flex:none;padding:0 5px;border-radius:8px;background:#10182814;font-size:10px;font-weight:600;font-variant-numeric:tabular-nums}.fb-group__stale{flex:none;padding:0 5px;border-radius:8px;background:color-mix(in srgb,var(--fb-warning, #b7791f) 14%,transparent);color:var(--fb-warning, #b7791f);font-size:10px;font-weight:600}.fb-group__note{margin:6px 8px 0;color:var(--fb-text-muted, #667085);font-size:11px;line-height:1.35;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}.fb-group__resize{position:absolute;right:0;bottom:0;width:14px;height:14px;border-right:2px solid var(--fb-group-border, #d6dae1);border-bottom:2px solid var(--fb-group-border, #d6dae1);border-bottom-right-radius:var(--fb-radius, 10px);pointer-events:auto;cursor:nwse-resize}.fb-group--giallo{--fb-group-border: #e0c26a;--fb-group-fill: rgb(234 179 8 / 10%);--fb-group-bar: rgb(234 179 8 / 24%)}.fb-group--verde{--fb-group-border: #86c79b;--fb-group-fill: rgb(34 160 89 / 10%);--fb-group-bar: rgb(34 160 89 / 22%)}.fb-group--blu{--fb-group-border: #93b4ea;--fb-group-fill: rgb(47 111 235 / 9%);--fb-group-bar: rgb(47 111 235 / 20%)}.fb-group--viola{--fb-group-border: #bda6e6;--fb-group-fill: rgb(139 92 246 / 10%);--fb-group-bar: rgb(139 92 246 / 22%)}.fb-group--rosso{--fb-group-border: #e6a3a3;--fb-group-fill: rgb(214 69 69 / 9%);--fb-group-bar: rgb(214 69 69 / 20%)}.fb-group--grigio,.fb-group--neutro{--fb-group-border: #d6dae1;--fb-group-fill: rgb(148 163 184 / 10%);--fb-group-bar: rgb(148 163 184 / 22%)}\n"], dependencies: [{ kind: "ngmodule", type: FFlowModule }, { kind: "component", type: i1.FFlowComponent, selector: "f-flow", inputs: ["fFlowId", "fCache"], outputs: ["fNodesRendered", "fFullRendered", "fLoaded"] }, { kind: "component", type: i1.FCanvasComponent, selector: "f-canvas", inputs: ["position", "scale", "debounceTime", "fLayers"], outputs: ["fCanvasChange"] }, { kind: "component", type: i1.FBackgroundComponent, selector: "f-background" }, { kind: "component", type: i1.FCirclePatternComponent, selector: "f-circle-pattern", inputs: ["id", "color", "radius"] }, { kind: "directive", type: i1.FZoomDirective, selector: "f-canvas[fZoom]", inputs: ["fZoom", "fWheelTrigger", "fDblClickTrigger", "fZoomMinimum", "fZoomMaximum", "fZoomStep", "fPinchStep", "fZoomDblClickStep"] }, { kind: "component", type: i1.FSelectionArea, selector: "f-selection-area", inputs: ["fTrigger"] }, { kind: "directive", type: i1.FConnectionContent, selector: "[fConnectionContent]", inputs: ["position", "offset", "align"] }, { kind: "component", type: i1.FConnectionMarkerArrow, selector: "f-connection-marker-arrow", inputs: ["type"] }, { kind: "component", type: i1.FConnectionComponent, selector: "f-connection", inputs: ["fConnectionId", "fSourceId", "fTargetId", "fOutputId", "fInputId", "fRadius", "fOffset", "fBehavior", "fType", "fSelectionDisabled", "fReassignableStart", "fReassignDisabled", "fSourceSide", "fTargetSide", "fInputSide", "fOutputSide"], exportAs: ["fComponent"] }, { kind: "component", type: i1.FConnectionForCreateComponent, selector: "f-connection-for-create", inputs: ["fRadius", "fOffset", "fBehavior", "fType", "fInputSide", "fOutputSide"] }, { kind: "directive", type: i1.FConnectorDirective, selector: "[fConnector]", inputs: ["fConnectorId", "fConnectorType", "fConnectorDisabled", "fConnectorMultiple", "fConnectorCategory", "fConnectorConnectableSide", "fConnectorSelfConnectable", "fCanBeConnectedTo", "fConnectionFromOutlet"], exportAs: ["fConnector"] }, { kind: "component", type: i1.FMinimapComponent, selector: "f-minimap", inputs: ["fMinSize", "fNodeRenderLimit"], exportAs: ["fComponent"] }, { kind: "directive", type: i1.FGroupDirective, selector: "[fGroup]", inputs: ["fGroupId", "fGroupParentId", "fGroupPosition", "fGroupSize", "fGroupRotate", "fConnectOnNode", "fMinimapClass", "fGroupDraggingDisabled", "fGroupSelectionDisabled", "fIncludePadding", "fAutoExpandOnChildHit", "fAutoSizeToFitChildren"], outputs: ["fGroupPositionChange", "fGroupSizeChange", "fGroupRotateChange"], exportAs: ["fComponent"] }, { kind: "directive", type: i1.FNodeDirective, selector: "[fNode]", inputs: ["fNodeId", "fNodeParentId", "fNodePosition", "fNodeSize", "fNodeRotate", "fConnectOnNode", "fMinimapClass", "fNodeDraggingDisabled", "fNodeSelectionDisabled", "fIncludePadding", "fAutoExpandOnChildHit", "fAutoSizeToFitChildren"], outputs: ["fNodePositionChange", "fNodeSizeChange", "fNodeRotateChange"], exportAs: ["fComponent"] }, { kind: "directive", type: i1.FDragHandleDirective, selector: "[fDragHandle]" }, { kind: "directive", type: i1.FResizeHandleDirective, selector: "[fResizeHandle]", inputs: ["fResizeHandleType"] }, { kind: "directive", type: i1.FDragBlockerDirective, selector: "[fDragBlocker]" }, { kind: "directive", type: i1.FDraggableDirective, selector: "f-flow[fDraggable]", inputs: ["fDraggableDisabled", "fDropToGroup", "fMultiSelectTrigger", "fReassignConnectionTrigger", "fCreateConnectionTrigger", "fConnectionWaypointsTrigger", "fMoveControlPointTrigger", "fNodeResizeTrigger", "fNodeRotateTrigger", "fNodeMoveTrigger", "fCanvasMoveTrigger", "fExternalItemTrigger", "fEmitOnNodeIntersect", "vCellSize", "hCellSize", "fCellSizeWhileDragging"], outputs: ["fSelectionChange", "fDeleteSelected", "fNodeIntersectedWithConnections", "fNodeConnectionsIntersection", "fCreateNode", "fMoveNodes", "fReassignConnection", "fCreateConnection", "fConnectionWaypointsChanged", "fDropToGroup", "fDragStarted", "fDragEnded"], exportAs: ["fDraggable"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6193
+ ], viewQueries: [{ propertyName: "canvas", first: true, predicate: FCanvasComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!--\r\n Gerarchia obbligatoria: f-flow > f-canvas > fNode / f-connection.\r\n I `@for` sono direttamente dentro <f-canvas>: nessun wrapper, quindi non serve\r\n `ngProjectAs` (che sarebbe indispensabile con blocchi annidati).\r\n-->\r\n<f-flow\r\n fDraggable\r\n (fCreateConnection)=\"onCreateConnection($event)\"\r\n (fReassignConnection)=\"onReassignConnection($event)\"\r\n (fMoveNodes)=\"onMoveNodes($event)\"\r\n (fSelectionChange)=\"onSelectionChange($event)\"\r\n (fDeleteSelected)=\"onDeleteSelected($event)\"\r\n (fCreateNode)=\"onCreateNode($event)\"\r\n (fNodesRendered)=\"onNodesRendered()\"\r\n (fDragStarted)=\"onDragStarted($event)\"\r\n (fDragEnded)=\"onDragEnded()\"\r\n>\r\n <!--\r\n `#canvas` + `(fCanvasChange)`: il primo serve per chiamare `fitToScreen()`, il secondo per\r\n sapere che l'utente ha mosso la vista. `[debounceTime]` non si imposta: l'evento serve solo\r\n ad accendere un flag, non a ricalcolare niente.\r\n -->\r\n <f-canvas fZoom #canvas (fCanvasChange)=\"onCanvasChange()\">\r\n <f-background>\r\n <f-circle-pattern />\r\n </f-background>\r\n\r\n <!--\r\n \u00A73.6 \u2014 i riquadri di raggruppamento. Vanno **sotto** i node, e non c'e' niente da fare per\r\n ottenerlo: `[fGroup]` viene proiettato nel layer dei gruppi, che nell'ordine della libreria\r\n sta sotto connessioni e node.\r\n\r\n Sono commenti: nessun connettore, nessun arco, nessuna raggiungibilita'. Il corpo del\r\n riquadro ha `pointer-events: none` (nel CSS) e solo la barra del titolo risponde: senza,\r\n una cornice grande quanto mezzo canvas si mangerebbe i click destinati alla panoramica e\r\n alla selezione a rettangolo, e i node dentro non si potrebbero piu' prendere.\r\n -->\r\n @for (group of groups(); track group.name) {\r\n <div\r\n fGroup\r\n [fGroupId]=\"group.id\"\r\n [fGroupPosition]=\"group.position\"\r\n [fGroupSize]=\"group.size\"\r\n [fGroupDraggingDisabled]=\"!isEditable()\"\r\n [class]=\"'fb-group ' + group.colorClass\"\r\n [class.fb-group--selected]=\"isGroupSelected(group)\"\r\n (fGroupSizeChange)=\"onGroupResized(group.name, $event)\"\r\n >\r\n <!--\r\n `fDragHandle` sulla sola barra: il riquadro si sposta dal titolo, come una finestra.\r\n Trascinandolo da dentro si sposterebbe la cornice ogni volta che si prova a prendere\r\n un node che le sta sopra.\r\n -->\r\n <div\r\n class=\"fb-group__bar\"\r\n fDragHandle\r\n [title]=\"group.description || group.label\"\r\n (click)=\"onGroupClick($event, group.name)\"\r\n >\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-group__fold\"\r\n aria-label=\"Chiudi il riquadro\"\r\n title=\"Chiudi il riquadro: gli elementi si nascondono e gli archi che lo attraversano si attaccano alla pastiglia\"\r\n (click)=\"toggleGroupCollapsed($event, group.name)\"\r\n >\r\n \u25BE\r\n </button>\r\n <span class=\"fb-group__title\">{{ group.label }}</span>\r\n <span class=\"fb-group__count\" [title]=\"group.memberCount + ' elementi nel riquadro'\">\r\n {{ group.memberCount }}\r\n </span>\r\n @if (group.unknownMembers.length > 0) {\r\n <!--\r\n Membri che nominano un elemento che non c'e' piu'. Non e' un difetto (\u00A73.6): e' cio'\r\n che resta dopo una cancellazione, ed e' un avviso \u2014 l'attivazione non si blocca.\r\n -->\r\n <span\r\n class=\"fb-group__stale\"\r\n [title]=\"'Elementi non piu\u2019 esistenti: ' + group.unknownMembers.join(', ')\"\r\n >\r\n {{ group.unknownMembers.length }} da ripulire\r\n </span>\r\n }\r\n @if (isEditable()) {\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-group__remove\"\r\n [attr.aria-label]=\"'Elimina il riquadro ' + group.label\"\r\n title=\"Elimina il riquadro: gli elementi che contiene restano\"\r\n (click)=\"onGroupRemoveClick($event, group.name)\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (group.description) {\r\n <p class=\"fb-group__note\">{{ group.description }}</p>\r\n }\r\n\r\n @if (isEditable()) {\r\n <!--\r\n Una sola maniglia, in basso a destra: quattro bersagli su una cornice che sta sotto\r\n i node si prendono per sbaglio. Ridimensionare **dichiara** la geometria, che e' il\r\n modo di uscire dal \u00ABcalcolala sui membri\u00BB (\u00A73.6).\r\n -->\r\n <div\r\n fResizeHandle\r\n [fResizeHandleType]=\"resizeHandles.RIGHT_BOTTOM\"\r\n class=\"fb-group__resize\"\r\n title=\"Ridimensiona il riquadro\"\r\n ></div>\r\n }\r\n </div>\r\n }\r\n\r\n <!--\r\n \u00A73.6 \u2014 un riquadro **chiuso**: una pastiglia al posto dei suoi elementi.\r\n\r\n \u00C8 un `fNode` e non un `fGroup` per una ragione sola, ed e' la regola 4 di @foblex/flow: per\r\n nascondere i node bisogna che gli archi che entravano e uscivano trovino un connettore\r\n **vero**, altrimenti hanno geometria 0\u00D70 e si attaccano all'angolo del canvas. La pastiglia e'\r\n quel connettore. Un solo connettore in uscita: dietro ci stanno elementi diversi, e il nome di\r\n quello che esce sta nell'etichetta dell'arco.\r\n -->\r\n @for (fold of folds(); track fold.id) {\r\n <div\r\n fNode\r\n fDragHandle\r\n [fNodeId]=\"fold.id\"\r\n [fNodePosition]=\"fold.position\"\r\n [class]=\"'fb-fold ' + fold.colorClass\"\r\n [class.fb-fold--selected]=\"isFoldSelected(fold)\"\r\n [class.fb-fold--error]=\"fold.severity === 'Error'\"\r\n [class.fb-fold--warning]=\"fold.severity === 'Warning'\"\r\n (dblclick)=\"toggleGroupCollapsed($event, fold.name)\"\r\n >\r\n <div\r\n fConnector\r\n fConnectorType=\"target\"\r\n [fConnectorId]=\"foldTargetIdOf(fold)\"\r\n [fConnectorConnectableSide]=\"sides.TOP\"\r\n fConnectorMultiple=\"true\"\r\n class=\"fb-connector fb-connector--in\"\r\n title=\"Ingresso del riquadro chiuso\"\r\n ></div>\r\n\r\n <div class=\"fb-fold__head\">\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-fold__open\"\r\n [attr.aria-label]=\"'Apri il riquadro ' + fold.label\"\r\n title=\"Apri il riquadro: gli elementi tornano visibili al loro posto\"\r\n (click)=\"toggleGroupCollapsed($event, fold.name)\"\r\n >\r\n \u25B8\r\n </button>\r\n <span class=\"fb-fold__title\" [title]=\"fold.description || fold.label\">{{ fold.label }}</span>\r\n @if (isEditable()) {\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-fold__remove\"\r\n [attr.aria-label]=\"'Elimina il riquadro ' + fold.label\"\r\n title=\"Elimina il riquadro: gli elementi che contiene restano\"\r\n (click)=\"onGroupRemoveClick($event, fold.name)\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n <div class=\"fb-fold__meta\">\r\n <span class=\"fb-fold__count\">\r\n {{ fold.memberCount }} {{ fold.memberCount === 1 ? 'elemento nascosto' : 'elementi nascosti' }}\r\n </span>\r\n @if (fold.issueCount > 0) {\r\n <!-- I rilievi di cio' che e' nascosto: chiudere un riquadro non nasconde un errore (\u00A77). -->\r\n <span\r\n class=\"fb-badge\"\r\n [class.fb-badge--error]=\"fold.severity === 'Error'\"\r\n [class.fb-badge--warning]=\"fold.severity === 'Warning'\"\r\n [class.fb-badge--info]=\"fold.severity === 'Info'\"\r\n [title]=\"fold.issueCount + ' rilievi negli elementi nascosti'\"\r\n >\r\n {{ fold.issueCount }}\r\n </span>\r\n }\r\n @if (fold.unknownMembers.length > 0) {\r\n <span\r\n class=\"fb-badge fb-badge--dangling\"\r\n [title]=\"'Elementi non piu\u2019 esistenti: ' + fold.unknownMembers.join(', ')\"\r\n >\r\n {{ fold.unknownMembers.length }} da ripulire\r\n </span>\r\n }\r\n </div>\r\n\r\n <div class=\"fb-fold__outlets\">\r\n <div\r\n fConnector\r\n fConnectorType=\"source\"\r\n [fConnectorId]=\"foldSourceIdOf(fold)\"\r\n [fConnectorConnectableSide]=\"sides.BOTTOM\"\r\n class=\"fb-connector fb-connector--out fb-connector--Next\"\r\n title=\"Uscite del riquadro chiuso\"\r\n ></div>\r\n </div>\r\n </div>\r\n }\r\n\r\n @for (edge of edges(); track edge.id) {\r\n <f-connection\r\n [fConnectionId]=\"edge.id\"\r\n [fSourceId]=\"edge.sourceId\"\r\n [fTargetId]=\"edge.targetId\"\r\n fBehavior=\"floating\"\r\n [fType]=\"edge.isGoTo ? 'segment' : 'bezier'\"\r\n [class]=\"'fb-edge fb-edge--' + edge.kind + (edge.isGoTo ? ' fb-edge--goto' : '')\"\r\n [class.fb-edge--insert]=\"edge.id === insertionEdgeId()\"\r\n [class.fb-edge--folded]=\"edge.isFolded\"\r\n >\r\n <f-connection-marker-arrow [type]=\"markerEnd\" />\r\n @if (edge.label) {\r\n <div fConnectionContent class=\"fb-edge-label\">{{ edge.label }}</div>\r\n }\r\n </f-connection>\r\n }\r\n\r\n @for (node of nodes(); track node.id) {\r\n <div\r\n fNode\r\n fDragHandle\r\n [fNodeId]=\"node.id\"\r\n [fNodePosition]=\"node.position\"\r\n [class]=\"'fb-node ' + categoryClass(node) + ' ' + node.widthClass\"\r\n [class.fb-node--start]=\"node.isStart\"\r\n [class.fb-node--selected]=\"isSelected(node)\"\r\n [class.fb-node--unreachable]=\"!node.isReachable\"\r\n [class.fb-node--error]=\"node.severity === 'Error'\"\r\n [class.fb-node--warning]=\"node.severity === 'Warning'\"\r\n (dblclick)=\"onNodeDoubleClick(node.name)\"\r\n >\r\n <!--\r\n Lo Start non ha ingresso: e' il punto di partenza (\u00A73.4).\r\n `fConnectorConnectableSide` e' cio' che fa entrare l'arco dall'alto: senza, foblex\r\n calcola il lato e un arco che scende dal node sopra potrebbe agganciarsi di fianco.\r\n -->\r\n @if (!node.isStart) {\r\n <div\r\n fConnector\r\n fConnectorType=\"target\"\r\n [fConnectorId]=\"targetIdOf(node)\"\r\n [fConnectorConnectableSide]=\"sides.TOP\"\r\n fConnectorMultiple=\"true\"\r\n class=\"fb-connector fb-connector--in\"\r\n title=\"Ingresso\"\r\n ></div>\r\n }\r\n\r\n <div class=\"fb-node__head\">\r\n <span class=\"fb-node__icon\" aria-hidden=\"true\">{{ node.icon }}</span>\r\n <div class=\"fb-node__text\">\r\n <span class=\"fb-node__title\" [title]=\"node.description || node.label\">{{ node.label }}</span>\r\n <span class=\"fb-node__sub\">\r\n {{ node.typeLabel }}\r\n @if (node.subtitle) {\r\n <span class=\"fb-node__sub-dot\">\u00B7</span>{{ node.subtitle }}\r\n }\r\n </span>\r\n </div>\r\n @if (node.hasAutomaticOutput) {\r\n <!-- L'elemento espone il proprio risultato sotto il proprio nome (\u00A74.5). -->\r\n <span class=\"fb-node__auto\" title=\"Espone un output automatico referenziabile come \u00AB{{ node.name }}\u00BB\">\r\n \u0192\r\n </span>\r\n }\r\n <!--\r\n `fDragBlocker` impedisce che il pointerdown sul bottone diventi un trascinamento\r\n del node: senza, aprire il dettaglio sposterebbe l'elemento di qualche pixel.\r\n -->\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-node__edit\"\r\n [attr.aria-label]=\"'Apri il dettaglio di ' + node.label\"\r\n title=\"Apri il dettaglio\"\r\n (click)=\"onEditClick($event, node.name)\"\r\n >\r\n \u270E\r\n </button>\r\n @if (!node.isStart && isEditable()) {\r\n <!--\r\n Duplica: sta accanto a \u270E e non solo nell'inspector perche' e' il gesto con cui si\r\n riusa un elemento gi\u00E0 configurato, e cercarlo dentro il form dell'elemento da\r\n copiare e' il posto in cui non lo si cerca. Compare come \u00AB\u00D7\u00BB, sul solo node\r\n selezionato: aggiunge un elemento al documento, quindi non deve trovarsi sotto il\r\n puntatore di chi sta solo attraversando il grafo.\r\n -->\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-node__duplicate\"\r\n [attr.aria-label]=\"'Duplica ' + node.label\"\r\n title=\"Duplica questo elemento (Ctrl+D)\"\r\n (click)=\"onDuplicateClick($event, node.name)\"\r\n >\r\n \u29C9\r\n </button>\r\n <!--\r\n Si mostra solo sul node **selezionato**, non al passaggio del mouse come \u270E:\r\n cancellare non e' reversibile con un altro clic, e un comando distruttivo che\r\n appare sotto il puntatore mentre si attraversa il grafo si preme per sbaglio.\r\n Lo Start non lo espone: un flow senza ingresso non esisterebbe.\r\n -->\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-node__remove\"\r\n [attr.aria-label]=\"'Elimina ' + node.label\"\r\n title=\"Elimina questo elemento (si annulla con \u21B6)\"\r\n (click)=\"onRemoveClick($event, node.name)\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n <div class=\"fb-node__meta\">\r\n @if (!node.isStart) {\r\n <span class=\"fb-node__name\" [title]=\"'Nome tecnico: ' + node.name\">{{ node.name }}</span>\r\n }\r\n @if (!node.isReachable) {\r\n <span class=\"fb-badge fb-badge--unreachable\" title=\"Nessun percorso raggiunge questo elemento dallo Start\">\r\n scollegato\r\n </span>\r\n }\r\n @if (node.issueCount > 0) {\r\n <span\r\n class=\"fb-badge\"\r\n [class.fb-badge--error]=\"node.severity === 'Error'\"\r\n [class.fb-badge--warning]=\"node.severity === 'Warning'\"\r\n [class.fb-badge--info]=\"node.severity === 'Info'\"\r\n [title]=\"node.issueCount + ' rilievi di validazione'\"\r\n >\r\n {{ node.issueCount }}\r\n </span>\r\n }\r\n @if (node.danglingOutlets.length > 0) {\r\n <span\r\n class=\"fb-badge fb-badge--dangling\"\r\n [title]=\"'Rami dichiarati senza destinazione: ' + danglingLabels(node)\"\r\n >\r\n ramo incompleto\r\n </span>\r\n }\r\n </div>\r\n\r\n <!--\r\n Le uscite stanno sul bordo **inferiore**, una per ramo, nell'ordine in cui il modello\r\n le dichiara: per una Decision e' l'ordine di valutazione delle regole, che e'\r\n semantico (\u00A75.4), e da sinistra a destra si legge come la lista nell'inspector.\r\n L'etichetta si mostra solo quando i rami sono piu' di uno: su un `next` unico\r\n direbbe soltanto \u00ABSuccessivo\u00BB.\r\n -->\r\n <div class=\"fb-node__outlets\" [class.fb-node__outlets--labelled]=\"node.showsOutletLabels\">\r\n @for (outlet of node.outlets; track outlet.key) {\r\n <div class=\"fb-outlet\">\r\n @if (node.showsOutletLabels) {\r\n <span class=\"fb-outlet__label\" [title]=\"outlet.label\">{{ outlet.label }}</span>\r\n }\r\n <div\r\n fConnector\r\n fConnectorType=\"source\"\r\n [fConnectorId]=\"connectorIdOf(node, outlet.key)\"\r\n [fConnectorConnectableSide]=\"sides.BOTTOM\"\r\n [class]=\"'fb-connector fb-connector--out fb-connector--' + outlet.kind\"\r\n [title]=\"outlet.label\"\r\n ></div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Anteprima dell'arco durante il trascinamento. -->\r\n <f-connection-for-create fBehavior=\"floating\" fType=\"bezier\" class=\"fb-edge fb-edge--creating\">\r\n <f-connection-marker-arrow [type]=\"markerEnd\" />\r\n </f-connection-for-create>\r\n\r\n <f-selection-area />\r\n </f-canvas>\r\n\r\n <!--\r\n Fuori da <f-canvas> come la minimappa: dentro, la trasformazione del canvas se lo\r\n porterebbe via insieme ai node. `fDragBlocker` perche' il pointerdown non inizi una\r\n panoramica invece di premere il bottone.\r\n -->\r\n <!--\r\n Sempre nel DOM, nascosto con una classe e non con `@if`: dentro una proiezione di contenuto\r\n un blocco di controllo e' una complicazione gratuita, e `visibility: hidden` lo toglie\r\n anche dall'ordine di tabulazione. Il costo e' un bottone in piu' nel DOM, non un rischio.\r\n -->\r\n <div class=\"fb-viewport\" fDragBlocker role=\"group\" aria-label=\"Vista\">\r\n <!--\r\n Lo zoom ha bisogno di **comandi e di un numero**: con la sola rotella si finisce all'8% su\r\n un flow grande e sembra che il canvas si sia svuotato, senza un modo evidente di tornare.\r\n Il numero e' un bottone: riporta al 100% sull'elemento corrente.\r\n -->\r\n <button type=\"button\" class=\"fb-btn fb-viewport__btn\" title=\"Riduci (rotella indietro)\" (click)=\"zoomOut()\">\r\n \u2212\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-viewport__level\"\r\n title=\"Torna al 100% sull\u2019elemento selezionato\"\r\n (click)=\"zoomToActual()\"\r\n >\r\n {{ zoomPercent() }}%\r\n </button>\r\n <button type=\"button\" class=\"fb-btn fb-viewport__btn\" title=\"Ingrandisci (rotella avanti)\" (click)=\"zoomIn()\">\r\n +\r\n </button>\r\n <!--\r\n La modalita\u2019 selezione. Esiste perche\u2019 i due gesti che la libreria offre di serie \u2014\r\n `Ctrl`+clic per aggiungere, `Shift`+trascina per il rettangolo \u2014 non si vedono: senza un\r\n interruttore, su un flow grande si finisce a spostare un elemento alla volta. Accesa, il\r\n trascinamento sul canvas vuoto **seleziona** e la vista si sposta col tasto centrale.\r\n -->\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-viewport__select\"\r\n [class.fb-viewport__select--on]=\"isSelecting()\"\r\n [attr.aria-pressed]=\"isSelecting()\"\r\n [title]=\"\r\n isSelecting()\r\n ? 'Modalita\u2019 selezione attiva: trascina sul canvas per selezionare piu\u2019 elementi. La vista si sposta col tasto centrale del mouse'\r\n : 'Attiva la selezione a rettangolo: trascinando sul canvas selezioni piu\u2019 elementi invece di spostare la vista (Shift+trascina fa lo stesso, sempre)'\r\n \"\r\n (click)=\"toggleSelectionMode()\"\r\n >\r\n <span class=\"fb-viewport__icon\" aria-hidden=\"true\">\u25A4</span>\r\n Seleziona\r\n </button>\r\n <!--\r\n Sempre nel DOM, nascosto con una classe e non con `@if`: dentro una proiezione di contenuto\r\n un blocco di controllo e' una complicazione gratuita, e `visibility: hidden` lo toglie\r\n anche dall'ordine di tabulazione. Il costo e' un bottone in piu' nel DOM, non un rischio.\r\n -->\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-viewport__fit\"\r\n [class.fb-viewport__fit--hidden]=\"!hasMovedViewport()\"\r\n title=\"Rimetti il flow in vista. Su un flow molto grande inquadra l\u2019inizio, perche\u2019 l\u2019intero non sarebbe leggibile\"\r\n (click)=\"resetViewport()\"\r\n >\r\n <span class=\"fb-viewport__icon\" aria-hidden=\"true\">\u2922</span>\r\n Inquadra\r\n </button>\r\n </div>\r\n\r\n @if (selectionCount() > 1) {\r\n <!--\r\n Il dato che rende utile la selezione: che si spostano **insieme**. Senza dirlo, dopo aver\r\n selezionato cinque elementi si continua a trascinarne uno per volta.\r\n -->\r\n <p class=\"fb-select-hint\" role=\"status\">\r\n {{ selectionCount() }} elementi selezionati: trascinane uno per spostarli tutti \u00B7 Ctrl+clic\r\n aggiunge o toglie \u00B7 Canc li elimina\r\n </p>\r\n } @else if (isSelecting()) {\r\n <p class=\"fb-select-hint\" role=\"status\">\r\n Trascina sul canvas per selezionare \u00B7 la vista si sposta col tasto centrale\r\n </p>\r\n }\r\n\r\n @if (insertionEdgeId()) {\r\n <!-- Il rilascio su un arco non e' un gesto che si indovina: mentre e' possibile, si dice. -->\r\n <p class=\"fb-insert-hint\" role=\"status\">Rilascia qui per inserire l\u2019elemento in questo percorso</p>\r\n }\r\n\r\n <f-minimap [fMinSize]=\"1200\" class=\"fb-minimap\" />\r\n</f-flow>\r\n", styles: [":host{display:block;position:relative;width:100%;height:100%;overflow:hidden;background:var(--fb-canvas-bg, #f4f5f7)}f-flow{display:block;width:100%;height:100%}.fb-minimap{position:absolute;right:14px;bottom:14px;width:150px;height:300px;max-height:40%;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius, 10px);background:var(--fb-surface, #fff);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));overflow:hidden}.fb-viewport{position:absolute;right:14px;bottom:322px;z-index:1;display:flex;align-items:center;gap:4px;flex-wrap:wrap;justify-content:flex-end;max-width:200px}.fb-viewport__btn,.fb-viewport__level,.fb-viewport__fit{border-radius:var(--fb-radius-xs, 6px);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));font-size:11px}.fb-viewport__btn{width:24px;padding:2px 0;font-size:14px;line-height:1}.fb-viewport__level{min-width:52px;font-variant-numeric:tabular-nums}.fb-viewport__fit--hidden{opacity:0;visibility:hidden}.fb-viewport__fit{transition:opacity .15s ease,visibility .15s ease}.fb-viewport__icon{font-size:13px;line-height:1;color:var(--fb-text-muted, #667085)}.fb-insert-hint{position:absolute;bottom:18px;left:50%;transform:translate(-50%);z-index:1;margin:0;padding:6px 12px;border-radius:999px;background:var(--fb-accent, #2f6feb);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));color:#fff;font-size:11px;font-weight:600;pointer-events:none}.fb-select-hint{position:absolute;bottom:18px;left:50%;transform:translate(-50%);z-index:1;margin:0;padding:6px 12px;border-radius:999px;border:1px solid var(--fb-border, #e2e5eb);background:var(--fb-surface, #fff);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));color:var(--fb-text-muted, #667085);font-size:11px;font-weight:600;pointer-events:none}.fb-viewport__select{border-radius:var(--fb-radius-xs, 6px);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));font-size:11px}.fb-viewport__select--on{border-color:var(--fb-accent, #2f6feb);background:var(--fb-accent, #2f6feb);color:#fff}.fb-node{position:absolute;display:flex;flex-direction:column;box-sizing:border-box;width:240px;padding:0;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius-lg, 12px);background:var(--fb-surface, #fff);box-shadow:var(--fb-shadow-sm, 0 1px 2px rgb(16 24 40 / 6%));font:inherit;cursor:grab;-webkit-user-select:none;user-select:none;transition:box-shadow .12s ease,border-color .12s ease}.fb-node--outlets-3{width:304px}.fb-node--outlets-4{width:380px}.fb-node--outlets-5{width:456px}.fb-node--outlets-6{width:520px}.fb-node:hover{box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%))}.fb-node--selected{border-color:var(--fb-accent, #4f6ef7);box-shadow:0 0 0 3px color-mix(in srgb,var(--fb-accent, #4f6ef7) 22%,transparent)}.fb-node--unreachable{border-style:dashed;opacity:.8}.fb-node--error{border-color:var(--fb-error, #c9372c)}.fb-node--warning{border-color:var(--fb-warning, #b7791f)}.fb-node__head{display:flex;align-items:center;gap:8px;padding:10px 10px 6px 12px}.fb-node__icon{display:grid;place-items:center;flex:0 0 auto;width:28px;height:28px;border-radius:var(--fb-radius-sm, 8px);background:var(--fb-node-accent, #667085);color:#fff;font-size:14px;line-height:1}.cat-start{--fb-node-accent: #22a06b}.cat-screen{--fb-node-accent: #3b82f6}.cat-logic{--fb-node-accent: #8b5cf6}.cat-data{--fb-node-accent: #06b6d4}.cat-action{--fb-node-accent: #f59e0b}.cat-flow{--fb-node-accent: #14b8a6}.cat-other{--fb-node-accent: #667085}.fb-node__text{flex:1;min-width:0}.fb-node__title{display:block;font-size:13px;font-weight:600;line-height:17px;color:var(--fb-text, #1a1c23);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-node__sub{display:block;margin-top:1px;font-size:11px;line-height:14px;color:var(--fb-text-muted, #6b7086);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-node__sub-dot{margin:0 4px;color:var(--fb-text-subtle, #98a2b3)}.fb-node__auto{flex:0 0 auto;font-size:12px;font-weight:700;color:var(--fb-accent, #4f6ef7);cursor:help}.fb-node__edit{display:grid;place-items:center;flex:0 0 auto;width:22px;height:22px;padding:0;border:0;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text-subtle, #98a2b3);font:inherit;font-size:12px;cursor:pointer;opacity:0;transition:opacity .12s ease,background .12s ease}.fb-node:hover .fb-node__edit,.fb-node--selected .fb-node__edit,.fb-node__edit:focus-visible{opacity:1}.fb-node__edit:hover{background:var(--fb-surface-alt, #f7f8fa);color:var(--fb-text, #1a1c23)}.fb-node__duplicate,.fb-node__remove{display:grid;place-items:center;flex:0 0 auto;width:22px;height:22px;padding:0;border:0;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text-subtle, #98a2b3);font:inherit;font-size:15px;line-height:1;cursor:pointer;opacity:0;transition:opacity .12s ease,background .12s ease,color .12s ease}.fb-node--selected .fb-node__duplicate,.fb-node--selected .fb-node__remove,.fb-node__duplicate:focus-visible,.fb-node__remove:focus-visible{opacity:1}.fb-node__remove:hover{background:color-mix(in srgb,var(--fb-error, #c9372c) 12%,transparent);color:var(--fb-error, #c9372c)}.fb-node__duplicate:hover{background:color-mix(in srgb,var(--fb-accent, #3b6ef2) 12%,transparent);color:var(--fb-accent, #3b6ef2)}.fb-node__meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px;min-height:14px;padding:0 12px 6px}.fb-node__name{flex:0 1 auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;color:var(--fb-text-subtle, #98a2b3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-badge{padding:1px 6px;border-radius:10px;background:var(--fb-badge-bg, #eef0f4);font-size:10px;font-weight:600;color:var(--fb-text-muted, #6b7086);white-space:nowrap;cursor:help}.fb-badge--error{background:color-mix(in srgb,var(--fb-error, #c9372c) 14%,transparent);color:var(--fb-error, #c9372c)}.fb-badge--warning{background:color-mix(in srgb,var(--fb-warning, #b7791f) 16%,transparent);color:var(--fb-warning, #b7791f)}.fb-badge--info{background:color-mix(in srgb,var(--fb-accent, #4f6ef7) 12%,transparent);color:var(--fb-accent, #4f6ef7)}.fb-badge--unreachable,.fb-badge--dangling{background:color-mix(in srgb,var(--fb-warning, #b7791f) 12%,transparent);color:var(--fb-warning, #b7791f)}.fb-node__outlets{display:flex;align-items:flex-end;justify-content:space-evenly;gap:4px;padding:0 8px 4px}.fb-node__outlets--labelled{padding-top:5px;border-top:1px solid var(--fb-border-subtle, #eef0f4)}.fb-outlet{display:flex;flex:1 1 0;flex-direction:column;align-items:center;gap:2px;min-width:0}.fb-outlet__label{max-width:100%;font-size:10px;line-height:13px;color:var(--fb-text-muted, #6b7086);text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-connector{box-sizing:border-box;width:12px;height:12px;flex:0 0 auto;border:2px solid var(--fb-surface, #fff);border-radius:50%;background:var(--fb-connector, #98a2b3);cursor:crosshair;transition:transform .12s ease,box-shadow .12s ease}f-flow .fb-connector--out{position:relative;inset:auto;margin-bottom:-12px}.fb-connector--out:hover{transform:scale(1.2)}f-flow .fb-connector--in{position:absolute;inset:-2px auto auto 50%;width:24px;height:4px;border:0;border-radius:2px;transform:translate(-50%);background:var(--fb-border-strong, #cfd4de)}.fb-connector--out{--ff-connector-connected-color: var(--fb-connector, #98a2b3)}.fb-connector--Next,.fb-connector--Start,.fb-connector--LoopNext{--fb-connector: var(--fb-edge-next, #98a2b3)}.fb-connector--Rule,.fb-connector--WaitEvent{--fb-connector: var(--fb-edge-rule, #8b5cf6)}.fb-connector--Default,.fb-connector--LoopEnd{--fb-connector: var(--fb-edge-default, #06b6d4)}.fb-connector--Fault{--fb-connector: var(--fb-edge-fault, #dc2626)}.fb-connector--Timeout,.fb-connector--ScheduledPath{--fb-connector: var(--fb-edge-timeout, #f59e0b)}.fb-connector.f-connector-connectable{box-shadow:0 0 0 4px color-mix(in srgb,var(--fb-accent, #4f6ef7) 28%,transparent)}.fb-group{position:absolute;box-sizing:border-box;border:1px solid var(--fb-group-border, #d6dae1);border-radius:var(--fb-radius, 10px);background:var(--fb-group-fill, rgb(148 163 184 / 10%));pointer-events:none;overflow:hidden}.fb-group--selected{border-color:var(--fb-accent, #2f6feb);box-shadow:0 0 0 2px color-mix(in srgb,var(--fb-accent, #2f6feb) 25%,transparent)}.fb-group__bar{display:flex;align-items:center;gap:6px;height:30px;padding:0 6px;border-bottom:1px solid var(--fb-group-border, #d6dae1);background:var(--fb-group-bar, rgb(148 163 184 / 22%));color:var(--fb-text, #1d2939);font-size:12px;font-weight:600;pointer-events:auto;cursor:move;-webkit-user-select:none;user-select:none}.fb-fold{position:absolute;box-sizing:border-box;display:flex;flex-direction:column;justify-content:center;width:260px;min-height:64px;padding:6px 8px;border:1px solid var(--fb-group-border, #d6dae1);border-left:4px solid var(--fb-group-border, #d6dae1);border-radius:var(--fb-radius-lg, 12px);background:var(--fb-group-bar, rgb(148 163 184 / 22%));box-shadow:var(--fb-shadow-sm, 0 1px 2px rgb(16 24 40 / 6%));color:var(--fb-text, #1d2939);font:inherit;font-size:12px;cursor:grab;-webkit-user-select:none;user-select:none}.fb-fold:hover{box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%))}.fb-fold--selected{border-color:var(--fb-accent, #4f6ef7);box-shadow:0 0 0 3px color-mix(in srgb,var(--fb-accent, #4f6ef7) 22%,transparent)}.fb-fold--error{border-color:var(--fb-error, #c9372c)}.fb-fold--warning{border-color:var(--fb-warning, #b7791f)}.fb-fold__head{display:flex;align-items:center;gap:6px}.fb-fold__title{flex:1;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-weight:600}.fb-fold__open,.fb-fold__remove{flex:none;width:18px;height:18px;padding:0;border:none;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:inherit;font:inherit;font-size:12px;line-height:1;cursor:pointer}.fb-fold__remove{font-size:14px}.fb-fold__open:hover,.fb-fold__remove:hover{background:#10182814}.fb-fold__meta{display:flex;align-items:center;gap:6px;margin-top:2px;color:var(--fb-text-muted, #667085);font-size:11px}.fb-fold__count{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fb-fold__outlets{display:flex;justify-content:center;margin-top:4px}.fb-group__fold,.fb-group__remove{flex:none;width:18px;height:18px;padding:0;border:none;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:inherit;font:inherit;font-size:12px;line-height:1;cursor:pointer}.fb-group__fold:hover,.fb-group__remove:hover{background:#10182814}.fb-group__remove{margin-left:auto;font-size:14px}.fb-group__title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fb-group__count{flex:none;padding:0 5px;border-radius:8px;background:#10182814;font-size:10px;font-weight:600;font-variant-numeric:tabular-nums}.fb-group__stale{flex:none;padding:0 5px;border-radius:8px;background:color-mix(in srgb,var(--fb-warning, #b7791f) 14%,transparent);color:var(--fb-warning, #b7791f);font-size:10px;font-weight:600}.fb-group__note{margin:6px 8px 0;color:var(--fb-text-muted, #667085);font-size:11px;line-height:1.35;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}.fb-group__resize{position:absolute;right:0;bottom:0;width:14px;height:14px;border-right:2px solid var(--fb-group-border, #d6dae1);border-bottom:2px solid var(--fb-group-border, #d6dae1);border-bottom-right-radius:var(--fb-radius, 10px);pointer-events:auto;cursor:nwse-resize}.fb-group--giallo{--fb-group-border: #e0c26a;--fb-group-fill: rgb(234 179 8 / 10%);--fb-group-bar: rgb(234 179 8 / 24%)}.fb-group--verde{--fb-group-border: #86c79b;--fb-group-fill: rgb(34 160 89 / 10%);--fb-group-bar: rgb(34 160 89 / 22%)}.fb-group--blu{--fb-group-border: #93b4ea;--fb-group-fill: rgb(47 111 235 / 9%);--fb-group-bar: rgb(47 111 235 / 20%)}.fb-group--viola{--fb-group-border: #bda6e6;--fb-group-fill: rgb(139 92 246 / 10%);--fb-group-bar: rgb(139 92 246 / 22%)}.fb-group--rosso{--fb-group-border: #e6a3a3;--fb-group-fill: rgb(214 69 69 / 9%);--fb-group-bar: rgb(214 69 69 / 20%)}.fb-group--grigio,.fb-group--neutro{--fb-group-border: #d6dae1;--fb-group-fill: rgb(148 163 184 / 10%);--fb-group-bar: rgb(148 163 184 / 22%)}\n"], dependencies: [{ kind: "ngmodule", type: FFlowModule }, { kind: "component", type: i1.FFlowComponent, selector: "f-flow", inputs: ["fFlowId", "fCache"], outputs: ["fNodesRendered", "fFullRendered", "fLoaded"] }, { kind: "component", type: i1.FCanvasComponent, selector: "f-canvas", inputs: ["position", "scale", "debounceTime", "fLayers"], outputs: ["fCanvasChange"] }, { kind: "component", type: i1.FBackgroundComponent, selector: "f-background" }, { kind: "component", type: i1.FCirclePatternComponent, selector: "f-circle-pattern", inputs: ["id", "color", "radius"] }, { kind: "directive", type: i1.FZoomDirective, selector: "f-canvas[fZoom]", inputs: ["fZoom", "fWheelTrigger", "fDblClickTrigger", "fZoomMinimum", "fZoomMaximum", "fZoomStep", "fPinchStep", "fZoomDblClickStep"] }, { kind: "component", type: i1.FSelectionArea, selector: "f-selection-area", inputs: ["fTrigger"] }, { kind: "directive", type: i1.FConnectionContent, selector: "[fConnectionContent]", inputs: ["position", "offset", "align"] }, { kind: "component", type: i1.FConnectionMarkerArrow, selector: "f-connection-marker-arrow", inputs: ["type"] }, { kind: "component", type: i1.FConnectionComponent, selector: "f-connection", inputs: ["fConnectionId", "fSourceId", "fTargetId", "fOutputId", "fInputId", "fRadius", "fOffset", "fBehavior", "fType", "fSelectionDisabled", "fReassignableStart", "fReassignDisabled", "fSourceSide", "fTargetSide", "fInputSide", "fOutputSide"], exportAs: ["fComponent"] }, { kind: "component", type: i1.FConnectionForCreateComponent, selector: "f-connection-for-create", inputs: ["fRadius", "fOffset", "fBehavior", "fType", "fInputSide", "fOutputSide"] }, { kind: "directive", type: i1.FConnectorDirective, selector: "[fConnector]", inputs: ["fConnectorId", "fConnectorType", "fConnectorDisabled", "fConnectorMultiple", "fConnectorCategory", "fConnectorConnectableSide", "fConnectorSelfConnectable", "fCanBeConnectedTo", "fConnectionFromOutlet"], exportAs: ["fConnector"] }, { kind: "component", type: i1.FMinimapComponent, selector: "f-minimap", inputs: ["fMinSize", "fNodeRenderLimit"], exportAs: ["fComponent"] }, { kind: "directive", type: i1.FGroupDirective, selector: "[fGroup]", inputs: ["fGroupId", "fGroupParentId", "fGroupPosition", "fGroupSize", "fGroupRotate", "fConnectOnNode", "fMinimapClass", "fGroupDraggingDisabled", "fGroupSelectionDisabled", "fIncludePadding", "fAutoExpandOnChildHit", "fAutoSizeToFitChildren"], outputs: ["fGroupPositionChange", "fGroupSizeChange", "fGroupRotateChange"], exportAs: ["fComponent"] }, { kind: "directive", type: i1.FNodeDirective, selector: "[fNode]", inputs: ["fNodeId", "fNodeParentId", "fNodePosition", "fNodeSize", "fNodeRotate", "fConnectOnNode", "fMinimapClass", "fNodeDraggingDisabled", "fNodeSelectionDisabled", "fIncludePadding", "fAutoExpandOnChildHit", "fAutoSizeToFitChildren"], outputs: ["fNodePositionChange", "fNodeSizeChange", "fNodeRotateChange"], exportAs: ["fComponent"] }, { kind: "directive", type: i1.FDragHandleDirective, selector: "[fDragHandle]" }, { kind: "directive", type: i1.FResizeHandleDirective, selector: "[fResizeHandle]", inputs: ["fResizeHandleType"] }, { kind: "directive", type: i1.FDragBlockerDirective, selector: "[fDragBlocker]" }, { kind: "directive", type: i1.FDraggableDirective, selector: "f-flow[fDraggable]", inputs: ["fDraggableDisabled", "fDropToGroup", "fMultiSelectTrigger", "fReassignConnectionTrigger", "fCreateConnectionTrigger", "fConnectionWaypointsTrigger", "fMoveControlPointTrigger", "fNodeResizeTrigger", "fNodeRotateTrigger", "fNodeMoveTrigger", "fCanvasMoveTrigger", "fExternalItemTrigger", "fEmitOnNodeIntersect", "vCellSize", "hCellSize", "fCellSizeWhileDragging"], outputs: ["fSelectionChange", "fDeleteSelected", "fNodeIntersectedWithConnections", "fNodeConnectionsIntersection", "fCreateNode", "fMoveNodes", "fReassignConnection", "fCreateConnection", "fConnectionWaypointsChanged", "fDropToGroup", "fDragStarted", "fDragEnded"], exportAs: ["fDraggable"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5891
6194
  }
5892
6195
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: FlowCanvasComponent, decorators: [{
5893
6196
  type: Component,
@@ -5900,7 +6203,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImpo
5900
6203
  * esisterebbe. Registrarlo col preset di default non cambia niente di per se'.
5901
6204
  */
5902
6205
  withControlScheme(F_DEFAULT_CONTROL_SCHEME)),
5903
- ], template: "<!--\r\n Gerarchia obbligatoria: f-flow > f-canvas > fNode / f-connection.\r\n I `@for` sono direttamente dentro <f-canvas>: nessun wrapper, quindi non serve\r\n `ngProjectAs` (che sarebbe indispensabile con blocchi annidati).\r\n-->\r\n<f-flow\r\n fDraggable\r\n (fCreateConnection)=\"onCreateConnection($event)\"\r\n (fReassignConnection)=\"onReassignConnection($event)\"\r\n (fMoveNodes)=\"onMoveNodes($event)\"\r\n (fSelectionChange)=\"onSelectionChange($event)\"\r\n (fDeleteSelected)=\"onDeleteSelected($event)\"\r\n (fCreateNode)=\"onCreateNode($event)\"\r\n (fNodesRendered)=\"onNodesRendered()\"\r\n (fDragStarted)=\"onDragStarted($event)\"\r\n (fDragEnded)=\"onDragEnded()\"\r\n>\r\n <!--\r\n `#canvas` + `(fCanvasChange)`: il primo serve per chiamare `fitToScreen()`, il secondo per\r\n sapere che l'utente ha mosso la vista. `[debounceTime]` non si imposta: l'evento serve solo\r\n ad accendere un flag, non a ricalcolare niente.\r\n -->\r\n <f-canvas fZoom #canvas (fCanvasChange)=\"onCanvasChange()\">\r\n <f-background>\r\n <f-circle-pattern />\r\n </f-background>\r\n\r\n <!--\r\n \u00A73.6 \u2014 i riquadri di raggruppamento. Vanno **sotto** i node, e non c'e' niente da fare per\r\n ottenerlo: `[fGroup]` viene proiettato nel layer dei gruppi, che nell'ordine della libreria\r\n sta sotto connessioni e node.\r\n\r\n Sono commenti: nessun connettore, nessun arco, nessuna raggiungibilita'. Il corpo del\r\n riquadro ha `pointer-events: none` (nel CSS) e solo la barra del titolo risponde: senza,\r\n una cornice grande quanto mezzo canvas si mangerebbe i click destinati alla panoramica e\r\n alla selezione a rettangolo, e i node dentro non si potrebbero piu' prendere.\r\n -->\r\n @for (group of groups(); track group.name) {\r\n <div\r\n fGroup\r\n [fGroupId]=\"group.id\"\r\n [fGroupPosition]=\"group.position\"\r\n [fGroupSize]=\"group.size\"\r\n [fGroupDraggingDisabled]=\"!isEditable()\"\r\n [class]=\"'fb-group ' + group.colorClass\"\r\n [class.fb-group--collapsed]=\"group.isCollapsed\"\r\n [class.fb-group--selected]=\"isGroupSelected(group)\"\r\n (fGroupSizeChange)=\"onGroupResized(group.name, $event)\"\r\n >\r\n <!--\r\n `fDragHandle` sulla sola barra: il riquadro si sposta dal titolo, come una finestra.\r\n Trascinandolo da dentro si sposterebbe la cornice ogni volta che si prova a prendere\r\n un node che le sta sopra.\r\n -->\r\n <div\r\n class=\"fb-group__bar\"\r\n fDragHandle\r\n [title]=\"group.description || group.label\"\r\n (click)=\"onGroupClick($event, group.name)\"\r\n >\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-group__fold\"\r\n [attr.aria-label]=\"group.isCollapsed ? 'Apri il riquadro' : 'Chiudi il riquadro'\"\r\n [title]=\"\r\n group.isCollapsed\r\n ? 'Apri il riquadro'\r\n : 'Chiudi il riquadro: resta la barra del titolo, gli elementi restano dove sono'\r\n \"\r\n (click)=\"toggleGroupCollapsed($event, group.name)\"\r\n >\r\n {{ group.isCollapsed ? '\u25B8' : '\u25BE' }}\r\n </button>\r\n <span class=\"fb-group__title\">{{ group.label }}</span>\r\n <span class=\"fb-group__count\" [title]=\"group.memberCount + ' elementi nel riquadro'\">\r\n {{ group.memberCount }}\r\n </span>\r\n @if (group.unknownMembers.length > 0) {\r\n <!--\r\n Membri che nominano un elemento che non c'e' piu'. Non e' un difetto (\u00A73.6): e' cio'\r\n che resta dopo una cancellazione, ed e' un avviso \u2014 l'attivazione non si blocca.\r\n -->\r\n <span\r\n class=\"fb-group__stale\"\r\n [title]=\"'Elementi non piu\u2019 esistenti: ' + group.unknownMembers.join(', ')\"\r\n >\r\n {{ group.unknownMembers.length }} da ripulire\r\n </span>\r\n }\r\n @if (isEditable()) {\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-group__remove\"\r\n [attr.aria-label]=\"'Elimina il riquadro ' + group.label\"\r\n title=\"Elimina il riquadro: gli elementi che contiene restano\"\r\n (click)=\"onGroupRemoveClick($event, group.name)\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (!group.isCollapsed && group.description) {\r\n <p class=\"fb-group__note\">{{ group.description }}</p>\r\n }\r\n\r\n @if (isEditable() && !group.isCollapsed) {\r\n <!--\r\n Una sola maniglia, in basso a destra: quattro bersagli su una cornice che sta sotto\r\n i node si prendono per sbaglio. Ridimensionare **dichiara** la geometria, che e' il\r\n modo di uscire dal \u00ABcalcolala sui membri\u00BB (\u00A73.6).\r\n -->\r\n <div\r\n fResizeHandle\r\n [fResizeHandleType]=\"resizeHandles.RIGHT_BOTTOM\"\r\n class=\"fb-group__resize\"\r\n title=\"Ridimensiona il riquadro\"\r\n ></div>\r\n }\r\n </div>\r\n }\r\n\r\n @for (edge of edges(); track edge.id) {\r\n <f-connection\r\n [fConnectionId]=\"edge.id\"\r\n [fSourceId]=\"edge.sourceId\"\r\n [fTargetId]=\"edge.targetId\"\r\n fBehavior=\"floating\"\r\n [fType]=\"edge.isGoTo ? 'segment' : 'bezier'\"\r\n [class]=\"'fb-edge fb-edge--' + edge.kind + (edge.isGoTo ? ' fb-edge--goto' : '')\"\r\n [class.fb-edge--insert]=\"edge.id === insertionEdgeId()\"\r\n >\r\n <f-connection-marker-arrow [type]=\"markerEnd\" />\r\n @if (edge.label) {\r\n <div fConnectionContent class=\"fb-edge-label\">{{ edge.label }}</div>\r\n }\r\n </f-connection>\r\n }\r\n\r\n @for (node of nodes(); track node.id) {\r\n <div\r\n fNode\r\n fDragHandle\r\n [fNodeId]=\"node.id\"\r\n [fNodePosition]=\"node.position\"\r\n [class]=\"'fb-node ' + categoryClass(node) + ' ' + node.widthClass\"\r\n [class.fb-node--start]=\"node.isStart\"\r\n [class.fb-node--selected]=\"isSelected(node)\"\r\n [class.fb-node--unreachable]=\"!node.isReachable\"\r\n [class.fb-node--error]=\"node.severity === 'Error'\"\r\n [class.fb-node--warning]=\"node.severity === 'Warning'\"\r\n (dblclick)=\"onNodeDoubleClick(node.name)\"\r\n >\r\n <!--\r\n Lo Start non ha ingresso: e' il punto di partenza (\u00A73.4).\r\n `fConnectorConnectableSide` e' cio' che fa entrare l'arco dall'alto: senza, foblex\r\n calcola il lato e un arco che scende dal node sopra potrebbe agganciarsi di fianco.\r\n -->\r\n @if (!node.isStart) {\r\n <div\r\n fConnector\r\n fConnectorType=\"target\"\r\n [fConnectorId]=\"targetIdOf(node)\"\r\n [fConnectorConnectableSide]=\"sides.TOP\"\r\n fConnectorMultiple=\"true\"\r\n class=\"fb-connector fb-connector--in\"\r\n title=\"Ingresso\"\r\n ></div>\r\n }\r\n\r\n <div class=\"fb-node__head\">\r\n <span class=\"fb-node__icon\" aria-hidden=\"true\">{{ node.icon }}</span>\r\n <div class=\"fb-node__text\">\r\n <span class=\"fb-node__title\" [title]=\"node.description || node.label\">{{ node.label }}</span>\r\n <span class=\"fb-node__sub\">\r\n {{ node.typeLabel }}\r\n @if (node.subtitle) {\r\n <span class=\"fb-node__sub-dot\">\u00B7</span>{{ node.subtitle }}\r\n }\r\n </span>\r\n </div>\r\n @if (node.hasAutomaticOutput) {\r\n <!-- L'elemento espone il proprio risultato sotto il proprio nome (\u00A74.5). -->\r\n <span class=\"fb-node__auto\" title=\"Espone un output automatico referenziabile come \u00AB{{ node.name }}\u00BB\">\r\n \u0192\r\n </span>\r\n }\r\n <!--\r\n `fDragBlocker` impedisce che il pointerdown sul bottone diventi un trascinamento\r\n del node: senza, aprire il dettaglio sposterebbe l'elemento di qualche pixel.\r\n -->\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-node__edit\"\r\n [attr.aria-label]=\"'Apri il dettaglio di ' + node.label\"\r\n title=\"Apri il dettaglio\"\r\n (click)=\"onEditClick($event, node.name)\"\r\n >\r\n \u270E\r\n </button>\r\n @if (!node.isStart && isEditable()) {\r\n <!--\r\n Duplica: sta accanto a \u270E e non solo nell'inspector perche' e' il gesto con cui si\r\n riusa un elemento gi\u00E0 configurato, e cercarlo dentro il form dell'elemento da\r\n copiare e' il posto in cui non lo si cerca. Compare come \u00AB\u00D7\u00BB, sul solo node\r\n selezionato: aggiunge un elemento al documento, quindi non deve trovarsi sotto il\r\n puntatore di chi sta solo attraversando il grafo.\r\n -->\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-node__duplicate\"\r\n [attr.aria-label]=\"'Duplica ' + node.label\"\r\n title=\"Duplica questo elemento (Ctrl+D)\"\r\n (click)=\"onDuplicateClick($event, node.name)\"\r\n >\r\n \u29C9\r\n </button>\r\n <!--\r\n Si mostra solo sul node **selezionato**, non al passaggio del mouse come \u270E:\r\n cancellare non e' reversibile con un altro clic, e un comando distruttivo che\r\n appare sotto il puntatore mentre si attraversa il grafo si preme per sbaglio.\r\n Lo Start non lo espone: un flow senza ingresso non esisterebbe.\r\n -->\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-node__remove\"\r\n [attr.aria-label]=\"'Elimina ' + node.label\"\r\n title=\"Elimina questo elemento (si annulla con \u21B6)\"\r\n (click)=\"onRemoveClick($event, node.name)\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n <div class=\"fb-node__meta\">\r\n @if (!node.isStart) {\r\n <span class=\"fb-node__name\" [title]=\"'Nome tecnico: ' + node.name\">{{ node.name }}</span>\r\n }\r\n @if (!node.isReachable) {\r\n <span class=\"fb-badge fb-badge--unreachable\" title=\"Nessun percorso raggiunge questo elemento dallo Start\">\r\n scollegato\r\n </span>\r\n }\r\n @if (node.issueCount > 0) {\r\n <span\r\n class=\"fb-badge\"\r\n [class.fb-badge--error]=\"node.severity === 'Error'\"\r\n [class.fb-badge--warning]=\"node.severity === 'Warning'\"\r\n [class.fb-badge--info]=\"node.severity === 'Info'\"\r\n [title]=\"node.issueCount + ' rilievi di validazione'\"\r\n >\r\n {{ node.issueCount }}\r\n </span>\r\n }\r\n @if (node.danglingOutlets.length > 0) {\r\n <span\r\n class=\"fb-badge fb-badge--dangling\"\r\n [title]=\"'Rami dichiarati senza destinazione: ' + danglingLabels(node)\"\r\n >\r\n ramo incompleto\r\n </span>\r\n }\r\n </div>\r\n\r\n <!--\r\n Le uscite stanno sul bordo **inferiore**, una per ramo, nell'ordine in cui il modello\r\n le dichiara: per una Decision e' l'ordine di valutazione delle regole, che e'\r\n semantico (\u00A75.4), e da sinistra a destra si legge come la lista nell'inspector.\r\n L'etichetta si mostra solo quando i rami sono piu' di uno: su un `next` unico\r\n direbbe soltanto \u00ABSuccessivo\u00BB.\r\n -->\r\n <div class=\"fb-node__outlets\" [class.fb-node__outlets--labelled]=\"node.showsOutletLabels\">\r\n @for (outlet of node.outlets; track outlet.key) {\r\n <div class=\"fb-outlet\">\r\n @if (node.showsOutletLabels) {\r\n <span class=\"fb-outlet__label\" [title]=\"outlet.label\">{{ outlet.label }}</span>\r\n }\r\n <div\r\n fConnector\r\n fConnectorType=\"source\"\r\n [fConnectorId]=\"connectorIdOf(node, outlet.key)\"\r\n [fConnectorConnectableSide]=\"sides.BOTTOM\"\r\n [class]=\"'fb-connector fb-connector--out fb-connector--' + outlet.kind\"\r\n [title]=\"outlet.label\"\r\n ></div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Anteprima dell'arco durante il trascinamento. -->\r\n <f-connection-for-create fBehavior=\"floating\" fType=\"bezier\" class=\"fb-edge fb-edge--creating\">\r\n <f-connection-marker-arrow [type]=\"markerEnd\" />\r\n </f-connection-for-create>\r\n\r\n <f-selection-area />\r\n </f-canvas>\r\n\r\n <!--\r\n Fuori da <f-canvas> come la minimappa: dentro, la trasformazione del canvas se lo\r\n porterebbe via insieme ai node. `fDragBlocker` perche' il pointerdown non inizi una\r\n panoramica invece di premere il bottone.\r\n -->\r\n <!--\r\n Sempre nel DOM, nascosto con una classe e non con `@if`: dentro una proiezione di contenuto\r\n un blocco di controllo e' una complicazione gratuita, e `visibility: hidden` lo toglie\r\n anche dall'ordine di tabulazione. Il costo e' un bottone in piu' nel DOM, non un rischio.\r\n -->\r\n <div class=\"fb-viewport\" fDragBlocker role=\"group\" aria-label=\"Vista\">\r\n <!--\r\n Lo zoom ha bisogno di **comandi e di un numero**: con la sola rotella si finisce all'8% su\r\n un flow grande e sembra che il canvas si sia svuotato, senza un modo evidente di tornare.\r\n Il numero e' un bottone: riporta al 100% sull'elemento corrente.\r\n -->\r\n <button type=\"button\" class=\"fb-btn fb-viewport__btn\" title=\"Riduci (rotella indietro)\" (click)=\"zoomOut()\">\r\n \u2212\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-viewport__level\"\r\n title=\"Torna al 100% sull\u2019elemento selezionato\"\r\n (click)=\"zoomToActual()\"\r\n >\r\n {{ zoomPercent() }}%\r\n </button>\r\n <button type=\"button\" class=\"fb-btn fb-viewport__btn\" title=\"Ingrandisci (rotella avanti)\" (click)=\"zoomIn()\">\r\n +\r\n </button>\r\n <!--\r\n La modalita\u2019 selezione. Esiste perche\u2019 i due gesti che la libreria offre di serie \u2014\r\n `Ctrl`+clic per aggiungere, `Shift`+trascina per il rettangolo \u2014 non si vedono: senza un\r\n interruttore, su un flow grande si finisce a spostare un elemento alla volta. Accesa, il\r\n trascinamento sul canvas vuoto **seleziona** e la vista si sposta col tasto centrale.\r\n -->\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-viewport__select\"\r\n [class.fb-viewport__select--on]=\"isSelecting()\"\r\n [attr.aria-pressed]=\"isSelecting()\"\r\n [title]=\"\r\n isSelecting()\r\n ? 'Modalita\u2019 selezione attiva: trascina sul canvas per selezionare piu\u2019 elementi. La vista si sposta col tasto centrale del mouse'\r\n : 'Attiva la selezione a rettangolo: trascinando sul canvas selezioni piu\u2019 elementi invece di spostare la vista (Shift+trascina fa lo stesso, sempre)'\r\n \"\r\n (click)=\"toggleSelectionMode()\"\r\n >\r\n <span class=\"fb-viewport__icon\" aria-hidden=\"true\">\u25A4</span>\r\n Seleziona\r\n </button>\r\n <!--\r\n Sempre nel DOM, nascosto con una classe e non con `@if`: dentro una proiezione di contenuto\r\n un blocco di controllo e' una complicazione gratuita, e `visibility: hidden` lo toglie\r\n anche dall'ordine di tabulazione. Il costo e' un bottone in piu' nel DOM, non un rischio.\r\n -->\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-viewport__fit\"\r\n [class.fb-viewport__fit--hidden]=\"!hasMovedViewport()\"\r\n title=\"Rimetti il flow in vista. Su un flow molto grande inquadra l\u2019inizio, perche\u2019 l\u2019intero non sarebbe leggibile\"\r\n (click)=\"resetViewport()\"\r\n >\r\n <span class=\"fb-viewport__icon\" aria-hidden=\"true\">\u2922</span>\r\n Inquadra\r\n </button>\r\n </div>\r\n\r\n @if (selectionCount() > 1) {\r\n <!--\r\n Il dato che rende utile la selezione: che si spostano **insieme**. Senza dirlo, dopo aver\r\n selezionato cinque elementi si continua a trascinarne uno per volta.\r\n -->\r\n <p class=\"fb-select-hint\" role=\"status\">\r\n {{ selectionCount() }} elementi selezionati: trascinane uno per spostarli tutti \u00B7 Ctrl+clic\r\n aggiunge o toglie \u00B7 Canc li elimina\r\n </p>\r\n } @else if (isSelecting()) {\r\n <p class=\"fb-select-hint\" role=\"status\">\r\n Trascina sul canvas per selezionare \u00B7 la vista si sposta col tasto centrale\r\n </p>\r\n }\r\n\r\n @if (insertionEdgeId()) {\r\n <!-- Il rilascio su un arco non e' un gesto che si indovina: mentre e' possibile, si dice. -->\r\n <p class=\"fb-insert-hint\" role=\"status\">Rilascia qui per inserire l\u2019elemento in questo percorso</p>\r\n }\r\n\r\n <f-minimap [fMinSize]=\"1200\" class=\"fb-minimap\" />\r\n</f-flow>\r\n", styles: [":host{display:block;position:relative;width:100%;height:100%;overflow:hidden;background:var(--fb-canvas-bg, #f4f5f7)}f-flow{display:block;width:100%;height:100%}.fb-minimap{position:absolute;right:14px;bottom:14px;width:150px;height:300px;max-height:40%;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius, 10px);background:var(--fb-surface, #fff);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));overflow:hidden}.fb-viewport{position:absolute;right:14px;bottom:322px;z-index:1;display:flex;align-items:center;gap:4px;flex-wrap:wrap;justify-content:flex-end;max-width:200px}.fb-viewport__btn,.fb-viewport__level,.fb-viewport__fit{border-radius:var(--fb-radius-xs, 6px);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));font-size:11px}.fb-viewport__btn{width:24px;padding:2px 0;font-size:14px;line-height:1}.fb-viewport__level{min-width:52px;font-variant-numeric:tabular-nums}.fb-viewport__fit--hidden{opacity:0;visibility:hidden}.fb-viewport__fit{transition:opacity .15s ease,visibility .15s ease}.fb-viewport__icon{font-size:13px;line-height:1;color:var(--fb-text-muted, #667085)}.fb-insert-hint{position:absolute;bottom:18px;left:50%;transform:translate(-50%);z-index:1;margin:0;padding:6px 12px;border-radius:999px;background:var(--fb-accent, #2f6feb);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));color:#fff;font-size:11px;font-weight:600;pointer-events:none}.fb-select-hint{position:absolute;bottom:18px;left:50%;transform:translate(-50%);z-index:1;margin:0;padding:6px 12px;border-radius:999px;border:1px solid var(--fb-border, #e2e5eb);background:var(--fb-surface, #fff);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));color:var(--fb-text-muted, #667085);font-size:11px;font-weight:600;pointer-events:none}.fb-viewport__select{border-radius:var(--fb-radius-xs, 6px);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));font-size:11px}.fb-viewport__select--on{border-color:var(--fb-accent, #2f6feb);background:var(--fb-accent, #2f6feb);color:#fff}.fb-node{position:absolute;display:flex;flex-direction:column;box-sizing:border-box;width:240px;padding:0;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius-lg, 12px);background:var(--fb-surface, #fff);box-shadow:var(--fb-shadow-sm, 0 1px 2px rgb(16 24 40 / 6%));font:inherit;cursor:grab;-webkit-user-select:none;user-select:none;transition:box-shadow .12s ease,border-color .12s ease}.fb-node--outlets-3{width:304px}.fb-node--outlets-4{width:380px}.fb-node--outlets-5{width:456px}.fb-node--outlets-6{width:520px}.fb-node:hover{box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%))}.fb-node--selected{border-color:var(--fb-accent, #4f6ef7);box-shadow:0 0 0 3px color-mix(in srgb,var(--fb-accent, #4f6ef7) 22%,transparent)}.fb-node--unreachable{border-style:dashed;opacity:.8}.fb-node--error{border-color:var(--fb-error, #c9372c)}.fb-node--warning{border-color:var(--fb-warning, #b7791f)}.fb-node__head{display:flex;align-items:center;gap:8px;padding:10px 10px 6px 12px}.fb-node__icon{display:grid;place-items:center;flex:0 0 auto;width:28px;height:28px;border-radius:var(--fb-radius-sm, 8px);background:var(--fb-node-accent, #667085);color:#fff;font-size:14px;line-height:1}.cat-start{--fb-node-accent: #22a06b}.cat-screen{--fb-node-accent: #3b82f6}.cat-logic{--fb-node-accent: #8b5cf6}.cat-data{--fb-node-accent: #06b6d4}.cat-action{--fb-node-accent: #f59e0b}.cat-flow{--fb-node-accent: #14b8a6}.cat-other{--fb-node-accent: #667085}.fb-node__text{flex:1;min-width:0}.fb-node__title{display:block;font-size:13px;font-weight:600;line-height:17px;color:var(--fb-text, #1a1c23);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-node__sub{display:block;margin-top:1px;font-size:11px;line-height:14px;color:var(--fb-text-muted, #6b7086);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-node__sub-dot{margin:0 4px;color:var(--fb-text-subtle, #98a2b3)}.fb-node__auto{flex:0 0 auto;font-size:12px;font-weight:700;color:var(--fb-accent, #4f6ef7);cursor:help}.fb-node__edit{display:grid;place-items:center;flex:0 0 auto;width:22px;height:22px;padding:0;border:0;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text-subtle, #98a2b3);font:inherit;font-size:12px;cursor:pointer;opacity:0;transition:opacity .12s ease,background .12s ease}.fb-node:hover .fb-node__edit,.fb-node--selected .fb-node__edit,.fb-node__edit:focus-visible{opacity:1}.fb-node__edit:hover{background:var(--fb-surface-alt, #f7f8fa);color:var(--fb-text, #1a1c23)}.fb-node__duplicate,.fb-node__remove{display:grid;place-items:center;flex:0 0 auto;width:22px;height:22px;padding:0;border:0;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text-subtle, #98a2b3);font:inherit;font-size:15px;line-height:1;cursor:pointer;opacity:0;transition:opacity .12s ease,background .12s ease,color .12s ease}.fb-node--selected .fb-node__duplicate,.fb-node--selected .fb-node__remove,.fb-node__duplicate:focus-visible,.fb-node__remove:focus-visible{opacity:1}.fb-node__remove:hover{background:color-mix(in srgb,var(--fb-error, #c9372c) 12%,transparent);color:var(--fb-error, #c9372c)}.fb-node__duplicate:hover{background:color-mix(in srgb,var(--fb-accent, #3b6ef2) 12%,transparent);color:var(--fb-accent, #3b6ef2)}.fb-node__meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px;min-height:14px;padding:0 12px 6px}.fb-node__name{flex:0 1 auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;color:var(--fb-text-subtle, #98a2b3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-badge{padding:1px 6px;border-radius:10px;background:var(--fb-badge-bg, #eef0f4);font-size:10px;font-weight:600;color:var(--fb-text-muted, #6b7086);white-space:nowrap;cursor:help}.fb-badge--error{background:color-mix(in srgb,var(--fb-error, #c9372c) 14%,transparent);color:var(--fb-error, #c9372c)}.fb-badge--warning{background:color-mix(in srgb,var(--fb-warning, #b7791f) 16%,transparent);color:var(--fb-warning, #b7791f)}.fb-badge--info{background:color-mix(in srgb,var(--fb-accent, #4f6ef7) 12%,transparent);color:var(--fb-accent, #4f6ef7)}.fb-badge--unreachable,.fb-badge--dangling{background:color-mix(in srgb,var(--fb-warning, #b7791f) 12%,transparent);color:var(--fb-warning, #b7791f)}.fb-node__outlets{display:flex;align-items:flex-end;justify-content:space-evenly;gap:4px;padding:0 8px 4px}.fb-node__outlets--labelled{padding-top:5px;border-top:1px solid var(--fb-border-subtle, #eef0f4)}.fb-outlet{display:flex;flex:1 1 0;flex-direction:column;align-items:center;gap:2px;min-width:0}.fb-outlet__label{max-width:100%;font-size:10px;line-height:13px;color:var(--fb-text-muted, #6b7086);text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-connector{box-sizing:border-box;width:12px;height:12px;flex:0 0 auto;border:2px solid var(--fb-surface, #fff);border-radius:50%;background:var(--fb-connector, #98a2b3);cursor:crosshair;transition:transform .12s ease,box-shadow .12s ease}f-flow .fb-connector--out{position:relative;inset:auto;margin-bottom:-12px}.fb-connector--out:hover{transform:scale(1.2)}f-flow .fb-connector--in{position:absolute;inset:-2px auto auto 50%;width:24px;height:4px;border:0;border-radius:2px;transform:translate(-50%);background:var(--fb-border-strong, #cfd4de)}.fb-connector--out{--ff-connector-connected-color: var(--fb-connector, #98a2b3)}.fb-connector--Next,.fb-connector--Start,.fb-connector--LoopNext{--fb-connector: var(--fb-edge-next, #98a2b3)}.fb-connector--Rule,.fb-connector--WaitEvent{--fb-connector: var(--fb-edge-rule, #8b5cf6)}.fb-connector--Default,.fb-connector--LoopEnd{--fb-connector: var(--fb-edge-default, #06b6d4)}.fb-connector--Fault{--fb-connector: var(--fb-edge-fault, #dc2626)}.fb-connector--Timeout,.fb-connector--ScheduledPath{--fb-connector: var(--fb-edge-timeout, #f59e0b)}.fb-connector.f-connector-connectable{box-shadow:0 0 0 4px color-mix(in srgb,var(--fb-accent, #4f6ef7) 28%,transparent)}.fb-group{position:absolute;box-sizing:border-box;border:1px solid var(--fb-group-border, #d6dae1);border-radius:var(--fb-radius, 10px);background:var(--fb-group-fill, rgb(148 163 184 / 10%));pointer-events:none;overflow:hidden}.fb-group--selected{border-color:var(--fb-accent, #2f6feb);box-shadow:0 0 0 2px color-mix(in srgb,var(--fb-accent, #2f6feb) 25%,transparent)}.fb-group__bar{display:flex;align-items:center;gap:6px;height:30px;padding:0 6px;border-bottom:1px solid var(--fb-group-border, #d6dae1);background:var(--fb-group-bar, rgb(148 163 184 / 22%));color:var(--fb-text, #1d2939);font-size:12px;font-weight:600;pointer-events:auto;cursor:move;-webkit-user-select:none;user-select:none}.fb-group--collapsed .fb-group__bar{border-bottom:none}.fb-group__fold,.fb-group__remove{flex:none;width:18px;height:18px;padding:0;border:none;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:inherit;font:inherit;font-size:12px;line-height:1;cursor:pointer}.fb-group__fold:hover,.fb-group__remove:hover{background:#10182814}.fb-group__remove{margin-left:auto;font-size:14px}.fb-group__title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fb-group__count{flex:none;padding:0 5px;border-radius:8px;background:#10182814;font-size:10px;font-weight:600;font-variant-numeric:tabular-nums}.fb-group__stale{flex:none;padding:0 5px;border-radius:8px;background:color-mix(in srgb,var(--fb-warning, #b7791f) 14%,transparent);color:var(--fb-warning, #b7791f);font-size:10px;font-weight:600}.fb-group__note{margin:6px 8px 0;color:var(--fb-text-muted, #667085);font-size:11px;line-height:1.35;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}.fb-group__resize{position:absolute;right:0;bottom:0;width:14px;height:14px;border-right:2px solid var(--fb-group-border, #d6dae1);border-bottom:2px solid var(--fb-group-border, #d6dae1);border-bottom-right-radius:var(--fb-radius, 10px);pointer-events:auto;cursor:nwse-resize}.fb-group--giallo{--fb-group-border: #e0c26a;--fb-group-fill: rgb(234 179 8 / 10%);--fb-group-bar: rgb(234 179 8 / 24%)}.fb-group--verde{--fb-group-border: #86c79b;--fb-group-fill: rgb(34 160 89 / 10%);--fb-group-bar: rgb(34 160 89 / 22%)}.fb-group--blu{--fb-group-border: #93b4ea;--fb-group-fill: rgb(47 111 235 / 9%);--fb-group-bar: rgb(47 111 235 / 20%)}.fb-group--viola{--fb-group-border: #bda6e6;--fb-group-fill: rgb(139 92 246 / 10%);--fb-group-bar: rgb(139 92 246 / 22%)}.fb-group--rosso{--fb-group-border: #e6a3a3;--fb-group-fill: rgb(214 69 69 / 9%);--fb-group-bar: rgb(214 69 69 / 20%)}.fb-group--grigio,.fb-group--neutro{--fb-group-border: #d6dae1;--fb-group-fill: rgb(148 163 184 / 10%);--fb-group-bar: rgb(148 163 184 / 22%)}\n"] }]
6206
+ ], template: "<!--\r\n Gerarchia obbligatoria: f-flow > f-canvas > fNode / f-connection.\r\n I `@for` sono direttamente dentro <f-canvas>: nessun wrapper, quindi non serve\r\n `ngProjectAs` (che sarebbe indispensabile con blocchi annidati).\r\n-->\r\n<f-flow\r\n fDraggable\r\n (fCreateConnection)=\"onCreateConnection($event)\"\r\n (fReassignConnection)=\"onReassignConnection($event)\"\r\n (fMoveNodes)=\"onMoveNodes($event)\"\r\n (fSelectionChange)=\"onSelectionChange($event)\"\r\n (fDeleteSelected)=\"onDeleteSelected($event)\"\r\n (fCreateNode)=\"onCreateNode($event)\"\r\n (fNodesRendered)=\"onNodesRendered()\"\r\n (fDragStarted)=\"onDragStarted($event)\"\r\n (fDragEnded)=\"onDragEnded()\"\r\n>\r\n <!--\r\n `#canvas` + `(fCanvasChange)`: il primo serve per chiamare `fitToScreen()`, il secondo per\r\n sapere che l'utente ha mosso la vista. `[debounceTime]` non si imposta: l'evento serve solo\r\n ad accendere un flag, non a ricalcolare niente.\r\n -->\r\n <f-canvas fZoom #canvas (fCanvasChange)=\"onCanvasChange()\">\r\n <f-background>\r\n <f-circle-pattern />\r\n </f-background>\r\n\r\n <!--\r\n \u00A73.6 \u2014 i riquadri di raggruppamento. Vanno **sotto** i node, e non c'e' niente da fare per\r\n ottenerlo: `[fGroup]` viene proiettato nel layer dei gruppi, che nell'ordine della libreria\r\n sta sotto connessioni e node.\r\n\r\n Sono commenti: nessun connettore, nessun arco, nessuna raggiungibilita'. Il corpo del\r\n riquadro ha `pointer-events: none` (nel CSS) e solo la barra del titolo risponde: senza,\r\n una cornice grande quanto mezzo canvas si mangerebbe i click destinati alla panoramica e\r\n alla selezione a rettangolo, e i node dentro non si potrebbero piu' prendere.\r\n -->\r\n @for (group of groups(); track group.name) {\r\n <div\r\n fGroup\r\n [fGroupId]=\"group.id\"\r\n [fGroupPosition]=\"group.position\"\r\n [fGroupSize]=\"group.size\"\r\n [fGroupDraggingDisabled]=\"!isEditable()\"\r\n [class]=\"'fb-group ' + group.colorClass\"\r\n [class.fb-group--selected]=\"isGroupSelected(group)\"\r\n (fGroupSizeChange)=\"onGroupResized(group.name, $event)\"\r\n >\r\n <!--\r\n `fDragHandle` sulla sola barra: il riquadro si sposta dal titolo, come una finestra.\r\n Trascinandolo da dentro si sposterebbe la cornice ogni volta che si prova a prendere\r\n un node che le sta sopra.\r\n -->\r\n <div\r\n class=\"fb-group__bar\"\r\n fDragHandle\r\n [title]=\"group.description || group.label\"\r\n (click)=\"onGroupClick($event, group.name)\"\r\n >\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-group__fold\"\r\n aria-label=\"Chiudi il riquadro\"\r\n title=\"Chiudi il riquadro: gli elementi si nascondono e gli archi che lo attraversano si attaccano alla pastiglia\"\r\n (click)=\"toggleGroupCollapsed($event, group.name)\"\r\n >\r\n \u25BE\r\n </button>\r\n <span class=\"fb-group__title\">{{ group.label }}</span>\r\n <span class=\"fb-group__count\" [title]=\"group.memberCount + ' elementi nel riquadro'\">\r\n {{ group.memberCount }}\r\n </span>\r\n @if (group.unknownMembers.length > 0) {\r\n <!--\r\n Membri che nominano un elemento che non c'e' piu'. Non e' un difetto (\u00A73.6): e' cio'\r\n che resta dopo una cancellazione, ed e' un avviso \u2014 l'attivazione non si blocca.\r\n -->\r\n <span\r\n class=\"fb-group__stale\"\r\n [title]=\"'Elementi non piu\u2019 esistenti: ' + group.unknownMembers.join(', ')\"\r\n >\r\n {{ group.unknownMembers.length }} da ripulire\r\n </span>\r\n }\r\n @if (isEditable()) {\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-group__remove\"\r\n [attr.aria-label]=\"'Elimina il riquadro ' + group.label\"\r\n title=\"Elimina il riquadro: gli elementi che contiene restano\"\r\n (click)=\"onGroupRemoveClick($event, group.name)\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (group.description) {\r\n <p class=\"fb-group__note\">{{ group.description }}</p>\r\n }\r\n\r\n @if (isEditable()) {\r\n <!--\r\n Una sola maniglia, in basso a destra: quattro bersagli su una cornice che sta sotto\r\n i node si prendono per sbaglio. Ridimensionare **dichiara** la geometria, che e' il\r\n modo di uscire dal \u00ABcalcolala sui membri\u00BB (\u00A73.6).\r\n -->\r\n <div\r\n fResizeHandle\r\n [fResizeHandleType]=\"resizeHandles.RIGHT_BOTTOM\"\r\n class=\"fb-group__resize\"\r\n title=\"Ridimensiona il riquadro\"\r\n ></div>\r\n }\r\n </div>\r\n }\r\n\r\n <!--\r\n \u00A73.6 \u2014 un riquadro **chiuso**: una pastiglia al posto dei suoi elementi.\r\n\r\n \u00C8 un `fNode` e non un `fGroup` per una ragione sola, ed e' la regola 4 di @foblex/flow: per\r\n nascondere i node bisogna che gli archi che entravano e uscivano trovino un connettore\r\n **vero**, altrimenti hanno geometria 0\u00D70 e si attaccano all'angolo del canvas. La pastiglia e'\r\n quel connettore. Un solo connettore in uscita: dietro ci stanno elementi diversi, e il nome di\r\n quello che esce sta nell'etichetta dell'arco.\r\n -->\r\n @for (fold of folds(); track fold.id) {\r\n <div\r\n fNode\r\n fDragHandle\r\n [fNodeId]=\"fold.id\"\r\n [fNodePosition]=\"fold.position\"\r\n [class]=\"'fb-fold ' + fold.colorClass\"\r\n [class.fb-fold--selected]=\"isFoldSelected(fold)\"\r\n [class.fb-fold--error]=\"fold.severity === 'Error'\"\r\n [class.fb-fold--warning]=\"fold.severity === 'Warning'\"\r\n (dblclick)=\"toggleGroupCollapsed($event, fold.name)\"\r\n >\r\n <div\r\n fConnector\r\n fConnectorType=\"target\"\r\n [fConnectorId]=\"foldTargetIdOf(fold)\"\r\n [fConnectorConnectableSide]=\"sides.TOP\"\r\n fConnectorMultiple=\"true\"\r\n class=\"fb-connector fb-connector--in\"\r\n title=\"Ingresso del riquadro chiuso\"\r\n ></div>\r\n\r\n <div class=\"fb-fold__head\">\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-fold__open\"\r\n [attr.aria-label]=\"'Apri il riquadro ' + fold.label\"\r\n title=\"Apri il riquadro: gli elementi tornano visibili al loro posto\"\r\n (click)=\"toggleGroupCollapsed($event, fold.name)\"\r\n >\r\n \u25B8\r\n </button>\r\n <span class=\"fb-fold__title\" [title]=\"fold.description || fold.label\">{{ fold.label }}</span>\r\n @if (isEditable()) {\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-fold__remove\"\r\n [attr.aria-label]=\"'Elimina il riquadro ' + fold.label\"\r\n title=\"Elimina il riquadro: gli elementi che contiene restano\"\r\n (click)=\"onGroupRemoveClick($event, fold.name)\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n <div class=\"fb-fold__meta\">\r\n <span class=\"fb-fold__count\">\r\n {{ fold.memberCount }} {{ fold.memberCount === 1 ? 'elemento nascosto' : 'elementi nascosti' }}\r\n </span>\r\n @if (fold.issueCount > 0) {\r\n <!-- I rilievi di cio' che e' nascosto: chiudere un riquadro non nasconde un errore (\u00A77). -->\r\n <span\r\n class=\"fb-badge\"\r\n [class.fb-badge--error]=\"fold.severity === 'Error'\"\r\n [class.fb-badge--warning]=\"fold.severity === 'Warning'\"\r\n [class.fb-badge--info]=\"fold.severity === 'Info'\"\r\n [title]=\"fold.issueCount + ' rilievi negli elementi nascosti'\"\r\n >\r\n {{ fold.issueCount }}\r\n </span>\r\n }\r\n @if (fold.unknownMembers.length > 0) {\r\n <span\r\n class=\"fb-badge fb-badge--dangling\"\r\n [title]=\"'Elementi non piu\u2019 esistenti: ' + fold.unknownMembers.join(', ')\"\r\n >\r\n {{ fold.unknownMembers.length }} da ripulire\r\n </span>\r\n }\r\n </div>\r\n\r\n <div class=\"fb-fold__outlets\">\r\n <div\r\n fConnector\r\n fConnectorType=\"source\"\r\n [fConnectorId]=\"foldSourceIdOf(fold)\"\r\n [fConnectorConnectableSide]=\"sides.BOTTOM\"\r\n class=\"fb-connector fb-connector--out fb-connector--Next\"\r\n title=\"Uscite del riquadro chiuso\"\r\n ></div>\r\n </div>\r\n </div>\r\n }\r\n\r\n @for (edge of edges(); track edge.id) {\r\n <f-connection\r\n [fConnectionId]=\"edge.id\"\r\n [fSourceId]=\"edge.sourceId\"\r\n [fTargetId]=\"edge.targetId\"\r\n fBehavior=\"floating\"\r\n [fType]=\"edge.isGoTo ? 'segment' : 'bezier'\"\r\n [class]=\"'fb-edge fb-edge--' + edge.kind + (edge.isGoTo ? ' fb-edge--goto' : '')\"\r\n [class.fb-edge--insert]=\"edge.id === insertionEdgeId()\"\r\n [class.fb-edge--folded]=\"edge.isFolded\"\r\n >\r\n <f-connection-marker-arrow [type]=\"markerEnd\" />\r\n @if (edge.label) {\r\n <div fConnectionContent class=\"fb-edge-label\">{{ edge.label }}</div>\r\n }\r\n </f-connection>\r\n }\r\n\r\n @for (node of nodes(); track node.id) {\r\n <div\r\n fNode\r\n fDragHandle\r\n [fNodeId]=\"node.id\"\r\n [fNodePosition]=\"node.position\"\r\n [class]=\"'fb-node ' + categoryClass(node) + ' ' + node.widthClass\"\r\n [class.fb-node--start]=\"node.isStart\"\r\n [class.fb-node--selected]=\"isSelected(node)\"\r\n [class.fb-node--unreachable]=\"!node.isReachable\"\r\n [class.fb-node--error]=\"node.severity === 'Error'\"\r\n [class.fb-node--warning]=\"node.severity === 'Warning'\"\r\n (dblclick)=\"onNodeDoubleClick(node.name)\"\r\n >\r\n <!--\r\n Lo Start non ha ingresso: e' il punto di partenza (\u00A73.4).\r\n `fConnectorConnectableSide` e' cio' che fa entrare l'arco dall'alto: senza, foblex\r\n calcola il lato e un arco che scende dal node sopra potrebbe agganciarsi di fianco.\r\n -->\r\n @if (!node.isStart) {\r\n <div\r\n fConnector\r\n fConnectorType=\"target\"\r\n [fConnectorId]=\"targetIdOf(node)\"\r\n [fConnectorConnectableSide]=\"sides.TOP\"\r\n fConnectorMultiple=\"true\"\r\n class=\"fb-connector fb-connector--in\"\r\n title=\"Ingresso\"\r\n ></div>\r\n }\r\n\r\n <div class=\"fb-node__head\">\r\n <span class=\"fb-node__icon\" aria-hidden=\"true\">{{ node.icon }}</span>\r\n <div class=\"fb-node__text\">\r\n <span class=\"fb-node__title\" [title]=\"node.description || node.label\">{{ node.label }}</span>\r\n <span class=\"fb-node__sub\">\r\n {{ node.typeLabel }}\r\n @if (node.subtitle) {\r\n <span class=\"fb-node__sub-dot\">\u00B7</span>{{ node.subtitle }}\r\n }\r\n </span>\r\n </div>\r\n @if (node.hasAutomaticOutput) {\r\n <!-- L'elemento espone il proprio risultato sotto il proprio nome (\u00A74.5). -->\r\n <span class=\"fb-node__auto\" title=\"Espone un output automatico referenziabile come \u00AB{{ node.name }}\u00BB\">\r\n \u0192\r\n </span>\r\n }\r\n <!--\r\n `fDragBlocker` impedisce che il pointerdown sul bottone diventi un trascinamento\r\n del node: senza, aprire il dettaglio sposterebbe l'elemento di qualche pixel.\r\n -->\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-node__edit\"\r\n [attr.aria-label]=\"'Apri il dettaglio di ' + node.label\"\r\n title=\"Apri il dettaglio\"\r\n (click)=\"onEditClick($event, node.name)\"\r\n >\r\n \u270E\r\n </button>\r\n @if (!node.isStart && isEditable()) {\r\n <!--\r\n Duplica: sta accanto a \u270E e non solo nell'inspector perche' e' il gesto con cui si\r\n riusa un elemento gi\u00E0 configurato, e cercarlo dentro il form dell'elemento da\r\n copiare e' il posto in cui non lo si cerca. Compare come \u00AB\u00D7\u00BB, sul solo node\r\n selezionato: aggiunge un elemento al documento, quindi non deve trovarsi sotto il\r\n puntatore di chi sta solo attraversando il grafo.\r\n -->\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-node__duplicate\"\r\n [attr.aria-label]=\"'Duplica ' + node.label\"\r\n title=\"Duplica questo elemento (Ctrl+D)\"\r\n (click)=\"onDuplicateClick($event, node.name)\"\r\n >\r\n \u29C9\r\n </button>\r\n <!--\r\n Si mostra solo sul node **selezionato**, non al passaggio del mouse come \u270E:\r\n cancellare non e' reversibile con un altro clic, e un comando distruttivo che\r\n appare sotto il puntatore mentre si attraversa il grafo si preme per sbaglio.\r\n Lo Start non lo espone: un flow senza ingresso non esisterebbe.\r\n -->\r\n <button\r\n type=\"button\"\r\n fDragBlocker\r\n class=\"fb-node__remove\"\r\n [attr.aria-label]=\"'Elimina ' + node.label\"\r\n title=\"Elimina questo elemento (si annulla con \u21B6)\"\r\n (click)=\"onRemoveClick($event, node.name)\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n <div class=\"fb-node__meta\">\r\n @if (!node.isStart) {\r\n <span class=\"fb-node__name\" [title]=\"'Nome tecnico: ' + node.name\">{{ node.name }}</span>\r\n }\r\n @if (!node.isReachable) {\r\n <span class=\"fb-badge fb-badge--unreachable\" title=\"Nessun percorso raggiunge questo elemento dallo Start\">\r\n scollegato\r\n </span>\r\n }\r\n @if (node.issueCount > 0) {\r\n <span\r\n class=\"fb-badge\"\r\n [class.fb-badge--error]=\"node.severity === 'Error'\"\r\n [class.fb-badge--warning]=\"node.severity === 'Warning'\"\r\n [class.fb-badge--info]=\"node.severity === 'Info'\"\r\n [title]=\"node.issueCount + ' rilievi di validazione'\"\r\n >\r\n {{ node.issueCount }}\r\n </span>\r\n }\r\n @if (node.danglingOutlets.length > 0) {\r\n <span\r\n class=\"fb-badge fb-badge--dangling\"\r\n [title]=\"'Rami dichiarati senza destinazione: ' + danglingLabels(node)\"\r\n >\r\n ramo incompleto\r\n </span>\r\n }\r\n </div>\r\n\r\n <!--\r\n Le uscite stanno sul bordo **inferiore**, una per ramo, nell'ordine in cui il modello\r\n le dichiara: per una Decision e' l'ordine di valutazione delle regole, che e'\r\n semantico (\u00A75.4), e da sinistra a destra si legge come la lista nell'inspector.\r\n L'etichetta si mostra solo quando i rami sono piu' di uno: su un `next` unico\r\n direbbe soltanto \u00ABSuccessivo\u00BB.\r\n -->\r\n <div class=\"fb-node__outlets\" [class.fb-node__outlets--labelled]=\"node.showsOutletLabels\">\r\n @for (outlet of node.outlets; track outlet.key) {\r\n <div class=\"fb-outlet\">\r\n @if (node.showsOutletLabels) {\r\n <span class=\"fb-outlet__label\" [title]=\"outlet.label\">{{ outlet.label }}</span>\r\n }\r\n <div\r\n fConnector\r\n fConnectorType=\"source\"\r\n [fConnectorId]=\"connectorIdOf(node, outlet.key)\"\r\n [fConnectorConnectableSide]=\"sides.BOTTOM\"\r\n [class]=\"'fb-connector fb-connector--out fb-connector--' + outlet.kind\"\r\n [title]=\"outlet.label\"\r\n ></div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Anteprima dell'arco durante il trascinamento. -->\r\n <f-connection-for-create fBehavior=\"floating\" fType=\"bezier\" class=\"fb-edge fb-edge--creating\">\r\n <f-connection-marker-arrow [type]=\"markerEnd\" />\r\n </f-connection-for-create>\r\n\r\n <f-selection-area />\r\n </f-canvas>\r\n\r\n <!--\r\n Fuori da <f-canvas> come la minimappa: dentro, la trasformazione del canvas se lo\r\n porterebbe via insieme ai node. `fDragBlocker` perche' il pointerdown non inizi una\r\n panoramica invece di premere il bottone.\r\n -->\r\n <!--\r\n Sempre nel DOM, nascosto con una classe e non con `@if`: dentro una proiezione di contenuto\r\n un blocco di controllo e' una complicazione gratuita, e `visibility: hidden` lo toglie\r\n anche dall'ordine di tabulazione. Il costo e' un bottone in piu' nel DOM, non un rischio.\r\n -->\r\n <div class=\"fb-viewport\" fDragBlocker role=\"group\" aria-label=\"Vista\">\r\n <!--\r\n Lo zoom ha bisogno di **comandi e di un numero**: con la sola rotella si finisce all'8% su\r\n un flow grande e sembra che il canvas si sia svuotato, senza un modo evidente di tornare.\r\n Il numero e' un bottone: riporta al 100% sull'elemento corrente.\r\n -->\r\n <button type=\"button\" class=\"fb-btn fb-viewport__btn\" title=\"Riduci (rotella indietro)\" (click)=\"zoomOut()\">\r\n \u2212\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-viewport__level\"\r\n title=\"Torna al 100% sull\u2019elemento selezionato\"\r\n (click)=\"zoomToActual()\"\r\n >\r\n {{ zoomPercent() }}%\r\n </button>\r\n <button type=\"button\" class=\"fb-btn fb-viewport__btn\" title=\"Ingrandisci (rotella avanti)\" (click)=\"zoomIn()\">\r\n +\r\n </button>\r\n <!--\r\n La modalita\u2019 selezione. Esiste perche\u2019 i due gesti che la libreria offre di serie \u2014\r\n `Ctrl`+clic per aggiungere, `Shift`+trascina per il rettangolo \u2014 non si vedono: senza un\r\n interruttore, su un flow grande si finisce a spostare un elemento alla volta. Accesa, il\r\n trascinamento sul canvas vuoto **seleziona** e la vista si sposta col tasto centrale.\r\n -->\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-viewport__select\"\r\n [class.fb-viewport__select--on]=\"isSelecting()\"\r\n [attr.aria-pressed]=\"isSelecting()\"\r\n [title]=\"\r\n isSelecting()\r\n ? 'Modalita\u2019 selezione attiva: trascina sul canvas per selezionare piu\u2019 elementi. La vista si sposta col tasto centrale del mouse'\r\n : 'Attiva la selezione a rettangolo: trascinando sul canvas selezioni piu\u2019 elementi invece di spostare la vista (Shift+trascina fa lo stesso, sempre)'\r\n \"\r\n (click)=\"toggleSelectionMode()\"\r\n >\r\n <span class=\"fb-viewport__icon\" aria-hidden=\"true\">\u25A4</span>\r\n Seleziona\r\n </button>\r\n <!--\r\n Sempre nel DOM, nascosto con una classe e non con `@if`: dentro una proiezione di contenuto\r\n un blocco di controllo e' una complicazione gratuita, e `visibility: hidden` lo toglie\r\n anche dall'ordine di tabulazione. Il costo e' un bottone in piu' nel DOM, non un rischio.\r\n -->\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-viewport__fit\"\r\n [class.fb-viewport__fit--hidden]=\"!hasMovedViewport()\"\r\n title=\"Rimetti il flow in vista. Su un flow molto grande inquadra l\u2019inizio, perche\u2019 l\u2019intero non sarebbe leggibile\"\r\n (click)=\"resetViewport()\"\r\n >\r\n <span class=\"fb-viewport__icon\" aria-hidden=\"true\">\u2922</span>\r\n Inquadra\r\n </button>\r\n </div>\r\n\r\n @if (selectionCount() > 1) {\r\n <!--\r\n Il dato che rende utile la selezione: che si spostano **insieme**. Senza dirlo, dopo aver\r\n selezionato cinque elementi si continua a trascinarne uno per volta.\r\n -->\r\n <p class=\"fb-select-hint\" role=\"status\">\r\n {{ selectionCount() }} elementi selezionati: trascinane uno per spostarli tutti \u00B7 Ctrl+clic\r\n aggiunge o toglie \u00B7 Canc li elimina\r\n </p>\r\n } @else if (isSelecting()) {\r\n <p class=\"fb-select-hint\" role=\"status\">\r\n Trascina sul canvas per selezionare \u00B7 la vista si sposta col tasto centrale\r\n </p>\r\n }\r\n\r\n @if (insertionEdgeId()) {\r\n <!-- Il rilascio su un arco non e' un gesto che si indovina: mentre e' possibile, si dice. -->\r\n <p class=\"fb-insert-hint\" role=\"status\">Rilascia qui per inserire l\u2019elemento in questo percorso</p>\r\n }\r\n\r\n <f-minimap [fMinSize]=\"1200\" class=\"fb-minimap\" />\r\n</f-flow>\r\n", styles: [":host{display:block;position:relative;width:100%;height:100%;overflow:hidden;background:var(--fb-canvas-bg, #f4f5f7)}f-flow{display:block;width:100%;height:100%}.fb-minimap{position:absolute;right:14px;bottom:14px;width:150px;height:300px;max-height:40%;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius, 10px);background:var(--fb-surface, #fff);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));overflow:hidden}.fb-viewport{position:absolute;right:14px;bottom:322px;z-index:1;display:flex;align-items:center;gap:4px;flex-wrap:wrap;justify-content:flex-end;max-width:200px}.fb-viewport__btn,.fb-viewport__level,.fb-viewport__fit{border-radius:var(--fb-radius-xs, 6px);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));font-size:11px}.fb-viewport__btn{width:24px;padding:2px 0;font-size:14px;line-height:1}.fb-viewport__level{min-width:52px;font-variant-numeric:tabular-nums}.fb-viewport__fit--hidden{opacity:0;visibility:hidden}.fb-viewport__fit{transition:opacity .15s ease,visibility .15s ease}.fb-viewport__icon{font-size:13px;line-height:1;color:var(--fb-text-muted, #667085)}.fb-insert-hint{position:absolute;bottom:18px;left:50%;transform:translate(-50%);z-index:1;margin:0;padding:6px 12px;border-radius:999px;background:var(--fb-accent, #2f6feb);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));color:#fff;font-size:11px;font-weight:600;pointer-events:none}.fb-select-hint{position:absolute;bottom:18px;left:50%;transform:translate(-50%);z-index:1;margin:0;padding:6px 12px;border-radius:999px;border:1px solid var(--fb-border, #e2e5eb);background:var(--fb-surface, #fff);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));color:var(--fb-text-muted, #667085);font-size:11px;font-weight:600;pointer-events:none}.fb-viewport__select{border-radius:var(--fb-radius-xs, 6px);box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%));font-size:11px}.fb-viewport__select--on{border-color:var(--fb-accent, #2f6feb);background:var(--fb-accent, #2f6feb);color:#fff}.fb-node{position:absolute;display:flex;flex-direction:column;box-sizing:border-box;width:240px;padding:0;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius-lg, 12px);background:var(--fb-surface, #fff);box-shadow:var(--fb-shadow-sm, 0 1px 2px rgb(16 24 40 / 6%));font:inherit;cursor:grab;-webkit-user-select:none;user-select:none;transition:box-shadow .12s ease,border-color .12s ease}.fb-node--outlets-3{width:304px}.fb-node--outlets-4{width:380px}.fb-node--outlets-5{width:456px}.fb-node--outlets-6{width:520px}.fb-node:hover{box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%))}.fb-node--selected{border-color:var(--fb-accent, #4f6ef7);box-shadow:0 0 0 3px color-mix(in srgb,var(--fb-accent, #4f6ef7) 22%,transparent)}.fb-node--unreachable{border-style:dashed;opacity:.8}.fb-node--error{border-color:var(--fb-error, #c9372c)}.fb-node--warning{border-color:var(--fb-warning, #b7791f)}.fb-node__head{display:flex;align-items:center;gap:8px;padding:10px 10px 6px 12px}.fb-node__icon{display:grid;place-items:center;flex:0 0 auto;width:28px;height:28px;border-radius:var(--fb-radius-sm, 8px);background:var(--fb-node-accent, #667085);color:#fff;font-size:14px;line-height:1}.cat-start{--fb-node-accent: #22a06b}.cat-screen{--fb-node-accent: #3b82f6}.cat-logic{--fb-node-accent: #8b5cf6}.cat-data{--fb-node-accent: #06b6d4}.cat-action{--fb-node-accent: #f59e0b}.cat-flow{--fb-node-accent: #14b8a6}.cat-other{--fb-node-accent: #667085}.fb-node__text{flex:1;min-width:0}.fb-node__title{display:block;font-size:13px;font-weight:600;line-height:17px;color:var(--fb-text, #1a1c23);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-node__sub{display:block;margin-top:1px;font-size:11px;line-height:14px;color:var(--fb-text-muted, #6b7086);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-node__sub-dot{margin:0 4px;color:var(--fb-text-subtle, #98a2b3)}.fb-node__auto{flex:0 0 auto;font-size:12px;font-weight:700;color:var(--fb-accent, #4f6ef7);cursor:help}.fb-node__edit{display:grid;place-items:center;flex:0 0 auto;width:22px;height:22px;padding:0;border:0;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text-subtle, #98a2b3);font:inherit;font-size:12px;cursor:pointer;opacity:0;transition:opacity .12s ease,background .12s ease}.fb-node:hover .fb-node__edit,.fb-node--selected .fb-node__edit,.fb-node__edit:focus-visible{opacity:1}.fb-node__edit:hover{background:var(--fb-surface-alt, #f7f8fa);color:var(--fb-text, #1a1c23)}.fb-node__duplicate,.fb-node__remove{display:grid;place-items:center;flex:0 0 auto;width:22px;height:22px;padding:0;border:0;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text-subtle, #98a2b3);font:inherit;font-size:15px;line-height:1;cursor:pointer;opacity:0;transition:opacity .12s ease,background .12s ease,color .12s ease}.fb-node--selected .fb-node__duplicate,.fb-node--selected .fb-node__remove,.fb-node__duplicate:focus-visible,.fb-node__remove:focus-visible{opacity:1}.fb-node__remove:hover{background:color-mix(in srgb,var(--fb-error, #c9372c) 12%,transparent);color:var(--fb-error, #c9372c)}.fb-node__duplicate:hover{background:color-mix(in srgb,var(--fb-accent, #3b6ef2) 12%,transparent);color:var(--fb-accent, #3b6ef2)}.fb-node__meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px;min-height:14px;padding:0 12px 6px}.fb-node__name{flex:0 1 auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;color:var(--fb-text-subtle, #98a2b3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-badge{padding:1px 6px;border-radius:10px;background:var(--fb-badge-bg, #eef0f4);font-size:10px;font-weight:600;color:var(--fb-text-muted, #6b7086);white-space:nowrap;cursor:help}.fb-badge--error{background:color-mix(in srgb,var(--fb-error, #c9372c) 14%,transparent);color:var(--fb-error, #c9372c)}.fb-badge--warning{background:color-mix(in srgb,var(--fb-warning, #b7791f) 16%,transparent);color:var(--fb-warning, #b7791f)}.fb-badge--info{background:color-mix(in srgb,var(--fb-accent, #4f6ef7) 12%,transparent);color:var(--fb-accent, #4f6ef7)}.fb-badge--unreachable,.fb-badge--dangling{background:color-mix(in srgb,var(--fb-warning, #b7791f) 12%,transparent);color:var(--fb-warning, #b7791f)}.fb-node__outlets{display:flex;align-items:flex-end;justify-content:space-evenly;gap:4px;padding:0 8px 4px}.fb-node__outlets--labelled{padding-top:5px;border-top:1px solid var(--fb-border-subtle, #eef0f4)}.fb-outlet{display:flex;flex:1 1 0;flex-direction:column;align-items:center;gap:2px;min-width:0}.fb-outlet__label{max-width:100%;font-size:10px;line-height:13px;color:var(--fb-text-muted, #6b7086);text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-connector{box-sizing:border-box;width:12px;height:12px;flex:0 0 auto;border:2px solid var(--fb-surface, #fff);border-radius:50%;background:var(--fb-connector, #98a2b3);cursor:crosshair;transition:transform .12s ease,box-shadow .12s ease}f-flow .fb-connector--out{position:relative;inset:auto;margin-bottom:-12px}.fb-connector--out:hover{transform:scale(1.2)}f-flow .fb-connector--in{position:absolute;inset:-2px auto auto 50%;width:24px;height:4px;border:0;border-radius:2px;transform:translate(-50%);background:var(--fb-border-strong, #cfd4de)}.fb-connector--out{--ff-connector-connected-color: var(--fb-connector, #98a2b3)}.fb-connector--Next,.fb-connector--Start,.fb-connector--LoopNext{--fb-connector: var(--fb-edge-next, #98a2b3)}.fb-connector--Rule,.fb-connector--WaitEvent{--fb-connector: var(--fb-edge-rule, #8b5cf6)}.fb-connector--Default,.fb-connector--LoopEnd{--fb-connector: var(--fb-edge-default, #06b6d4)}.fb-connector--Fault{--fb-connector: var(--fb-edge-fault, #dc2626)}.fb-connector--Timeout,.fb-connector--ScheduledPath{--fb-connector: var(--fb-edge-timeout, #f59e0b)}.fb-connector.f-connector-connectable{box-shadow:0 0 0 4px color-mix(in srgb,var(--fb-accent, #4f6ef7) 28%,transparent)}.fb-group{position:absolute;box-sizing:border-box;border:1px solid var(--fb-group-border, #d6dae1);border-radius:var(--fb-radius, 10px);background:var(--fb-group-fill, rgb(148 163 184 / 10%));pointer-events:none;overflow:hidden}.fb-group--selected{border-color:var(--fb-accent, #2f6feb);box-shadow:0 0 0 2px color-mix(in srgb,var(--fb-accent, #2f6feb) 25%,transparent)}.fb-group__bar{display:flex;align-items:center;gap:6px;height:30px;padding:0 6px;border-bottom:1px solid var(--fb-group-border, #d6dae1);background:var(--fb-group-bar, rgb(148 163 184 / 22%));color:var(--fb-text, #1d2939);font-size:12px;font-weight:600;pointer-events:auto;cursor:move;-webkit-user-select:none;user-select:none}.fb-fold{position:absolute;box-sizing:border-box;display:flex;flex-direction:column;justify-content:center;width:260px;min-height:64px;padding:6px 8px;border:1px solid var(--fb-group-border, #d6dae1);border-left:4px solid var(--fb-group-border, #d6dae1);border-radius:var(--fb-radius-lg, 12px);background:var(--fb-group-bar, rgb(148 163 184 / 22%));box-shadow:var(--fb-shadow-sm, 0 1px 2px rgb(16 24 40 / 6%));color:var(--fb-text, #1d2939);font:inherit;font-size:12px;cursor:grab;-webkit-user-select:none;user-select:none}.fb-fold:hover{box-shadow:var(--fb-shadow-md, 0 6px 18px rgb(16 24 40 / 10%))}.fb-fold--selected{border-color:var(--fb-accent, #4f6ef7);box-shadow:0 0 0 3px color-mix(in srgb,var(--fb-accent, #4f6ef7) 22%,transparent)}.fb-fold--error{border-color:var(--fb-error, #c9372c)}.fb-fold--warning{border-color:var(--fb-warning, #b7791f)}.fb-fold__head{display:flex;align-items:center;gap:6px}.fb-fold__title{flex:1;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-weight:600}.fb-fold__open,.fb-fold__remove{flex:none;width:18px;height:18px;padding:0;border:none;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:inherit;font:inherit;font-size:12px;line-height:1;cursor:pointer}.fb-fold__remove{font-size:14px}.fb-fold__open:hover,.fb-fold__remove:hover{background:#10182814}.fb-fold__meta{display:flex;align-items:center;gap:6px;margin-top:2px;color:var(--fb-text-muted, #667085);font-size:11px}.fb-fold__count{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fb-fold__outlets{display:flex;justify-content:center;margin-top:4px}.fb-group__fold,.fb-group__remove{flex:none;width:18px;height:18px;padding:0;border:none;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:inherit;font:inherit;font-size:12px;line-height:1;cursor:pointer}.fb-group__fold:hover,.fb-group__remove:hover{background:#10182814}.fb-group__remove{margin-left:auto;font-size:14px}.fb-group__title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fb-group__count{flex:none;padding:0 5px;border-radius:8px;background:#10182814;font-size:10px;font-weight:600;font-variant-numeric:tabular-nums}.fb-group__stale{flex:none;padding:0 5px;border-radius:8px;background:color-mix(in srgb,var(--fb-warning, #b7791f) 14%,transparent);color:var(--fb-warning, #b7791f);font-size:10px;font-weight:600}.fb-group__note{margin:6px 8px 0;color:var(--fb-text-muted, #667085);font-size:11px;line-height:1.35;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}.fb-group__resize{position:absolute;right:0;bottom:0;width:14px;height:14px;border-right:2px solid var(--fb-group-border, #d6dae1);border-bottom:2px solid var(--fb-group-border, #d6dae1);border-bottom-right-radius:var(--fb-radius, 10px);pointer-events:auto;cursor:nwse-resize}.fb-group--giallo{--fb-group-border: #e0c26a;--fb-group-fill: rgb(234 179 8 / 10%);--fb-group-bar: rgb(234 179 8 / 24%)}.fb-group--verde{--fb-group-border: #86c79b;--fb-group-fill: rgb(34 160 89 / 10%);--fb-group-bar: rgb(34 160 89 / 22%)}.fb-group--blu{--fb-group-border: #93b4ea;--fb-group-fill: rgb(47 111 235 / 9%);--fb-group-bar: rgb(47 111 235 / 20%)}.fb-group--viola{--fb-group-border: #bda6e6;--fb-group-fill: rgb(139 92 246 / 10%);--fb-group-bar: rgb(139 92 246 / 22%)}.fb-group--rosso{--fb-group-border: #e6a3a3;--fb-group-fill: rgb(214 69 69 / 9%);--fb-group-bar: rgb(214 69 69 / 20%)}.fb-group--grigio,.fb-group--neutro{--fb-group-border: #d6dae1;--fb-group-fill: rgb(148 163 184 / 10%);--fb-group-bar: rgb(148 163 184 / 22%)}\n"] }]
5904
6207
  }], propDecorators: { selectedName: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedName", required: false }] }], selectedGroupName: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedGroupName", required: false }] }], selectedNames: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedNames", required: false }] }], outline: [{ type: i0.Input, args: [{ isSignal: true, alias: "outline", required: false }] }], isEditable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditable", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], nodeOpened: [{ type: i0.Output, args: ["nodeOpened"] }], nodeRemoveRequested: [{ type: i0.Output, args: ["nodeRemoveRequested"] }], nodeDuplicateRequested: [{ type: i0.Output, args: ["nodeDuplicateRequested"] }], groupSelected: [{ type: i0.Output, args: ["groupSelected"] }], groupRemoveRequested: [{ type: i0.Output, args: ["groupRemoveRequested"] }], elementDropped: [{ type: i0.Output, args: ["elementDropped"] }], canvas: [{ type: i0.ViewChild, args: [i0.forwardRef(() => FCanvasComponent), { isSignal: true }] }] } });
5905
6208
 
5906
6209
  /**
@@ -8515,7 +8818,15 @@ class ValueEditorComponent {
8515
8818
  }
8516
8819
  return String(raw);
8517
8820
  }, ...(ngDevMode ? [{ debugName: "literalText" }] : []));
8518
- booleanValue = computed(() => this.value()?.booleanValue === true, ...(ngDevMode ? [{ debugName: "booleanValue" }] : []));
8821
+ /**
8822
+ * Il valore booleano come testo per il `<select>` a tre stati: `''` = nessun valore, altrimenti
8823
+ * `'true'` / `'false'`. `isValued` e non `=== true`: `booleanValue: false` **e'** un valore, e
8824
+ * confonderlo con l'assenza e' esattamente il difetto che il tri-stato risolve.
8825
+ */
8826
+ booleanText = computed(() => {
8827
+ const value = this.value()?.booleanValue;
8828
+ return isValued(value) ? String(value) : '';
8829
+ }, ...(ngDevMode ? [{ debugName: "booleanText" }] : []));
8519
8830
  /** Il fuso dichiarato nel valore data: `Z`, un offset, oppure niente = ora locale (§4.2). */
8520
8831
  dateIsUtc = computed(() => ValueEditorComponent.hasZone(this.value()?.dateValue), ...(ngDevMode ? [{ debugName: "dateIsUtc" }] : []));
8521
8832
  static ZONE_PATTERN = /(Z|[+-]\d{2}:?\d{2})$/;
@@ -8550,7 +8861,13 @@ class ValueEditorComponent {
8550
8861
  this.emit({ elementReference: '' });
8551
8862
  break;
8552
8863
  case 'literal':
8553
- this.emit(this.dataType() === 'Boolean' ? { booleanValue: false } : { [this.literalField()]: '' });
8864
+ /**
8865
+ * Su un `Boolean` non si scrive niente: `false` sarebbe un valore che l'utente non ha
8866
+ * scelto — sceglierlo e' il gesto successivo, sul tri-stato. Con il modo già dedotto a
8867
+ * `literal` quando il tipo e' noto, questo bottone serve a **tornare** al letterale da un
8868
+ * riferimento o da una formula, e allora deve lasciare il campo da compilare, non compilarlo.
8869
+ */
8870
+ this.emit(this.dataType() === 'Boolean' ? undefined : { [this.literalField()]: '' });
8554
8871
  break;
8555
8872
  case 'formula':
8556
8873
  // L'unico caso con due campi: espressione + tipo atteso del risultato.
@@ -8613,8 +8930,9 @@ class ValueEditorComponent {
8613
8930
  onEnumChange(name) {
8614
8931
  this.emit(name?.trim() ? { enumValue: name.trim() } : undefined);
8615
8932
  }
8616
- onBooleanChange(checked) {
8617
- this.emit({ booleanValue: checked });
8933
+ /** `''` toglie il campo: «non impostato» e' l'assenza, non un terzo valore da scrivere (§2). */
8934
+ onBooleanTextChange(raw) {
8935
+ this.emit(raw === '' ? undefined : { booleanValue: raw === 'true' });
8618
8936
  }
8619
8937
  withDateZone(local, utc = this.dateIsUtc()) {
8620
8938
  return utc ? `${ValueEditorComponent.stripZone(local)}Z` : ValueEditorComponent.stripZone(local);
@@ -8680,7 +8998,7 @@ class ValueEditorComponent {
8680
8998
  }
8681
8999
  dataTypeOptions = computed(() => this.dictionaries.dataTypes(), ...(ngDevMode ? [{ debugName: "dataTypeOptions" }] : []));
8682
9000
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: ValueEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8683
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: ValueEditorComponent, isStandalone: true, selector: "fb-value-editor", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, dataType: { classPropertyName: "dataType", publicName: "dataType", isSignal: true, isRequired: false, transformFunction: null }, objectType: { classPropertyName: "objectType", publicName: "objectType", isSignal: true, isRequired: false, transformFunction: null }, isCollection: { classPropertyName: "isCollection", publicName: "isCollection", isSignal: true, isRequired: false, transformFunction: null }, valueSet: { classPropertyName: "valueSet", publicName: "valueSet", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, allowFormula: { classPropertyName: "allowFormula", publicName: "allowFormula", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<div class=\"fb-value\">\r\n <div class=\"fb-value__modes\" role=\"group\" [attr.aria-label]=\"label() + ': modalita\u2019'\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'reference'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Riferimento a una risorsa o all\u2019output di un elemento\"\r\n (click)=\"setMode('reference')\"\r\n >\r\n Riferimento\r\n </button>\r\n @if (literalAllowed()) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'literal'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Valore letterale del tipo della destinazione\"\r\n (click)=\"setMode('literal')\"\r\n >\r\n Valore\r\n </button>\r\n }\r\n @if (allowFormula()) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'formula'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Espressione calcolata dal motore di regole\"\r\n (click)=\"setMode('formula')\"\r\n >\r\n Formula\r\n </button>\r\n }\r\n @if (globalConstants().length) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'globalConstant'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Costante globale\"\r\n (click)=\"setMode('globalConstant')\"\r\n >\r\n Costante globale\r\n </button>\r\n }\r\n @if (mode() !== 'empty') {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode fb-value__mode--clear\"\r\n [disabled]=\"disabled()\"\r\n title=\"Nessun valore\"\r\n (click)=\"setMode('empty')\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (isAmbiguous()) {\r\n <!-- Piu' campi di valore insieme: il comportamento a runtime dipende dall'ordine di lettura. -->\r\n <p class=\"fb-callout fb-callout--warn\">\r\n Questo valore ha {{ filledFieldCount() }} campi valorizzati insieme: a runtime conta l\u2019ordine di lettura.\r\n <button type=\"button\" class=\"fb-btn fb-btn--ghost fb-btn--icon\" (click)=\"normalize()\">\r\n Tieni solo \u00AB{{ mode() === 'formula' ? 'formula' : mode() === 'literal' ? 'valore' : 'riferimento' }}\u00BB\r\n </button>\r\n </p>\r\n }\r\n\r\n @switch (mode()) {\r\n @case ('reference') {\r\n <fb-reference-picker\r\n [value]=\"value()?.elementReference\"\r\n [label]=\"label()\"\r\n [dataType]=\"dataType()\"\r\n [isCollection]=\"isCollection()\"\r\n [objectType]=\"objectType()\"\r\n [disabled]=\"disabled()\"\r\n (valueChange)=\"onReferenceChange($event)\"\r\n />\r\n @if (isStructureTarget()) {\r\n <p class=\"fb-field__hint\">\r\n Un\u2019istanza di classe si passa per riferimento: non esiste un valore letterale con cui scriverla.\r\n </p>\r\n }\r\n }\r\n\r\n @case ('globalConstant') {\r\n <select\r\n class=\"fb-select\"\r\n [disabled]=\"disabled()\"\r\n [fbValue]=\"value()?.elementReference || ''\"\r\n (change)=\"onGlobalConstantChange($any($event.target).value)\"\r\n >\r\n @for (constant of globalConstants(); track constant) {\r\n <option [value]=\"constant\">{{ constant }}</option>\r\n }\r\n </select>\r\n }\r\n\r\n @case ('literal') {\r\n @if (dataType() === 'Boolean') {\r\n <label class=\"fb-check\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"booleanValue()\"\r\n [disabled]=\"disabled()\"\r\n (change)=\"onBooleanChange($any($event.target).checked)\"\r\n />\r\n {{ booleanValue() ? 'vero' : 'falso' }}\r\n </label>\r\n } @else if (dataType() === 'Enum') {\r\n <!--\r\n Un enum si scrive **per nome** su un insieme chiuso: i nomi ammessi sono quelli del tipo\r\n (\u00A74.2, \u00A74.6), e farli digitare e' il modo piu' facile di scriverne uno che il runtime non\r\n riconosce. Senza `objectType` \u2014 o senza la primitiva \u2014 il picker resta una casella di\r\n testo, che e' l'unica cosa onesta quando i valori non si sanno.\r\n -->\r\n <fb-enum-value-picker\r\n [value]=\"value()?.enumValue\"\r\n [enumType]=\"objectType()\"\r\n [label]=\"label()\"\r\n [disabled]=\"disabled()\"\r\n placeholder=\"Nome del valore di enum\"\r\n (valueChange)=\"onEnumChange($event)\"\r\n />\r\n } @else if (hasValueSet()) {\r\n <!--\r\n Campo a scelta chiusa (\u00A74.4): i valori ammessi sono della **colonna** e si chiedono con\r\n `listFieldValues`, non si deducono dal tipo. Farli digitare su un insieme di quattro e' il\r\n modo piu' facile di scrivere un valore che nessun record ha \u2014 e il filtro non troverebbe\r\n niente senza nessun errore da nessuna parte.\r\n -->\r\n <fb-field-value-picker\r\n [value]=\"literalText()\"\r\n [valueSet]=\"valueSet()\"\r\n [label]=\"label()\"\r\n [disabled]=\"disabled()\"\r\n (valueChange)=\"onClosedValueChange($event)\"\r\n />\r\n } @else {\r\n <input\r\n class=\"fb-input\"\r\n [type]=\"literalInputType()\"\r\n [value]=\"literalText()\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"label()\"\r\n (input)=\"onLiteralChange($any($event.target).value)\"\r\n />\r\n @if (numericHint()) {\r\n <p class=\"fb-field__hint\">{{ numericHint() }}</p>\r\n }\r\n @if (dataType() === 'Date') {\r\n <!--\r\n Il fuso e' una scelta, non un dettaglio: `09:00Z` e `09:00` sono due istanti\r\n diversi, e il motore converte in UTC prima di ogni confronto (\u00A74.2).\r\n -->\r\n <div class=\"fb-value__modes\" role=\"group\" aria-label=\"Fuso del valore data\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"!dateIsUtc()\"\r\n [disabled]=\"disabled()\"\r\n title=\"Interpretato nel fuso dell\u2019applicazione\"\r\n (click)=\"setDateZone(false)\"\r\n >\r\n Ora locale\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"dateIsUtc()\"\r\n [disabled]=\"disabled()\"\r\n title=\"Scrive il suffisso Z: l\u2019orario e\u2019 in UTC\"\r\n (click)=\"setDateZone(true)\"\r\n >\r\n UTC (Z)\r\n </button>\r\n </div>\r\n <p class=\"fb-field__hint\">\r\n Data e ora insieme.\r\n {{\r\n dateIsUtc()\r\n ? 'Con \u00ABUTC\u00BB l\u2019orario e\u2019 assoluto.'\r\n : 'Senza fuso l\u2019orario e\u2019 interpretato nel fuso dell\u2019applicazione, non in UTC.'\r\n }}\r\n Cambiare fuso riscrive l\u2019orario, non lo converte.\r\n </p>\r\n }\r\n }\r\n }\r\n\r\n @case ('formula') {\r\n <!--\r\n Il tipo atteso e' quello **dichiarato sulla formula** (`formulaDataType`), non quello del\r\n campo: e' cio' che il documento porta, ed e' su quello che il motore va misurato (\u00A76.3).\r\n -->\r\n <fb-formula-editor\r\n [expression]=\"value()?.formulaExpression || ''\"\r\n usage=\"Value\"\r\n [expectedDataType]=\"$any(value()?.formulaDataType) || dataType()\"\r\n [disabled]=\"disabled()\"\r\n placeholder=\"Importo * 1.22\"\r\n [ariaLabel]=\"label() + ': espressione'\"\r\n (expressionChange)=\"onFormulaChange($event)\"\r\n />\r\n <div class=\"fb-field__row\">\r\n <label class=\"fb-field__hint\">Tipo del risultato</label>\r\n <select\r\n class=\"fb-select\"\r\n [fbValue]=\"value()?.formulaDataType || ''\"\r\n [disabled]=\"disabled()\"\r\n (change)=\"onFormulaTypeChange($any($event.target).value)\"\r\n >\r\n @for (type of dataTypeOptions(); track type.value) {\r\n <option [value]=\"type.value\">{{ type.label }}</option>\r\n }\r\n </select>\r\n </div>\r\n <p class=\"fb-field__hint\">\r\n L\u2019espressione va al motore di regole: la sintassi delle funzioni e\u2019 del motore.\r\n </p>\r\n }\r\n\r\n @case ('empty') {\r\n <p class=\"fb-field__hint\">Nessun valore.</p>\r\n }\r\n }\r\n</div>\r\n", styles: [":host{display:block}.fb-value{display:flex;flex-direction:column;gap:6px}\n"], dependencies: [{ kind: "component", type: EnumValuePickerComponent, selector: "fb-enum-value-picker", inputs: ["value", "enumType", "label", "placeholder", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: FieldValuePickerComponent, selector: "fb-field-value-picker", inputs: ["value", "valueSet", "label", "placeholder", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: FormulaEditorComponent, selector: "fb-formula-editor", inputs: ["expression", "usage", "expectedDataType", "scale", "placeholder", "ariaLabel", "disabled", "rows", "commitOn"], outputs: ["expressionChange"] }, { kind: "component", type: ReferencePickerComponent, selector: "fb-reference-picker", inputs: ["value", "label", "placeholder", "disabled", "dataType", "isCollection", "objectType", "writableOnly", "elementsOnly", "extraReferences"], outputs: ["valueChange"] }, { kind: "directive", type: SelectValueDirective, selector: "select[fbValue]", inputs: ["fbValue"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9001
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: ValueEditorComponent, isStandalone: true, selector: "fb-value-editor", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, dataType: { classPropertyName: "dataType", publicName: "dataType", isSignal: true, isRequired: false, transformFunction: null }, objectType: { classPropertyName: "objectType", publicName: "objectType", isSignal: true, isRequired: false, transformFunction: null }, isCollection: { classPropertyName: "isCollection", publicName: "isCollection", isSignal: true, isRequired: false, transformFunction: null }, valueSet: { classPropertyName: "valueSet", publicName: "valueSet", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, allowFormula: { classPropertyName: "allowFormula", publicName: "allowFormula", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<div class=\"fb-value\">\r\n <div class=\"fb-value__modes\" role=\"group\" [attr.aria-label]=\"label() + ': modalita\u2019'\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'reference'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Riferimento a una risorsa o all\u2019output di un elemento\"\r\n (click)=\"setMode('reference')\"\r\n >\r\n Riferimento\r\n </button>\r\n @if (literalAllowed()) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'literal'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Valore letterale del tipo della destinazione\"\r\n (click)=\"setMode('literal')\"\r\n >\r\n Valore\r\n </button>\r\n }\r\n @if (allowFormula()) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'formula'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Espressione calcolata dal motore di regole\"\r\n (click)=\"setMode('formula')\"\r\n >\r\n Formula\r\n </button>\r\n }\r\n @if (globalConstants().length) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'globalConstant'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Costante globale\"\r\n (click)=\"setMode('globalConstant')\"\r\n >\r\n Costante globale\r\n </button>\r\n }\r\n @if (mode() !== 'empty') {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode fb-value__mode--clear\"\r\n [disabled]=\"disabled()\"\r\n title=\"Nessun valore\"\r\n (click)=\"setMode('empty')\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (isAmbiguous()) {\r\n <!-- Piu' campi di valore insieme: il comportamento a runtime dipende dall'ordine di lettura. -->\r\n <p class=\"fb-callout fb-callout--warn\">\r\n Questo valore ha {{ filledFieldCount() }} campi valorizzati insieme: a runtime conta l\u2019ordine di lettura.\r\n <button type=\"button\" class=\"fb-btn fb-btn--ghost fb-btn--icon\" (click)=\"normalize()\">\r\n Tieni solo \u00AB{{ mode() === 'formula' ? 'formula' : mode() === 'literal' ? 'valore' : 'riferimento' }}\u00BB\r\n </button>\r\n </p>\r\n }\r\n\r\n @switch (mode()) {\r\n @case ('reference') {\r\n <fb-reference-picker\r\n [value]=\"value()?.elementReference\"\r\n [label]=\"label()\"\r\n [dataType]=\"dataType()\"\r\n [isCollection]=\"isCollection()\"\r\n [objectType]=\"objectType()\"\r\n [disabled]=\"disabled()\"\r\n (valueChange)=\"onReferenceChange($event)\"\r\n />\r\n @if (isStructureTarget()) {\r\n <p class=\"fb-field__hint\">\r\n Un\u2019istanza di classe si passa per riferimento: non esiste un valore letterale con cui scriverla.\r\n </p>\r\n }\r\n }\r\n\r\n @case ('globalConstant') {\r\n <select\r\n class=\"fb-select\"\r\n [disabled]=\"disabled()\"\r\n [fbValue]=\"value()?.elementReference || ''\"\r\n (change)=\"onGlobalConstantChange($any($event.target).value)\"\r\n >\r\n @for (constant of globalConstants(); track constant) {\r\n <option [value]=\"constant\">{{ constant }}</option>\r\n }\r\n </select>\r\n }\r\n\r\n @case ('literal') {\r\n @if (dataType() === 'Boolean') {\r\n <!--\r\n Tre stati, non due: **assente**, `false` e `true`. Una casella di spunta ne rappresenta\r\n due, e quella non spuntata finiva per dire \u00ABfalso\u00BB su un valore che nel documento non\r\n c'era \u2014 chi non la toccava leggeva una scelta che nessuno aveva fatto e che al backend non\r\n arrivava. `false` e' un valore e si scrive; \u00ABnon impostato\u00BB e' l'assenza del campo (\u00A72,\r\n \u00A74.2), ed e' l'unica cosa che una spunta non sa dire. Stesso controllo del form di\r\n \u00ABEsegui\u00BB, dove il problema era gi\u00E0 stato risolto cos\u00EC.\r\n -->\r\n <select\r\n class=\"fb-select\"\r\n [disabled]=\"disabled()\"\r\n [fbValue]=\"booleanText()\"\r\n (change)=\"onBooleanTextChange($any($event.target).value)\"\r\n >\r\n <option value=\"\">non impostato</option>\r\n <option value=\"true\">vero</option>\r\n <option value=\"false\">falso</option>\r\n </select>\r\n @if (!booleanText()) {\r\n <p class=\"fb-field__hint\">\r\n Nessun valore nel documento: \u00ABfalso\u00BB e\u2019 una scelta e va detta, perche\u2019 un campo assente\r\n non e\u2019 <code>false</code>.\r\n </p>\r\n }\r\n } @else if (dataType() === 'Enum') {\r\n <!--\r\n Un enum si scrive **per nome** su un insieme chiuso: i nomi ammessi sono quelli del tipo\r\n (\u00A74.2, \u00A74.6), e farli digitare e' il modo piu' facile di scriverne uno che il runtime non\r\n riconosce. Senza `objectType` \u2014 o senza la primitiva \u2014 il picker resta una casella di\r\n testo, che e' l'unica cosa onesta quando i valori non si sanno.\r\n -->\r\n <fb-enum-value-picker\r\n [value]=\"value()?.enumValue\"\r\n [enumType]=\"objectType()\"\r\n [label]=\"label()\"\r\n [disabled]=\"disabled()\"\r\n placeholder=\"Nome del valore di enum\"\r\n (valueChange)=\"onEnumChange($event)\"\r\n />\r\n } @else if (hasValueSet()) {\r\n <!--\r\n Campo a scelta chiusa (\u00A74.4): i valori ammessi sono della **colonna** e si chiedono con\r\n `listFieldValues`, non si deducono dal tipo. Farli digitare su un insieme di quattro e' il\r\n modo piu' facile di scrivere un valore che nessun record ha \u2014 e il filtro non troverebbe\r\n niente senza nessun errore da nessuna parte.\r\n -->\r\n <fb-field-value-picker\r\n [value]=\"literalText()\"\r\n [valueSet]=\"valueSet()\"\r\n [label]=\"label()\"\r\n [disabled]=\"disabled()\"\r\n (valueChange)=\"onClosedValueChange($event)\"\r\n />\r\n } @else {\r\n <input\r\n class=\"fb-input\"\r\n [type]=\"literalInputType()\"\r\n [value]=\"literalText()\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"label()\"\r\n (input)=\"onLiteralChange($any($event.target).value)\"\r\n />\r\n @if (numericHint()) {\r\n <p class=\"fb-field__hint\">{{ numericHint() }}</p>\r\n }\r\n @if (dataType() === 'Date') {\r\n <!--\r\n Il fuso e' una scelta, non un dettaglio: `09:00Z` e `09:00` sono due istanti\r\n diversi, e il motore converte in UTC prima di ogni confronto (\u00A74.2).\r\n -->\r\n <div class=\"fb-value__modes\" role=\"group\" aria-label=\"Fuso del valore data\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"!dateIsUtc()\"\r\n [disabled]=\"disabled()\"\r\n title=\"Interpretato nel fuso dell\u2019applicazione\"\r\n (click)=\"setDateZone(false)\"\r\n >\r\n Ora locale\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"dateIsUtc()\"\r\n [disabled]=\"disabled()\"\r\n title=\"Scrive il suffisso Z: l\u2019orario e\u2019 in UTC\"\r\n (click)=\"setDateZone(true)\"\r\n >\r\n UTC (Z)\r\n </button>\r\n </div>\r\n <p class=\"fb-field__hint\">\r\n Data e ora insieme.\r\n {{\r\n dateIsUtc()\r\n ? 'Con \u00ABUTC\u00BB l\u2019orario e\u2019 assoluto.'\r\n : 'Senza fuso l\u2019orario e\u2019 interpretato nel fuso dell\u2019applicazione, non in UTC.'\r\n }}\r\n Cambiare fuso riscrive l\u2019orario, non lo converte.\r\n </p>\r\n }\r\n }\r\n }\r\n\r\n @case ('formula') {\r\n <!--\r\n Il tipo atteso e' quello **dichiarato sulla formula** (`formulaDataType`), non quello del\r\n campo: e' cio' che il documento porta, ed e' su quello che il motore va misurato (\u00A76.3).\r\n -->\r\n <fb-formula-editor\r\n [expression]=\"value()?.formulaExpression || ''\"\r\n usage=\"Value\"\r\n [expectedDataType]=\"$any(value()?.formulaDataType) || dataType()\"\r\n [disabled]=\"disabled()\"\r\n placeholder=\"Importo * 1.22\"\r\n [ariaLabel]=\"label() + ': espressione'\"\r\n (expressionChange)=\"onFormulaChange($event)\"\r\n />\r\n <div class=\"fb-field__row\">\r\n <label class=\"fb-field__hint\">Tipo del risultato</label>\r\n <select\r\n class=\"fb-select\"\r\n [fbValue]=\"value()?.formulaDataType || ''\"\r\n [disabled]=\"disabled()\"\r\n (change)=\"onFormulaTypeChange($any($event.target).value)\"\r\n >\r\n @for (type of dataTypeOptions(); track type.value) {\r\n <option [value]=\"type.value\">{{ type.label }}</option>\r\n }\r\n </select>\r\n </div>\r\n <p class=\"fb-field__hint\">\r\n L\u2019espressione va al motore di regole: la sintassi delle funzioni e\u2019 del motore.\r\n </p>\r\n }\r\n\r\n @case ('empty') {\r\n <p class=\"fb-field__hint\">Nessun valore.</p>\r\n }\r\n }\r\n</div>\r\n", styles: [":host{display:block}.fb-value{display:flex;flex-direction:column;gap:6px}\n"], dependencies: [{ kind: "component", type: EnumValuePickerComponent, selector: "fb-enum-value-picker", inputs: ["value", "enumType", "label", "placeholder", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: FieldValuePickerComponent, selector: "fb-field-value-picker", inputs: ["value", "valueSet", "label", "placeholder", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: FormulaEditorComponent, selector: "fb-formula-editor", inputs: ["expression", "usage", "expectedDataType", "scale", "placeholder", "ariaLabel", "disabled", "rows", "commitOn"], outputs: ["expressionChange"] }, { kind: "component", type: ReferencePickerComponent, selector: "fb-reference-picker", inputs: ["value", "label", "placeholder", "disabled", "dataType", "isCollection", "objectType", "writableOnly", "elementsOnly", "extraReferences"], outputs: ["valueChange"] }, { kind: "directive", type: SelectValueDirective, selector: "select[fbValue]", inputs: ["fbValue"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8684
9002
  }
8685
9003
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: ValueEditorComponent, decorators: [{
8686
9004
  type: Component,
@@ -8690,7 +9008,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImpo
8690
9008
  FormulaEditorComponent,
8691
9009
  ReferencePickerComponent,
8692
9010
  SelectValueDirective,
8693
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"fb-value\">\r\n <div class=\"fb-value__modes\" role=\"group\" [attr.aria-label]=\"label() + ': modalita\u2019'\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'reference'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Riferimento a una risorsa o all\u2019output di un elemento\"\r\n (click)=\"setMode('reference')\"\r\n >\r\n Riferimento\r\n </button>\r\n @if (literalAllowed()) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'literal'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Valore letterale del tipo della destinazione\"\r\n (click)=\"setMode('literal')\"\r\n >\r\n Valore\r\n </button>\r\n }\r\n @if (allowFormula()) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'formula'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Espressione calcolata dal motore di regole\"\r\n (click)=\"setMode('formula')\"\r\n >\r\n Formula\r\n </button>\r\n }\r\n @if (globalConstants().length) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'globalConstant'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Costante globale\"\r\n (click)=\"setMode('globalConstant')\"\r\n >\r\n Costante globale\r\n </button>\r\n }\r\n @if (mode() !== 'empty') {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode fb-value__mode--clear\"\r\n [disabled]=\"disabled()\"\r\n title=\"Nessun valore\"\r\n (click)=\"setMode('empty')\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (isAmbiguous()) {\r\n <!-- Piu' campi di valore insieme: il comportamento a runtime dipende dall'ordine di lettura. -->\r\n <p class=\"fb-callout fb-callout--warn\">\r\n Questo valore ha {{ filledFieldCount() }} campi valorizzati insieme: a runtime conta l\u2019ordine di lettura.\r\n <button type=\"button\" class=\"fb-btn fb-btn--ghost fb-btn--icon\" (click)=\"normalize()\">\r\n Tieni solo \u00AB{{ mode() === 'formula' ? 'formula' : mode() === 'literal' ? 'valore' : 'riferimento' }}\u00BB\r\n </button>\r\n </p>\r\n }\r\n\r\n @switch (mode()) {\r\n @case ('reference') {\r\n <fb-reference-picker\r\n [value]=\"value()?.elementReference\"\r\n [label]=\"label()\"\r\n [dataType]=\"dataType()\"\r\n [isCollection]=\"isCollection()\"\r\n [objectType]=\"objectType()\"\r\n [disabled]=\"disabled()\"\r\n (valueChange)=\"onReferenceChange($event)\"\r\n />\r\n @if (isStructureTarget()) {\r\n <p class=\"fb-field__hint\">\r\n Un\u2019istanza di classe si passa per riferimento: non esiste un valore letterale con cui scriverla.\r\n </p>\r\n }\r\n }\r\n\r\n @case ('globalConstant') {\r\n <select\r\n class=\"fb-select\"\r\n [disabled]=\"disabled()\"\r\n [fbValue]=\"value()?.elementReference || ''\"\r\n (change)=\"onGlobalConstantChange($any($event.target).value)\"\r\n >\r\n @for (constant of globalConstants(); track constant) {\r\n <option [value]=\"constant\">{{ constant }}</option>\r\n }\r\n </select>\r\n }\r\n\r\n @case ('literal') {\r\n @if (dataType() === 'Boolean') {\r\n <label class=\"fb-check\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"booleanValue()\"\r\n [disabled]=\"disabled()\"\r\n (change)=\"onBooleanChange($any($event.target).checked)\"\r\n />\r\n {{ booleanValue() ? 'vero' : 'falso' }}\r\n </label>\r\n } @else if (dataType() === 'Enum') {\r\n <!--\r\n Un enum si scrive **per nome** su un insieme chiuso: i nomi ammessi sono quelli del tipo\r\n (\u00A74.2, \u00A74.6), e farli digitare e' il modo piu' facile di scriverne uno che il runtime non\r\n riconosce. Senza `objectType` \u2014 o senza la primitiva \u2014 il picker resta una casella di\r\n testo, che e' l'unica cosa onesta quando i valori non si sanno.\r\n -->\r\n <fb-enum-value-picker\r\n [value]=\"value()?.enumValue\"\r\n [enumType]=\"objectType()\"\r\n [label]=\"label()\"\r\n [disabled]=\"disabled()\"\r\n placeholder=\"Nome del valore di enum\"\r\n (valueChange)=\"onEnumChange($event)\"\r\n />\r\n } @else if (hasValueSet()) {\r\n <!--\r\n Campo a scelta chiusa (\u00A74.4): i valori ammessi sono della **colonna** e si chiedono con\r\n `listFieldValues`, non si deducono dal tipo. Farli digitare su un insieme di quattro e' il\r\n modo piu' facile di scrivere un valore che nessun record ha \u2014 e il filtro non troverebbe\r\n niente senza nessun errore da nessuna parte.\r\n -->\r\n <fb-field-value-picker\r\n [value]=\"literalText()\"\r\n [valueSet]=\"valueSet()\"\r\n [label]=\"label()\"\r\n [disabled]=\"disabled()\"\r\n (valueChange)=\"onClosedValueChange($event)\"\r\n />\r\n } @else {\r\n <input\r\n class=\"fb-input\"\r\n [type]=\"literalInputType()\"\r\n [value]=\"literalText()\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"label()\"\r\n (input)=\"onLiteralChange($any($event.target).value)\"\r\n />\r\n @if (numericHint()) {\r\n <p class=\"fb-field__hint\">{{ numericHint() }}</p>\r\n }\r\n @if (dataType() === 'Date') {\r\n <!--\r\n Il fuso e' una scelta, non un dettaglio: `09:00Z` e `09:00` sono due istanti\r\n diversi, e il motore converte in UTC prima di ogni confronto (\u00A74.2).\r\n -->\r\n <div class=\"fb-value__modes\" role=\"group\" aria-label=\"Fuso del valore data\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"!dateIsUtc()\"\r\n [disabled]=\"disabled()\"\r\n title=\"Interpretato nel fuso dell\u2019applicazione\"\r\n (click)=\"setDateZone(false)\"\r\n >\r\n Ora locale\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"dateIsUtc()\"\r\n [disabled]=\"disabled()\"\r\n title=\"Scrive il suffisso Z: l\u2019orario e\u2019 in UTC\"\r\n (click)=\"setDateZone(true)\"\r\n >\r\n UTC (Z)\r\n </button>\r\n </div>\r\n <p class=\"fb-field__hint\">\r\n Data e ora insieme.\r\n {{\r\n dateIsUtc()\r\n ? 'Con \u00ABUTC\u00BB l\u2019orario e\u2019 assoluto.'\r\n : 'Senza fuso l\u2019orario e\u2019 interpretato nel fuso dell\u2019applicazione, non in UTC.'\r\n }}\r\n Cambiare fuso riscrive l\u2019orario, non lo converte.\r\n </p>\r\n }\r\n }\r\n }\r\n\r\n @case ('formula') {\r\n <!--\r\n Il tipo atteso e' quello **dichiarato sulla formula** (`formulaDataType`), non quello del\r\n campo: e' cio' che il documento porta, ed e' su quello che il motore va misurato (\u00A76.3).\r\n -->\r\n <fb-formula-editor\r\n [expression]=\"value()?.formulaExpression || ''\"\r\n usage=\"Value\"\r\n [expectedDataType]=\"$any(value()?.formulaDataType) || dataType()\"\r\n [disabled]=\"disabled()\"\r\n placeholder=\"Importo * 1.22\"\r\n [ariaLabel]=\"label() + ': espressione'\"\r\n (expressionChange)=\"onFormulaChange($event)\"\r\n />\r\n <div class=\"fb-field__row\">\r\n <label class=\"fb-field__hint\">Tipo del risultato</label>\r\n <select\r\n class=\"fb-select\"\r\n [fbValue]=\"value()?.formulaDataType || ''\"\r\n [disabled]=\"disabled()\"\r\n (change)=\"onFormulaTypeChange($any($event.target).value)\"\r\n >\r\n @for (type of dataTypeOptions(); track type.value) {\r\n <option [value]=\"type.value\">{{ type.label }}</option>\r\n }\r\n </select>\r\n </div>\r\n <p class=\"fb-field__hint\">\r\n L\u2019espressione va al motore di regole: la sintassi delle funzioni e\u2019 del motore.\r\n </p>\r\n }\r\n\r\n @case ('empty') {\r\n <p class=\"fb-field__hint\">Nessun valore.</p>\r\n }\r\n }\r\n</div>\r\n", styles: [":host{display:block}.fb-value{display:flex;flex-direction:column;gap:6px}\n"] }]
9011
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"fb-value\">\r\n <div class=\"fb-value__modes\" role=\"group\" [attr.aria-label]=\"label() + ': modalita\u2019'\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'reference'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Riferimento a una risorsa o all\u2019output di un elemento\"\r\n (click)=\"setMode('reference')\"\r\n >\r\n Riferimento\r\n </button>\r\n @if (literalAllowed()) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'literal'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Valore letterale del tipo della destinazione\"\r\n (click)=\"setMode('literal')\"\r\n >\r\n Valore\r\n </button>\r\n }\r\n @if (allowFormula()) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'formula'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Espressione calcolata dal motore di regole\"\r\n (click)=\"setMode('formula')\"\r\n >\r\n Formula\r\n </button>\r\n }\r\n @if (globalConstants().length) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"mode() === 'globalConstant'\"\r\n [disabled]=\"disabled()\"\r\n title=\"Costante globale\"\r\n (click)=\"setMode('globalConstant')\"\r\n >\r\n Costante globale\r\n </button>\r\n }\r\n @if (mode() !== 'empty') {\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode fb-value__mode--clear\"\r\n [disabled]=\"disabled()\"\r\n title=\"Nessun valore\"\r\n (click)=\"setMode('empty')\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (isAmbiguous()) {\r\n <!-- Piu' campi di valore insieme: il comportamento a runtime dipende dall'ordine di lettura. -->\r\n <p class=\"fb-callout fb-callout--warn\">\r\n Questo valore ha {{ filledFieldCount() }} campi valorizzati insieme: a runtime conta l\u2019ordine di lettura.\r\n <button type=\"button\" class=\"fb-btn fb-btn--ghost fb-btn--icon\" (click)=\"normalize()\">\r\n Tieni solo \u00AB{{ mode() === 'formula' ? 'formula' : mode() === 'literal' ? 'valore' : 'riferimento' }}\u00BB\r\n </button>\r\n </p>\r\n }\r\n\r\n @switch (mode()) {\r\n @case ('reference') {\r\n <fb-reference-picker\r\n [value]=\"value()?.elementReference\"\r\n [label]=\"label()\"\r\n [dataType]=\"dataType()\"\r\n [isCollection]=\"isCollection()\"\r\n [objectType]=\"objectType()\"\r\n [disabled]=\"disabled()\"\r\n (valueChange)=\"onReferenceChange($event)\"\r\n />\r\n @if (isStructureTarget()) {\r\n <p class=\"fb-field__hint\">\r\n Un\u2019istanza di classe si passa per riferimento: non esiste un valore letterale con cui scriverla.\r\n </p>\r\n }\r\n }\r\n\r\n @case ('globalConstant') {\r\n <select\r\n class=\"fb-select\"\r\n [disabled]=\"disabled()\"\r\n [fbValue]=\"value()?.elementReference || ''\"\r\n (change)=\"onGlobalConstantChange($any($event.target).value)\"\r\n >\r\n @for (constant of globalConstants(); track constant) {\r\n <option [value]=\"constant\">{{ constant }}</option>\r\n }\r\n </select>\r\n }\r\n\r\n @case ('literal') {\r\n @if (dataType() === 'Boolean') {\r\n <!--\r\n Tre stati, non due: **assente**, `false` e `true`. Una casella di spunta ne rappresenta\r\n due, e quella non spuntata finiva per dire \u00ABfalso\u00BB su un valore che nel documento non\r\n c'era \u2014 chi non la toccava leggeva una scelta che nessuno aveva fatto e che al backend non\r\n arrivava. `false` e' un valore e si scrive; \u00ABnon impostato\u00BB e' l'assenza del campo (\u00A72,\r\n \u00A74.2), ed e' l'unica cosa che una spunta non sa dire. Stesso controllo del form di\r\n \u00ABEsegui\u00BB, dove il problema era gi\u00E0 stato risolto cos\u00EC.\r\n -->\r\n <select\r\n class=\"fb-select\"\r\n [disabled]=\"disabled()\"\r\n [fbValue]=\"booleanText()\"\r\n (change)=\"onBooleanTextChange($any($event.target).value)\"\r\n >\r\n <option value=\"\">non impostato</option>\r\n <option value=\"true\">vero</option>\r\n <option value=\"false\">falso</option>\r\n </select>\r\n @if (!booleanText()) {\r\n <p class=\"fb-field__hint\">\r\n Nessun valore nel documento: \u00ABfalso\u00BB e\u2019 una scelta e va detta, perche\u2019 un campo assente\r\n non e\u2019 <code>false</code>.\r\n </p>\r\n }\r\n } @else if (dataType() === 'Enum') {\r\n <!--\r\n Un enum si scrive **per nome** su un insieme chiuso: i nomi ammessi sono quelli del tipo\r\n (\u00A74.2, \u00A74.6), e farli digitare e' il modo piu' facile di scriverne uno che il runtime non\r\n riconosce. Senza `objectType` \u2014 o senza la primitiva \u2014 il picker resta una casella di\r\n testo, che e' l'unica cosa onesta quando i valori non si sanno.\r\n -->\r\n <fb-enum-value-picker\r\n [value]=\"value()?.enumValue\"\r\n [enumType]=\"objectType()\"\r\n [label]=\"label()\"\r\n [disabled]=\"disabled()\"\r\n placeholder=\"Nome del valore di enum\"\r\n (valueChange)=\"onEnumChange($event)\"\r\n />\r\n } @else if (hasValueSet()) {\r\n <!--\r\n Campo a scelta chiusa (\u00A74.4): i valori ammessi sono della **colonna** e si chiedono con\r\n `listFieldValues`, non si deducono dal tipo. Farli digitare su un insieme di quattro e' il\r\n modo piu' facile di scrivere un valore che nessun record ha \u2014 e il filtro non troverebbe\r\n niente senza nessun errore da nessuna parte.\r\n -->\r\n <fb-field-value-picker\r\n [value]=\"literalText()\"\r\n [valueSet]=\"valueSet()\"\r\n [label]=\"label()\"\r\n [disabled]=\"disabled()\"\r\n (valueChange)=\"onClosedValueChange($event)\"\r\n />\r\n } @else {\r\n <input\r\n class=\"fb-input\"\r\n [type]=\"literalInputType()\"\r\n [value]=\"literalText()\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"label()\"\r\n (input)=\"onLiteralChange($any($event.target).value)\"\r\n />\r\n @if (numericHint()) {\r\n <p class=\"fb-field__hint\">{{ numericHint() }}</p>\r\n }\r\n @if (dataType() === 'Date') {\r\n <!--\r\n Il fuso e' una scelta, non un dettaglio: `09:00Z` e `09:00` sono due istanti\r\n diversi, e il motore converte in UTC prima di ogni confronto (\u00A74.2).\r\n -->\r\n <div class=\"fb-value__modes\" role=\"group\" aria-label=\"Fuso del valore data\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"!dateIsUtc()\"\r\n [disabled]=\"disabled()\"\r\n title=\"Interpretato nel fuso dell\u2019applicazione\"\r\n (click)=\"setDateZone(false)\"\r\n >\r\n Ora locale\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fb-value__mode\"\r\n [class.fb-value__mode--active]=\"dateIsUtc()\"\r\n [disabled]=\"disabled()\"\r\n title=\"Scrive il suffisso Z: l\u2019orario e\u2019 in UTC\"\r\n (click)=\"setDateZone(true)\"\r\n >\r\n UTC (Z)\r\n </button>\r\n </div>\r\n <p class=\"fb-field__hint\">\r\n Data e ora insieme.\r\n {{\r\n dateIsUtc()\r\n ? 'Con \u00ABUTC\u00BB l\u2019orario e\u2019 assoluto.'\r\n : 'Senza fuso l\u2019orario e\u2019 interpretato nel fuso dell\u2019applicazione, non in UTC.'\r\n }}\r\n Cambiare fuso riscrive l\u2019orario, non lo converte.\r\n </p>\r\n }\r\n }\r\n }\r\n\r\n @case ('formula') {\r\n <!--\r\n Il tipo atteso e' quello **dichiarato sulla formula** (`formulaDataType`), non quello del\r\n campo: e' cio' che il documento porta, ed e' su quello che il motore va misurato (\u00A76.3).\r\n -->\r\n <fb-formula-editor\r\n [expression]=\"value()?.formulaExpression || ''\"\r\n usage=\"Value\"\r\n [expectedDataType]=\"$any(value()?.formulaDataType) || dataType()\"\r\n [disabled]=\"disabled()\"\r\n placeholder=\"Importo * 1.22\"\r\n [ariaLabel]=\"label() + ': espressione'\"\r\n (expressionChange)=\"onFormulaChange($event)\"\r\n />\r\n <div class=\"fb-field__row\">\r\n <label class=\"fb-field__hint\">Tipo del risultato</label>\r\n <select\r\n class=\"fb-select\"\r\n [fbValue]=\"value()?.formulaDataType || ''\"\r\n [disabled]=\"disabled()\"\r\n (change)=\"onFormulaTypeChange($any($event.target).value)\"\r\n >\r\n @for (type of dataTypeOptions(); track type.value) {\r\n <option [value]=\"type.value\">{{ type.label }}</option>\r\n }\r\n </select>\r\n </div>\r\n <p class=\"fb-field__hint\">\r\n L\u2019espressione va al motore di regole: la sintassi delle funzioni e\u2019 del motore.\r\n </p>\r\n }\r\n\r\n @case ('empty') {\r\n <p class=\"fb-field__hint\">Nessun valore.</p>\r\n }\r\n }\r\n</div>\r\n", styles: [":host{display:block}.fb-value{display:flex;flex-direction:column;gap:6px}\n"] }]
8694
9012
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], dataType: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataType", required: false }] }], objectType: [{ type: i0.Input, args: [{ isSignal: true, alias: "objectType", required: false }] }], isCollection: [{ type: i0.Input, args: [{ isSignal: true, alias: "isCollection", required: false }] }], valueSet: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueSet", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], allowFormula: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowFormula", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
8695
9013
 
8696
9014
  /**
@@ -10623,7 +10941,9 @@ class DynamicScreenInspectorComponent extends NodeInspectorBase {
10623
10941
  value.integerValue ??
10624
10942
  value.dateValue ??
10625
10943
  value.enumValue ??
10626
- (value.booleanValue === undefined ? undefined : String(value.booleanValue));
10944
+ // `isValued` e non `!== undefined`: `false` e' un valore da mostrare, `null` e' un'assenza che
10945
+ // un backend che serializza tutto manda comunque — e stamparlo darebbe «null» nel controllo.
10946
+ (isValued(value.booleanValue) ? String(value.booleanValue) : undefined);
10627
10947
  return literal === undefined ? '' : String(literal);
10628
10948
  }
10629
10949
  // -------------------------------------------------------------------------
@@ -12938,80 +13258,63 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImpo
12938
13258
  * l'input `aggregationValues`; `Sum` anche `aggregationField`. Il form li gestisce come
12939
13259
  * campi propri, invece di lasciare all'utente il compito di indovinare i nomi dei parametri.
12940
13260
  *
13261
+ * **Non e' un form: e' una mappatura**, a tre pannelli come il compositore dello screen dinamico
13262
+ * (§5.2) e come il Transform delle ultime versioni di Salesforce. A sinistra le **sorgenti**
13263
+ * (i riferimenti del flow, navigabili dentro record e classi), al centro la **destinazione** —
13264
+ * una riga per ogni campo o membro che si puo' scrivere, mappato o no — a destra il **dettaglio**
13265
+ * della riga scelta. Si mappa trascinando una sorgente su una riga, oppure selezionando la riga e
13266
+ * cliccando la sorgente.
13267
+ *
13268
+ * Perche' l'elenco delle righe non e' l'elenco delle azioni: un elenco di azioni dice cio' che
13269
+ * **e' già** mappato e tace su cio' che manca, e su una classe di dieci membri «cosa resta da
13270
+ * riempire» era una domanda a cui si rispondeva contando a mano. Le righe vengono quindi dal
13271
+ * **catalogo della destinazione** (i membri di una classe, §4.7.1; i campi di un'entita', §4.4) e
13272
+ * l'azione e' cio' che una riga porta, non cio' che la fa esistere. Dove il catalogo non c'e' —
13273
+ * destinazione scalare, oppure una classe che non dichiara i membri — le righe restano quelle
13274
+ * delle azioni: si digita il nome, come prima, e nessuno accusa nulla.
13275
+ *
12941
13276
  * Il target puo' anche essere una **classe** (`dataType: 'Structure'`): in quel caso
12942
13277
  * `outputFieldApiName` porta il nome del **membro** invece del nome del campo (§4.7), ed e' il
12943
- * modo di comporre un'istanza intera in un elemento solo invece di un Assignment per membro. Il
12944
- * form propone allora i membri scrivibili, non un campo di testo: un membro calcolato si legge
12945
- * ma assegnarlo e' `TARGET_NOT_WRITABLE`.
13278
+ * modo di comporre un'istanza intera in un elemento solo invece di un Assignment per membro. Un
13279
+ * membro calcolato si legge ma assegnarlo e' `TARGET_NOT_WRITABLE`: qui non compare fra le righe.
12946
13280
  *
12947
13281
  * Nota: `connector` qui e' un **array** — la mappa delle uscite lo nasconde al resto.
12948
13282
  */
12949
13283
  /** I nomi dei parametri che le aggregazioni richiedono, fissati dal modello. */
12950
13284
  const AGGREGATION_VALUES = 'aggregationValues';
12951
13285
  const AGGREGATION_FIELD = 'aggregationField';
13286
+ /** Il glifo di un'operazione. È presentazione: l'elenco autoritativo resta il dizionario. */
13287
+ const OPERATION_ICONS = {
13288
+ Map: '→',
13289
+ Sum: 'Σ',
13290
+ Count: '#',
13291
+ };
12952
13292
  class TransformInspectorComponent extends NodeInspectorBase {
13293
+ api = inject(FlowBuilderApi);
12953
13294
  catalog = inject(FlowCatalogStore);
13295
+ document = inject(DOCUMENT);
12954
13296
  elementType = 'Transform';
12955
13297
  transform = computed(() => this.node(), ...(ngDevMode ? [{ debugName: "transform" }] : []));
12956
13298
  transformTypes = computed(() => this.dictionaries.transformTypes(), ...(ngDevMode ? [{ debugName: "transformTypes" }] : []));
12957
13299
  dataTypes = computed(() => this.dictionaries.dataTypes(), ...(ngDevMode ? [{ debugName: "dataTypes" }] : []));
12958
13300
  enumTypes = signal([], ...(ngDevMode ? [{ debugName: "enumTypes" }] : []));
12959
- /**
12960
- * §4.7 — i membri della classe di destinazione: servono a sapere **il tipo** di cio' che ogni
12961
- * `Map` scrive, che e' quello che guida l'editor del valore. Su un membro `Enum` e' la sola
12962
- * strada per proporre i valori del tipo invece di farli digitare (§4.6).
12963
- */
12964
- members = signal([], ...(ngDevMode ? [{ debugName: "members" }] : []));
12965
13301
  constructor() {
12966
13302
  super();
12967
13303
  void this.catalog.listEnumTypes().then((list) => this.enumTypes.set(list ?? []));
12968
- effect(() => {
12969
- const className = this.isStructureTarget() ? this.transform().objectType : undefined;
12970
- if (!className) {
12971
- if (untracked(this.members).length) {
12972
- this.members.set([]);
12973
- }
12974
- return;
12975
- }
12976
- void this.catalog
12977
- .listStructureMembers(className)
12978
- .then((list) => this.members.set(list ?? []))
12979
- // Catalogo assente: il tipo del membro resta ignoto e il valore resta generico.
12980
- .catch(() => this.members.set([]));
12981
- });
12982
- }
12983
- /**
12984
- * Il tipo di cio' che una `Map` scrive: il **membro** su un target `Structure`, il target stesso
12985
- * altrove. Membro non in catalogo — o percorso annidato — il tipo resta ignoto: si digita.
12986
- */
12987
- memberOf(action) {
12988
- const name = action.outputFieldApiName;
12989
- return name ? this.members().find((member) => member.name === name) : undefined;
12990
- }
12991
- mapDataType(action) {
12992
- if (this.isStructureTarget()) {
12993
- return this.memberOf(action)?.dataType ?? undefined;
12994
- }
12995
- return this.transform().dataType ?? undefined;
12996
- }
12997
- mapObjectType(action) {
12998
- if (this.isStructureTarget()) {
12999
- return this.memberOf(action)?.objectType ?? undefined;
13000
- }
13001
- return this.transform().objectType ?? undefined;
13304
+ this.loadReferences();
13305
+ this.loadTargetLevel();
13002
13306
  }
13307
+ // ------------------------------------------------------------------ il risultato
13003
13308
  enumOptions = computed(() => this.enumTypes(), ...(ngDevMode ? [{ debugName: "enumOptions" }] : []));
13004
13309
  requiresObjectType = computed(() => this.dictionaries.requiresObjectType(this.transform().dataType), ...(ngDevMode ? [{ debugName: "requiresObjectType" }] : []));
13005
13310
  /** §4.7 — con un target `Structure` la destinazione di ogni azione e' un **membro**. */
13006
13311
  isStructureTarget = computed(() => this.dictionaries.isStructure(this.transform().dataType), ...(ngDevMode ? [{ debugName: "isStructureTarget" }] : []));
13007
13312
  /** Su una `Structure` la classe mancante e' un errore, non un avviso (§4.7). */
13008
13313
  missingObjectType = computed(() => this.isStructureTarget() && !this.transform().objectType, ...(ngDevMode ? [{ debugName: "missingObjectType" }] : []));
13009
- /** Il modello prevede una lista di liste; l'editor lavora sulla prima, che e' il caso d'uso. */
13010
- actions = computed(() => this.transform().transformValues?.[0]?.transformValueActions ?? [], ...(ngDevMode ? [{ debugName: "actions" }] : []));
13011
13314
  setDataType(dataType) {
13012
13315
  this.patch((node) => {
13013
13316
  const transform = node;
13014
- transform.dataType = dataType || undefined;
13317
+ transform.dataType = (dataType || undefined);
13015
13318
  if (!this.dictionaries.requiresObjectType(dataType)) {
13016
13319
  delete transform.objectType;
13017
13320
  }
@@ -13031,7 +13334,356 @@ class TransformInspectorComponent extends NodeInspectorBase {
13031
13334
  }
13032
13335
  });
13033
13336
  }
13337
+ // ------------------------------------------------------------------ le sorgenti
13338
+ /**
13339
+ * I riferimenti del flow: la stessa fonte del reference picker (§6.4). Serve **sia** per la
13340
+ * colonna delle sorgenti **sia** per riconoscere la radice di un percorso quando si entra in un
13341
+ * record o in una classe.
13342
+ */
13343
+ references = signal([], ...(ngDevMode ? [{ debugName: "references" }] : []));
13344
+ referencesPending = signal(false, ...(ngDevMode ? [{ debugName: "referencesPending" }] : []));
13345
+ referencesError = signal(null, ...(ngDevMode ? [{ debugName: "referencesError" }] : []));
13346
+ /** Dove si sta guardando: vuoto = le radici, altrimenti il percorso in cui si e' entrati. */
13347
+ sourcePath = signal('', ...(ngDevMode ? [{ debugName: "sourcePath" }] : []));
13348
+ sourceQuery = signal('', ...(ngDevMode ? [{ debugName: "sourceQuery" }] : []));
13349
+ /**
13350
+ * Il punto in cui navigare, scritto **col punto in fondo**: e' la stessa convenzione del
13351
+ * reference picker — un percorso che finisce col punto ha come ultimo segmento il vuoto, e la
13352
+ * navigazione risponde con cio' che c'e' **dentro** invece di verificare un nome.
13353
+ */
13354
+ sourceRoot = computed(() => {
13355
+ const path = this.sourcePath();
13356
+ if (!path) {
13357
+ return undefined;
13358
+ }
13359
+ return navigableRootOf(this.references(), `${path}.`, (dataType) => this.dictionaries.isStructure(dataType), {
13360
+ // Gli output automatici si **propongono** e non si giudicano (§4.5): senza questo, entrare in
13361
+ // `Leggi_Ordine` sarebbe impossibile pur essendo `Leggi_Ordine.Numero` un riferimento valido.
13362
+ proposeOnly: true,
13363
+ });
13364
+ }, ...(ngDevMode ? [{ debugName: "sourceRoot" }] : []));
13365
+ sourceNavigation = navigatePath(this.catalog, () => {
13366
+ const root = this.sourceRoot();
13367
+ return root ? { root: root.container, path: root.path } : null;
13368
+ });
13369
+ sourcePending = computed(() => this.referencesPending() || this.sourceNavigation.isPending(), ...(ngDevMode ? [{ debugName: "sourcePending" }] : []));
13370
+ sourceError = computed(() => this.referencesError(), ...(ngDevMode ? [{ debugName: "sourceError" }] : []));
13371
+ /** Le briciole di pane: «Tutte» piu' un livello per segmento del percorso in cui si e' entrati. */
13372
+ sourceCrumbs = computed(() => {
13373
+ const path = this.sourcePath();
13374
+ if (!path) {
13375
+ return [];
13376
+ }
13377
+ const segments = path.split('.');
13378
+ return segments.map((segment, index) => ({
13379
+ label: segment,
13380
+ path: segments.slice(0, index + 1).join('.'),
13381
+ }));
13382
+ }, ...(ngDevMode ? [{ debugName: "sourceCrumbs" }] : []));
13383
+ /** Cio' che si sta guardando: le radici, oppure i campi o i membri della tappa. */
13384
+ sourceItems = computed(() => {
13385
+ const path = this.sourcePath();
13386
+ if (!path) {
13387
+ return this.references().map((reference) => ({
13388
+ path: reference.name,
13389
+ label: reference.label || reference.name,
13390
+ detail: this.describeReference(reference),
13391
+ isCollection: reference.isCollection === true,
13392
+ canEnter: !!navigableRootOf(this.references(), reference.name, (dataType) => this.dictionaries.isStructure(dataType), { proposeOnly: true }),
13393
+ }));
13394
+ }
13395
+ const tail = this.sourceNavigation.resolution()?.tail;
13396
+ if (!tail) {
13397
+ return [];
13398
+ }
13399
+ return tail.level.entries.map((entry) => ({
13400
+ path: `${path}.${entry.name}`,
13401
+ label: entry.label || entry.name,
13402
+ detail: this.describeEntry(entry),
13403
+ isCollection: entry.isCollection === true,
13404
+ // Dentro una collection non si naviga: il percorso designa l'insieme, non un elemento (§4.4).
13405
+ canEnter: !!entry.next && !entry.isCollection,
13406
+ }));
13407
+ }, ...(ngDevMode ? [{ debugName: "sourceItems" }] : []));
13408
+ /** Il filtro e' locale: la ricerca serve su venti variabili, non su tre. */
13409
+ sources = computed(() => {
13410
+ const query = this.sourceQuery().trim().toLowerCase();
13411
+ const items = this.sourceItems();
13412
+ if (!query) {
13413
+ return items;
13414
+ }
13415
+ return items.filter((item) => item.path.toLowerCase().includes(query) || item.label.toLowerCase().includes(query));
13416
+ }, ...(ngDevMode ? [{ debugName: "sources" }] : []));
13417
+ /**
13418
+ * La tappa in cui si e' entrati non dichiara campi ne' membri: e' un "non lo so" del catalogo
13419
+ * (§4.7.1), non «qui non c'e' niente», e i due meritano parole diverse.
13420
+ */
13421
+ sourceUndeclared = computed(() => {
13422
+ const status = this.sourceNavigation.resolution()?.tail?.level.status;
13423
+ return !!this.sourcePath() && !!status && status !== 'declared';
13424
+ }, ...(ngDevMode ? [{ debugName: "sourceUndeclared" }] : []));
13425
+ enterSource(path) {
13426
+ this.sourcePath.set(path);
13427
+ this.sourceQuery.set('');
13428
+ }
13429
+ leaveSource() {
13430
+ const segments = this.sourcePath().split('.');
13431
+ segments.pop();
13432
+ this.enterSource(segments.join('.'));
13433
+ }
13434
+ describeReference(reference) {
13435
+ const parts = [];
13436
+ if (reference.dataType) {
13437
+ parts.push(reference.isCollection ? `${reference.dataType}[]` : reference.dataType);
13438
+ }
13439
+ if (reference.objectType) {
13440
+ parts.push(reference.objectType);
13441
+ }
13442
+ return parts.join(' · ');
13443
+ }
13444
+ describeEntry(entry) {
13445
+ const parts = [];
13446
+ if (entry.dataType) {
13447
+ parts.push(entry.isCollection ? `${entry.dataType}[]` : entry.dataType);
13448
+ }
13449
+ if (entry.objectType) {
13450
+ parts.push(entry.objectType);
13451
+ }
13452
+ return parts.join(' · ');
13453
+ }
13454
+ loadReferences() {
13455
+ let sequence = 0;
13456
+ effect(() => {
13457
+ const definition = this.store.document();
13458
+ const token = ++sequence;
13459
+ this.referencesPending.set(true);
13460
+ this.referencesError.set(null);
13461
+ void this.api
13462
+ .getReferences(definition)
13463
+ .then((list) => {
13464
+ if (token !== sequence) {
13465
+ return;
13466
+ }
13467
+ this.references.set(list ?? []);
13468
+ this.referencesPending.set(false);
13469
+ })
13470
+ .catch(() => {
13471
+ if (token !== sequence) {
13472
+ return;
13473
+ }
13474
+ // Senza l'elenco si scrive a mano nel dettaglio: la colonna lo dice invece di restare vuota.
13475
+ this.references.set([]);
13476
+ this.referencesError.set('I riferimenti del flow non sono disponibili.');
13477
+ this.referencesPending.set(false);
13478
+ });
13479
+ });
13480
+ }
13481
+ // ------------------------------------------------------------------ la destinazione
13482
+ /**
13483
+ * Il catalogo della destinazione: i membri di una classe o i campi di un'entita', a seconda del
13484
+ * tipo del risultato. `loadPathLevel` copre entrambi i casi e porta con se' lo **stato** — solo
13485
+ * `declared` autorizza a dire che un nome non esiste (§4.7.1).
13486
+ */
13487
+ targetLevel = signal(null, ...(ngDevMode ? [{ debugName: "targetLevel" }] : []));
13488
+ targetContainer = computed(() => {
13489
+ const transform = this.transform();
13490
+ if (!transform.objectType) {
13491
+ return null;
13492
+ }
13493
+ if (this.isStructureTarget()) {
13494
+ return { kind: 'structure', name: transform.objectType };
13495
+ }
13496
+ return transform.dataType === 'Object' ? { kind: 'object', name: transform.objectType } : null;
13497
+ }, ...(ngDevMode ? [{ debugName: "targetContainer" }] : []));
13498
+ loadTargetLevel() {
13499
+ let sequence = 0;
13500
+ effect(() => {
13501
+ const container = this.targetContainer();
13502
+ const token = ++sequence;
13503
+ if (!container) {
13504
+ this.targetLevel.set(null);
13505
+ return;
13506
+ }
13507
+ void loadPathLevel(this.catalog, container).then((level) => {
13508
+ if (token === sequence) {
13509
+ this.targetLevel.set(level);
13510
+ }
13511
+ });
13512
+ });
13513
+ }
13514
+ /**
13515
+ * L'entita' del risultato, quando il risultato e' un record: le destinazioni sono i suoi campi,
13516
+ * ed e' cio' che rende la casella del nome un elenco.
13517
+ */
13518
+ targetObject = computed(() => this.transform().dataType === 'Object' ? this.transform().objectType : undefined, ...(ngDevMode ? [{ debugName: "targetObject" }] : []));
13519
+ /**
13520
+ * La destinazione ha una forma **dichiarata**: c'e' un catalogo da cui prendere le righe. Il caso
13521
+ * opposto non e' un difetto — un risultato scalare non ha campi — ed e' per questo che le due
13522
+ * domande sono separate da {@link targetUndeclared}.
13523
+ */
13524
+ hasTargetCatalog = computed(() => !!this.targetContainer(), ...(ngDevMode ? [{ debugName: "hasTargetCatalog" }] : []));
13525
+ /**
13526
+ * Tappa valida di cui campi o membri non sono dichiarati (§4.7.1): si **digita** il nome, e non
13527
+ * si accusa nessuno. Dirlo evita di cercare un difetto dove c'e' un catalogo incompleto.
13528
+ */
13529
+ targetUndeclared = computed(() => this.hasTargetCatalog() && !this.targetIsDeclared(), ...(ngDevMode ? [{ debugName: "targetUndeclared" }] : []));
13530
+ /** Il catalogo e' autorevole: solo allora un nome fuori elenco e' un nome sbagliato. */
13531
+ targetIsDeclared = computed(() => this.targetLevel()?.status === 'declared', ...(ngDevMode ? [{ debugName: "targetIsDeclared" }] : []));
13532
+ /**
13533
+ * Le destinazioni proponibili. Un membro calcolato non c'e': assegnarlo e' `TARGET_NOT_WRITABLE`
13534
+ * (§4.7). Su un'entita' `isWritable` e' già `createable || updateable` (§5.8).
13535
+ */
13536
+ targetSlots = computed(() => (this.targetLevel()?.entries ?? []).filter((entry) => entry.isWritable), ...(ngDevMode ? [{ debugName: "targetSlots" }] : []));
13537
+ /** Il modello prevede una lista di liste; l'editor lavora sulla prima, che e' il caso d'uso. */
13538
+ actions = computed(() => this.transform().transformValues?.[0]?.transformValueActions ?? [], ...(ngDevMode ? [{ debugName: "actions" }] : []));
13539
+ /**
13540
+ * Le righe della destinazione: prima il catalogo — mappato o no — poi le azioni che nominano
13541
+ * qualcosa che il catalogo non ha. L'ordine e' quello, e non quello delle azioni, perche' la
13542
+ * colonna dice **la forma del risultato**: un elenco che si riordina a ogni mappatura non si
13543
+ * legge.
13544
+ */
13545
+ targets = computed(() => {
13546
+ const actions = this.actions();
13547
+ const taken = new Set();
13548
+ const rows = [];
13549
+ for (const slot of this.targetSlots()) {
13550
+ const index = actions.findIndex((action, position) => !taken.has(position) && action.outputFieldApiName === slot.name);
13551
+ if (index >= 0) {
13552
+ taken.add(index);
13553
+ }
13554
+ rows.push({
13555
+ key: `slot:${slot.name}`,
13556
+ field: slot.name,
13557
+ label: slot.label || slot.name,
13558
+ dataType: slot.dataType,
13559
+ objectType: slot.objectType ?? undefined,
13560
+ isCollection: slot.isCollection,
13561
+ index,
13562
+ action: index >= 0 ? actions[index] : undefined,
13563
+ isUnknown: false,
13564
+ });
13565
+ }
13566
+ actions.forEach((action, index) => {
13567
+ if (taken.has(index)) {
13568
+ return;
13569
+ }
13570
+ const transform = this.transform();
13571
+ rows.push({
13572
+ key: `action:${index}`,
13573
+ field: action.outputFieldApiName ?? '',
13574
+ label: action.outputFieldApiName || '(destinazione da indicare)',
13575
+ // Senza catalogo il tipo di cio' che si scrive e' quello del risultato: e' quanto si sa.
13576
+ dataType: this.targetContainer() ? undefined : transform.dataType,
13577
+ objectType: this.targetContainer() ? undefined : transform.objectType,
13578
+ isCollection: this.targetContainer() ? undefined : transform.isCollection,
13579
+ index,
13580
+ action,
13581
+ isUnknown: this.targetIsDeclared() && !!action.outputFieldApiName,
13582
+ });
13583
+ });
13584
+ return rows;
13585
+ }, ...(ngDevMode ? [{ debugName: "targets" }] : []));
13586
+ mappedCount = computed(() => this.targets().filter((row) => row.index >= 0).length, ...(ngDevMode ? [{ debugName: "mappedCount" }] : []));
13587
+ /** Quante destinazioni il catalogo dichiara: senza catalogo non c'e' un totale da mostrare. */
13588
+ slotCount = computed(() => (this.targetIsDeclared() ? this.targetSlots().length : 0), ...(ngDevMode ? [{ debugName: "slotCount" }] : []));
13589
+ selectedKey = signal(null, ...(ngDevMode ? [{ debugName: "selectedKey" }] : []));
13590
+ /**
13591
+ * La riga aperta nel dettaglio. Il ripiego sulla prima non e' cosmetico: la colonna di destra
13592
+ * altrimenti resta vuota all'apertura, e il primo gesto sarebbe un click che non modifica niente.
13593
+ */
13594
+ selected = computed(() => {
13595
+ const rows = this.targets();
13596
+ const key = this.selectedKey();
13597
+ return rows.find((row) => row.key === key) ?? rows[0] ?? null;
13598
+ }, ...(ngDevMode ? [{ debugName: "selected" }] : []));
13599
+ select(key) {
13600
+ this.selectedKey.set(key);
13601
+ }
13602
+ isSelected(row) {
13603
+ return this.selected()?.key === row.key;
13604
+ }
13605
+ /** I rilievi ancorati all'azione di una riga (§7): la riga li mostra, il dettaglio li spiega. */
13606
+ issuesOf(row) {
13607
+ return row.index >= 0 ? this.issuesAt(`transformValues[0].transformValueActions[${row.index}]`) : [];
13608
+ }
13609
+ // ------------------------------------------------------------------ le azioni
13610
+ operationOf(row) {
13611
+ return row.action?.transformType ?? 'Map';
13612
+ }
13613
+ operationIcon(row) {
13614
+ const type = this.operationOf(row);
13615
+ return OPERATION_ICONS[type] ?? '·';
13616
+ }
13617
+ operationLabel(type) {
13618
+ return this.transformTypes().find((entry) => entry.value === type)?.label ?? type;
13619
+ }
13620
+ /**
13621
+ * Cosa la riga scrive, in una riga di testo. È il solo modo di leggere una mappatura senza
13622
+ * aprirla: la colonna centrale mostra questo, il dettaglio i controlli.
13623
+ */
13624
+ sourceSummary(row) {
13625
+ const action = row.action;
13626
+ if (!action) {
13627
+ return '';
13628
+ }
13629
+ const type = action.transformType ?? 'Map';
13630
+ if (type === 'Map') {
13631
+ return this.describeValue(action.value);
13632
+ }
13633
+ const collection = this.aggregationCollection(action) ?? '';
13634
+ if (type === 'Sum') {
13635
+ const field = this.aggregationField(action);
13636
+ return collection && field ? `${collection} · ${field}` : collection;
13637
+ }
13638
+ return collection;
13639
+ }
13640
+ describeValue(value) {
13641
+ const field = valuedFieldOf(value);
13642
+ if (!value || !field) {
13643
+ return '';
13644
+ }
13645
+ if (field === 'formulaExpression') {
13646
+ return `ƒ ${value.formulaExpression}`;
13647
+ }
13648
+ if (field === 'elementReference') {
13649
+ return value.elementReference ?? '';
13650
+ }
13651
+ return String(value[field]);
13652
+ }
13653
+ mutateAction(index, mutate) {
13654
+ this.patch((node) => {
13655
+ const action = node.transformValues?.[0]?.transformValueActions?.[index];
13656
+ if (action) {
13657
+ mutate(action);
13658
+ }
13659
+ });
13660
+ }
13661
+ /**
13662
+ * L'indice dell'azione della riga, creandola se non c'e'. È la conseguenza del «le righe vengono
13663
+ * dal catalogo»: una riga esiste anche prima di essere mappata, e la prima modifica e' cio' che
13664
+ * la fa diventare un'azione del documento.
13665
+ */
13666
+ ensureAction(row) {
13667
+ if (row.index >= 0) {
13668
+ return row.index;
13669
+ }
13670
+ const index = this.actions().length;
13671
+ this.patch((node) => {
13672
+ const transform = node;
13673
+ transform.transformValues ??= [{}];
13674
+ const first = transform.transformValues[0];
13675
+ first.transformValueActions ??= [];
13676
+ first.transformValueActions.push({
13677
+ transformType: 'Map',
13678
+ outputFieldApiName: row.field || undefined,
13679
+ });
13680
+ });
13681
+ this.selectedKey.set(row.key);
13682
+ return index;
13683
+ }
13684
+ /** Una destinazione fuori catalogo: l'unico modo di scrivere un nome che il catalogo non ha. */
13034
13685
  addAction() {
13686
+ const index = this.actions().length;
13035
13687
  this.patch((node) => {
13036
13688
  const transform = node;
13037
13689
  transform.transformValues ??= [{}];
@@ -13039,26 +13691,26 @@ class TransformInspectorComponent extends NodeInspectorBase {
13039
13691
  first.transformValueActions ??= [];
13040
13692
  first.transformValueActions.push({ transformType: 'Map' });
13041
13693
  });
13694
+ this.selectedKey.set(`action:${index}`);
13042
13695
  }
13043
- removeAction(index) {
13696
+ removeAction(row) {
13697
+ if (row.index < 0) {
13698
+ return;
13699
+ }
13044
13700
  this.patch((node) => {
13045
13701
  const transform = node;
13046
13702
  const actions = transform.transformValues?.[0]?.transformValueActions;
13047
- actions?.splice(index, 1);
13703
+ actions?.splice(row.index, 1);
13048
13704
  if (actions?.length === 0) {
13049
13705
  delete transform.transformValues;
13050
13706
  }
13051
13707
  });
13708
+ // La riga resta (viene dal catalogo) o sparisce (era un'azione): in entrambi i casi la
13709
+ // selezione per chiave ritrova cio' che c'e', o ripiega sulla prima.
13710
+ this.selectedKey.set(row.key);
13052
13711
  }
13053
- mutateAction(index, mutate) {
13054
- this.patch((node) => {
13055
- const action = node.transformValues?.[0]?.transformValueActions?.[index];
13056
- if (action) {
13057
- mutate(action);
13058
- }
13059
- });
13060
- }
13061
- setActionType(index, transformType) {
13712
+ setOperation(row, transformType) {
13713
+ const index = this.ensureAction(row);
13062
13714
  this.mutateAction(index, (action) => {
13063
13715
  action.transformType = transformType;
13064
13716
  // Map usa `value`, Sum e Count usano `inputParameters`: tenere entrambi renderebbe
@@ -13078,24 +13730,38 @@ class TransformInspectorComponent extends NodeInspectorBase {
13078
13730
  }
13079
13731
  });
13080
13732
  }
13081
- setOutputField(index, field) {
13733
+ setOutputField(row, field) {
13734
+ const index = this.ensureAction(row);
13082
13735
  this.mutateAction(index, (action) => {
13083
13736
  action.outputFieldApiName = field || undefined;
13084
13737
  });
13738
+ /**
13739
+ * La chiave di una riga contiene il nome della destinazione, quindi cambiando nome la riga
13740
+ * selezionata **e' un'altra**: la si ritrova per indice dell'azione. Ricomporre la chiave a
13741
+ * mano (`slot:<nome>`) sbagliava proprio nel caso interessante — un nome fuori catalogo non ha
13742
+ * nessuno `slot:`, la selezione ripiegava sulla prima riga e la casella perdeva il focus a
13743
+ * ogni battuta.
13744
+ */
13745
+ const moved = this.targets().find((candidate) => candidate.index === index);
13746
+ if (moved) {
13747
+ this.selectedKey.set(moved.key);
13748
+ }
13085
13749
  }
13086
- setMapValue(index, value) {
13750
+ setMapValue(row, value) {
13751
+ const index = this.ensureAction(row);
13087
13752
  this.mutateAction(index, (action) => {
13088
13753
  action.value = value;
13089
13754
  });
13090
13755
  }
13091
13756
  aggregationCollection(action) {
13092
- return action.inputParameters?.find((parameter) => parameter.name === AGGREGATION_VALUES)?.value
13757
+ return action?.inputParameters?.find((parameter) => parameter.name === AGGREGATION_VALUES)?.value
13093
13758
  ?.elementReference;
13094
13759
  }
13095
13760
  aggregationField(action) {
13096
- return (action.inputParameters?.find((parameter) => parameter.name === AGGREGATION_FIELD)?.value?.stringValue ?? '');
13761
+ return (action?.inputParameters?.find((parameter) => parameter.name === AGGREGATION_FIELD)?.value?.stringValue ?? '');
13097
13762
  }
13098
- setAggregationCollection(index, reference) {
13763
+ setAggregationCollection(row, reference) {
13764
+ const index = this.ensureAction(row);
13099
13765
  this.mutateAction(index, (action) => {
13100
13766
  action.inputParameters ??= [];
13101
13767
  const existing = action.inputParameters.find((parameter) => parameter.name === AGGREGATION_VALUES);
@@ -13110,7 +13776,8 @@ class TransformInspectorComponent extends NodeInspectorBase {
13110
13776
  }
13111
13777
  });
13112
13778
  }
13113
- setAggregationField(index, field) {
13779
+ setAggregationField(row, field) {
13780
+ const index = this.ensureAction(row);
13114
13781
  this.mutateAction(index, (action) => {
13115
13782
  action.inputParameters ??= [];
13116
13783
  const existing = action.inputParameters.find((parameter) => parameter.name === AGGREGATION_FIELD);
@@ -13125,29 +13792,136 @@ class TransformInspectorComponent extends NodeInspectorBase {
13125
13792
  }
13126
13793
  });
13127
13794
  }
13128
- isMap(action) {
13129
- return (action.transformType ?? 'Map') === 'Map';
13795
+ isMap(row) {
13796
+ return this.operationOf(row) === 'Map';
13130
13797
  }
13131
- needsAggregationField(action) {
13132
- return action.transformType === 'Sum';
13798
+ needsAggregationField(row) {
13799
+ return this.operationOf(row) === 'Sum';
13133
13800
  }
13134
- needsAggregationValues(action) {
13135
- return action.transformType === 'Sum' || action.transformType === 'Count';
13801
+ needsAggregationValues(row) {
13802
+ const type = this.operationOf(row);
13803
+ return type === 'Sum' || type === 'Count';
13804
+ }
13805
+ // ------------------------------------------------------------------ tipi dedotti
13806
+ /**
13807
+ * Il tipo dell'elemento della collection aggregata: e' cio' che rende `aggregationField` un
13808
+ * elenco di campi invece di una casella di testo. Il valore nel documento resta il **nome** del
13809
+ * campo (`stringValue`), come vuole la §5.13.
13810
+ */
13811
+ aggregationTypes = referenceTypes(this.catalog, (dataType) => this.dictionaries.isStructure(dataType), () => ({
13812
+ references: this.references(),
13813
+ values: this.actions().map((action) => this.aggregationCollection(action)),
13814
+ }));
13815
+ /** L'entita' degli elementi della collection aggregata, se e' una collection di record. */
13816
+ aggregationObject(row) {
13817
+ const path = this.aggregationCollection(row.action);
13818
+ const type = this.aggregationTypes.typeOf(path);
13819
+ return type?.dataType === 'Object' ? (type.objectType ?? undefined) : undefined;
13820
+ }
13821
+ /** La classe degli elementi della collection aggregata, se e' una collection di istanze (§4.7). */
13822
+ aggregationClass(row) {
13823
+ const path = this.aggregationCollection(row.action);
13824
+ const type = this.aggregationTypes.typeOf(path);
13825
+ return this.dictionaries.isStructure(type?.dataType) ? (type?.objectType ?? undefined) : undefined;
13826
+ }
13827
+ /**
13828
+ * Il tipo di cio' che una `Map` scrive: il campo o il membro quando il catalogo lo dichiara, il
13829
+ * risultato stesso altrove. Su un `Enum` e' la sola strada per proporre i valori del tipo invece
13830
+ * di farli digitare (§4.6).
13831
+ */
13832
+ mapDataType(row) {
13833
+ return row.dataType ?? undefined;
13834
+ }
13835
+ mapObjectType(row) {
13836
+ return row.objectType ?? undefined;
13837
+ }
13838
+ // ------------------------------------------------------------------ mappatura a gesti
13839
+ /** La riga sotto il puntatore durante il trascinamento: solo evidenziazione, niente geometria. */
13840
+ dropKey = signal(null, ...(ngDevMode ? [{ debugName: "dropKey" }] : []));
13841
+ draggingPath = signal(null, ...(ngDevMode ? [{ debugName: "draggingPath" }] : []));
13842
+ onSourceDragStarted(source) {
13843
+ this.draggingPath.set(source.path);
13844
+ }
13845
+ onSourceDragMoved(event) {
13846
+ this.dropKey.set(this.hitTest(event.pointerPosition.x, event.pointerPosition.y));
13847
+ }
13848
+ onSourceDragEnded(event, source) {
13849
+ // Un trascinamento non deve valere anche come click: la stessa sorgente porta i due gesti, e il
13850
+ // click — che mappa sulla riga **selezionata** — arriverebbe dopo il rilascio, scrivendo due
13851
+ // righe con un solo gesto. Il flag lo esclude fino al task successivo.
13852
+ this.dragged = true;
13853
+ setTimeout(() => (this.dragged = false));
13854
+ const key = this.dropKey();
13855
+ this.dropKey.set(null);
13856
+ this.draggingPath.set(null);
13857
+ // A spostare qualcosa e' il documento: senza `reset()` resterebbe la trasformazione del
13858
+ // trascinamento sulla riga della palette, che Angular riusa per un'altra sorgente.
13859
+ event.source.reset();
13860
+ const row = this.targets().find((candidate) => candidate.key === key);
13861
+ if (!row) {
13862
+ return;
13863
+ }
13864
+ // Il documento si muta **dopo** il gesto: mutarlo qui ridisegna le due colonne mentre il CDK
13865
+ // sta ancora chiudendo il trascinamento sull'elemento che sta rilasciando.
13866
+ setTimeout(() => this.applySource(row, source));
13867
+ }
13868
+ /**
13869
+ * Fuori dai signal di proposito: e' la coda di un gesto, non uno stato da cui ridisegnare.
13870
+ */
13871
+ dragged = false;
13872
+ /** Il gesto alternativo: riga selezionata, click sulla sorgente. Un click e' sempre riproducibile. */
13873
+ mapToSelected(source) {
13874
+ if (this.dragged) {
13875
+ return;
13876
+ }
13877
+ const row = this.selected();
13878
+ if (row) {
13879
+ this.applySource(row, source);
13880
+ }
13881
+ }
13882
+ /**
13883
+ * Dove finisce la sorgente rilasciata: nel valore di una `Map`, nella collection di un `Sum` o di
13884
+ * un `Count`. Non si cambia l'operazione da soli: chi ha scelto `Sum` non si aspetta che
13885
+ * trascinare una collection lo riporti a `Map`.
13886
+ */
13887
+ applySource(row, source) {
13888
+ this.selectedKey.set(row.key);
13889
+ if (this.needsAggregationValues(row)) {
13890
+ this.setAggregationCollection(row, source.path);
13891
+ return;
13892
+ }
13893
+ this.setMapValue(row, { elementReference: source.path });
13894
+ }
13895
+ /**
13896
+ * La riga bersaglio, dal **DOM**. Si scorre tutto lo stack e non il solo primo elemento perche'
13897
+ * sopra al puntatore c'e' l'anteprima del trascinamento, che e' cio' che `elementFromPoint`
13898
+ * trova per prima.
13899
+ */
13900
+ hitTest(x, y) {
13901
+ for (const element of this.document.elementsFromPoint(x, y)) {
13902
+ const row = element.closest?.('[data-fb-slot]');
13903
+ if (row) {
13904
+ return row.getAttribute('data-fb-slot');
13905
+ }
13906
+ }
13907
+ return null;
13136
13908
  }
13137
13909
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: TransformInspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13138
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: TransformInspectorComponent, isStandalone: true, selector: "fb-transform-inspector", usesInheritance: true, ngImport: i0, template: "<div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">Tipo del risultato</label>\r\n <select class=\"fb-select\" [fbValue]=\"transform().dataType || ''\" (change)=\"setDataType($any($event.target).value)\">\r\n <option value=\"\">\u2014 scegli \u2014</option>\r\n @for (type of dataTypes(); track type.value) {\r\n <option [value]=\"type.value\">{{ type.label }}</option>\r\n }\r\n </select>\r\n @if (!transform().dataType) {\r\n <p class=\"fb-field__error\">Obbligatorio (TRANSFORM_DATA_TYPE_MISSING).</p>\r\n }\r\n</div>\r\n\r\n@if (requiresObjectType()) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">\r\n {{ isStructureTarget() ? 'Classe del risultato' : 'Tipo dell\u2019oggetto' }}\r\n </label>\r\n @if (isStructureTarget()) {\r\n <!-- \u00A74.7: una classe del backend. Le destinazioni delle azioni sono i suoi membri. -->\r\n <fb-structure-picker\r\n [value]=\"transform().objectType\"\r\n label=\"Classe del risultato\"\r\n (valueChange)=\"setObjectType($event ?? '')\"\r\n />\r\n @if (missingObjectType()) {\r\n <p class=\"fb-field__error\">\r\n La classe e\u2019 obbligatoria: senza, l\u2019attivazione e\u2019 bloccata (OBJECT_TYPE_MISSING).\r\n </p>\r\n } @else {\r\n <p class=\"fb-field__hint\">\r\n Componi l\u2019istanza intera qui: ogni trasformazione scrive un <strong>membro</strong> della classe,\r\n invece di un Assignment per membro.\r\n </p>\r\n }\r\n } @else if (transform().dataType === 'Enum') {\r\n <!-- Le enumerazioni sono un dizionario chiuso: non c'e' scrittura libera da concedere. -->\r\n <select\r\n class=\"fb-select\"\r\n [fbValue]=\"transform().objectType || ''\"\r\n (change)=\"setObjectType($any($event.target).value)\"\r\n >\r\n <option value=\"\">\u2014 scegli \u2014</option>\r\n @for (entry of enumOptions(); track entry.name) {\r\n <option [value]=\"entry.name\">{{ entry.label || entry.name }}</option>\r\n }\r\n </select>\r\n } @else {\r\n <fb-object-picker\r\n [value]=\"transform().objectType\"\r\n label=\"Tipo dell\u2019oggetto\"\r\n placeholder=\"Scrivi o scegli un oggetto\"\r\n (valueChange)=\"setObjectType($event ?? '')\"\r\n />\r\n }\r\n </div>\r\n}\r\n\r\n<label class=\"fb-check\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"transform().isCollection === true\"\r\n (change)=\"setIsCollection($any($event.target).checked)\"\r\n />\r\n Il risultato e\u2019 una collection\r\n</label>\r\n\r\n<p class=\"fb-callout\">\r\n Il risultato e\u2019 l\u2019<strong>output automatico</strong> dell\u2019elemento: si referenzia con\r\n <code>{{ name() }}</code>.\r\n</p>\r\n\r\n<fieldset class=\"fb-section\">\r\n <legend class=\"fb-section__title\">Trasformazioni</legend>\r\n\r\n <div class=\"fb-list\">\r\n @for (action of actions(); track $index) {\r\n <div class=\"fb-list__item\">\r\n <div class=\"fb-list__header\">\r\n <span class=\"fb-list__index\">{{ $index + 1 }}</span>\r\n <span class=\"fb-list__spacer\"></span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon\"\r\n aria-label=\"Rimuovi\"\r\n (click)=\"removeAction($index)\"\r\n >\r\n \u00D7\r\n </button>\r\n </div>\r\n\r\n <!-- Operazione, destinazione e sorgente sono una frase sola: si leggono in riga. -->\r\n <div class=\"fb-fields-row\">\r\n <div class=\"fb-field fb-field--compact\">\r\n <label class=\"fb-field__label\">Operazione</label>\r\n <select\r\n class=\"fb-select\"\r\n [fbValue]=\"action.transformType || 'Map'\"\r\n (change)=\"setActionType($index, $any($event.target).value)\"\r\n >\r\n @for (type of transformTypes(); track type.value) {\r\n <option [value]=\"type.value\">{{ type.label }}</option>\r\n }\r\n </select>\r\n </div>\r\n\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">\r\n {{ isStructureTarget() ? 'Membro di destinazione' : 'Campo di destinazione' }}\r\n </label>\r\n @if (isStructureTarget()) {\r\n <!-- Solo i membri scrivibili: un membro calcolato e' TARGET_NOT_WRITABLE (\u00A74.7). -->\r\n <fb-structure-member-picker\r\n [value]=\"action.outputFieldApiName\"\r\n [className]=\"transform().objectType\"\r\n usage=\"writable\"\r\n label=\"Membro di destinazione\"\r\n (valueChange)=\"setOutputField($index, $event ?? '')\"\r\n />\r\n } @else {\r\n <input\r\n class=\"fb-input\"\r\n [value]=\"action.outputFieldApiName || ''\"\r\n placeholder=\"Totale\"\r\n (input)=\"setOutputField($index, $any($event.target).value)\"\r\n />\r\n }\r\n </div>\r\n\r\n @if (isMap(action)) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label\">Valore</label>\r\n <!--\r\n Il tipo di cio' che si scrive guida il controllo: su un membro Enum i valori\r\n proponibili sono quelli del suo tipo (\u00A74.6).\r\n -->\r\n <fb-value-editor\r\n [value]=\"action.value\"\r\n label=\"Valore\"\r\n [dataType]=\"$any(mapDataType(action))\"\r\n [objectType]=\"mapObjectType(action)\"\r\n (valueChange)=\"setMapValue($index, $event)\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (needsAggregationValues(action)) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">Collection su cui aggregare</label>\r\n <fb-reference-picker\r\n [value]=\"aggregationCollection(action)\"\r\n [isCollection]=\"true\"\r\n placeholder=\"Scegli una collection\"\r\n (valueChange)=\"setAggregationCollection($index, $event)\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (needsAggregationField(action)) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">Campo da sommare</label>\r\n <input\r\n class=\"fb-input\"\r\n [value]=\"aggregationField(action)\"\r\n placeholder=\"Importo\"\r\n (input)=\"setAggregationField($index, $any($event.target).value)\"\r\n />\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n } @empty {\r\n <p class=\"fb-empty\">Nessuna trasformazione: l\u2019elemento non produce nulla (TRANSFORM_WITHOUT_VALUES).</p>\r\n }\r\n </div>\r\n\r\n <button type=\"button\" class=\"fb-btn\" (click)=\"addAction()\">Aggiungi trasformazione</button>\r\n</fieldset>\r\n\r\n<fb-connector-editor\r\n [nodeName]=\"name()\"\r\n [node]=\"node()\"\r\n [outlets]=\"outlets()\"\r\n (connectorChanged)=\"onConnectorChanged($event)\"\r\n/>\r\n", dependencies: [{ kind: "component", type: ConnectorEditorComponent, selector: "fb-connector-editor", inputs: ["nodeName", "node", "outlets", "title"], outputs: ["connectorChanged"] }, { kind: "component", type: ObjectPickerComponent, selector: "fb-object-picker", inputs: ["value", "label", "placeholder", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: ReferencePickerComponent, selector: "fb-reference-picker", inputs: ["value", "label", "placeholder", "disabled", "dataType", "isCollection", "objectType", "writableOnly", "elementsOnly", "extraReferences"], outputs: ["valueChange"] }, { kind: "component", type: StructureMemberPickerComponent, selector: "fb-structure-member-picker", inputs: ["value", "className", "usage", "label", "placeholder", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: StructurePickerComponent, selector: "fb-structure-picker", inputs: ["value", "label", "placeholder", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: ValueEditorComponent, selector: "fb-value-editor", inputs: ["value", "label", "dataType", "objectType", "isCollection", "valueSet", "disabled", "allowFormula"], outputs: ["valueChange"] }, { kind: "directive", type: SelectValueDirective, selector: "select[fbValue]", inputs: ["fbValue"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13910
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: TransformInspectorComponent, isStandalone: true, selector: "fb-transform-inspector", usesInheritance: true, ngImport: i0, template: "<div class=\"fb-tr\">\r\n <!-- ============================================================ sorgenti -->\r\n <aside class=\"fb-tr__panel\" aria-label=\"Sorgenti\">\r\n <h3 class=\"fb-tr__panel-title\">Sorgenti</h3>\r\n\r\n <input\r\n class=\"fb-input fb-tr__search\"\r\n type=\"search\"\r\n placeholder=\"Cerca una sorgente\"\r\n aria-label=\"Cerca una sorgente\"\r\n [value]=\"sourceQuery()\"\r\n (input)=\"sourceQuery.set($any($event.target).value)\"\r\n />\r\n\r\n @if (sourceCrumbs().length) {\r\n <nav class=\"fb-tr__crumbs\" aria-label=\"Percorso della sorgente\">\r\n <button type=\"button\" class=\"fb-tr__crumb\" (click)=\"enterSource('')\">Tutte</button>\r\n @for (crumb of sourceCrumbs(); track crumb.path) {\r\n <span class=\"fb-tr__crumb-sep\" aria-hidden=\"true\">\u203A</span>\r\n <button type=\"button\" class=\"fb-tr__crumb\" (click)=\"enterSource(crumb.path)\">{{ crumb.label }}</button>\r\n }\r\n </nav>\r\n }\r\n\r\n <p class=\"fb-tr__hint\">\r\n Trascina su una destinazione, oppure clicca per mapparla su quella selezionata.\r\n </p>\r\n\r\n @if (sourceError()) {\r\n <p class=\"fb-callout fb-callout--warn\">\r\n {{ sourceError() }} Le sorgenti si scrivono a mano nel dettaglio: l\u2019elenco non c\u2019e\u2019, i\r\n riferimenti s\u00EC.\r\n </p>\r\n }\r\n\r\n <ul class=\"fb-tr__list\">\r\n @for (source of sources(); track source.path) {\r\n <li class=\"fb-tr__source-row\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-tr__source\"\r\n cdkDrag\r\n [class.fb-tr__source--dragging]=\"draggingPath() === source.path\"\r\n [title]=\"source.path + (source.detail ? ' \u00B7 ' + source.detail : '')\"\r\n (cdkDragStarted)=\"onSourceDragStarted(source)\"\r\n (cdkDragMoved)=\"onSourceDragMoved($event)\"\r\n (cdkDragEnded)=\"onSourceDragEnded($event, source)\"\r\n (click)=\"mapToSelected(source)\"\r\n >\r\n <span class=\"fb-tr__source-name\">\r\n {{ source.label }}\r\n @if (source.isCollection) {\r\n <span class=\"fb-tr__badge\" aria-label=\"collection\">[ ]</span>\r\n }\r\n </span>\r\n @if (source.detail) {\r\n <span class=\"fb-tr__source-detail\">{{ source.detail }}</span>\r\n }\r\n </button>\r\n @if (source.canEnter) {\r\n <!-- Entrare e mappare sono due gesti diversi: un record intero e' una sorgente valida. -->\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon fb-tr__enter\"\r\n [attr.aria-label]=\"'Entra in ' + source.label\"\r\n (click)=\"enterSource(source.path)\"\r\n >\r\n \u203A\r\n </button>\r\n }\r\n </li>\r\n } @empty {\r\n @if (sourcePending()) {\r\n <li class=\"fb-tr__empty\">Caricamento\u2026</li>\r\n } @else if (sourceUndeclared()) {\r\n <li class=\"fb-tr__empty\">\r\n Il catalogo non dichiara cosa c\u2019e\u2019 qui dentro: non significa che non ci sia niente. Il\r\n percorso si scrive a mano nel dettaglio.\r\n </li>\r\n } @else {\r\n <li class=\"fb-tr__empty\">Nessuna sorgente con questo nome.</li>\r\n }\r\n }\r\n </ul>\r\n </aside>\r\n\r\n <!-- ========================================================= destinazione -->\r\n <section class=\"fb-tr__panel fb-tr__map\" aria-label=\"Destinazione\">\r\n <h3 class=\"fb-tr__panel-title\">\r\n Destinazione\r\n @if (slotCount()) {\r\n <span class=\"fb-tr__count\">{{ mappedCount() }} di {{ slotCount() }} mappate</span>\r\n } @else if (mappedCount()) {\r\n <span class=\"fb-tr__count\">{{ mappedCount() }} mappate</span>\r\n }\r\n </h3>\r\n\r\n <!-- Il tipo del risultato e' la forma della colonna: sta in cima, non in fondo. -->\r\n <div class=\"fb-tr__result\">\r\n <div class=\"fb-fields-row\">\r\n <div class=\"fb-field fb-field--compact\">\r\n <label class=\"fb-field__label fb-field__label--required\">Tipo del risultato</label>\r\n <select\r\n class=\"fb-select\"\r\n [fbValue]=\"transform().dataType || ''\"\r\n (change)=\"setDataType($any($event.target).value)\"\r\n >\r\n <option value=\"\">\u2014 scegli \u2014</option>\r\n @for (type of dataTypes(); track type.value) {\r\n <option [value]=\"type.value\">{{ type.label }}</option>\r\n }\r\n </select>\r\n </div>\r\n\r\n @if (requiresObjectType()) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">\r\n {{ isStructureTarget() ? 'Classe del risultato' : 'Tipo dell\u2019oggetto' }}\r\n </label>\r\n @if (isStructureTarget()) {\r\n <!-- \u00A74.7: una classe del backend. Le destinazioni delle azioni sono i suoi membri. -->\r\n <fb-structure-picker\r\n [value]=\"transform().objectType\"\r\n label=\"Classe del risultato\"\r\n (valueChange)=\"setObjectType($event ?? '')\"\r\n />\r\n } @else if (transform().dataType === 'Enum') {\r\n <!-- Le enumerazioni sono un dizionario chiuso: non c'e' scrittura libera da concedere. -->\r\n <select\r\n class=\"fb-select\"\r\n [fbValue]=\"transform().objectType || ''\"\r\n (change)=\"setObjectType($any($event.target).value)\"\r\n >\r\n <option value=\"\">\u2014 scegli \u2014</option>\r\n @for (entry of enumOptions(); track entry.name) {\r\n <option [value]=\"entry.name\">{{ entry.label || entry.name }}</option>\r\n }\r\n </select>\r\n } @else {\r\n <fb-object-picker\r\n [value]=\"transform().objectType\"\r\n label=\"Tipo dell\u2019oggetto\"\r\n placeholder=\"Scrivi o scegli un oggetto\"\r\n (valueChange)=\"setObjectType($event ?? '')\"\r\n />\r\n }\r\n </div>\r\n }\r\n\r\n <label class=\"fb-check fb-tr__collection\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"transform().isCollection === true\"\r\n (change)=\"setIsCollection($any($event.target).checked)\"\r\n />\r\n Collection\r\n </label>\r\n </div>\r\n\r\n @if (!transform().dataType) {\r\n <p class=\"fb-field__error\">Obbligatorio (TRANSFORM_DATA_TYPE_MISSING).</p>\r\n }\r\n @if (missingObjectType()) {\r\n <p class=\"fb-field__error\">\r\n La classe e\u2019 obbligatoria: senza, l\u2019attivazione e\u2019 bloccata (OBJECT_TYPE_MISSING).\r\n </p>\r\n }\r\n @if (targetUndeclared()) {\r\n <p class=\"fb-callout fb-callout--warn\">\r\n Il catalogo non dichiara i {{ isStructureTarget() ? 'membri' : 'campi' }} di\r\n <code>{{ transform().objectType }}</code>: le destinazioni si scrivono a mano, e nessuna di\r\n esse viene accusata di non esistere.\r\n </p>\r\n }\r\n </div>\r\n\r\n <ul class=\"fb-tr__list fb-tr__rows\">\r\n @for (row of targets(); track row.key) {\r\n <!--\r\n `data-fb-slot` e' cio' che il rilascio cerca: il bersaglio lo calcola `hitTest` dal DOM,\r\n non una drop list del CDK.\r\n -->\r\n <li\r\n class=\"fb-tr__row\"\r\n [attr.data-fb-slot]=\"row.key\"\r\n [class.fb-tr__row--selected]=\"isSelected(row)\"\r\n [class.fb-tr__row--drop]=\"dropKey() === row.key\"\r\n [class.fb-tr__row--empty]=\"row.index < 0\"\r\n >\r\n <button type=\"button\" class=\"fb-tr__row-main\" (click)=\"select(row.key)\">\r\n <span class=\"fb-tr__op\" [attr.title]=\"operationLabel(operationOf(row))\" aria-hidden=\"true\">\r\n {{ operationIcon(row) }}\r\n </span>\r\n <span class=\"fb-tr__row-text\">\r\n <span class=\"fb-tr__row-name\">\r\n {{ row.label }}\r\n @if (row.isCollection) {\r\n <span class=\"fb-tr__badge\" aria-label=\"collection\">[ ]</span>\r\n }\r\n @if (row.isUnknown) {\r\n <span class=\"fb-tr__badge fb-tr__badge--warn\">fuori catalogo</span>\r\n }\r\n </span>\r\n @if (row.index < 0) {\r\n <span class=\"fb-tr__row-src fb-tr__row-src--empty\">non mappata</span>\r\n } @else if (sourceSummary(row)) {\r\n <span class=\"fb-tr__row-src\">\u2190 {{ sourceSummary(row) }}</span>\r\n } @else {\r\n <span class=\"fb-tr__row-src fb-tr__row-src--empty\">sorgente da indicare</span>\r\n }\r\n </span>\r\n @if (row.dataType) {\r\n <span class=\"fb-tr__row-type\">{{ row.dataType }}</span>\r\n }\r\n @if (issuesOf(row).length) {\r\n <span class=\"fb-tr__row-issue\" [attr.title]=\"issuesOf(row)[0].message\">!</span>\r\n }\r\n </button>\r\n @if (row.index >= 0) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon\"\r\n [attr.aria-label]=\"'Togli la mappatura di ' + row.label\"\r\n (click)=\"removeAction(row)\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </li>\r\n } @empty {\r\n <li class=\"fb-empty\">\r\n Nessuna trasformazione: l\u2019elemento non produce nulla (TRANSFORM_WITHOUT_VALUES).\r\n </li>\r\n }\r\n </ul>\r\n\r\n <button type=\"button\" class=\"fb-btn\" (click)=\"addAction()\">\r\n {{ hasTargetCatalog() ? 'Aggiungi una destinazione fuori catalogo' : 'Aggiungi trasformazione' }}\r\n </button>\r\n\r\n <p class=\"fb-callout\">\r\n Il risultato e\u2019 l\u2019<strong>output automatico</strong> dell\u2019elemento: si referenzia con\r\n <code>{{ name() }}</code>.\r\n </p>\r\n </section>\r\n\r\n <!-- ============================================================ dettaglio -->\r\n <aside class=\"fb-tr__panel fb-tr__detail\" aria-label=\"Dettaglio della mappatura\">\r\n @if (selected(); as row) {\r\n <h3 class=\"fb-tr__panel-title\">{{ row.label }}</h3>\r\n\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label\">Operazione</label>\r\n <div class=\"fb-tr__modes\" role=\"group\" aria-label=\"Operazione\">\r\n @for (type of transformTypes(); track type.value) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-tr__mode\"\r\n [class.fb-tr__mode--active]=\"operationOf(row) === type.value\"\r\n (click)=\"setOperation(row, type.value)\"\r\n >\r\n {{ type.label }}\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">\r\n {{ isStructureTarget() ? 'Membro di destinazione' : 'Campo di destinazione' }}\r\n </label>\r\n @if (isStructureTarget()) {\r\n <!-- Solo i membri scrivibili: un membro calcolato e' TARGET_NOT_WRITABLE (\u00A74.7). -->\r\n <fb-structure-member-picker\r\n [value]=\"row.field\"\r\n [className]=\"transform().objectType\"\r\n usage=\"writable\"\r\n label=\"Membro di destinazione\"\r\n (valueChange)=\"setOutputField(row, $event ?? '')\"\r\n />\r\n } @else if (targetObject()) {\r\n <!-- Il risultato e' un record: le destinazioni sono i campi della sua entita' (\u00A74.4). -->\r\n <fb-field-picker\r\n [value]=\"row.field\"\r\n [object]=\"targetObject()\"\r\n label=\"Campo di destinazione\"\r\n (valueChange)=\"setOutputField(row, $event ?? '')\"\r\n />\r\n } @else {\r\n <input\r\n class=\"fb-input\"\r\n [value]=\"row.field\"\r\n placeholder=\"Totale\"\r\n (input)=\"setOutputField(row, $any($event.target).value)\"\r\n />\r\n }\r\n </div>\r\n\r\n @if (isMap(row)) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label\">Valore</label>\r\n <!--\r\n Il tipo di cio' che si scrive guida il controllo: su un campo o un membro Enum i valori\r\n proponibili sono quelli del suo tipo (\u00A74.6).\r\n -->\r\n <fb-value-editor\r\n [value]=\"row.action?.value ?? undefined\"\r\n label=\"Valore\"\r\n [dataType]=\"mapDataType(row)\"\r\n [objectType]=\"mapObjectType(row)\"\r\n (valueChange)=\"setMapValue(row, $event)\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (needsAggregationValues(row)) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">Collection su cui aggregare</label>\r\n <fb-reference-picker\r\n [value]=\"aggregationCollection(row.action)\"\r\n [isCollection]=\"true\"\r\n placeholder=\"Scegli una collection\"\r\n (valueChange)=\"setAggregationCollection(row, $event)\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (needsAggregationField(row)) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">Campo da sommare</label>\r\n <!--\r\n Nel documento va il **nome** del campo (`stringValue`, \u00A75.13); dove il tipo della\r\n collection e' noto, il nome lo si sceglie da un elenco invece di ricordarlo.\r\n -->\r\n @if (aggregationObject(row); as object) {\r\n <fb-field-picker\r\n [value]=\"aggregationField(row.action)\"\r\n [object]=\"object\"\r\n label=\"Campo da sommare\"\r\n (valueChange)=\"setAggregationField(row, $event ?? '')\"\r\n />\r\n } @else if (aggregationClass(row); as className) {\r\n <fb-structure-member-picker\r\n [value]=\"aggregationField(row.action)\"\r\n [className]=\"className\"\r\n label=\"Membro da sommare\"\r\n (valueChange)=\"setAggregationField(row, $event ?? '')\"\r\n />\r\n } @else {\r\n <input\r\n class=\"fb-input\"\r\n [value]=\"aggregationField(row.action)\"\r\n placeholder=\"Importo\"\r\n (input)=\"setAggregationField(row, $any($event.target).value)\"\r\n />\r\n }\r\n </div>\r\n }\r\n\r\n @for (issue of issuesOf(row); track $index) {\r\n <p class=\"fb-callout fb-callout--error\">{{ issue.message }} ({{ issue.code }})</p>\r\n }\r\n } @else {\r\n <p class=\"fb-tr__hint\">\r\n Scegli il tipo del risultato: le destinazioni compaiono da l\u00EC, e la mappatura si fa\r\n trascinando una sorgente su una di esse.\r\n </p>\r\n }\r\n </aside>\r\n</div>\r\n\r\n<fb-connector-editor\r\n [nodeName]=\"name()\"\r\n [node]=\"node()\"\r\n [outlets]=\"outlets()\"\r\n (connectorChanged)=\"onConnectorChanged($event)\"\r\n/>\r\n", styles: [":host{display:block;container-type:inline-size}.fb-tr{display:grid;grid-template-columns:240px minmax(0,1fr) 320px;gap:12px;align-items:start;margin-bottom:14px}@container (max-width: 1040px){.fb-tr{grid-template-columns:minmax(0,1fr)}}.fb-tr__panel{min-width:0;padding:10px;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius, 10px);background:var(--fb-surface-alt, #f7f8fa)}.fb-tr__map{background:var(--fb-surface, #fff)}.fb-tr__panel-title{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin:0 0 8px;font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--fb-text-subtle, #98a2b3);overflow-wrap:anywhere}.fb-tr__count{font-size:10px;font-weight:600;letter-spacing:0;text-transform:none;color:var(--fb-text-muted, #6b7086)}.fb-tr__hint{margin:6px 0;font-size:11px;line-height:1.45;color:var(--fb-text-muted, #6b7086)}.fb-tr__search{margin-bottom:6px}.fb-tr__crumbs{display:flex;flex-wrap:wrap;align-items:center;gap:2px;margin-bottom:6px}.fb-tr__crumb{padding:2px 4px;border:0;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-accent, #4f6ef7);font:inherit;font-size:11px;cursor:pointer}.fb-tr__crumb:hover{background:color-mix(in srgb,var(--fb-accent, #4f6ef7) 10%,transparent)}.fb-tr__crumb-sep{color:var(--fb-text-subtle, #98a2b3);font-size:11px}.fb-tr__list{display:flex;flex-direction:column;gap:4px;margin:0;padding:0;max-height:340px;overflow-y:auto;overscroll-behavior:contain;list-style:none}.fb-tr__empty{padding:8px;border:1px dashed var(--fb-border, #d6dae1);border-radius:var(--fb-radius-xs, 6px);font-size:11px;line-height:1.45;color:var(--fb-text-muted, #667085)}.fb-tr__source-row{display:flex;align-items:stretch;gap:2px}.fb-tr__source{display:flex;flex:1;flex-direction:column;gap:1px;min-width:0;padding:5px 8px;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius-xs, 6px);background:var(--fb-surface, #fff);color:var(--fb-text, #1a1c23);font:inherit;font-size:12px;text-align:left;cursor:grab}.fb-tr__source:hover{border-color:var(--fb-accent, #4f6ef7)}.fb-tr__source--dragging{opacity:.6}.fb-tr__source-name,.fb-tr__source-detail{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-tr__source-detail{font-size:10px;color:var(--fb-text-subtle, #98a2b3)}.fb-tr__enter{flex:none}.fb-tr__badge{margin-left:4px;padding:0 4px;border-radius:4px;background:var(--fb-surface-sunken, #eef0f4);font-size:9px;font-weight:700;color:var(--fb-text-muted, #6b7086)}.fb-tr__badge--warn{background:color-mix(in srgb,var(--fb-warning, #b7791f) 16%,transparent);color:var(--fb-warning, #b7791f)}.fb-tr__result{margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--fb-border-subtle, #eef0f4)}.fb-tr__collection{align-self:end;padding-bottom:6px;white-space:nowrap}.fb-tr__rows{max-height:420px;margin-bottom:8px}.fb-tr__row{display:flex;align-items:stretch;gap:2px;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius-xs, 6px);background:var(--fb-surface, #fff)}.fb-tr__row--empty{border-style:dashed;background:var(--fb-surface-alt, #f7f8fa)}.fb-tr__row--selected{border-color:var(--fb-accent, #4f6ef7);box-shadow:0 0 0 1px var(--fb-accent, #4f6ef7) inset}.fb-tr__row--drop{border-color:var(--fb-accent, #4f6ef7);border-style:solid;background:color-mix(in srgb,var(--fb-accent, #4f6ef7) 12%,transparent)}.fb-tr__row-main{display:flex;flex:1;align-items:center;gap:8px;min-width:0;padding:6px 8px;border:0;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text, #1a1c23);font:inherit;text-align:left;cursor:pointer}.fb-tr__op{display:grid;flex:none;place-items:center;width:20px;height:20px;border-radius:50%;background:var(--fb-surface-sunken, #eef0f4);font-size:11px;font-weight:700;color:var(--fb-text-muted, #6b7086)}.fb-tr__row-text{display:flex;flex:1;flex-direction:column;gap:1px;min-width:0}.fb-tr__row-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:600}.fb-tr__row-src{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px;color:var(--fb-text-muted, #6b7086)}.fb-tr__row-src--empty{color:var(--fb-text-subtle, #98a2b3);font-style:italic}.fb-tr__row-type{flex:none;font-size:10px;color:var(--fb-text-subtle, #98a2b3)}.fb-tr__row-issue{display:grid;flex:none;place-items:center;width:16px;height:16px;border-radius:50%;background:color-mix(in srgb,var(--fb-error, #dc2626) 14%,transparent);font-size:10px;font-weight:700;color:var(--fb-error, #dc2626)}.fb-tr__modes{margin-top:2px}\n"], dependencies: [{ kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "component", type: ConnectorEditorComponent, selector: "fb-connector-editor", inputs: ["nodeName", "node", "outlets", "title"], outputs: ["connectorChanged"] }, { kind: "component", type: FieldPickerComponent, selector: "fb-field-picker", inputs: ["value", "object", "usage", "label", "placeholder", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: ObjectPickerComponent, selector: "fb-object-picker", inputs: ["value", "label", "placeholder", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: ReferencePickerComponent, selector: "fb-reference-picker", inputs: ["value", "label", "placeholder", "disabled", "dataType", "isCollection", "objectType", "writableOnly", "elementsOnly", "extraReferences"], outputs: ["valueChange"] }, { kind: "component", type: StructureMemberPickerComponent, selector: "fb-structure-member-picker", inputs: ["value", "className", "usage", "label", "placeholder", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: StructurePickerComponent, selector: "fb-structure-picker", inputs: ["value", "label", "placeholder", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: ValueEditorComponent, selector: "fb-value-editor", inputs: ["value", "label", "dataType", "objectType", "isCollection", "valueSet", "disabled", "allowFormula"], outputs: ["valueChange"] }, { kind: "directive", type: SelectValueDirective, selector: "select[fbValue]", inputs: ["fbValue"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13139
13911
  }
13140
13912
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: TransformInspectorComponent, decorators: [{
13141
13913
  type: Component,
13142
13914
  args: [{ selector: 'fb-transform-inspector', standalone: true, imports: [
13915
+ CdkDrag,
13143
13916
  ConnectorEditorComponent,
13917
+ FieldPickerComponent,
13144
13918
  ObjectPickerComponent,
13145
13919
  ReferencePickerComponent,
13146
13920
  StructureMemberPickerComponent,
13147
13921
  StructurePickerComponent,
13148
13922
  ValueEditorComponent,
13149
13923
  SelectValueDirective,
13150
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">Tipo del risultato</label>\r\n <select class=\"fb-select\" [fbValue]=\"transform().dataType || ''\" (change)=\"setDataType($any($event.target).value)\">\r\n <option value=\"\">\u2014 scegli \u2014</option>\r\n @for (type of dataTypes(); track type.value) {\r\n <option [value]=\"type.value\">{{ type.label }}</option>\r\n }\r\n </select>\r\n @if (!transform().dataType) {\r\n <p class=\"fb-field__error\">Obbligatorio (TRANSFORM_DATA_TYPE_MISSING).</p>\r\n }\r\n</div>\r\n\r\n@if (requiresObjectType()) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">\r\n {{ isStructureTarget() ? 'Classe del risultato' : 'Tipo dell\u2019oggetto' }}\r\n </label>\r\n @if (isStructureTarget()) {\r\n <!-- \u00A74.7: una classe del backend. Le destinazioni delle azioni sono i suoi membri. -->\r\n <fb-structure-picker\r\n [value]=\"transform().objectType\"\r\n label=\"Classe del risultato\"\r\n (valueChange)=\"setObjectType($event ?? '')\"\r\n />\r\n @if (missingObjectType()) {\r\n <p class=\"fb-field__error\">\r\n La classe e\u2019 obbligatoria: senza, l\u2019attivazione e\u2019 bloccata (OBJECT_TYPE_MISSING).\r\n </p>\r\n } @else {\r\n <p class=\"fb-field__hint\">\r\n Componi l\u2019istanza intera qui: ogni trasformazione scrive un <strong>membro</strong> della classe,\r\n invece di un Assignment per membro.\r\n </p>\r\n }\r\n } @else if (transform().dataType === 'Enum') {\r\n <!-- Le enumerazioni sono un dizionario chiuso: non c'e' scrittura libera da concedere. -->\r\n <select\r\n class=\"fb-select\"\r\n [fbValue]=\"transform().objectType || ''\"\r\n (change)=\"setObjectType($any($event.target).value)\"\r\n >\r\n <option value=\"\">\u2014 scegli \u2014</option>\r\n @for (entry of enumOptions(); track entry.name) {\r\n <option [value]=\"entry.name\">{{ entry.label || entry.name }}</option>\r\n }\r\n </select>\r\n } @else {\r\n <fb-object-picker\r\n [value]=\"transform().objectType\"\r\n label=\"Tipo dell\u2019oggetto\"\r\n placeholder=\"Scrivi o scegli un oggetto\"\r\n (valueChange)=\"setObjectType($event ?? '')\"\r\n />\r\n }\r\n </div>\r\n}\r\n\r\n<label class=\"fb-check\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"transform().isCollection === true\"\r\n (change)=\"setIsCollection($any($event.target).checked)\"\r\n />\r\n Il risultato e\u2019 una collection\r\n</label>\r\n\r\n<p class=\"fb-callout\">\r\n Il risultato e\u2019 l\u2019<strong>output automatico</strong> dell\u2019elemento: si referenzia con\r\n <code>{{ name() }}</code>.\r\n</p>\r\n\r\n<fieldset class=\"fb-section\">\r\n <legend class=\"fb-section__title\">Trasformazioni</legend>\r\n\r\n <div class=\"fb-list\">\r\n @for (action of actions(); track $index) {\r\n <div class=\"fb-list__item\">\r\n <div class=\"fb-list__header\">\r\n <span class=\"fb-list__index\">{{ $index + 1 }}</span>\r\n <span class=\"fb-list__spacer\"></span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon\"\r\n aria-label=\"Rimuovi\"\r\n (click)=\"removeAction($index)\"\r\n >\r\n \u00D7\r\n </button>\r\n </div>\r\n\r\n <!-- Operazione, destinazione e sorgente sono una frase sola: si leggono in riga. -->\r\n <div class=\"fb-fields-row\">\r\n <div class=\"fb-field fb-field--compact\">\r\n <label class=\"fb-field__label\">Operazione</label>\r\n <select\r\n class=\"fb-select\"\r\n [fbValue]=\"action.transformType || 'Map'\"\r\n (change)=\"setActionType($index, $any($event.target).value)\"\r\n >\r\n @for (type of transformTypes(); track type.value) {\r\n <option [value]=\"type.value\">{{ type.label }}</option>\r\n }\r\n </select>\r\n </div>\r\n\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">\r\n {{ isStructureTarget() ? 'Membro di destinazione' : 'Campo di destinazione' }}\r\n </label>\r\n @if (isStructureTarget()) {\r\n <!-- Solo i membri scrivibili: un membro calcolato e' TARGET_NOT_WRITABLE (\u00A74.7). -->\r\n <fb-structure-member-picker\r\n [value]=\"action.outputFieldApiName\"\r\n [className]=\"transform().objectType\"\r\n usage=\"writable\"\r\n label=\"Membro di destinazione\"\r\n (valueChange)=\"setOutputField($index, $event ?? '')\"\r\n />\r\n } @else {\r\n <input\r\n class=\"fb-input\"\r\n [value]=\"action.outputFieldApiName || ''\"\r\n placeholder=\"Totale\"\r\n (input)=\"setOutputField($index, $any($event.target).value)\"\r\n />\r\n }\r\n </div>\r\n\r\n @if (isMap(action)) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label\">Valore</label>\r\n <!--\r\n Il tipo di cio' che si scrive guida il controllo: su un membro Enum i valori\r\n proponibili sono quelli del suo tipo (\u00A74.6).\r\n -->\r\n <fb-value-editor\r\n [value]=\"action.value\"\r\n label=\"Valore\"\r\n [dataType]=\"$any(mapDataType(action))\"\r\n [objectType]=\"mapObjectType(action)\"\r\n (valueChange)=\"setMapValue($index, $event)\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (needsAggregationValues(action)) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">Collection su cui aggregare</label>\r\n <fb-reference-picker\r\n [value]=\"aggregationCollection(action)\"\r\n [isCollection]=\"true\"\r\n placeholder=\"Scegli una collection\"\r\n (valueChange)=\"setAggregationCollection($index, $event)\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (needsAggregationField(action)) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">Campo da sommare</label>\r\n <input\r\n class=\"fb-input\"\r\n [value]=\"aggregationField(action)\"\r\n placeholder=\"Importo\"\r\n (input)=\"setAggregationField($index, $any($event.target).value)\"\r\n />\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n } @empty {\r\n <p class=\"fb-empty\">Nessuna trasformazione: l\u2019elemento non produce nulla (TRANSFORM_WITHOUT_VALUES).</p>\r\n }\r\n </div>\r\n\r\n <button type=\"button\" class=\"fb-btn\" (click)=\"addAction()\">Aggiungi trasformazione</button>\r\n</fieldset>\r\n\r\n<fb-connector-editor\r\n [nodeName]=\"name()\"\r\n [node]=\"node()\"\r\n [outlets]=\"outlets()\"\r\n (connectorChanged)=\"onConnectorChanged($event)\"\r\n/>\r\n" }]
13924
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"fb-tr\">\r\n <!-- ============================================================ sorgenti -->\r\n <aside class=\"fb-tr__panel\" aria-label=\"Sorgenti\">\r\n <h3 class=\"fb-tr__panel-title\">Sorgenti</h3>\r\n\r\n <input\r\n class=\"fb-input fb-tr__search\"\r\n type=\"search\"\r\n placeholder=\"Cerca una sorgente\"\r\n aria-label=\"Cerca una sorgente\"\r\n [value]=\"sourceQuery()\"\r\n (input)=\"sourceQuery.set($any($event.target).value)\"\r\n />\r\n\r\n @if (sourceCrumbs().length) {\r\n <nav class=\"fb-tr__crumbs\" aria-label=\"Percorso della sorgente\">\r\n <button type=\"button\" class=\"fb-tr__crumb\" (click)=\"enterSource('')\">Tutte</button>\r\n @for (crumb of sourceCrumbs(); track crumb.path) {\r\n <span class=\"fb-tr__crumb-sep\" aria-hidden=\"true\">\u203A</span>\r\n <button type=\"button\" class=\"fb-tr__crumb\" (click)=\"enterSource(crumb.path)\">{{ crumb.label }}</button>\r\n }\r\n </nav>\r\n }\r\n\r\n <p class=\"fb-tr__hint\">\r\n Trascina su una destinazione, oppure clicca per mapparla su quella selezionata.\r\n </p>\r\n\r\n @if (sourceError()) {\r\n <p class=\"fb-callout fb-callout--warn\">\r\n {{ sourceError() }} Le sorgenti si scrivono a mano nel dettaglio: l\u2019elenco non c\u2019e\u2019, i\r\n riferimenti s\u00EC.\r\n </p>\r\n }\r\n\r\n <ul class=\"fb-tr__list\">\r\n @for (source of sources(); track source.path) {\r\n <li class=\"fb-tr__source-row\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-tr__source\"\r\n cdkDrag\r\n [class.fb-tr__source--dragging]=\"draggingPath() === source.path\"\r\n [title]=\"source.path + (source.detail ? ' \u00B7 ' + source.detail : '')\"\r\n (cdkDragStarted)=\"onSourceDragStarted(source)\"\r\n (cdkDragMoved)=\"onSourceDragMoved($event)\"\r\n (cdkDragEnded)=\"onSourceDragEnded($event, source)\"\r\n (click)=\"mapToSelected(source)\"\r\n >\r\n <span class=\"fb-tr__source-name\">\r\n {{ source.label }}\r\n @if (source.isCollection) {\r\n <span class=\"fb-tr__badge\" aria-label=\"collection\">[ ]</span>\r\n }\r\n </span>\r\n @if (source.detail) {\r\n <span class=\"fb-tr__source-detail\">{{ source.detail }}</span>\r\n }\r\n </button>\r\n @if (source.canEnter) {\r\n <!-- Entrare e mappare sono due gesti diversi: un record intero e' una sorgente valida. -->\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon fb-tr__enter\"\r\n [attr.aria-label]=\"'Entra in ' + source.label\"\r\n (click)=\"enterSource(source.path)\"\r\n >\r\n \u203A\r\n </button>\r\n }\r\n </li>\r\n } @empty {\r\n @if (sourcePending()) {\r\n <li class=\"fb-tr__empty\">Caricamento\u2026</li>\r\n } @else if (sourceUndeclared()) {\r\n <li class=\"fb-tr__empty\">\r\n Il catalogo non dichiara cosa c\u2019e\u2019 qui dentro: non significa che non ci sia niente. Il\r\n percorso si scrive a mano nel dettaglio.\r\n </li>\r\n } @else {\r\n <li class=\"fb-tr__empty\">Nessuna sorgente con questo nome.</li>\r\n }\r\n }\r\n </ul>\r\n </aside>\r\n\r\n <!-- ========================================================= destinazione -->\r\n <section class=\"fb-tr__panel fb-tr__map\" aria-label=\"Destinazione\">\r\n <h3 class=\"fb-tr__panel-title\">\r\n Destinazione\r\n @if (slotCount()) {\r\n <span class=\"fb-tr__count\">{{ mappedCount() }} di {{ slotCount() }} mappate</span>\r\n } @else if (mappedCount()) {\r\n <span class=\"fb-tr__count\">{{ mappedCount() }} mappate</span>\r\n }\r\n </h3>\r\n\r\n <!-- Il tipo del risultato e' la forma della colonna: sta in cima, non in fondo. -->\r\n <div class=\"fb-tr__result\">\r\n <div class=\"fb-fields-row\">\r\n <div class=\"fb-field fb-field--compact\">\r\n <label class=\"fb-field__label fb-field__label--required\">Tipo del risultato</label>\r\n <select\r\n class=\"fb-select\"\r\n [fbValue]=\"transform().dataType || ''\"\r\n (change)=\"setDataType($any($event.target).value)\"\r\n >\r\n <option value=\"\">\u2014 scegli \u2014</option>\r\n @for (type of dataTypes(); track type.value) {\r\n <option [value]=\"type.value\">{{ type.label }}</option>\r\n }\r\n </select>\r\n </div>\r\n\r\n @if (requiresObjectType()) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">\r\n {{ isStructureTarget() ? 'Classe del risultato' : 'Tipo dell\u2019oggetto' }}\r\n </label>\r\n @if (isStructureTarget()) {\r\n <!-- \u00A74.7: una classe del backend. Le destinazioni delle azioni sono i suoi membri. -->\r\n <fb-structure-picker\r\n [value]=\"transform().objectType\"\r\n label=\"Classe del risultato\"\r\n (valueChange)=\"setObjectType($event ?? '')\"\r\n />\r\n } @else if (transform().dataType === 'Enum') {\r\n <!-- Le enumerazioni sono un dizionario chiuso: non c'e' scrittura libera da concedere. -->\r\n <select\r\n class=\"fb-select\"\r\n [fbValue]=\"transform().objectType || ''\"\r\n (change)=\"setObjectType($any($event.target).value)\"\r\n >\r\n <option value=\"\">\u2014 scegli \u2014</option>\r\n @for (entry of enumOptions(); track entry.name) {\r\n <option [value]=\"entry.name\">{{ entry.label || entry.name }}</option>\r\n }\r\n </select>\r\n } @else {\r\n <fb-object-picker\r\n [value]=\"transform().objectType\"\r\n label=\"Tipo dell\u2019oggetto\"\r\n placeholder=\"Scrivi o scegli un oggetto\"\r\n (valueChange)=\"setObjectType($event ?? '')\"\r\n />\r\n }\r\n </div>\r\n }\r\n\r\n <label class=\"fb-check fb-tr__collection\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"transform().isCollection === true\"\r\n (change)=\"setIsCollection($any($event.target).checked)\"\r\n />\r\n Collection\r\n </label>\r\n </div>\r\n\r\n @if (!transform().dataType) {\r\n <p class=\"fb-field__error\">Obbligatorio (TRANSFORM_DATA_TYPE_MISSING).</p>\r\n }\r\n @if (missingObjectType()) {\r\n <p class=\"fb-field__error\">\r\n La classe e\u2019 obbligatoria: senza, l\u2019attivazione e\u2019 bloccata (OBJECT_TYPE_MISSING).\r\n </p>\r\n }\r\n @if (targetUndeclared()) {\r\n <p class=\"fb-callout fb-callout--warn\">\r\n Il catalogo non dichiara i {{ isStructureTarget() ? 'membri' : 'campi' }} di\r\n <code>{{ transform().objectType }}</code>: le destinazioni si scrivono a mano, e nessuna di\r\n esse viene accusata di non esistere.\r\n </p>\r\n }\r\n </div>\r\n\r\n <ul class=\"fb-tr__list fb-tr__rows\">\r\n @for (row of targets(); track row.key) {\r\n <!--\r\n `data-fb-slot` e' cio' che il rilascio cerca: il bersaglio lo calcola `hitTest` dal DOM,\r\n non una drop list del CDK.\r\n -->\r\n <li\r\n class=\"fb-tr__row\"\r\n [attr.data-fb-slot]=\"row.key\"\r\n [class.fb-tr__row--selected]=\"isSelected(row)\"\r\n [class.fb-tr__row--drop]=\"dropKey() === row.key\"\r\n [class.fb-tr__row--empty]=\"row.index < 0\"\r\n >\r\n <button type=\"button\" class=\"fb-tr__row-main\" (click)=\"select(row.key)\">\r\n <span class=\"fb-tr__op\" [attr.title]=\"operationLabel(operationOf(row))\" aria-hidden=\"true\">\r\n {{ operationIcon(row) }}\r\n </span>\r\n <span class=\"fb-tr__row-text\">\r\n <span class=\"fb-tr__row-name\">\r\n {{ row.label }}\r\n @if (row.isCollection) {\r\n <span class=\"fb-tr__badge\" aria-label=\"collection\">[ ]</span>\r\n }\r\n @if (row.isUnknown) {\r\n <span class=\"fb-tr__badge fb-tr__badge--warn\">fuori catalogo</span>\r\n }\r\n </span>\r\n @if (row.index < 0) {\r\n <span class=\"fb-tr__row-src fb-tr__row-src--empty\">non mappata</span>\r\n } @else if (sourceSummary(row)) {\r\n <span class=\"fb-tr__row-src\">\u2190 {{ sourceSummary(row) }}</span>\r\n } @else {\r\n <span class=\"fb-tr__row-src fb-tr__row-src--empty\">sorgente da indicare</span>\r\n }\r\n </span>\r\n @if (row.dataType) {\r\n <span class=\"fb-tr__row-type\">{{ row.dataType }}</span>\r\n }\r\n @if (issuesOf(row).length) {\r\n <span class=\"fb-tr__row-issue\" [attr.title]=\"issuesOf(row)[0].message\">!</span>\r\n }\r\n </button>\r\n @if (row.index >= 0) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon\"\r\n [attr.aria-label]=\"'Togli la mappatura di ' + row.label\"\r\n (click)=\"removeAction(row)\"\r\n >\r\n \u00D7\r\n </button>\r\n }\r\n </li>\r\n } @empty {\r\n <li class=\"fb-empty\">\r\n Nessuna trasformazione: l\u2019elemento non produce nulla (TRANSFORM_WITHOUT_VALUES).\r\n </li>\r\n }\r\n </ul>\r\n\r\n <button type=\"button\" class=\"fb-btn\" (click)=\"addAction()\">\r\n {{ hasTargetCatalog() ? 'Aggiungi una destinazione fuori catalogo' : 'Aggiungi trasformazione' }}\r\n </button>\r\n\r\n <p class=\"fb-callout\">\r\n Il risultato e\u2019 l\u2019<strong>output automatico</strong> dell\u2019elemento: si referenzia con\r\n <code>{{ name() }}</code>.\r\n </p>\r\n </section>\r\n\r\n <!-- ============================================================ dettaglio -->\r\n <aside class=\"fb-tr__panel fb-tr__detail\" aria-label=\"Dettaglio della mappatura\">\r\n @if (selected(); as row) {\r\n <h3 class=\"fb-tr__panel-title\">{{ row.label }}</h3>\r\n\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label\">Operazione</label>\r\n <div class=\"fb-tr__modes\" role=\"group\" aria-label=\"Operazione\">\r\n @for (type of transformTypes(); track type.value) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-tr__mode\"\r\n [class.fb-tr__mode--active]=\"operationOf(row) === type.value\"\r\n (click)=\"setOperation(row, type.value)\"\r\n >\r\n {{ type.label }}\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">\r\n {{ isStructureTarget() ? 'Membro di destinazione' : 'Campo di destinazione' }}\r\n </label>\r\n @if (isStructureTarget()) {\r\n <!-- Solo i membri scrivibili: un membro calcolato e' TARGET_NOT_WRITABLE (\u00A74.7). -->\r\n <fb-structure-member-picker\r\n [value]=\"row.field\"\r\n [className]=\"transform().objectType\"\r\n usage=\"writable\"\r\n label=\"Membro di destinazione\"\r\n (valueChange)=\"setOutputField(row, $event ?? '')\"\r\n />\r\n } @else if (targetObject()) {\r\n <!-- Il risultato e' un record: le destinazioni sono i campi della sua entita' (\u00A74.4). -->\r\n <fb-field-picker\r\n [value]=\"row.field\"\r\n [object]=\"targetObject()\"\r\n label=\"Campo di destinazione\"\r\n (valueChange)=\"setOutputField(row, $event ?? '')\"\r\n />\r\n } @else {\r\n <input\r\n class=\"fb-input\"\r\n [value]=\"row.field\"\r\n placeholder=\"Totale\"\r\n (input)=\"setOutputField(row, $any($event.target).value)\"\r\n />\r\n }\r\n </div>\r\n\r\n @if (isMap(row)) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label\">Valore</label>\r\n <!--\r\n Il tipo di cio' che si scrive guida il controllo: su un campo o un membro Enum i valori\r\n proponibili sono quelli del suo tipo (\u00A74.6).\r\n -->\r\n <fb-value-editor\r\n [value]=\"row.action?.value ?? undefined\"\r\n label=\"Valore\"\r\n [dataType]=\"mapDataType(row)\"\r\n [objectType]=\"mapObjectType(row)\"\r\n (valueChange)=\"setMapValue(row, $event)\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (needsAggregationValues(row)) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">Collection su cui aggregare</label>\r\n <fb-reference-picker\r\n [value]=\"aggregationCollection(row.action)\"\r\n [isCollection]=\"true\"\r\n placeholder=\"Scegli una collection\"\r\n (valueChange)=\"setAggregationCollection(row, $event)\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (needsAggregationField(row)) {\r\n <div class=\"fb-field\">\r\n <label class=\"fb-field__label fb-field__label--required\">Campo da sommare</label>\r\n <!--\r\n Nel documento va il **nome** del campo (`stringValue`, \u00A75.13); dove il tipo della\r\n collection e' noto, il nome lo si sceglie da un elenco invece di ricordarlo.\r\n -->\r\n @if (aggregationObject(row); as object) {\r\n <fb-field-picker\r\n [value]=\"aggregationField(row.action)\"\r\n [object]=\"object\"\r\n label=\"Campo da sommare\"\r\n (valueChange)=\"setAggregationField(row, $event ?? '')\"\r\n />\r\n } @else if (aggregationClass(row); as className) {\r\n <fb-structure-member-picker\r\n [value]=\"aggregationField(row.action)\"\r\n [className]=\"className\"\r\n label=\"Membro da sommare\"\r\n (valueChange)=\"setAggregationField(row, $event ?? '')\"\r\n />\r\n } @else {\r\n <input\r\n class=\"fb-input\"\r\n [value]=\"aggregationField(row.action)\"\r\n placeholder=\"Importo\"\r\n (input)=\"setAggregationField(row, $any($event.target).value)\"\r\n />\r\n }\r\n </div>\r\n }\r\n\r\n @for (issue of issuesOf(row); track $index) {\r\n <p class=\"fb-callout fb-callout--error\">{{ issue.message }} ({{ issue.code }})</p>\r\n }\r\n } @else {\r\n <p class=\"fb-tr__hint\">\r\n Scegli il tipo del risultato: le destinazioni compaiono da l\u00EC, e la mappatura si fa\r\n trascinando una sorgente su una di esse.\r\n </p>\r\n }\r\n </aside>\r\n</div>\r\n\r\n<fb-connector-editor\r\n [nodeName]=\"name()\"\r\n [node]=\"node()\"\r\n [outlets]=\"outlets()\"\r\n (connectorChanged)=\"onConnectorChanged($event)\"\r\n/>\r\n", styles: [":host{display:block;container-type:inline-size}.fb-tr{display:grid;grid-template-columns:240px minmax(0,1fr) 320px;gap:12px;align-items:start;margin-bottom:14px}@container (max-width: 1040px){.fb-tr{grid-template-columns:minmax(0,1fr)}}.fb-tr__panel{min-width:0;padding:10px;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius, 10px);background:var(--fb-surface-alt, #f7f8fa)}.fb-tr__map{background:var(--fb-surface, #fff)}.fb-tr__panel-title{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin:0 0 8px;font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--fb-text-subtle, #98a2b3);overflow-wrap:anywhere}.fb-tr__count{font-size:10px;font-weight:600;letter-spacing:0;text-transform:none;color:var(--fb-text-muted, #6b7086)}.fb-tr__hint{margin:6px 0;font-size:11px;line-height:1.45;color:var(--fb-text-muted, #6b7086)}.fb-tr__search{margin-bottom:6px}.fb-tr__crumbs{display:flex;flex-wrap:wrap;align-items:center;gap:2px;margin-bottom:6px}.fb-tr__crumb{padding:2px 4px;border:0;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-accent, #4f6ef7);font:inherit;font-size:11px;cursor:pointer}.fb-tr__crumb:hover{background:color-mix(in srgb,var(--fb-accent, #4f6ef7) 10%,transparent)}.fb-tr__crumb-sep{color:var(--fb-text-subtle, #98a2b3);font-size:11px}.fb-tr__list{display:flex;flex-direction:column;gap:4px;margin:0;padding:0;max-height:340px;overflow-y:auto;overscroll-behavior:contain;list-style:none}.fb-tr__empty{padding:8px;border:1px dashed var(--fb-border, #d6dae1);border-radius:var(--fb-radius-xs, 6px);font-size:11px;line-height:1.45;color:var(--fb-text-muted, #667085)}.fb-tr__source-row{display:flex;align-items:stretch;gap:2px}.fb-tr__source{display:flex;flex:1;flex-direction:column;gap:1px;min-width:0;padding:5px 8px;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius-xs, 6px);background:var(--fb-surface, #fff);color:var(--fb-text, #1a1c23);font:inherit;font-size:12px;text-align:left;cursor:grab}.fb-tr__source:hover{border-color:var(--fb-accent, #4f6ef7)}.fb-tr__source--dragging{opacity:.6}.fb-tr__source-name,.fb-tr__source-detail{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fb-tr__source-detail{font-size:10px;color:var(--fb-text-subtle, #98a2b3)}.fb-tr__enter{flex:none}.fb-tr__badge{margin-left:4px;padding:0 4px;border-radius:4px;background:var(--fb-surface-sunken, #eef0f4);font-size:9px;font-weight:700;color:var(--fb-text-muted, #6b7086)}.fb-tr__badge--warn{background:color-mix(in srgb,var(--fb-warning, #b7791f) 16%,transparent);color:var(--fb-warning, #b7791f)}.fb-tr__result{margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--fb-border-subtle, #eef0f4)}.fb-tr__collection{align-self:end;padding-bottom:6px;white-space:nowrap}.fb-tr__rows{max-height:420px;margin-bottom:8px}.fb-tr__row{display:flex;align-items:stretch;gap:2px;border:1px solid var(--fb-border, #e2e5eb);border-radius:var(--fb-radius-xs, 6px);background:var(--fb-surface, #fff)}.fb-tr__row--empty{border-style:dashed;background:var(--fb-surface-alt, #f7f8fa)}.fb-tr__row--selected{border-color:var(--fb-accent, #4f6ef7);box-shadow:0 0 0 1px var(--fb-accent, #4f6ef7) inset}.fb-tr__row--drop{border-color:var(--fb-accent, #4f6ef7);border-style:solid;background:color-mix(in srgb,var(--fb-accent, #4f6ef7) 12%,transparent)}.fb-tr__row-main{display:flex;flex:1;align-items:center;gap:8px;min-width:0;padding:6px 8px;border:0;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text, #1a1c23);font:inherit;text-align:left;cursor:pointer}.fb-tr__op{display:grid;flex:none;place-items:center;width:20px;height:20px;border-radius:50%;background:var(--fb-surface-sunken, #eef0f4);font-size:11px;font-weight:700;color:var(--fb-text-muted, #6b7086)}.fb-tr__row-text{display:flex;flex:1;flex-direction:column;gap:1px;min-width:0}.fb-tr__row-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:600}.fb-tr__row-src{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px;color:var(--fb-text-muted, #6b7086)}.fb-tr__row-src--empty{color:var(--fb-text-subtle, #98a2b3);font-style:italic}.fb-tr__row-type{flex:none;font-size:10px;color:var(--fb-text-subtle, #98a2b3)}.fb-tr__row-issue{display:grid;flex:none;place-items:center;width:16px;height:16px;border-radius:50%;background:color-mix(in srgb,var(--fb-error, #dc2626) 14%,transparent);font-size:10px;font-weight:700;color:var(--fb-error, #dc2626)}.fb-tr__modes{margin-top:2px}\n"] }]
13151
13925
  }], ctorParameters: () => [] });
13152
13926
 
13153
13927
  /**
@@ -14346,11 +15120,11 @@ class GroupPanelComponent {
14346
15120
  this.createRequested.emit();
14347
15121
  }
14348
15122
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: GroupPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14349
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: GroupPanelComponent, isStandalone: true, selector: "fb-group-panel", inputs: { isEditable: { classPropertyName: "isEditable", publicName: "isEditable", isSignal: true, isRequired: false, transformFunction: null }, selectedNodeNames: { classPropertyName: "selectedNodeNames", publicName: "selectedNodeNames", isSignal: true, isRequired: false, transformFunction: null }, target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", createRequested: "createRequested", memberFocused: "memberFocused" }, ngImport: i0, template: "<header class=\"fb-grp__header\">\r\n <h2 class=\"fb-grp__title\">Riquadri</h2>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n title=\"Crea un riquadro attorno agli elementi selezionati\"\r\n (click)=\"create()\"\r\n >\r\n Nuovo\r\n </button>\r\n <button type=\"button\" class=\"fb-btn fb-btn--ghost fb-btn--icon\" aria-label=\"Chiudi\" (click)=\"close()\">\u00D7</button>\r\n</header>\r\n\r\n<div class=\"fb-grp__body\">\r\n <p class=\"fb-section__note\">\r\n Un riquadro documenta un pezzo di flow e non ne cambia il comportamento: il runtime non lo\r\n legge mai, non ha collegamenti e i suoi rilievi sono tutti avvisi. Il nome sta in uno spazio\r\n dei nomi a parte, quindi puo\u2019 coincidere con quello di una variabile.\r\n </p>\r\n\r\n @if (!groups().length) {\r\n <p class=\"fb-empty\">\r\n Nessun riquadro. Seleziona gli elementi sul canvas e premi \u00ABNuovo\u00BB: il riquadro nasce\r\n attorno a loro.\r\n </p>\r\n }\r\n\r\n <div class=\"fb-list\">\r\n @for (item of groups(); track $index) {\r\n <div class=\"fb-list__item\">\r\n <div class=\"fb-list__header\">\r\n <button type=\"button\" class=\"fb-grp__name\" (click)=\"toggleOpen(item.name)\">\r\n <span class=\"fb-list__title\">{{ item.group.label || item.name || '(senza nome)' }}</span>\r\n <span class=\"fb-grp__meta\">\r\n {{ membersOf($index).length }}\r\n {{ membersOf($index).length === 1 ? 'elemento' : 'elementi' }}\r\n @if (item.group.isCollapsed) {\r\n \u00B7 chiuso\r\n }\r\n @if (isAutoSized($index)) {\r\n \u00B7 misura sui membri\r\n }\r\n </span>\r\n </button>\r\n <span class=\"fb-list__spacer\"></span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n aria-label=\"Elimina il riquadro\"\r\n title=\"Elimina il riquadro: gli elementi che contiene restano\"\r\n (click)=\"remove($index)\"\r\n >\r\n \u00D7\r\n </button>\r\n </div>\r\n\r\n @if (nameProblem($index)) {\r\n <!-- Avviso, non errore: nessun rilievo su un riquadro blocca l\u2019attivazione (\u00A73.6). -->\r\n <p class=\"fb-field__hint fb-field__hint--warn\">{{ nameProblem($index) }}</p>\r\n }\r\n\r\n @if (isOpen(item.name)) {\r\n <div class=\"fb-fields-row\">\r\n <label class=\"fb-field\">\r\n <span class=\"fb-field__label\">Intestazione</span>\r\n <input\r\n [value]=\"item.group.label || ''\"\r\n placeholder=\"Preparazione dell\u2019ordine\"\r\n [disabled]=\"!isEditable()\"\r\n (input)=\"setLabel($index, $any($event.target).value)\"\r\n />\r\n </label>\r\n <label class=\"fb-field\">\r\n <span class=\"fb-field__label\">Nome tecnico</span>\r\n <input\r\n [value]=\"item.name\"\r\n placeholder=\"Riquadro_Preparazione\"\r\n [disabled]=\"!isEditable()\"\r\n (input)=\"setName($index, $any($event.target).value)\"\r\n />\r\n </label>\r\n </div>\r\n\r\n <label class=\"fb-field\">\r\n <span class=\"fb-field__label\">Commento</span>\r\n <textarea\r\n rows=\"3\"\r\n [value]=\"item.group.description || ''\"\r\n placeholder=\"Cosa fa questo pezzo di flow, e perche\u2019.\"\r\n [disabled]=\"!isEditable()\"\r\n (input)=\"setDescription($index, $any($event.target).value)\"\r\n ></textarea>\r\n <span class=\"fb-field__hint\">\r\n Sul canvas ne compaiono le prime righe; per intero sta qui e nel titolo della barra.\r\n </span>\r\n </label>\r\n\r\n <div class=\"fb-fields-row\">\r\n <label class=\"fb-field fb-field--compact\">\r\n <span class=\"fb-field__label\">Colore</span>\r\n <select\r\n [fbValue]=\"colorOf($index)\"\r\n [disabled]=\"!isEditable()\"\r\n (change)=\"setColor($index, $any($event.target).value)\"\r\n >\r\n <option value=\"\">(nessuno)</option>\r\n @for (color of colors; track color.value) {\r\n <option [value]=\"color.value\">{{ color.label }}</option>\r\n }\r\n <!--\r\n `color` e\u2019 un token concordato fra editor e backend (\u00A73.6): un valore che non\r\n proponiamo non e\u2019 sbagliato, e senza questa opzione la select resterebbe bianca\r\n su un riquadro scritto altrove.\r\n -->\r\n @if (isColorOutOfList($index)) {\r\n <option [value]=\"colorOf($index)\">{{ colorOf($index) }} (fuori elenco)</option>\r\n }\r\n </select>\r\n </label>\r\n <label class=\"fb-field fb-field--compact fb-grp__check\">\r\n <span class=\"fb-field__label\">Chiuso</span>\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"!!item.group.isCollapsed\"\r\n [disabled]=\"!isEditable()\"\r\n (change)=\"setCollapsed($index, $any($event.target).checked)\"\r\n />\r\n </label>\r\n </div>\r\n\r\n <div class=\"fb-grp__members\">\r\n <div class=\"fb-list__header\">\r\n <span class=\"fb-field__label\">Elementi nel riquadro</span>\r\n <span class=\"fb-list__spacer\"></span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--icon\"\r\n [disabled]=\"!isEditable() || !canAddSelection()\"\r\n [title]=\"\r\n canAddSelection()\r\n ? 'Aggiungi gli elementi selezionati sul canvas'\r\n : 'Seleziona degli elementi sul canvas, poi aggiungili qui'\r\n \"\r\n (click)=\"addSelection($index)\"\r\n >\r\n Aggiungi la selezione\r\n </button>\r\n </div>\r\n\r\n @if (!membersOf($index).length) {\r\n <p class=\"fb-empty\">\r\n Vuoto. Trascina un elemento dentro la cornice sul canvas, oppure selezionalo e usa\r\n \u00ABAggiungi la selezione\u00BB.\r\n </p>\r\n }\r\n\r\n @for (member of membersOf($index); track $index) {\r\n <div class=\"fb-grp__member\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-grp__member-name\"\r\n [class.fb-grp__member-name--stale]=\"!isKnownMember(member)\"\r\n [title]=\"isKnownMember(member) ? 'Porta la vista su questo elemento' : 'Questo elemento non esiste piu\u2019'\"\r\n [disabled]=\"!isKnownMember(member)\"\r\n (click)=\"memberFocused.emit(member)\"\r\n >\r\n {{ member }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n aria-label=\"Togli dal riquadro\"\r\n title=\"Togli dal riquadro: l\u2019elemento resta nel flow\"\r\n (click)=\"removeMember($index, member)\"\r\n >\r\n \u00D7\r\n </button>\r\n </div>\r\n }\r\n\r\n @if (staleMembersOf($index).length) {\r\n <p class=\"fb-callout fb-callout--warn\">\r\n <span>\r\n {{ staleMembersOf($index).length }}\r\n {{ staleMembersOf($index).length === 1 ? 'membro nomina un elemento' : 'membri nominano elementi' }}\r\n che non esiste piu\u2019 (GROUP_MEMBER_UNKNOWN). E\u2019 un avviso: non blocca\r\n l\u2019attivazione, e la ripulitura la fa l\u2019editor.\r\n </span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n (click)=\"cleanStale($index)\"\r\n >\r\n Ripulisci\r\n </button>\r\n </p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--fb-surface, #fff)}.fb-grp__header{display:flex;align-items:center;gap:6px;padding:10px 12px;border-bottom:1px solid var(--fb-border-subtle, #e6e9ee)}.fb-grp__title{margin:0;flex:1;font-size:14px;font-weight:600;color:var(--fb-text, #1d2939)}.fb-grp__body{flex:1;min-height:0;padding:10px 12px 16px;overflow-y:auto}.fb-grp__name{display:flex;flex-direction:column;gap:2px;padding:0;border:none;background:transparent;font:inherit;text-align:left;cursor:pointer}.fb-grp__meta{font-size:10px;color:var(--fb-text-muted, #667085)}.fb-grp__check{flex-direction:row;align-items:center;gap:6px}.fb-grp__members{margin-top:4px;padding-top:6px;border-top:1px dashed var(--fb-border, #d6dae1)}.fb-grp__member{display:flex;align-items:center;gap:4px}.fb-grp__member-name{flex:1;min-width:0;padding:3px 4px;border:none;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text, #1d2939);font:inherit;font-size:12px;text-align:left;overflow-wrap:anywhere;cursor:pointer}.fb-grp__member-name:hover:not(:disabled){background:var(--fb-surface-alt, #f7f8fa)}.fb-grp__member-name--stale{color:var(--fb-warning, #b7791f);text-decoration:line-through;cursor:default}\n"], dependencies: [{ kind: "directive", type: SelectValueDirective, selector: "select[fbValue]", inputs: ["fbValue"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
15123
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: GroupPanelComponent, isStandalone: true, selector: "fb-group-panel", inputs: { isEditable: { classPropertyName: "isEditable", publicName: "isEditable", isSignal: true, isRequired: false, transformFunction: null }, selectedNodeNames: { classPropertyName: "selectedNodeNames", publicName: "selectedNodeNames", isSignal: true, isRequired: false, transformFunction: null }, target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", createRequested: "createRequested", memberFocused: "memberFocused" }, ngImport: i0, template: "<header class=\"fb-grp__header\">\r\n <h2 class=\"fb-grp__title\">Riquadri</h2>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n title=\"Crea un riquadro attorno agli elementi selezionati\"\r\n (click)=\"create()\"\r\n >\r\n Nuovo\r\n </button>\r\n <button type=\"button\" class=\"fb-btn fb-btn--ghost fb-btn--icon\" aria-label=\"Chiudi\" (click)=\"close()\">\u00D7</button>\r\n</header>\r\n\r\n<div class=\"fb-grp__body\">\r\n <p class=\"fb-section__note\">\r\n Un riquadro documenta un pezzo di flow e non ne cambia il comportamento: il runtime non lo\r\n legge mai, non ha collegamenti e i suoi rilievi sono tutti avvisi. Il nome sta in uno spazio\r\n dei nomi a parte, quindi puo\u2019 coincidere con quello di una variabile.\r\n </p>\r\n\r\n @if (!groups().length) {\r\n <p class=\"fb-empty\">\r\n Nessun riquadro. Seleziona gli elementi sul canvas e premi \u00ABNuovo\u00BB: il riquadro nasce\r\n attorno a loro.\r\n </p>\r\n }\r\n\r\n <div class=\"fb-list\">\r\n @for (item of groups(); track $index) {\r\n <div class=\"fb-list__item\">\r\n <div class=\"fb-list__header\">\r\n <button type=\"button\" class=\"fb-grp__name\" (click)=\"toggleOpen(item.name)\">\r\n <span class=\"fb-list__title\">{{ item.group.label || item.name || '(senza nome)' }}</span>\r\n <span class=\"fb-grp__meta\">\r\n {{ membersOf($index).length }}\r\n {{ membersOf($index).length === 1 ? 'elemento' : 'elementi' }}\r\n @if (item.group.isCollapsed) {\r\n \u00B7 chiuso: nascosti sul canvas\r\n }\r\n @if (isAutoSized($index)) {\r\n \u00B7 misura sui membri\r\n }\r\n </span>\r\n </button>\r\n <span class=\"fb-list__spacer\"></span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n aria-label=\"Elimina il riquadro\"\r\n title=\"Elimina il riquadro: gli elementi che contiene restano\"\r\n (click)=\"remove($index)\"\r\n >\r\n \u00D7\r\n </button>\r\n </div>\r\n\r\n @if (nameProblem($index)) {\r\n <!-- Avviso, non errore: nessun rilievo su un riquadro blocca l\u2019attivazione (\u00A73.6). -->\r\n <p class=\"fb-field__hint fb-field__hint--warn\">{{ nameProblem($index) }}</p>\r\n }\r\n\r\n @if (isOpen(item.name)) {\r\n <div class=\"fb-fields-row\">\r\n <label class=\"fb-field\">\r\n <span class=\"fb-field__label\">Intestazione</span>\r\n <input\r\n [value]=\"item.group.label || ''\"\r\n placeholder=\"Preparazione dell\u2019ordine\"\r\n [disabled]=\"!isEditable()\"\r\n (input)=\"setLabel($index, $any($event.target).value)\"\r\n />\r\n </label>\r\n <label class=\"fb-field\">\r\n <span class=\"fb-field__label\">Nome tecnico</span>\r\n <input\r\n [value]=\"item.name\"\r\n placeholder=\"Riquadro_Preparazione\"\r\n [disabled]=\"!isEditable()\"\r\n (input)=\"setName($index, $any($event.target).value)\"\r\n />\r\n </label>\r\n </div>\r\n\r\n <label class=\"fb-field\">\r\n <span class=\"fb-field__label\">Commento</span>\r\n <textarea\r\n rows=\"3\"\r\n [value]=\"item.group.description || ''\"\r\n placeholder=\"Cosa fa questo pezzo di flow, e perche\u2019.\"\r\n [disabled]=\"!isEditable()\"\r\n (input)=\"setDescription($index, $any($event.target).value)\"\r\n ></textarea>\r\n <span class=\"fb-field__hint\">\r\n Sul canvas ne compaiono le prime righe; per intero sta qui e nel titolo della barra.\r\n </span>\r\n </label>\r\n\r\n <div class=\"fb-fields-row\">\r\n <label class=\"fb-field fb-field--compact\">\r\n <span class=\"fb-field__label\">Colore</span>\r\n <select\r\n [fbValue]=\"colorOf($index)\"\r\n [disabled]=\"!isEditable()\"\r\n (change)=\"setColor($index, $any($event.target).value)\"\r\n >\r\n <option value=\"\">(nessuno)</option>\r\n @for (color of colors; track color.value) {\r\n <option [value]=\"color.value\">{{ color.label }}</option>\r\n }\r\n <!--\r\n `color` e\u2019 un token concordato fra editor e backend (\u00A73.6): un valore che non\r\n proponiamo non e\u2019 sbagliato, e senza questa opzione la select resterebbe bianca\r\n su un riquadro scritto altrove.\r\n -->\r\n @if (isColorOutOfList($index)) {\r\n <option [value]=\"colorOf($index)\">{{ colorOf($index) }} (fuori elenco)</option>\r\n }\r\n </select>\r\n </label>\r\n <!--\r\n Chiuso non e' un dettaglio grafico: sul canvas gli elementi del riquadro **non si\r\n disegnano piu'** e al loro posto compare una pastiglia, a cui si attaccano gli archi\r\n che entrano nel riquadro o ne escono. Il documento non cambia: e' un modo di guardarlo.\r\n -->\r\n <label class=\"fb-field fb-field--compact fb-grp__check\">\r\n <span class=\"fb-field__label\" title=\"Nasconde gli elementi del riquadro dietro una pastiglia\">\r\n Chiuso\r\n </span>\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"!!item.group.isCollapsed\"\r\n [disabled]=\"!isEditable()\"\r\n (change)=\"setCollapsed($index, $any($event.target).checked)\"\r\n />\r\n </label>\r\n </div>\r\n\r\n <div class=\"fb-grp__members\">\r\n <div class=\"fb-list__header\">\r\n <span class=\"fb-field__label\">Elementi nel riquadro</span>\r\n <span class=\"fb-list__spacer\"></span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--icon\"\r\n [disabled]=\"!isEditable() || !canAddSelection()\"\r\n [title]=\"\r\n canAddSelection()\r\n ? 'Aggiungi gli elementi selezionati sul canvas'\r\n : 'Seleziona degli elementi sul canvas, poi aggiungili qui'\r\n \"\r\n (click)=\"addSelection($index)\"\r\n >\r\n Aggiungi la selezione\r\n </button>\r\n </div>\r\n\r\n @if (!membersOf($index).length) {\r\n <p class=\"fb-empty\">\r\n Vuoto. Trascina un elemento dentro la cornice sul canvas, oppure selezionalo e usa\r\n \u00ABAggiungi la selezione\u00BB.\r\n </p>\r\n }\r\n\r\n @for (member of membersOf($index); track $index) {\r\n <div class=\"fb-grp__member\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-grp__member-name\"\r\n [class.fb-grp__member-name--stale]=\"!isKnownMember(member)\"\r\n [title]=\"isKnownMember(member) ? 'Porta la vista su questo elemento' : 'Questo elemento non esiste piu\u2019'\"\r\n [disabled]=\"!isKnownMember(member)\"\r\n (click)=\"memberFocused.emit(member)\"\r\n >\r\n {{ member }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n aria-label=\"Togli dal riquadro\"\r\n title=\"Togli dal riquadro: l\u2019elemento resta nel flow\"\r\n (click)=\"removeMember($index, member)\"\r\n >\r\n \u00D7\r\n </button>\r\n </div>\r\n }\r\n\r\n @if (staleMembersOf($index).length) {\r\n <p class=\"fb-callout fb-callout--warn\">\r\n <span>\r\n {{ staleMembersOf($index).length }}\r\n {{ staleMembersOf($index).length === 1 ? 'membro nomina un elemento' : 'membri nominano elementi' }}\r\n che non esiste piu\u2019 (GROUP_MEMBER_UNKNOWN). E\u2019 un avviso: non blocca\r\n l\u2019attivazione, e la ripulitura la fa l\u2019editor.\r\n </span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n (click)=\"cleanStale($index)\"\r\n >\r\n Ripulisci\r\n </button>\r\n </p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--fb-surface, #fff)}.fb-grp__header{display:flex;align-items:center;gap:6px;padding:10px 12px;border-bottom:1px solid var(--fb-border-subtle, #e6e9ee)}.fb-grp__title{margin:0;flex:1;font-size:14px;font-weight:600;color:var(--fb-text, #1d2939)}.fb-grp__body{flex:1;min-height:0;padding:10px 12px 16px;overflow-y:auto}.fb-grp__name{display:flex;flex-direction:column;gap:2px;padding:0;border:none;background:transparent;font:inherit;text-align:left;cursor:pointer}.fb-grp__meta{font-size:10px;color:var(--fb-text-muted, #667085)}.fb-grp__check{flex-direction:row;align-items:center;gap:6px}.fb-grp__members{margin-top:4px;padding-top:6px;border-top:1px dashed var(--fb-border, #d6dae1)}.fb-grp__member{display:flex;align-items:center;gap:4px}.fb-grp__member-name{flex:1;min-width:0;padding:3px 4px;border:none;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text, #1d2939);font:inherit;font-size:12px;text-align:left;overflow-wrap:anywhere;cursor:pointer}.fb-grp__member-name:hover:not(:disabled){background:var(--fb-surface-alt, #f7f8fa)}.fb-grp__member-name--stale{color:var(--fb-warning, #b7791f);text-decoration:line-through;cursor:default}\n"], dependencies: [{ kind: "directive", type: SelectValueDirective, selector: "select[fbValue]", inputs: ["fbValue"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14350
15124
  }
14351
15125
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: GroupPanelComponent, decorators: [{
14352
15126
  type: Component,
14353
- args: [{ selector: 'fb-group-panel', standalone: true, imports: [SelectValueDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<header class=\"fb-grp__header\">\r\n <h2 class=\"fb-grp__title\">Riquadri</h2>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n title=\"Crea un riquadro attorno agli elementi selezionati\"\r\n (click)=\"create()\"\r\n >\r\n Nuovo\r\n </button>\r\n <button type=\"button\" class=\"fb-btn fb-btn--ghost fb-btn--icon\" aria-label=\"Chiudi\" (click)=\"close()\">\u00D7</button>\r\n</header>\r\n\r\n<div class=\"fb-grp__body\">\r\n <p class=\"fb-section__note\">\r\n Un riquadro documenta un pezzo di flow e non ne cambia il comportamento: il runtime non lo\r\n legge mai, non ha collegamenti e i suoi rilievi sono tutti avvisi. Il nome sta in uno spazio\r\n dei nomi a parte, quindi puo\u2019 coincidere con quello di una variabile.\r\n </p>\r\n\r\n @if (!groups().length) {\r\n <p class=\"fb-empty\">\r\n Nessun riquadro. Seleziona gli elementi sul canvas e premi \u00ABNuovo\u00BB: il riquadro nasce\r\n attorno a loro.\r\n </p>\r\n }\r\n\r\n <div class=\"fb-list\">\r\n @for (item of groups(); track $index) {\r\n <div class=\"fb-list__item\">\r\n <div class=\"fb-list__header\">\r\n <button type=\"button\" class=\"fb-grp__name\" (click)=\"toggleOpen(item.name)\">\r\n <span class=\"fb-list__title\">{{ item.group.label || item.name || '(senza nome)' }}</span>\r\n <span class=\"fb-grp__meta\">\r\n {{ membersOf($index).length }}\r\n {{ membersOf($index).length === 1 ? 'elemento' : 'elementi' }}\r\n @if (item.group.isCollapsed) {\r\n \u00B7 chiuso\r\n }\r\n @if (isAutoSized($index)) {\r\n \u00B7 misura sui membri\r\n }\r\n </span>\r\n </button>\r\n <span class=\"fb-list__spacer\"></span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n aria-label=\"Elimina il riquadro\"\r\n title=\"Elimina il riquadro: gli elementi che contiene restano\"\r\n (click)=\"remove($index)\"\r\n >\r\n \u00D7\r\n </button>\r\n </div>\r\n\r\n @if (nameProblem($index)) {\r\n <!-- Avviso, non errore: nessun rilievo su un riquadro blocca l\u2019attivazione (\u00A73.6). -->\r\n <p class=\"fb-field__hint fb-field__hint--warn\">{{ nameProblem($index) }}</p>\r\n }\r\n\r\n @if (isOpen(item.name)) {\r\n <div class=\"fb-fields-row\">\r\n <label class=\"fb-field\">\r\n <span class=\"fb-field__label\">Intestazione</span>\r\n <input\r\n [value]=\"item.group.label || ''\"\r\n placeholder=\"Preparazione dell\u2019ordine\"\r\n [disabled]=\"!isEditable()\"\r\n (input)=\"setLabel($index, $any($event.target).value)\"\r\n />\r\n </label>\r\n <label class=\"fb-field\">\r\n <span class=\"fb-field__label\">Nome tecnico</span>\r\n <input\r\n [value]=\"item.name\"\r\n placeholder=\"Riquadro_Preparazione\"\r\n [disabled]=\"!isEditable()\"\r\n (input)=\"setName($index, $any($event.target).value)\"\r\n />\r\n </label>\r\n </div>\r\n\r\n <label class=\"fb-field\">\r\n <span class=\"fb-field__label\">Commento</span>\r\n <textarea\r\n rows=\"3\"\r\n [value]=\"item.group.description || ''\"\r\n placeholder=\"Cosa fa questo pezzo di flow, e perche\u2019.\"\r\n [disabled]=\"!isEditable()\"\r\n (input)=\"setDescription($index, $any($event.target).value)\"\r\n ></textarea>\r\n <span class=\"fb-field__hint\">\r\n Sul canvas ne compaiono le prime righe; per intero sta qui e nel titolo della barra.\r\n </span>\r\n </label>\r\n\r\n <div class=\"fb-fields-row\">\r\n <label class=\"fb-field fb-field--compact\">\r\n <span class=\"fb-field__label\">Colore</span>\r\n <select\r\n [fbValue]=\"colorOf($index)\"\r\n [disabled]=\"!isEditable()\"\r\n (change)=\"setColor($index, $any($event.target).value)\"\r\n >\r\n <option value=\"\">(nessuno)</option>\r\n @for (color of colors; track color.value) {\r\n <option [value]=\"color.value\">{{ color.label }}</option>\r\n }\r\n <!--\r\n `color` e\u2019 un token concordato fra editor e backend (\u00A73.6): un valore che non\r\n proponiamo non e\u2019 sbagliato, e senza questa opzione la select resterebbe bianca\r\n su un riquadro scritto altrove.\r\n -->\r\n @if (isColorOutOfList($index)) {\r\n <option [value]=\"colorOf($index)\">{{ colorOf($index) }} (fuori elenco)</option>\r\n }\r\n </select>\r\n </label>\r\n <label class=\"fb-field fb-field--compact fb-grp__check\">\r\n <span class=\"fb-field__label\">Chiuso</span>\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"!!item.group.isCollapsed\"\r\n [disabled]=\"!isEditable()\"\r\n (change)=\"setCollapsed($index, $any($event.target).checked)\"\r\n />\r\n </label>\r\n </div>\r\n\r\n <div class=\"fb-grp__members\">\r\n <div class=\"fb-list__header\">\r\n <span class=\"fb-field__label\">Elementi nel riquadro</span>\r\n <span class=\"fb-list__spacer\"></span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--icon\"\r\n [disabled]=\"!isEditable() || !canAddSelection()\"\r\n [title]=\"\r\n canAddSelection()\r\n ? 'Aggiungi gli elementi selezionati sul canvas'\r\n : 'Seleziona degli elementi sul canvas, poi aggiungili qui'\r\n \"\r\n (click)=\"addSelection($index)\"\r\n >\r\n Aggiungi la selezione\r\n </button>\r\n </div>\r\n\r\n @if (!membersOf($index).length) {\r\n <p class=\"fb-empty\">\r\n Vuoto. Trascina un elemento dentro la cornice sul canvas, oppure selezionalo e usa\r\n \u00ABAggiungi la selezione\u00BB.\r\n </p>\r\n }\r\n\r\n @for (member of membersOf($index); track $index) {\r\n <div class=\"fb-grp__member\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-grp__member-name\"\r\n [class.fb-grp__member-name--stale]=\"!isKnownMember(member)\"\r\n [title]=\"isKnownMember(member) ? 'Porta la vista su questo elemento' : 'Questo elemento non esiste piu\u2019'\"\r\n [disabled]=\"!isKnownMember(member)\"\r\n (click)=\"memberFocused.emit(member)\"\r\n >\r\n {{ member }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n aria-label=\"Togli dal riquadro\"\r\n title=\"Togli dal riquadro: l\u2019elemento resta nel flow\"\r\n (click)=\"removeMember($index, member)\"\r\n >\r\n \u00D7\r\n </button>\r\n </div>\r\n }\r\n\r\n @if (staleMembersOf($index).length) {\r\n <p class=\"fb-callout fb-callout--warn\">\r\n <span>\r\n {{ staleMembersOf($index).length }}\r\n {{ staleMembersOf($index).length === 1 ? 'membro nomina un elemento' : 'membri nominano elementi' }}\r\n che non esiste piu\u2019 (GROUP_MEMBER_UNKNOWN). E\u2019 un avviso: non blocca\r\n l\u2019attivazione, e la ripulitura la fa l\u2019editor.\r\n </span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n (click)=\"cleanStale($index)\"\r\n >\r\n Ripulisci\r\n </button>\r\n </p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--fb-surface, #fff)}.fb-grp__header{display:flex;align-items:center;gap:6px;padding:10px 12px;border-bottom:1px solid var(--fb-border-subtle, #e6e9ee)}.fb-grp__title{margin:0;flex:1;font-size:14px;font-weight:600;color:var(--fb-text, #1d2939)}.fb-grp__body{flex:1;min-height:0;padding:10px 12px 16px;overflow-y:auto}.fb-grp__name{display:flex;flex-direction:column;gap:2px;padding:0;border:none;background:transparent;font:inherit;text-align:left;cursor:pointer}.fb-grp__meta{font-size:10px;color:var(--fb-text-muted, #667085)}.fb-grp__check{flex-direction:row;align-items:center;gap:6px}.fb-grp__members{margin-top:4px;padding-top:6px;border-top:1px dashed var(--fb-border, #d6dae1)}.fb-grp__member{display:flex;align-items:center;gap:4px}.fb-grp__member-name{flex:1;min-width:0;padding:3px 4px;border:none;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text, #1d2939);font:inherit;font-size:12px;text-align:left;overflow-wrap:anywhere;cursor:pointer}.fb-grp__member-name:hover:not(:disabled){background:var(--fb-surface-alt, #f7f8fa)}.fb-grp__member-name--stale{color:var(--fb-warning, #b7791f);text-decoration:line-through;cursor:default}\n"] }]
15127
+ args: [{ selector: 'fb-group-panel', standalone: true, imports: [SelectValueDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<header class=\"fb-grp__header\">\r\n <h2 class=\"fb-grp__title\">Riquadri</h2>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n title=\"Crea un riquadro attorno agli elementi selezionati\"\r\n (click)=\"create()\"\r\n >\r\n Nuovo\r\n </button>\r\n <button type=\"button\" class=\"fb-btn fb-btn--ghost fb-btn--icon\" aria-label=\"Chiudi\" (click)=\"close()\">\u00D7</button>\r\n</header>\r\n\r\n<div class=\"fb-grp__body\">\r\n <p class=\"fb-section__note\">\r\n Un riquadro documenta un pezzo di flow e non ne cambia il comportamento: il runtime non lo\r\n legge mai, non ha collegamenti e i suoi rilievi sono tutti avvisi. Il nome sta in uno spazio\r\n dei nomi a parte, quindi puo\u2019 coincidere con quello di una variabile.\r\n </p>\r\n\r\n @if (!groups().length) {\r\n <p class=\"fb-empty\">\r\n Nessun riquadro. Seleziona gli elementi sul canvas e premi \u00ABNuovo\u00BB: il riquadro nasce\r\n attorno a loro.\r\n </p>\r\n }\r\n\r\n <div class=\"fb-list\">\r\n @for (item of groups(); track $index) {\r\n <div class=\"fb-list__item\">\r\n <div class=\"fb-list__header\">\r\n <button type=\"button\" class=\"fb-grp__name\" (click)=\"toggleOpen(item.name)\">\r\n <span class=\"fb-list__title\">{{ item.group.label || item.name || '(senza nome)' }}</span>\r\n <span class=\"fb-grp__meta\">\r\n {{ membersOf($index).length }}\r\n {{ membersOf($index).length === 1 ? 'elemento' : 'elementi' }}\r\n @if (item.group.isCollapsed) {\r\n \u00B7 chiuso: nascosti sul canvas\r\n }\r\n @if (isAutoSized($index)) {\r\n \u00B7 misura sui membri\r\n }\r\n </span>\r\n </button>\r\n <span class=\"fb-list__spacer\"></span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n aria-label=\"Elimina il riquadro\"\r\n title=\"Elimina il riquadro: gli elementi che contiene restano\"\r\n (click)=\"remove($index)\"\r\n >\r\n \u00D7\r\n </button>\r\n </div>\r\n\r\n @if (nameProblem($index)) {\r\n <!-- Avviso, non errore: nessun rilievo su un riquadro blocca l\u2019attivazione (\u00A73.6). -->\r\n <p class=\"fb-field__hint fb-field__hint--warn\">{{ nameProblem($index) }}</p>\r\n }\r\n\r\n @if (isOpen(item.name)) {\r\n <div class=\"fb-fields-row\">\r\n <label class=\"fb-field\">\r\n <span class=\"fb-field__label\">Intestazione</span>\r\n <input\r\n [value]=\"item.group.label || ''\"\r\n placeholder=\"Preparazione dell\u2019ordine\"\r\n [disabled]=\"!isEditable()\"\r\n (input)=\"setLabel($index, $any($event.target).value)\"\r\n />\r\n </label>\r\n <label class=\"fb-field\">\r\n <span class=\"fb-field__label\">Nome tecnico</span>\r\n <input\r\n [value]=\"item.name\"\r\n placeholder=\"Riquadro_Preparazione\"\r\n [disabled]=\"!isEditable()\"\r\n (input)=\"setName($index, $any($event.target).value)\"\r\n />\r\n </label>\r\n </div>\r\n\r\n <label class=\"fb-field\">\r\n <span class=\"fb-field__label\">Commento</span>\r\n <textarea\r\n rows=\"3\"\r\n [value]=\"item.group.description || ''\"\r\n placeholder=\"Cosa fa questo pezzo di flow, e perche\u2019.\"\r\n [disabled]=\"!isEditable()\"\r\n (input)=\"setDescription($index, $any($event.target).value)\"\r\n ></textarea>\r\n <span class=\"fb-field__hint\">\r\n Sul canvas ne compaiono le prime righe; per intero sta qui e nel titolo della barra.\r\n </span>\r\n </label>\r\n\r\n <div class=\"fb-fields-row\">\r\n <label class=\"fb-field fb-field--compact\">\r\n <span class=\"fb-field__label\">Colore</span>\r\n <select\r\n [fbValue]=\"colorOf($index)\"\r\n [disabled]=\"!isEditable()\"\r\n (change)=\"setColor($index, $any($event.target).value)\"\r\n >\r\n <option value=\"\">(nessuno)</option>\r\n @for (color of colors; track color.value) {\r\n <option [value]=\"color.value\">{{ color.label }}</option>\r\n }\r\n <!--\r\n `color` e\u2019 un token concordato fra editor e backend (\u00A73.6): un valore che non\r\n proponiamo non e\u2019 sbagliato, e senza questa opzione la select resterebbe bianca\r\n su un riquadro scritto altrove.\r\n -->\r\n @if (isColorOutOfList($index)) {\r\n <option [value]=\"colorOf($index)\">{{ colorOf($index) }} (fuori elenco)</option>\r\n }\r\n </select>\r\n </label>\r\n <!--\r\n Chiuso non e' un dettaglio grafico: sul canvas gli elementi del riquadro **non si\r\n disegnano piu'** e al loro posto compare una pastiglia, a cui si attaccano gli archi\r\n che entrano nel riquadro o ne escono. Il documento non cambia: e' un modo di guardarlo.\r\n -->\r\n <label class=\"fb-field fb-field--compact fb-grp__check\">\r\n <span class=\"fb-field__label\" title=\"Nasconde gli elementi del riquadro dietro una pastiglia\">\r\n Chiuso\r\n </span>\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"!!item.group.isCollapsed\"\r\n [disabled]=\"!isEditable()\"\r\n (change)=\"setCollapsed($index, $any($event.target).checked)\"\r\n />\r\n </label>\r\n </div>\r\n\r\n <div class=\"fb-grp__members\">\r\n <div class=\"fb-list__header\">\r\n <span class=\"fb-field__label\">Elementi nel riquadro</span>\r\n <span class=\"fb-list__spacer\"></span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--icon\"\r\n [disabled]=\"!isEditable() || !canAddSelection()\"\r\n [title]=\"\r\n canAddSelection()\r\n ? 'Aggiungi gli elementi selezionati sul canvas'\r\n : 'Seleziona degli elementi sul canvas, poi aggiungili qui'\r\n \"\r\n (click)=\"addSelection($index)\"\r\n >\r\n Aggiungi la selezione\r\n </button>\r\n </div>\r\n\r\n @if (!membersOf($index).length) {\r\n <p class=\"fb-empty\">\r\n Vuoto. Trascina un elemento dentro la cornice sul canvas, oppure selezionalo e usa\r\n \u00ABAggiungi la selezione\u00BB.\r\n </p>\r\n }\r\n\r\n @for (member of membersOf($index); track $index) {\r\n <div class=\"fb-grp__member\">\r\n <button\r\n type=\"button\"\r\n class=\"fb-grp__member-name\"\r\n [class.fb-grp__member-name--stale]=\"!isKnownMember(member)\"\r\n [title]=\"isKnownMember(member) ? 'Porta la vista su questo elemento' : 'Questo elemento non esiste piu\u2019'\"\r\n [disabled]=\"!isKnownMember(member)\"\r\n (click)=\"memberFocused.emit(member)\"\r\n >\r\n {{ member }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--ghost fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n aria-label=\"Togli dal riquadro\"\r\n title=\"Togli dal riquadro: l\u2019elemento resta nel flow\"\r\n (click)=\"removeMember($index, member)\"\r\n >\r\n \u00D7\r\n </button>\r\n </div>\r\n }\r\n\r\n @if (staleMembersOf($index).length) {\r\n <p class=\"fb-callout fb-callout--warn\">\r\n <span>\r\n {{ staleMembersOf($index).length }}\r\n {{ staleMembersOf($index).length === 1 ? 'membro nomina un elemento' : 'membri nominano elementi' }}\r\n che non esiste piu\u2019 (GROUP_MEMBER_UNKNOWN). E\u2019 un avviso: non blocca\r\n l\u2019attivazione, e la ripulitura la fa l\u2019editor.\r\n </span>\r\n <button\r\n type=\"button\"\r\n class=\"fb-btn fb-btn--icon\"\r\n [disabled]=\"!isEditable()\"\r\n (click)=\"cleanStale($index)\"\r\n >\r\n Ripulisci\r\n </button>\r\n </p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--fb-surface, #fff)}.fb-grp__header{display:flex;align-items:center;gap:6px;padding:10px 12px;border-bottom:1px solid var(--fb-border-subtle, #e6e9ee)}.fb-grp__title{margin:0;flex:1;font-size:14px;font-weight:600;color:var(--fb-text, #1d2939)}.fb-grp__body{flex:1;min-height:0;padding:10px 12px 16px;overflow-y:auto}.fb-grp__name{display:flex;flex-direction:column;gap:2px;padding:0;border:none;background:transparent;font:inherit;text-align:left;cursor:pointer}.fb-grp__meta{font-size:10px;color:var(--fb-text-muted, #667085)}.fb-grp__check{flex-direction:row;align-items:center;gap:6px}.fb-grp__members{margin-top:4px;padding-top:6px;border-top:1px dashed var(--fb-border, #d6dae1)}.fb-grp__member{display:flex;align-items:center;gap:4px}.fb-grp__member-name{flex:1;min-width:0;padding:3px 4px;border:none;border-radius:var(--fb-radius-xs, 6px);background:transparent;color:var(--fb-text, #1d2939);font:inherit;font-size:12px;text-align:left;overflow-wrap:anywhere;cursor:pointer}.fb-grp__member-name:hover:not(:disabled){background:var(--fb-surface-alt, #f7f8fa)}.fb-grp__member-name--stale{color:var(--fb-warning, #b7791f);text-decoration:line-through;cursor:default}\n"] }]
14354
15128
  }], ctorParameters: () => [], propDecorators: { isEditable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditable", required: false }] }], selectedNodeNames: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedNodeNames", required: false }] }], target: [{ type: i0.Input, args: [{ isSignal: true, alias: "target", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], createRequested: [{ type: i0.Output, args: ["createRequested"] }], memberFocused: [{ type: i0.Output, args: ["memberFocused"] }] } });
14355
15129
 
14356
15130
  /**
@@ -16506,5 +17280,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImpo
16506
17280
  * Generated bundle index. Do not edit.
16507
17281
  */
16508
17282
 
16509
- export { ConditionEditorComponent, ConnectorEditorComponent, DebugPanelComponent, DynamicScreenInspectorComponent, ENUM_CHOICE_SET_DEFAULT_DISPLAY_FIELD, ENUM_CHOICE_SET_DEFAULT_VALUE_FIELD, ENUM_CHOICE_SET_FIELDS, ElementDialogComponent, ElementInspectorComponent, ElementPaletteComponent, EnumValuePickerComponent, FALLBACK_COLLECTION_BY_TYPE, FALLBACK_TYPE_LABEL, FLOW_BUILDER_HTTP_CONFIG, FLOW_CLIPBOARD_KIND, FLOW_CLIPBOARD_VERSION, FLOW_ELEMENT_ICONS, FLOW_ELEMENT_VARIANT_FIELDS, FLOW_ELEMENT_VARIANT_ICONS, FLOW_ERROR_FALLBACK_MESSAGE, FLOW_ERROR_HTTP_STATUS, FLOW_GROUP_COLORS, FLOW_NAME_PATTERN, FLOW_NODE_COLLECTIONS, FLOW_NODE_HEIGHT, FLOW_NODE_WIDTH, FLOW_REFERENCE_FIELDS, FLOW_RESOURCE_COLLECTIONS, FLOW_VALUE_FIELDS, FLOW_VALUE_LITERAL_FIELDS, FieldAssignmentEditorComponent, FieldPickerComponent, FieldValuePickerComponent, FlowApiError, FlowBuilderApi, FlowBuilderComponent, FlowCanvasComponent, FlowCatalogStore, FlowClipboardService, FlowDictionaryStore, FlowDocumentStore, FlowEditorSession, FlowLayoutService, FlowValidationStore, FormulaEditorComponent, FormulaValidationService, GROUP_HEADER_HEIGHT, GROUP_MIN_HEIGHT, GROUP_MIN_WIDTH, GROUP_PADDING, GroupPanelComponent, HttpFlowBuilderApi, NamePickerComponent, NodeInspectorBase, ORCHESTRATION_CONDITION_OUTPUT, ObjectPickerComponent, OrchestratedStageInspectorComponent, ParameterEditorComponent, PasteDialogComponent, ProblemsPanelComponent, RecordFilterEditorComponent, ReferencePickerComponent, ResourcePanelComponent, RunDialogComponent, SEVERITY_BUCKETS, SEVERITY_ICON, SEVERITY_LABEL, START_NODE_NAME, SelectValueDirective, StartInspectorComponent, StructureMemberPickerComponent, StructurePickerComponent, TYPES_WITH_AUTOMATIC_OUTPUT, TYPE_BY_COLLECTION, UNSUPPORTED_TYPES, ValueEditorComponent, VersionPanelComponent, allFieldsOf, applyPaste, areTypesComparable, boundsAround, buildClipboardPayload, canvasGroupId, canvasNodeId, checkConditionLogic, checkFlowName, choiceSetSourceOf, declaredChoiceSetSources, describeFieldPath, describePathEntry, duplicateField, elementIcon, emptyFlowDefinition, enumChoiceSetFieldType, fieldAt, filterReferences, flattenFields, flowNodeWidth, flowNodeWidthClass, groupColorClass, groupRect, hasAutoSize, innerNamesOf, insertField, isClipboardPayload, isCustomConditionLogic, isEmptyReferenceFilter, isFieldResource, isGlobalReference, isNumericType, isPathInside, isTypeCheckedOperator, isUnknownEnumChoiceSetField, isValidFlowName, isValued, loadPathLevel, matchesReferenceFilter, membersInside, moveCondition, moveField, navigatePath, otherSourceFieldsOf, outletByKey, outletsOf, parseCanvasGroupId, parseCanvasNodeId, parseFieldPath, parseInvariantNumber, parseSourceConnectorId, parseTargetConnectorId, pathAvailableNames, pathContainerLabel, pathKey, pathNotVerifiableMessage, planPaste, recomputeMembers, referenceRoot, referencedRootsOf, remapConditionLogic, removeCondition, removeField, resolvePath, rewriteReferences, sameMembers, samePath, screenActionNames, screenFieldNames, severityBucket, slugifyFlowName, sourceConnectorId, stageStepNames, stageStepOutputReferenced, stepsOf, targetConnectorId, typeOfCollection, uniqueFlowName, valuedFieldOf, valuedFieldsOf, variantFieldOf, variantOf, variantPresetOf };
17283
+ export { ConditionEditorComponent, ConnectorEditorComponent, DebugPanelComponent, DynamicScreenInspectorComponent, ENUM_CHOICE_SET_DEFAULT_DISPLAY_FIELD, ENUM_CHOICE_SET_DEFAULT_VALUE_FIELD, ENUM_CHOICE_SET_FIELDS, ElementDialogComponent, ElementInspectorComponent, ElementPaletteComponent, EnumValuePickerComponent, FALLBACK_COLLECTION_BY_TYPE, FALLBACK_TYPE_LABEL, FLOW_BUILDER_HTTP_CONFIG, FLOW_CLIPBOARD_KIND, FLOW_CLIPBOARD_VERSION, FLOW_ELEMENT_ICONS, FLOW_ELEMENT_VARIANT_FIELDS, FLOW_ELEMENT_VARIANT_ICONS, FLOW_ERROR_FALLBACK_MESSAGE, FLOW_ERROR_HTTP_STATUS, FLOW_FOLD_HEIGHT, FLOW_FOLD_WIDTH, FLOW_GROUP_COLORS, FLOW_NAME_PATTERN, FLOW_NODE_COLLECTIONS, FLOW_NODE_HEIGHT, FLOW_NODE_WIDTH, FLOW_REFERENCE_FIELDS, FLOW_RESOURCE_COLLECTIONS, FLOW_VALUE_FIELDS, FLOW_VALUE_LITERAL_FIELDS, FieldAssignmentEditorComponent, FieldPickerComponent, FieldValuePickerComponent, FlowApiError, FlowBuilderApi, FlowBuilderComponent, FlowCanvasComponent, FlowCatalogStore, FlowClipboardService, FlowDictionaryStore, FlowDocumentStore, FlowEditorSession, FlowLayoutService, FlowValidationStore, FormulaEditorComponent, FormulaValidationService, GROUP_HEADER_HEIGHT, GROUP_MIN_HEIGHT, GROUP_MIN_WIDTH, GROUP_PADDING, GroupPanelComponent, HttpFlowBuilderApi, NamePickerComponent, NodeInspectorBase, ORCHESTRATION_CONDITION_OUTPUT, ObjectPickerComponent, OrchestratedStageInspectorComponent, ParameterEditorComponent, PasteDialogComponent, ProblemsPanelComponent, RecordFilterEditorComponent, ReferencePickerComponent, ResourcePanelComponent, RunDialogComponent, SEVERITY_BUCKETS, SEVERITY_ICON, SEVERITY_LABEL, START_NODE_NAME, SelectValueDirective, StartInspectorComponent, StructureMemberPickerComponent, StructurePickerComponent, TYPES_WITH_AUTOMATIC_OUTPUT, TYPE_BY_COLLECTION, UNSUPPORTED_TYPES, ValueEditorComponent, VersionPanelComponent, allFieldsOf, applyPaste, areTypesComparable, boundsAround, buildClipboardPayload, canvasFoldId, canvasGroupId, canvasNodeId, checkConditionLogic, checkFlowName, choiceSetSourceOf, declaredChoiceSetSources, describeFieldPath, describePathEntry, duplicateField, elementIcon, emptyFlowDefinition, enumChoiceSetFieldType, fieldAt, filterReferences, flattenFields, flowNodeWidth, flowNodeWidthClass, foldSourceConnectorId, foldTargetConnectorId, groupColorClass, groupRect, hasAutoSize, innerNamesOf, insertField, isClipboardPayload, isCustomConditionLogic, isEmptyReferenceFilter, isFieldResource, isFoldReference, isGlobalReference, isNumericType, isPathInside, isTypeCheckedOperator, isUnknownEnumChoiceSetField, isValidFlowName, isValued, loadPathLevel, matchesReferenceFilter, membersInside, moveCondition, moveField, navigatePath, otherSourceFieldsOf, outletByKey, outletsOf, parseCanvasFoldId, parseCanvasGroupId, parseCanvasNodeId, parseFieldPath, parseInvariantNumber, parseSourceConnectorId, parseTargetConnectorId, pathAvailableNames, pathContainerLabel, pathKey, pathNotVerifiableMessage, planPaste, recomputeMembers, referenceRoot, referencedRootsOf, remapConditionLogic, removeCondition, removeField, resolvePath, rewriteReferences, sameMembers, samePath, screenActionNames, screenFieldNames, severityBucket, slugifyFlowName, sourceConnectorId, stageStepNames, stageStepOutputReferenced, stepsOf, targetConnectorId, typeOfCollection, uniqueFlowName, valuedFieldOf, valuedFieldsOf, variantFieldOf, variantOf, variantPresetOf };
16510
17284
  //# sourceMappingURL=esfaenza-flow-builder.mjs.map