@hostwebhook/node-types 1.72.0 → 1.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,6 +33,28 @@
33
33
  * Los `slot` siguen haciendo falta: separan la identidad del documento de los
34
34
  * campos de la operación, aunque ahora las dos vayan debajo del selector. Un
35
35
  * bucle único ordenaría por el array de params y no por ese criterio.
36
+ *
37
+ * ── Qué exige el motor (y por qué está escrito aquí) ──
38
+ * Este esquema declaraba CERO obligatorios en las cinco operaciones, y el
39
+ * ejecutor —`api/src/nodes/docs-actions/docs-actions.service.ts`— rechaza cinco
40
+ * cosas. Esa distancia no se veía como un error de validación sino como un
41
+ * nodo que no guía: sin ningún obligatorio no hay nada que esperar, ningún paso
42
+ * desbloquea al siguiente, y el guiado que el panel SÍ monta no se nota.
43
+ *
44
+ * Los cinco 400 del ejecutor, uno por línea, que son los cinco `required` de
45
+ * este fichero:
46
+ *
47
+ * :323 readDoc documentId 'Document ID is required'
48
+ * :440 appendText documentId 'Document ID is required'
49
+ * :447 appendText contentTemplate 'Content template is required'
50
+ * :498 replaceText documentId 'Document ID is required'
51
+ * :505 replaceText searchText 'Search text is required'
52
+ * :560 insertTable documentId 'Document ID is required'
53
+ *
54
+ * Y los cuatro que NO se marcan, con el defecto que los salva: `documentTitle`
55
+ * cae en `entity.name`, `contentTemplate` de `createDoc` en un documento vacío,
56
+ * `tableRows`/`tableCols` en `|| 3`, y `replaceWith` vacío es un BORRADO, no un
57
+ * hueco. Cada uno lo explica en su sitio.
36
58
  */
37
59
  export declare const DOCS_OPERATIONS: readonly ["readDoc", "createDoc", "appendText", "replaceText", "insertTable"];
38
60
  export type DocsOperation = (typeof DOCS_OPERATIONS)[number];
@@ -67,6 +89,23 @@ export interface DocsParamSpec {
67
89
  label: string;
68
90
  type: DocsParamType;
69
91
  slot: DocsParamSlot;
92
+ /**
93
+ * Lo exige el EJECUTOR: sin él, `docs-actions.service.ts` devuelve un 400 y
94
+ * el nodo no corre.
95
+ *
96
+ * No es «lo que conviene rellenar», y la diferencia importa en las dos
97
+ * direcciones. De menos, un obligatorio que falta deja la familia con cero y
98
+ * `esGuiable` (`check-cuantos-faltan-por-guiar.mjs`) la echa del censo: el
99
+ * panel monta `useGuidedConfig`, pero sin nada que esperar ningún paso
100
+ * desbloquea al siguiente y el guiado se ve como si no existiera — que es el
101
+ * fallo que este campo arregla. De más, marcar algo que el ejecutor SÍ acepta
102
+ * vacío esconde tras un hueco imposible un nodo que habría funcionado, porque
103
+ * `frena()` sólo mira `obligatorio && !contestado`.
104
+ *
105
+ * Por eso cada `required: true` de este fichero apunta a la línea que lo
106
+ * exige, y los que no lo llevan dicen dónde el ejecutor pone su defecto.
107
+ */
108
+ required?: boolean;
70
109
  /** Texto de ayuda. Llano: el paquete no lleva React. */
71
110
  description?: string;
72
111
  placeholder?: string;
@@ -34,6 +34,28 @@
34
34
  * Los `slot` siguen haciendo falta: separan la identidad del documento de los
35
35
  * campos de la operación, aunque ahora las dos vayan debajo del selector. Un
36
36
  * bucle único ordenaría por el array de params y no por ese criterio.
37
+ *
38
+ * ── Qué exige el motor (y por qué está escrito aquí) ──
39
+ * Este esquema declaraba CERO obligatorios en las cinco operaciones, y el
40
+ * ejecutor —`api/src/nodes/docs-actions/docs-actions.service.ts`— rechaza cinco
41
+ * cosas. Esa distancia no se veía como un error de validación sino como un
42
+ * nodo que no guía: sin ningún obligatorio no hay nada que esperar, ningún paso
43
+ * desbloquea al siguiente, y el guiado que el panel SÍ monta no se nota.
44
+ *
45
+ * Los cinco 400 del ejecutor, uno por línea, que son los cinco `required` de
46
+ * este fichero:
47
+ *
48
+ * :323 readDoc documentId 'Document ID is required'
49
+ * :440 appendText documentId 'Document ID is required'
50
+ * :447 appendText contentTemplate 'Content template is required'
51
+ * :498 replaceText documentId 'Document ID is required'
52
+ * :505 replaceText searchText 'Search text is required'
53
+ * :560 insertTable documentId 'Document ID is required'
54
+ *
55
+ * Y los cuatro que NO se marcan, con el defecto que los salva: `documentTitle`
56
+ * cae en `entity.name`, `contentTemplate` de `createDoc` en un documento vacío,
57
+ * `tableRows`/`tableCols` en `|| 3`, y `replaceWith` vacío es un BORRADO, no un
58
+ * hueco. Cada uno lo explica en su sitio.
37
59
  */
38
60
  Object.defineProperty(exports, "__esModule", { value: true });
39
61
  exports.DOCS_OPERATION_SPECS = exports.DOCS_OPERATIONS = void 0;
@@ -50,19 +72,42 @@ function isDocsOperation(value) {
50
72
  return typeof value === 'string' && exports.DOCS_OPERATIONS.includes(value);
51
73
  }
52
74
  /* Los campos que comparten varias operaciones. */
75
+ /**
76
+ * `required: true` va aquí y no operación por operación porque las CUATRO que
77
+ * lo usan lo exigen: `executeRead` (:323), `executeAppend` (:440),
78
+ * `executeReplace` (:498) y `executeInsertTable` (:560) devuelven todas el
79
+ * mismo 400, «Document ID is required». La quinta, `createDoc`, no llama a este
80
+ * ayudante — el documento aún no existe.
81
+ */
53
82
  const documentId = () => ({
54
83
  name: 'documentId',
55
84
  label: 'Or paste Document ID / template',
56
85
  type: 'docPicker',
57
86
  slot: 'document',
87
+ required: true,
58
88
  placeholder: '1BxiMVs0XRA5nFMdKvBd... or {{payload.documentId}}',
59
89
  });
60
- const contentTemplate = () => ({
90
+ /**
91
+ * Éste NO puede llevar `required` en el ayudante, y es la única asimetría del
92
+ * fichero: sus dos usuarios no piden lo mismo.
93
+ *
94
+ * `appendText` lo exige (`executeAppend` :447, «Content template is required»):
95
+ * añadir la cadena vacía al final de un documento no es una operación, es un
96
+ * viaje a Google para no hacer nada. `createDoc` no (`executeCreate` :362 lo
97
+ * lee con un ternario y :375 sólo inserta `if (content)`): un documento nuevo y
98
+ * vacío es un resultado legítimo, y bloquearlo sería inventarse un requisito
99
+ * que la api no tiene.
100
+ *
101
+ * De ahí el `over`, como el `columnMapping` de Sheets: el que lo exige lo dice
102
+ * en su sitio.
103
+ */
104
+ const contentTemplate = (over = {}) => ({
61
105
  name: 'contentTemplate',
62
106
  label: 'Content',
63
107
  type: 'htmlTemplate',
64
108
  slot: 'config',
65
109
  placeholder: 'Text to insert... supports {{payload.x}} templates',
110
+ ...over,
66
111
  });
67
112
  exports.DOCS_OPERATION_SPECS = {
68
113
  readDoc: {
@@ -77,13 +122,22 @@ exports.DOCS_OPERATION_SPECS = {
77
122
  labelShort: 'Create',
78
123
  description: 'Create a new Google Doc',
79
124
  /* La única SIN `documentId`: aquí el documento no existe todavía. Eso es lo
80
- que en la página eran dos compuertas negadas (`!== "createDoc"`). */
125
+ que en la página eran dos compuertas negadas (`!== "createDoc"`).
126
+
127
+ Y la única con CERO obligatorios, porque el ejecutor no exige ninguno:
128
+ `executeCreate` no tiene un solo `return 400`. Un `createDoc` sin nada
129
+ contestado corre y crea un documento vacío con el nombre del nodo. */
81
130
  params: [
82
131
  {
83
132
  name: 'documentTitle',
84
133
  label: 'Document Title',
85
134
  type: 'template',
86
135
  slot: 'document',
136
+ /* Opcional a propósito: `executeCreate` (:361) lo lee como
137
+ `render(entity.documentTitle || entity.name)`. Sin título el
138
+ documento sale con el NOMBRE DEL NODO, que es un defecto de verdad y
139
+ no un descuido. Marcarlo obligatorio pararía la guía delante de un
140
+ campo que la api sabe rellenar sola. */
87
141
  placeholder: 'Report — {{payload.date}} or My Document',
88
142
  },
89
143
  contentTemplate(),
@@ -93,7 +147,7 @@ exports.DOCS_OPERATION_SPECS = {
93
147
  label: 'Append Text',
94
148
  labelShort: 'Append',
95
149
  description: 'Add text to end of document',
96
- params: [documentId(), contentTemplate()],
150
+ params: [documentId(), contentTemplate({ required: true })],
97
151
  },
98
152
  replaceText: {
99
153
  label: 'Replace Text',
@@ -106,6 +160,9 @@ exports.DOCS_OPERATION_SPECS = {
106
160
  label: 'Search Text',
107
161
  type: 'template',
108
162
  slot: 'config',
163
+ /* `executeReplace` (:505): «Search text is required». Sin qué buscar,
164
+ `replaceAllText` no tiene nada que hacer. */
165
+ required: true,
109
166
  placeholder: '{{PLACEHOLDER}} or text to find',
110
167
  },
111
168
  {
@@ -113,6 +170,11 @@ exports.DOCS_OPERATION_SPECS = {
113
170
  label: 'Replace With',
114
171
  type: 'template',
115
172
  slot: 'config',
173
+ /* Opcional, y aquí el vacío SIGNIFICA algo: `executeReplace` lo pasa
174
+ tal cual a `replaceAllText`, así que dejarlo en blanco BORRA todas
175
+ las apariciones de `searchText`. Es la única forma de pedir un
176
+ borrado, y por eso el ejecutor no lo comprueba: hacerlo obligatorio
177
+ quitaría una operación que hoy funciona. */
116
178
  placeholder: '{{payload.value}} or replacement text',
117
179
  },
118
180
  ],
@@ -123,7 +185,15 @@ exports.DOCS_OPERATION_SPECS = {
123
185
  description: 'Insert a table at end of document',
124
186
  params: [
125
187
  documentId(),
126
- /* Los topes son distintos y no es un descuido: 50 filas, 20 columnas. */
188
+ /* Los topes son distintos y no es un descuido: 50 filas, 20 columnas.
189
+
190
+ Y los dos siguen OPCIONALES teniendo `default: 3`, que es justo el
191
+ caso que el contrato vigila: `executeInsertTable` (:555) los lee con
192
+ `entity.tableRows || 3`, o sea que el 3×3 lo pone la api tanto si el
193
+ formulario lo manda como si no. Un obligatorio con `default` sería
194
+ además la trampa de la regla 10 — `defectoPintable` le borra el
195
+ `default` para no pintar como contestado algo que nadie eligió, y
196
+ estos dos sí quieren pintarlo. */
127
197
  { name: 'tableRows', label: 'Rows', type: 'number', slot: 'config', min: 1, max: 50, default: 3 },
128
198
  { name: 'tableCols', label: 'Columns', type: 'number', slot: 'config', min: 1, max: 20, default: 3 },
129
199
  ],
@@ -33,6 +33,28 @@
33
33
  * Los `slot` siguen haciendo falta: separan la identidad del documento de los
34
34
  * campos de la operación, aunque ahora las dos vayan debajo del selector. Un
35
35
  * bucle único ordenaría por el array de params y no por ese criterio.
36
+ *
37
+ * ── Qué exige el motor (y por qué está escrito aquí) ──
38
+ * Este esquema declaraba CERO obligatorios en las cinco operaciones, y el
39
+ * ejecutor —`api/src/nodes/docs-actions/docs-actions.service.ts`— rechaza cinco
40
+ * cosas. Esa distancia no se veía como un error de validación sino como un
41
+ * nodo que no guía: sin ningún obligatorio no hay nada que esperar, ningún paso
42
+ * desbloquea al siguiente, y el guiado que el panel SÍ monta no se nota.
43
+ *
44
+ * Los cinco 400 del ejecutor, uno por línea, que son los cinco `required` de
45
+ * este fichero:
46
+ *
47
+ * :323 readDoc documentId 'Document ID is required'
48
+ * :440 appendText documentId 'Document ID is required'
49
+ * :447 appendText contentTemplate 'Content template is required'
50
+ * :498 replaceText documentId 'Document ID is required'
51
+ * :505 replaceText searchText 'Search text is required'
52
+ * :560 insertTable documentId 'Document ID is required'
53
+ *
54
+ * Y los cuatro que NO se marcan, con el defecto que los salva: `documentTitle`
55
+ * cae en `entity.name`, `contentTemplate` de `createDoc` en un documento vacío,
56
+ * `tableRows`/`tableCols` en `|| 3`, y `replaceWith` vacío es un BORRADO, no un
57
+ * hueco. Cada uno lo explica en su sitio.
36
58
  */
37
59
  export declare const DOCS_OPERATIONS: readonly ["readDoc", "createDoc", "appendText", "replaceText", "insertTable"];
38
60
  export type DocsOperation = (typeof DOCS_OPERATIONS)[number];
@@ -67,6 +89,23 @@ export interface DocsParamSpec {
67
89
  label: string;
68
90
  type: DocsParamType;
69
91
  slot: DocsParamSlot;
92
+ /**
93
+ * Lo exige el EJECUTOR: sin él, `docs-actions.service.ts` devuelve un 400 y
94
+ * el nodo no corre.
95
+ *
96
+ * No es «lo que conviene rellenar», y la diferencia importa en las dos
97
+ * direcciones. De menos, un obligatorio que falta deja la familia con cero y
98
+ * `esGuiable` (`check-cuantos-faltan-por-guiar.mjs`) la echa del censo: el
99
+ * panel monta `useGuidedConfig`, pero sin nada que esperar ningún paso
100
+ * desbloquea al siguiente y el guiado se ve como si no existiera — que es el
101
+ * fallo que este campo arregla. De más, marcar algo que el ejecutor SÍ acepta
102
+ * vacío esconde tras un hueco imposible un nodo que habría funcionado, porque
103
+ * `frena()` sólo mira `obligatorio && !contestado`.
104
+ *
105
+ * Por eso cada `required: true` de este fichero apunta a la línea que lo
106
+ * exige, y los que no lo llevan dicen dónde el ejecutor pone su defecto.
107
+ */
108
+ required?: boolean;
70
109
  /** Texto de ayuda. Llano: el paquete no lleva React. */
71
110
  description?: string;
72
111
  placeholder?: string;
@@ -33,6 +33,28 @@
33
33
  * Los `slot` siguen haciendo falta: separan la identidad del documento de los
34
34
  * campos de la operación, aunque ahora las dos vayan debajo del selector. Un
35
35
  * bucle único ordenaría por el array de params y no por ese criterio.
36
+ *
37
+ * ── Qué exige el motor (y por qué está escrito aquí) ──
38
+ * Este esquema declaraba CERO obligatorios en las cinco operaciones, y el
39
+ * ejecutor —`api/src/nodes/docs-actions/docs-actions.service.ts`— rechaza cinco
40
+ * cosas. Esa distancia no se veía como un error de validación sino como un
41
+ * nodo que no guía: sin ningún obligatorio no hay nada que esperar, ningún paso
42
+ * desbloquea al siguiente, y el guiado que el panel SÍ monta no se nota.
43
+ *
44
+ * Los cinco 400 del ejecutor, uno por línea, que son los cinco `required` de
45
+ * este fichero:
46
+ *
47
+ * :323 readDoc documentId 'Document ID is required'
48
+ * :440 appendText documentId 'Document ID is required'
49
+ * :447 appendText contentTemplate 'Content template is required'
50
+ * :498 replaceText documentId 'Document ID is required'
51
+ * :505 replaceText searchText 'Search text is required'
52
+ * :560 insertTable documentId 'Document ID is required'
53
+ *
54
+ * Y los cuatro que NO se marcan, con el defecto que los salva: `documentTitle`
55
+ * cae en `entity.name`, `contentTemplate` de `createDoc` en un documento vacío,
56
+ * `tableRows`/`tableCols` en `|| 3`, y `replaceWith` vacío es un BORRADO, no un
57
+ * hueco. Cada uno lo explica en su sitio.
36
58
  */
37
59
  export const DOCS_OPERATIONS = [
38
60
  'readDoc', // leer el contenido como texto plano
@@ -46,19 +68,42 @@ export function isDocsOperation(value) {
46
68
  return typeof value === 'string' && DOCS_OPERATIONS.includes(value);
47
69
  }
48
70
  /* Los campos que comparten varias operaciones. */
71
+ /**
72
+ * `required: true` va aquí y no operación por operación porque las CUATRO que
73
+ * lo usan lo exigen: `executeRead` (:323), `executeAppend` (:440),
74
+ * `executeReplace` (:498) y `executeInsertTable` (:560) devuelven todas el
75
+ * mismo 400, «Document ID is required». La quinta, `createDoc`, no llama a este
76
+ * ayudante — el documento aún no existe.
77
+ */
49
78
  const documentId = () => ({
50
79
  name: 'documentId',
51
80
  label: 'Or paste Document ID / template',
52
81
  type: 'docPicker',
53
82
  slot: 'document',
83
+ required: true,
54
84
  placeholder: '1BxiMVs0XRA5nFMdKvBd... or {{payload.documentId}}',
55
85
  });
56
- const contentTemplate = () => ({
86
+ /**
87
+ * Éste NO puede llevar `required` en el ayudante, y es la única asimetría del
88
+ * fichero: sus dos usuarios no piden lo mismo.
89
+ *
90
+ * `appendText` lo exige (`executeAppend` :447, «Content template is required»):
91
+ * añadir la cadena vacía al final de un documento no es una operación, es un
92
+ * viaje a Google para no hacer nada. `createDoc` no (`executeCreate` :362 lo
93
+ * lee con un ternario y :375 sólo inserta `if (content)`): un documento nuevo y
94
+ * vacío es un resultado legítimo, y bloquearlo sería inventarse un requisito
95
+ * que la api no tiene.
96
+ *
97
+ * De ahí el `over`, como el `columnMapping` de Sheets: el que lo exige lo dice
98
+ * en su sitio.
99
+ */
100
+ const contentTemplate = (over = {}) => ({
57
101
  name: 'contentTemplate',
58
102
  label: 'Content',
59
103
  type: 'htmlTemplate',
60
104
  slot: 'config',
61
105
  placeholder: 'Text to insert... supports {{payload.x}} templates',
106
+ ...over,
62
107
  });
63
108
  export const DOCS_OPERATION_SPECS = {
64
109
  readDoc: {
@@ -73,13 +118,22 @@ export const DOCS_OPERATION_SPECS = {
73
118
  labelShort: 'Create',
74
119
  description: 'Create a new Google Doc',
75
120
  /* La única SIN `documentId`: aquí el documento no existe todavía. Eso es lo
76
- que en la página eran dos compuertas negadas (`!== "createDoc"`). */
121
+ que en la página eran dos compuertas negadas (`!== "createDoc"`).
122
+
123
+ Y la única con CERO obligatorios, porque el ejecutor no exige ninguno:
124
+ `executeCreate` no tiene un solo `return 400`. Un `createDoc` sin nada
125
+ contestado corre y crea un documento vacío con el nombre del nodo. */
77
126
  params: [
78
127
  {
79
128
  name: 'documentTitle',
80
129
  label: 'Document Title',
81
130
  type: 'template',
82
131
  slot: 'document',
132
+ /* Opcional a propósito: `executeCreate` (:361) lo lee como
133
+ `render(entity.documentTitle || entity.name)`. Sin título el
134
+ documento sale con el NOMBRE DEL NODO, que es un defecto de verdad y
135
+ no un descuido. Marcarlo obligatorio pararía la guía delante de un
136
+ campo que la api sabe rellenar sola. */
83
137
  placeholder: 'Report — {{payload.date}} or My Document',
84
138
  },
85
139
  contentTemplate(),
@@ -89,7 +143,7 @@ export const DOCS_OPERATION_SPECS = {
89
143
  label: 'Append Text',
90
144
  labelShort: 'Append',
91
145
  description: 'Add text to end of document',
92
- params: [documentId(), contentTemplate()],
146
+ params: [documentId(), contentTemplate({ required: true })],
93
147
  },
94
148
  replaceText: {
95
149
  label: 'Replace Text',
@@ -102,6 +156,9 @@ export const DOCS_OPERATION_SPECS = {
102
156
  label: 'Search Text',
103
157
  type: 'template',
104
158
  slot: 'config',
159
+ /* `executeReplace` (:505): «Search text is required». Sin qué buscar,
160
+ `replaceAllText` no tiene nada que hacer. */
161
+ required: true,
105
162
  placeholder: '{{PLACEHOLDER}} or text to find',
106
163
  },
107
164
  {
@@ -109,6 +166,11 @@ export const DOCS_OPERATION_SPECS = {
109
166
  label: 'Replace With',
110
167
  type: 'template',
111
168
  slot: 'config',
169
+ /* Opcional, y aquí el vacío SIGNIFICA algo: `executeReplace` lo pasa
170
+ tal cual a `replaceAllText`, así que dejarlo en blanco BORRA todas
171
+ las apariciones de `searchText`. Es la única forma de pedir un
172
+ borrado, y por eso el ejecutor no lo comprueba: hacerlo obligatorio
173
+ quitaría una operación que hoy funciona. */
112
174
  placeholder: '{{payload.value}} or replacement text',
113
175
  },
114
176
  ],
@@ -119,7 +181,15 @@ export const DOCS_OPERATION_SPECS = {
119
181
  description: 'Insert a table at end of document',
120
182
  params: [
121
183
  documentId(),
122
- /* Los topes son distintos y no es un descuido: 50 filas, 20 columnas. */
184
+ /* Los topes son distintos y no es un descuido: 50 filas, 20 columnas.
185
+
186
+ Y los dos siguen OPCIONALES teniendo `default: 3`, que es justo el
187
+ caso que el contrato vigila: `executeInsertTable` (:555) los lee con
188
+ `entity.tableRows || 3`, o sea que el 3×3 lo pone la api tanto si el
189
+ formulario lo manda como si no. Un obligatorio con `default` sería
190
+ además la trampa de la regla 10 — `defectoPintable` le borra el
191
+ `default` para no pintar como contestado algo que nadie eligió, y
192
+ estos dos sí quieren pintarlo. */
123
193
  { name: 'tableRows', label: 'Rows', type: 'number', slot: 'config', min: 1, max: 50, default: 3 },
124
194
  { name: 'tableCols', label: 'Columns', type: 'number', slot: 'config', min: 1, max: 20, default: 3 },
125
195
  ],
@@ -38,7 +38,20 @@ export interface NodeUIConfig {
38
38
  hasBranches?: boolean;
39
39
  isTerminal?: boolean;
40
40
  hasLoopBack?: boolean;
41
- connectModal?: 'branch' | 'rule' | 'output-group';
41
+ /**
42
+ * Aquí había también un `'rule'`, sólo para el router, y con él vivía en
43
+ * el lienzo un camino de conexión a medida: su propio modal, su propio
44
+ * `handleRuleSelect` y su propia puerta en `onConnect`. Dos mecanismos
45
+ * para lo mismo, y así es como uno se arregla y el otro no: el modal del
46
+ * router salía al ARRASTRAR y nunca al pulsar el `+`, porque ese segundo
47
+ * camino pasa por `outputGroups` — que el router no declaraba.
48
+ *
49
+ * El router ramifica igual que el conditional, el split y el clasificador,
50
+ * y su destino vive en el mismo sitio que el de ellos (`port: rule:<id>`),
51
+ * así que usa la misma maquinaria. Se quita del tipo para que no pueda
52
+ * volver a declararse sin que el compilador lo diga.
53
+ */
54
+ connectModal?: 'branch' | 'output-group';
42
55
  routerTargetLabel?: string;
43
56
  /**
44
57
  * Generic named output groups — used by connectModal: 'output-group'.
@@ -54,6 +67,20 @@ export interface NodeUIConfig {
54
67
  nameField?: string;
55
68
  /** Field within each group for summary/description (e.g. 'field', 'filterMode') */
56
69
  summaryField?: string;
70
+ /**
71
+ * Varios campos del grupo, unidos por espacios, cuando el resumen que
72
+ * de verdad identifica al grupo no cabe en uno solo.
73
+ *
74
+ * Existe por las reglas del router: su condición son TRES campos
75
+ * (`field`, `operator`, `value`) y el modal a medida que se retiró las
76
+ * pintaba juntas — «status eq active». Con un único `summaryField` una
77
+ * regla sin `label` quedaba en «rule 1» y «status», que no dice a qué
78
+ * se está conectando uno.
79
+ *
80
+ * Los campos vacíos se saltan, para que una regla con operador
81
+ * `exists` no salga con un espacio colgando al final.
82
+ */
83
+ summaryFields?: string[];
57
84
  /** Label shown in the modal (e.g. 'output', 'branch') */
58
85
  label: string;
59
86
  /** Entity field for the fallback/else path (e.g. 'elseOutputNodes') */
@@ -80,6 +107,29 @@ export interface NodeUIConfig {
80
107
  field: string;
81
108
  value: unknown;
82
109
  };
110
+ /**
111
+ * Qué es el puerto `main` de este nodo MIENTRAS su contenedor está
112
+ * vacío. Sin esto, conectar a un nodo que ramifica y todavía no tiene
113
+ * ninguna salida escribía una arista en `main` **en silencio**, y el
114
+ * usuario no tenía forma de saber qué había pasado con ella.
115
+ *
116
+ * No hay una respuesta única, y por eso es un dato y no una regla.
117
+ * Medido en `api/src/common/pipeline-run.service.ts`:
118
+ *
119
+ * 'passthrough' — router sin reglas: `rules.length === 0
120
+ * ? refsForPort(entity, MAIN_PORT) : []`. La
121
+ * arista SÍ dispara... hasta que se cree la
122
+ * primera regla, y entonces deja de hacerlo
123
+ * sin avisar.
124
+ * 'never-dispatched' — split: no tiene salida `main`, y el propio
125
+ * despachador registra un warning por cada
126
+ * arista que se quedó en ese puerto.
127
+ *
128
+ * Se declara sólo donde se ha medido. Sin declarar, el lienzo se
129
+ * comporta como siempre: conecta por el camino universal y no dice
130
+ * nada, que es lo que hacía antes de existir este campo.
131
+ */
132
+ emptyContainerMain?: 'passthrough' | 'never-dispatched';
83
133
  };
84
134
  /**
85
135
  * Maps output fields to alternate payload fields on the source entity.
package/dist/esm/ui.js CHANGED
@@ -50,12 +50,30 @@ export const NODE_UI = {
50
50
  fileTransform: { fromNodes: true, toNodes: true },
51
51
  limit: { fromNodes: true, toNodes: true, special: { loopBackAllowed: true } },
52
52
  // ── Flow control nodes ──
53
- split: { fromNodes: true, toNodes: true, outputHandles: ['right'], special: { connectModal: 'output-group', outputGroups: { field: 'outputs', nameField: 'name', summaryField: 'field', label: 'output' } } },
53
+ // `emptyContainerMain: 'never-dispatched'` dice lo que el despachador ya
54
+ // registra como warning: un split NO tiene salida `main`, así que una arista
55
+ // que se quede ahí —la que se guardaba al conectar un split sin salidas— no
56
+ // se despacha nunca. Ver `pipeline-run.service.ts`, el bloque `nodeType ===
57
+ // 'split'`. El lienzo lo usa para avisar en vez de callarse.
58
+ split: { fromNodes: true, toNodes: true, outputHandles: ['right'], special: { connectModal: 'output-group', outputGroups: { field: 'outputs', nameField: 'name', summaryField: 'field', label: 'output', emptyContainerMain: 'never-dispatched' } } },
54
59
  merge: { fromNodes: true, toNodes: true, inputHandles: ['left', 'top'] },
55
60
  approval: { fromNodes: true, toNodes: true, outputHandles: ['right'], special: { connectModal: 'output-group', outputGroups: { fixedGroups: [{ name: 'Approve', field: 'outputNodes' }, { name: 'Reject', field: 'rejectionOutputNodes' }], label: 'action' } } },
56
61
  loop: { fromNodes: true, toNodes: true, inputHandles: ['left'], outputHandles: ['right'], dotHandles: { input: ['left-in'], output: ['right-out-loop', 'right-in-loopback', 'right-out-done'] }, special: { hasLoopBack: true, alternatePayloads: { doneOutputNodes: 'lastDonePayload' } } },
57
62
  // ── Routing nodes ──
58
- router: { fromNodes: true, toNodes: true, outputHandles: ['right'], dotHandles: { output: ['right-out'] }, special: { connectModal: 'rule' } },
63
+ // El router ramifica igual que el conditional, el split y el clasificador:
64
+ // sus salidas son un contenedor (`rules[]`) y el destino de cada una vive en
65
+ // la arista, en `port: rule:<id>`. Lo único que le faltaba era DECIRLO aquí.
66
+ //
67
+ // Mientras no lo decía, tenía en el lienzo un camino propio —`connectModal:
68
+ // 'rule'`, con su modal, su `handleRuleSelect` y su puerta en `onConnect`— y
69
+ // eso dejaba el `+` fuera: ese camino no pasa por `onConnect`, pregunta por
70
+ // `outputGroups`, y el router no tenía. Arrastrando salía el modal; pulsando
71
+ // el `+` la arista se guardaba en `main` sin preguntar nada.
72
+ //
73
+ // `nameField: 'label'` porque el `label` de una regla es opcional; cuando
74
+ // falta, el modal numera («rule 1») y el `summaryFields` de al lado pone la
75
+ // condición, que es lo que de verdad la identifica.
76
+ router: { fromNodes: true, toNodes: true, outputHandles: ['right'], dotHandles: { output: ['right-out'] }, special: { connectModal: 'output-group', outputGroups: { field: 'rules', nameField: 'label', summaryFields: ['field', 'operator', 'value'], label: 'rule', emptyContainerMain: 'passthrough' } } },
59
77
  // ── Action nodes ──
60
78
  emailAction: { fromNodes: true, toNodes: true, inputHandles: ['left', 'top'], outputHandles: ['right'], dotHandles: { input: ['left-in', 'top-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'email' } },
61
79
  gmailAction: { fromNodes: true, toNodes: true, inputHandles: ['left', 'top'], outputHandles: ['right'], dotHandles: { input: ['left-in', 'top-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'gmail' } },
package/dist/types.d.ts CHANGED
@@ -38,7 +38,20 @@ export interface NodeUIConfig {
38
38
  hasBranches?: boolean;
39
39
  isTerminal?: boolean;
40
40
  hasLoopBack?: boolean;
41
- connectModal?: 'branch' | 'rule' | 'output-group';
41
+ /**
42
+ * Aquí había también un `'rule'`, sólo para el router, y con él vivía en
43
+ * el lienzo un camino de conexión a medida: su propio modal, su propio
44
+ * `handleRuleSelect` y su propia puerta en `onConnect`. Dos mecanismos
45
+ * para lo mismo, y así es como uno se arregla y el otro no: el modal del
46
+ * router salía al ARRASTRAR y nunca al pulsar el `+`, porque ese segundo
47
+ * camino pasa por `outputGroups` — que el router no declaraba.
48
+ *
49
+ * El router ramifica igual que el conditional, el split y el clasificador,
50
+ * y su destino vive en el mismo sitio que el de ellos (`port: rule:<id>`),
51
+ * así que usa la misma maquinaria. Se quita del tipo para que no pueda
52
+ * volver a declararse sin que el compilador lo diga.
53
+ */
54
+ connectModal?: 'branch' | 'output-group';
42
55
  routerTargetLabel?: string;
43
56
  /**
44
57
  * Generic named output groups — used by connectModal: 'output-group'.
@@ -54,6 +67,20 @@ export interface NodeUIConfig {
54
67
  nameField?: string;
55
68
  /** Field within each group for summary/description (e.g. 'field', 'filterMode') */
56
69
  summaryField?: string;
70
+ /**
71
+ * Varios campos del grupo, unidos por espacios, cuando el resumen que
72
+ * de verdad identifica al grupo no cabe en uno solo.
73
+ *
74
+ * Existe por las reglas del router: su condición son TRES campos
75
+ * (`field`, `operator`, `value`) y el modal a medida que se retiró las
76
+ * pintaba juntas — «status eq active». Con un único `summaryField` una
77
+ * regla sin `label` quedaba en «rule 1» y «status», que no dice a qué
78
+ * se está conectando uno.
79
+ *
80
+ * Los campos vacíos se saltan, para que una regla con operador
81
+ * `exists` no salga con un espacio colgando al final.
82
+ */
83
+ summaryFields?: string[];
57
84
  /** Label shown in the modal (e.g. 'output', 'branch') */
58
85
  label: string;
59
86
  /** Entity field for the fallback/else path (e.g. 'elseOutputNodes') */
@@ -80,6 +107,29 @@ export interface NodeUIConfig {
80
107
  field: string;
81
108
  value: unknown;
82
109
  };
110
+ /**
111
+ * Qué es el puerto `main` de este nodo MIENTRAS su contenedor está
112
+ * vacío. Sin esto, conectar a un nodo que ramifica y todavía no tiene
113
+ * ninguna salida escribía una arista en `main` **en silencio**, y el
114
+ * usuario no tenía forma de saber qué había pasado con ella.
115
+ *
116
+ * No hay una respuesta única, y por eso es un dato y no una regla.
117
+ * Medido en `api/src/common/pipeline-run.service.ts`:
118
+ *
119
+ * 'passthrough' — router sin reglas: `rules.length === 0
120
+ * ? refsForPort(entity, MAIN_PORT) : []`. La
121
+ * arista SÍ dispara... hasta que se cree la
122
+ * primera regla, y entonces deja de hacerlo
123
+ * sin avisar.
124
+ * 'never-dispatched' — split: no tiene salida `main`, y el propio
125
+ * despachador registra un warning por cada
126
+ * arista que se quedó en ese puerto.
127
+ *
128
+ * Se declara sólo donde se ha medido. Sin declarar, el lienzo se
129
+ * comporta como siempre: conecta por el camino universal y no dice
130
+ * nada, que es lo que hacía antes de existir este campo.
131
+ */
132
+ emptyContainerMain?: 'passthrough' | 'never-dispatched';
83
133
  };
84
134
  /**
85
135
  * Maps output fields to alternate payload fields on the source entity.
package/dist/ui.js CHANGED
@@ -54,12 +54,30 @@ exports.NODE_UI = {
54
54
  fileTransform: { fromNodes: true, toNodes: true },
55
55
  limit: { fromNodes: true, toNodes: true, special: { loopBackAllowed: true } },
56
56
  // ── Flow control nodes ──
57
- split: { fromNodes: true, toNodes: true, outputHandles: ['right'], special: { connectModal: 'output-group', outputGroups: { field: 'outputs', nameField: 'name', summaryField: 'field', label: 'output' } } },
57
+ // `emptyContainerMain: 'never-dispatched'` dice lo que el despachador ya
58
+ // registra como warning: un split NO tiene salida `main`, así que una arista
59
+ // que se quede ahí —la que se guardaba al conectar un split sin salidas— no
60
+ // se despacha nunca. Ver `pipeline-run.service.ts`, el bloque `nodeType ===
61
+ // 'split'`. El lienzo lo usa para avisar en vez de callarse.
62
+ split: { fromNodes: true, toNodes: true, outputHandles: ['right'], special: { connectModal: 'output-group', outputGroups: { field: 'outputs', nameField: 'name', summaryField: 'field', label: 'output', emptyContainerMain: 'never-dispatched' } } },
58
63
  merge: { fromNodes: true, toNodes: true, inputHandles: ['left', 'top'] },
59
64
  approval: { fromNodes: true, toNodes: true, outputHandles: ['right'], special: { connectModal: 'output-group', outputGroups: { fixedGroups: [{ name: 'Approve', field: 'outputNodes' }, { name: 'Reject', field: 'rejectionOutputNodes' }], label: 'action' } } },
60
65
  loop: { fromNodes: true, toNodes: true, inputHandles: ['left'], outputHandles: ['right'], dotHandles: { input: ['left-in'], output: ['right-out-loop', 'right-in-loopback', 'right-out-done'] }, special: { hasLoopBack: true, alternatePayloads: { doneOutputNodes: 'lastDonePayload' } } },
61
66
  // ── Routing nodes ──
62
- router: { fromNodes: true, toNodes: true, outputHandles: ['right'], dotHandles: { output: ['right-out'] }, special: { connectModal: 'rule' } },
67
+ // El router ramifica igual que el conditional, el split y el clasificador:
68
+ // sus salidas son un contenedor (`rules[]`) y el destino de cada una vive en
69
+ // la arista, en `port: rule:<id>`. Lo único que le faltaba era DECIRLO aquí.
70
+ //
71
+ // Mientras no lo decía, tenía en el lienzo un camino propio —`connectModal:
72
+ // 'rule'`, con su modal, su `handleRuleSelect` y su puerta en `onConnect`— y
73
+ // eso dejaba el `+` fuera: ese camino no pasa por `onConnect`, pregunta por
74
+ // `outputGroups`, y el router no tenía. Arrastrando salía el modal; pulsando
75
+ // el `+` la arista se guardaba en `main` sin preguntar nada.
76
+ //
77
+ // `nameField: 'label'` porque el `label` de una regla es opcional; cuando
78
+ // falta, el modal numera («rule 1») y el `summaryFields` de al lado pone la
79
+ // condición, que es lo que de verdad la identifica.
80
+ router: { fromNodes: true, toNodes: true, outputHandles: ['right'], dotHandles: { output: ['right-out'] }, special: { connectModal: 'output-group', outputGroups: { field: 'rules', nameField: 'label', summaryFields: ['field', 'operator', 'value'], label: 'rule', emptyContainerMain: 'passthrough' } } },
63
81
  // ── Action nodes ──
64
82
  emailAction: { fromNodes: true, toNodes: true, inputHandles: ['left', 'top'], outputHandles: ['right'], dotHandles: { input: ['left-in', 'top-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'email' } },
65
83
  gmailAction: { fromNodes: true, toNodes: true, inputHandles: ['left', 'top'], outputHandles: ['right'], dotHandles: { input: ['left-in', 'top-in'], output: ['right-out'] }, special: { loopBackAllowed: true, routerTargetLabel: 'gmail' } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/node-types",
3
- "version": "1.72.0",
3
+ "version": "1.74.0",
4
4
  "description": "Shared node type definitions, connection rules, and dispatch config for HostWebhook",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.js",