@revenexx/studio-integrations 1.2.0 → 1.4.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +581 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.d.vue.ts +9 -0
  4. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue +53 -0
  5. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue.d.ts +9 -0
  6. package/dist/runtime/components/Integrations/EditorTip.vue +2 -15
  7. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
  8. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +15 -1
  9. package/dist/runtime/components/Integrations/NodeConfigField.vue +158 -49
  10. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +15 -1
  11. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +5 -1
  12. package/dist/runtime/components/Integrations/NodeInspector.vue +136 -81
  13. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +5 -1
  14. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
  15. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
  16. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
  17. package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
  18. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
  19. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
  20. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
  21. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
  22. package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
  23. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
  24. package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
  25. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
  26. package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
  27. package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
  28. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
  29. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
  30. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
  31. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
  32. package/dist/runtime/components/Integrations/WorkflowEditor.vue +200 -54
  33. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
  34. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
  35. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
  36. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
  37. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
  38. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
  39. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
  40. package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
  41. package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
  42. package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
  43. package/dist/runtime/composables/useCredentialTypes.js +3 -9
  44. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
  45. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  46. package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
  47. package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
  48. package/dist/runtime/composables/useIntegrationsTemplates.js +10 -8
  49. package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
  50. package/dist/runtime/composables/useIntegrationsWorkflows.js +11 -7
  51. package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
  52. package/dist/runtime/data/canvasShortcuts.js +68 -0
  53. package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
  54. package/dist/runtime/data/integrationNodeCatalog.js +8 -0
  55. package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
  56. package/dist/runtime/data/nodeVersionMove.js +65 -0
  57. package/dist/runtime/i18n/messages/de.json +83 -20
  58. package/dist/runtime/i18n/messages/en.json +83 -20
  59. package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
  60. package/dist/runtime/pages/integrations/workflows/[id].vue +158 -53
  61. package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
  62. package/dist/runtime/types/integrations.d.ts +44 -1
  63. package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
  64. package/dist/runtime/utils/apiEnvelope.js +20 -0
  65. package/dist/runtime/utils/buildSettle.d.ts +68 -0
  66. package/dist/runtime/utils/buildSettle.js +29 -0
  67. package/dist/runtime/utils/configDefaults.d.ts +18 -0
  68. package/dist/runtime/utils/configDefaults.js +7 -0
  69. package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
  70. package/dist/runtime/utils/errorPathAdvice.js +19 -0
  71. package/dist/runtime/utils/notify.d.ts +2 -2
  72. package/dist/runtime/utils/runEnvelope.d.ts +12 -0
  73. package/dist/runtime/utils/runEnvelope.js +2 -1
  74. package/dist/runtime/utils/selectOptions.d.ts +48 -0
  75. package/dist/runtime/utils/selectOptions.js +19 -0
  76. package/dist/runtime/utils/settingCondition.d.ts +48 -0
  77. package/dist/runtime/utils/settingCondition.js +84 -0
  78. package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
  79. package/dist/runtime/utils/shortcutLabel.js +48 -0
  80. package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
  81. package/dist/runtime/utils/stateNamespaceName.js +8 -0
  82. package/package.json +1 -1
@@ -0,0 +1,9 @@
1
+ type __VLS_Props = {
2
+ open: boolean;
3
+ };
4
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
+ "update:open": (args_0: boolean) => any;
6
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
7
+ "onUpdate:open"?: ((args_0: boolean) => any) | undefined;
8
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ export default _default;
@@ -1,25 +1,12 @@
1
1
  <script setup>
2
2
  import { computed } from "vue";
3
3
  import { Tooltip, TooltipContent, TooltipTrigger } from "@revenexx/studio";
4
+ import { formatShortcut } from "../../utils/shortcutLabel";
4
5
  const props = defineProps({
5
6
  label: { type: String, required: true },
6
7
  shortcut: { type: String, required: false }
7
8
  });
8
- const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "");
9
- const keys = computed(() => {
10
- if (!props.shortcut) return "";
11
- const out = [];
12
- for (const raw of props.shortcut.split("+")) {
13
- const t = raw.toLowerCase();
14
- if (t === "mod") out.push(isMac ? "\u2318" : "Ctrl");
15
- else if (t === "shift") out.push("\u21E7");
16
- else if (t === "alt") out.push(isMac ? "\u2325" : "Alt");
17
- else if (t === "enter") out.push("\u21B5");
18
- else if (t === "escape") out.push("Esc");
19
- else out.push(t.toUpperCase());
20
- }
21
- return isMac ? out.join("") : out.join("+");
22
- });
9
+ const keys = computed(() => props.shortcut ? formatShortcut(props.shortcut) : "");
23
10
  </script>
24
11
 
25
12
  <template>
@@ -3,6 +3,7 @@ import { computed } from "vue";
3
3
  import { Button } from "@revenexx/studio";
4
4
  import { IntegrationsApiError } from "../../utils/apiError";
5
5
  import { resolveLocalized } from "../../utils/localize";
6
+ import { applyingFields, staleKeys } from "../../utils/settingCondition";
6
7
  const props = defineProps({
7
8
  slug: { type: String, required: true },
8
9
  version: { type: String, required: true },
@@ -24,6 +25,13 @@ const q = useNodeConfigResolve({
24
25
  dependsOn: () => props.field.dependsOn
25
26
  });
26
27
  const resolvedFields = computed(() => q.data.value?.fields ?? []);
28
+ const applyingResolvedFields = computed(() => applyingFields(resolvedFields.value, props.config));
29
+ function writeResolved(key, value) {
30
+ emit("set", key, value);
31
+ const after = { ...props.config, [key]: value };
32
+ for (const stale of staleKeys(resolvedFields.value, key, after))
33
+ emit("set", stale, void 0);
34
+ }
27
35
  </script>
28
36
 
29
37
  <template>
@@ -67,7 +75,7 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
67
75
  <legend class="px-1 text-mono text-muted-foreground">
68
76
  {{ resolveLocalized(field.label) }}
69
77
  </legend>
70
- <template v-for="f in resolvedFields" :key="f.key">
78
+ <template v-for="f in applyingResolvedFields" :key="f.key">
71
79
  <!-- collision guard: a resolved key that shadows a static config field is skipped -->
72
80
  <p v-if="staticKeys.includes(f.key)" class="text-sm text-amber-600">
73
81
  {{ t("integration.nodeConfig.dynamicSchema.fieldCollision", { key: f.key }) }}
@@ -89,7 +97,7 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
89
97
  :static-keys="staticKeys"
90
98
  :sibling-fields="resolvedFields"
91
99
  :depth="(depth ?? 0) + 1"
92
- @set="(k, v) => emit('set', k, v)"
100
+ @set="writeResolved"
93
101
  />
94
102
  </template>
95
103
 
@@ -1,4 +1,4 @@
1
- import type { ConfigField, WorkflowVariable } from '../../types/integrations.js';
1
+ import type { ConfigField, WorkflowStateNamespace, WorkflowStateRole, WorkflowStateVisibility, WorkflowVariable } from '../../types/integrations.js';
2
2
  type __VLS_Props = {
3
3
  field: ConfigField;
4
4
  config: Record<string, unknown>;
@@ -11,6 +11,8 @@ type __VLS_Props = {
11
11
  siblingFields?: ConfigField[];
12
12
  depth?: number;
13
13
  variables?: Record<string, WorkflowVariable>;
14
+ /** The workflow's declared state namespaces — the options a `state-ref` offers. */
15
+ stateNamespaces?: WorkflowStateNamespace[];
14
16
  upstreamOutputs?: Array<{
15
17
  nodeId: string;
16
18
  nodeName?: string;
@@ -19,10 +21,22 @@ type __VLS_Props = {
19
21
  }>;
20
22
  /** Why the last save refused this setting, if it did. */
21
23
  refused?: string;
24
+ /**
25
+ * The 1-based row this setting sits in, when it is a sub-setting of a list
26
+ * row — set only by the list branch below, rendering itself.
27
+ *
28
+ * It is what keeps a repeating control nameable: the visible label repeats
29
+ * per row, so every row's control would otherwise answer to the same name
30
+ * (specs/accessibility.md AC-1). With it, each says which setting AND which
31
+ * row it belongs to.
32
+ */
33
+ rowIndex?: number;
22
34
  };
23
35
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
24
36
  set: (key: string, value: unknown) => any;
37
+ "declare-state": (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
25
38
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
26
39
  onSet?: ((key: string, value: unknown) => any) | undefined;
40
+ "onDeclare-state"?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
27
41
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
42
  export default _default;
@@ -15,7 +15,10 @@ import {
15
15
  Textarea
16
16
  } from "@revenexx/studio";
17
17
  import { resolveLocalized } from "../../utils/localize";
18
+ import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
18
19
  import { credentialState } from "../../utils/credentialState";
20
+ import { defaultsFor } from "../../utils/configDefaults";
21
+ import { applyingFields, settingApplies, staleKeys } from "../../utils/settingCondition";
19
22
  const props = defineProps({
20
23
  field: { type: Object, required: true },
21
24
  config: { type: Object, required: true },
@@ -26,10 +29,12 @@ const props = defineProps({
26
29
  siblingFields: { type: Array, required: false },
27
30
  depth: { type: Number, required: false },
28
31
  variables: { type: Object, required: false },
32
+ stateNamespaces: { type: Array, required: false },
29
33
  upstreamOutputs: { type: Array, required: false },
30
- refused: { type: String, required: false }
34
+ refused: { type: String, required: false },
35
+ rowIndex: { type: Number, required: false }
31
36
  });
32
- const emit = defineEmits(["set"]);
37
+ const emit = defineEmits(["set", "declare-state"]);
33
38
  const t = useIntegrationsT();
34
39
  const value = computed(() => props.config[props.field.key]);
35
40
  function write(v) {
@@ -37,6 +42,11 @@ function write(v) {
37
42
  }
38
43
  const exprHint = "${{ \u2026 }}";
39
44
  const descriptionText = computed(() => resolveLocalized(props.field.description));
45
+ const inRow = computed(() => props.rowIndex !== void 0);
46
+ const controlName = computed(() => {
47
+ const label = resolveLocalized(props.field.label);
48
+ return inRow.value ? t("integration.nodeConfig.array.subFieldAria", { label, n: props.rowIndex }) : label;
49
+ });
40
50
  function isEmpty(v) {
41
51
  return v === void 0 || v === null || v === "" || Array.isArray(v) && v.length === 0;
42
52
  }
@@ -106,12 +116,12 @@ const credOptions = computed(
106
116
  () => credsForField(props.field).map((c) => ({ credential: c, state: credentialState(c) }))
107
117
  );
108
118
  const UNKNOWN_CREDENTIAL = computed(() => t("integration.nodeConfig.credential.unknown"));
109
- function credTriggerLabel(field) {
110
- const label = resolveLocalized(field.label);
119
+ const credTriggerLabel = computed(() => {
120
+ const label = controlName.value;
111
121
  if (bound.value) return t("integration.nodeConfig.credential.trigger.bound", { label, name: bound.value.credential.name, state: bound.value.state.label });
112
122
  if (value.value && credentialsLoaded.value) return t("integration.nodeConfig.credential.trigger.unknown", { label, unknown: UNKNOWN_CREDENTIAL.value });
113
123
  return label;
114
- }
124
+ });
115
125
  const credCreateOpen = ref(false);
116
126
  const sftpImportOpen = ref(false);
117
127
  const CRED_ACTION_NEW = "__action_new_credential__";
@@ -127,6 +137,30 @@ function onCredentialSelect(v) {
127
137
  }
128
138
  write(v);
129
139
  }
140
+ const stateOptions = computed(() => {
141
+ const role = props.field.stateRole;
142
+ const declared = props.stateNamespaces ?? [];
143
+ const list = role ? declared.filter((n) => n.role === role) : declared;
144
+ const selected = value.value;
145
+ if (typeof selected === "string" && selected && !list.some((n) => n.namespace === selected)) {
146
+ return [{ namespace: selected, role: role ?? "mapping", visibility: void 0 }, ...list];
147
+ }
148
+ return list;
149
+ });
150
+ const stateTakenNames = computed(() => (props.stateNamespaces ?? []).map((n) => n.namespace));
151
+ const stateCreateOpen = ref(false);
152
+ const STATE_ACTION_NEW = "__action_new_state_namespace__";
153
+ function onStateSelect(v) {
154
+ if (v === STATE_ACTION_NEW) {
155
+ stateCreateOpen.value = true;
156
+ return;
157
+ }
158
+ write(v);
159
+ }
160
+ function onStateDeclared(namespace, role, visibility) {
161
+ emit("declare-state", namespace, role, visibility);
162
+ write(namespace);
163
+ }
130
164
  function acceptsSftp(field) {
131
165
  const t2 = field.credentialType;
132
166
  return t2 === "revenexx:sftp" || Array.isArray(t2) && t2.includes("revenexx:sftp");
@@ -242,17 +276,36 @@ function writeJson(text) {
242
276
  function readArray() {
243
277
  return Array.isArray(value.value) ? value.value : [];
244
278
  }
279
+ const rowKeys = ref([]);
280
+ let nextRowKey = 0;
281
+ watch(() => readArray().length, (n) => {
282
+ while (rowKeys.value.length < n) rowKeys.value.push(nextRowKey++);
283
+ if (rowKeys.value.length > n) rowKeys.value.splice(n);
284
+ }, { immediate: true });
245
285
  function pushArrayItem(defaultItem) {
246
286
  write([...readArray(), defaultItem]);
247
287
  }
288
+ function newRow() {
289
+ const declared = props.field.items?.properties ?? [];
290
+ const row = defaultsFor(declared);
291
+ for (; ; ) {
292
+ const withdrawn = declared.filter((f) => f.key in row && !settingApplies(f, row));
293
+ if (!withdrawn.length) return row;
294
+ for (const f of withdrawn) delete row[f.key];
295
+ }
296
+ }
248
297
  function removeArrayItem(index) {
249
298
  const arr = readArray().slice();
250
299
  arr.splice(index, 1);
300
+ rowKeys.value.splice(index, 1);
251
301
  write(arr);
252
302
  }
253
303
  function setArrayItemField(index, fieldKey, v) {
254
304
  const arr = readArray().slice();
255
- arr[index] = { ...arr[index] ?? {}, [fieldKey]: v };
305
+ const after = { ...arr[index] ?? {}, [fieldKey]: v };
306
+ for (const key of staleKeys(props.field.items?.properties ?? [], fieldKey, after))
307
+ delete after[key];
308
+ arr[index] = after;
256
309
  write(arr);
257
310
  }
258
311
  </script>
@@ -326,7 +379,7 @@ function setArrayItemField(index, fieldKey, v) {
326
379
 
327
380
  <template v-else-if="isJsonField">
328
381
  <Textarea
329
- :aria-label="resolveLocalized(field.label)"
382
+ :aria-label="controlName"
330
383
  :model-value="jsonDraft"
331
384
  :rows="8"
332
385
  spellcheck="false"
@@ -343,14 +396,14 @@ function setArrayItemField(index, fieldKey, v) {
343
396
  <!-- A repeating row has no room for visible labels, so each control
344
397
  names itself and says which row of which field it belongs to. -->
345
398
  <Input
346
- :aria-label="t('integration.nodeConfig.object.row.keyAria', { label: resolveLocalized(field.label), n: idx + 1 })"
399
+ :aria-label="t('integration.nodeConfig.object.row.keyAria', { label: controlName, n: idx + 1 })"
347
400
  :model-value="row.key"
348
401
  :placeholder="t('integration.nodeConfig.object.row.keyPlaceholder')"
349
402
  class="h-7 w-[38%] rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
350
403
  @update:model-value="(v) => setKvRow(idx, { key: String(v) })"
351
404
  />
352
405
  <Input
353
- :aria-label="t('integration.nodeConfig.object.row.valueAria', { label: resolveLocalized(field.label), n: idx + 1 })"
406
+ :aria-label="t('integration.nodeConfig.object.row.valueAria', { label: controlName, n: idx + 1 })"
354
407
  :model-value="row.value"
355
408
  :placeholder="t('integration.nodeConfig.object.row.valuePlaceholder')"
356
409
  class="h-7 min-w-0 flex-1 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
@@ -358,7 +411,7 @@ function setArrayItemField(index, fieldKey, v) {
358
411
  />
359
412
  <button
360
413
  type="button"
361
- :aria-label="t('integration.nodeConfig.object.row.removeAria', { label: resolveLocalized(field.label), n: idx + 1 })"
414
+ :aria-label="t('integration.nodeConfig.object.row.removeAria', { label: controlName, n: idx + 1 })"
362
415
  class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
363
416
  @click="removeKvRow(idx)"
364
417
  >
@@ -385,16 +438,22 @@ function setArrayItemField(index, fieldKey, v) {
385
438
  :variables="variables"
386
439
  :upstream-outputs="upstreamOutputs"
387
440
  :invalid="invalid"
388
- :label="resolveLocalized(field.label)"
441
+ :label="controlName"
389
442
  :multiline="isMultiline"
390
443
  @update:model-value="(v) => write(v)"
391
444
  />
392
445
 
393
446
  <!-- dynamic options — resolved at author time via config:resolve. Ahead of
394
447
  the generic expression branch so a dynamic select with
395
- expressionAllowed still renders as a dropdown by default. -->
448
+ expressionAllowed still renders as a dropdown by default.
449
+
450
+ Not inside a list row: the resolve request carries the config the
451
+ setting belongs to, and a row is not that config — the platform would
452
+ be asked about a set of values it has never heard of. No published node
453
+ declares one there; a row's setting is drawn from what it declares
454
+ instead (specs/node-configuration.md, ## Gaps). -->
396
455
  <IntegrationsNodeConfigDynamicField
397
- v-else-if="field.dynamic"
456
+ v-else-if="field.dynamic && !inRow"
398
457
  :slug="slug"
399
458
  :version="version"
400
459
  :field="field"
@@ -412,7 +471,7 @@ function setArrayItemField(index, fieldKey, v) {
412
471
  :variables="variables"
413
472
  :upstream-outputs="upstreamOutputs"
414
473
  :invalid="invalid"
415
- :label="resolveLocalized(field.label)"
474
+ :label="controlName"
416
475
  :multiline="isMultiline"
417
476
  @update:model-value="(v) => write(v)"
418
477
  />
@@ -426,7 +485,7 @@ function setArrayItemField(index, fieldKey, v) {
426
485
  shared control sizes itself to its content and ignores it. -->
427
486
  <Textarea
428
487
  v-else-if="field.type === 'string' && isMultiline"
429
- :aria-label="resolveLocalized(field.label)"
488
+ :aria-label="controlName"
430
489
  :model-value="value ?? ''"
431
490
  :placeholder="resolveLocalized(field.placeholder)"
432
491
  :rows="4"
@@ -438,7 +497,7 @@ function setArrayItemField(index, fieldKey, v) {
438
497
  <!-- plain string (no expressions) -->
439
498
  <Input
440
499
  v-else-if="field.type === 'string'"
441
- :aria-label="resolveLocalized(field.label)"
500
+ :aria-label="controlName"
442
501
  :model-value="value ?? ''"
443
502
  :placeholder="resolveLocalized(field.placeholder)"
444
503
  class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
@@ -449,7 +508,7 @@ function setArrayItemField(index, fieldKey, v) {
449
508
  <!-- number -->
450
509
  <Input
451
510
  v-else-if="field.type === 'number'"
452
- :aria-label="resolveLocalized(field.label)"
511
+ :aria-label="controlName"
453
512
  type="number"
454
513
  :model-value="value ?? ''"
455
514
  class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
@@ -460,7 +519,7 @@ function setArrayItemField(index, fieldKey, v) {
460
519
  <!-- boolean -->
461
520
  <div v-else-if="field.type === 'boolean'" class="flex items-center gap-2 pt-1">
462
521
  <Switch
463
- :aria-label="resolveLocalized(field.label)"
522
+ :aria-label="controlName"
464
523
  class="data-[state=unchecked]:bg-slate-300"
465
524
  :model-value="Boolean(value)"
466
525
  @update:model-value="(v) => write(v)"
@@ -470,9 +529,12 @@ function setArrayItemField(index, fieldKey, v) {
470
529
  </span>
471
530
  </div>
472
531
 
473
- <!-- dynamic schema — child fields resolved at author time, written into the flat config -->
532
+ <!-- dynamic schema — child fields resolved at author time, written into the
533
+ flat config. Withheld inside a list row for the same reason as the
534
+ branch above, and one more: the fields it resolves are written into the
535
+ node's flat config, which a row is not. -->
474
536
  <IntegrationsNodeConfigDynamicSchema
475
- v-else-if="field.type === 'dynamic-schema'"
537
+ v-else-if="field.type === 'dynamic-schema' && !inRow"
476
538
  :slug="slug"
477
539
  :version="version"
478
540
  :field="field"
@@ -486,21 +548,21 @@ function setArrayItemField(index, fieldKey, v) {
486
548
  <!-- select -->
487
549
  <Select
488
550
  v-else-if="field.type === 'select'"
489
- :model-value="value ?? ''"
490
- @update:model-value="(v) => write(v)"
551
+ :model-value="toModelValue(value, field.options)"
552
+ @update:model-value="(v) => write(fromModelValue(v))"
491
553
  >
492
- <SelectTrigger :aria-label="resolveLocalized(field.label)" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
554
+ <SelectTrigger :aria-label="controlName" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
493
555
  <SelectValue :placeholder="resolveLocalized(field.placeholder) || t('integration.nodeConfig.select.placeholder')" />
494
556
  </SelectTrigger>
495
557
  <SelectContent>
496
- <SelectItem v-for="opt in field.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
558
+ <SelectItem v-for="opt in field.options ?? []" :key="String(opt.value)" :value="toItemValue(opt.value)">
497
559
  {{ resolveLocalized(opt.label) }}
498
560
  </SelectItem>
499
561
  </SelectContent>
500
562
  </Select>
501
563
 
502
564
  <!-- multiselect — pill toggles for now -->
503
- <div v-else-if="field.type === 'multiselect'" role="group" :aria-label="resolveLocalized(field.label)" class="flex flex-wrap gap-1">
565
+ <div v-else-if="field.type === 'multiselect'" role="group" :aria-label="controlName" class="flex flex-wrap gap-1">
504
566
  <button
505
567
  v-for="opt in field.options ?? []"
506
568
  :key="String(opt.value)"
@@ -523,7 +585,7 @@ function setArrayItemField(index, fieldKey, v) {
523
585
  :model-value="value ?? ''"
524
586
  @update:model-value="(v) => write(v)"
525
587
  >
526
- <SelectTrigger :aria-label="resolveLocalized(field.label)" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
588
+ <SelectTrigger :aria-label="controlName" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
527
589
  <SelectValue :placeholder="t('integration.nodeConfig.secretRef.placeholder')" />
528
590
  </SelectTrigger>
529
591
  <SelectContent>
@@ -539,7 +601,7 @@ function setArrayItemField(index, fieldKey, v) {
539
601
  :model-value="value ?? ''"
540
602
  @update:model-value="(v) => onCredentialSelect(v)"
541
603
  >
542
- <SelectTrigger :aria-label="credTriggerLabel(field)" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
604
+ <SelectTrigger :aria-label="credTriggerLabel" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
543
605
  <!-- The trigger renders the bound credential itself instead of letting
544
606
  `SelectValue` echo the chosen row. Left to itself it shows that row's
545
607
  `textContent`, which glues the name and the state pill into one word —
@@ -619,11 +681,50 @@ function setArrayItemField(index, fieldKey, v) {
619
681
  />
620
682
  </template>
621
683
 
684
+ <!-- state-ref — pick one of the workflow's declared namespaces of this
685
+ field's role. Declaring lives INSIDE the picker, like creating a
686
+ credential does: a picker whose list is empty must not be a dead end
687
+ that sends the author to another dialog and back. -->
688
+ <template v-else-if="field.type === 'state-ref'">
689
+ <Select
690
+ :model-value="value ?? ''"
691
+ @update:model-value="(v) => onStateSelect(v)"
692
+ >
693
+ <SelectTrigger :aria-label="controlName" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
694
+ <SelectValue :placeholder="t('integration.nodeConfig.state.placeholder')" />
695
+ </SelectTrigger>
696
+ <SelectContent>
697
+ <SelectItem v-for="o in stateOptions" :key="o.namespace" :value="o.namespace">
698
+ <span class="flex items-center gap-1.5">
699
+ <span class="min-w-0 truncate font-mono">{{ o.namespace }}</span>
700
+ <span
701
+ v-if="o.visibility === 'shared'"
702
+ class="shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium text-muted-foreground"
703
+ >{{ t("integration.workflows.state.visibility.shared.label") }}</span>
704
+ </span>
705
+ </SelectItem>
706
+ <div v-if="!stateOptions.length" class="px-2 py-1.5 text-sm text-muted-foreground">
707
+ {{ t("integration.nodeConfig.state.empty") }}
708
+ </div>
709
+ <SelectSeparator />
710
+ <SelectItem :value="STATE_ACTION_NEW" class="font-medium text-violet-600 data-[highlighted]:text-violet-700">
711
+ {{ t("integration.nodeConfig.state.action.new") }}
712
+ </SelectItem>
713
+ </SelectContent>
714
+ </Select>
715
+ <IntegrationsStateNamespaceCreateDialog
716
+ v-model:open="stateCreateOpen"
717
+ :role="field.stateRole ?? 'mapping'"
718
+ :taken="stateTakenNames"
719
+ @declared="onStateDeclared"
720
+ />
721
+ </template>
722
+
622
723
  <!-- array of objects (e.g. headers / mappings) -->
623
724
  <div v-else-if="field.type === 'array' && field.items?.type === 'object'" class="space-y-2">
624
725
  <div
625
726
  v-for="(item, idx) in readArray()"
626
- :key="idx"
727
+ :key="rowKeys[idx] ?? idx"
627
728
  class="rounded-md border border-black/[0.06] bg-muted/20 p-2 space-y-1.5"
628
729
  >
629
730
  <div class="flex items-center justify-between">
@@ -632,39 +733,47 @@ function setArrayItemField(index, fieldKey, v) {
632
733
  </span>
633
734
  <button
634
735
  type="button"
635
- :aria-label="t('integration.nodeConfig.array.removeItemAria', { label: resolveLocalized(field.label), n: idx + 1 })"
736
+ :aria-label="t('integration.nodeConfig.array.removeItemAria', { label: controlName, n: idx + 1 })"
636
737
  class="flex size-5 items-center justify-center rounded text-muted-foreground hover:text-rose-600"
637
738
  @click="removeArrayItem(idx)"
638
739
  >
639
740
  <TrashBin :size="11" />
640
741
  </button>
641
742
  </div>
642
- <div
643
- v-for="sub in field.items.properties ?? []"
743
+ <!-- Each sub-setting is drawn by this same component, one level down,
744
+ the way a dynamic-schema child is: the row is its config, its own
745
+ `key` addresses its value in it, and what it declares — a list of
746
+ options, a switch, a description, a required marker — decides the
747
+ control, instead of every one of them being a text box.
748
+
749
+ `refused` is deliberately not handed down: it says why the LAST
750
+ SAVE refused this setting as a whole, so passing it on would mark
751
+ every sub-control of every row red with a sentence about a
752
+ different value. -->
753
+ <IntegrationsNodeConfigField
754
+ v-for="sub in applyingFields(field.items.properties ?? [], item ?? {})"
644
755
  :key="sub.key"
645
- class="space-y-1"
646
- >
647
- <Label class="text-mono text-muted-foreground">
648
- {{ resolveLocalized(sub.label) }}
649
- </Label>
650
- <!-- The label above repeats per item, so it cannot own an id; the
651
- input carries its own name including the item number. -->
652
- <Input
653
- :aria-label="t('integration.nodeConfig.array.subFieldAria', { label: resolveLocalized(sub.label), n: idx + 1 })"
654
- :model-value="item[sub.key] ?? ''"
655
- :placeholder="resolveLocalized(sub.placeholder)"
656
- class="h-7 text-sm bg-white"
657
- :class="sub.type === 'expression' ? 'font-mono' : ''"
658
- @update:model-value="(v) => setArrayItemField(idx, sub.key, v)"
659
- />
660
- </div>
756
+ :field="sub"
757
+ :config="item"
758
+ :slug="slug"
759
+ :version="version"
760
+ :secret-keys="secretKeys"
761
+ :static-keys="staticKeys"
762
+ :depth="(depth ?? 0) + 1"
763
+ :variables="variables"
764
+ :state-namespaces="stateNamespaces"
765
+ :upstream-outputs="upstreamOutputs"
766
+ :row-index="idx + 1"
767
+ @set="(k, v) => setArrayItemField(idx, k, v)"
768
+ @declare-state="(ns, role, visibility) => emit('declare-state', ns, role, visibility)"
769
+ />
661
770
  </div>
662
771
  <Button
663
772
  type="button"
664
773
  variant="outline"
665
774
  size="sm"
666
775
  class="h-7 text-sm w-full"
667
- @click="pushArrayItem(Object.fromEntries((field.items?.properties ?? []).map((p) => [p.key, ''])))"
776
+ @click="pushArrayItem(newRow())"
668
777
  >
669
778
  <Plus :size="11" />
670
779
  {{ t("integration.nodeConfig.action.addRow") }}
@@ -674,7 +783,7 @@ function setArrayItemField(index, fieldKey, v) {
674
783
  <!-- fallback: JSON textarea for complex types -->
675
784
  <Textarea
676
785
  v-else
677
- :aria-label="resolveLocalized(field.label)"
786
+ :aria-label="controlName"
678
787
  :model-value="value === void 0 ? '' : JSON.stringify(value, null, 2)"
679
788
  rows="3"
680
789
  class="rounded-md border border-black/[0.08] ring-0 shadow-none text-sm font-mono bg-white"
@@ -1,4 +1,4 @@
1
- import type { ConfigField, WorkflowVariable } from '../../types/integrations.js';
1
+ import type { ConfigField, WorkflowStateNamespace, WorkflowStateRole, WorkflowStateVisibility, WorkflowVariable } from '../../types/integrations.js';
2
2
  type __VLS_Props = {
3
3
  field: ConfigField;
4
4
  config: Record<string, unknown>;
@@ -11,6 +11,8 @@ type __VLS_Props = {
11
11
  siblingFields?: ConfigField[];
12
12
  depth?: number;
13
13
  variables?: Record<string, WorkflowVariable>;
14
+ /** The workflow's declared state namespaces — the options a `state-ref` offers. */
15
+ stateNamespaces?: WorkflowStateNamespace[];
14
16
  upstreamOutputs?: Array<{
15
17
  nodeId: string;
16
18
  nodeName?: string;
@@ -19,10 +21,22 @@ type __VLS_Props = {
19
21
  }>;
20
22
  /** Why the last save refused this setting, if it did. */
21
23
  refused?: string;
24
+ /**
25
+ * The 1-based row this setting sits in, when it is a sub-setting of a list
26
+ * row — set only by the list branch below, rendering itself.
27
+ *
28
+ * It is what keeps a repeating control nameable: the visible label repeats
29
+ * per row, so every row's control would otherwise answer to the same name
30
+ * (specs/accessibility.md AC-1). With it, each says which setting AND which
31
+ * row it belongs to.
32
+ */
33
+ rowIndex?: number;
22
34
  };
23
35
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
24
36
  set: (key: string, value: unknown) => any;
37
+ "declare-state": (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
25
38
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
26
39
  onSet?: ((key: string, value: unknown) => any) | undefined;
40
+ "onDeclare-state"?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
27
41
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
42
  export default _default;
@@ -1,9 +1,11 @@
1
- import type { NodeManifestOutput, WorkflowEdge, WorkflowNode, WorkflowTrigger, WorkflowVariable } from '../../types/integrations.js';
1
+ import type { NodeManifestOutput, WorkflowEdge, WorkflowNode, WorkflowStateNamespace, WorkflowStateRole, WorkflowStateVisibility, WorkflowTrigger, WorkflowVariable } from '../../types/integrations.js';
2
2
  type __VLS_Props = {
3
3
  /** Either a workflow node OR a trigger. Distinguished by presence of `nodeSlug`. */
4
4
  selection: WorkflowNode | WorkflowTrigger | null;
5
5
  secretKeys: string[];
6
6
  variables?: Record<string, WorkflowVariable>;
7
+ /** The workflow's declared state namespaces — options for `state-ref` fields. */
8
+ stateNamespaces?: WorkflowStateNamespace[];
7
9
  upstreamOutputs?: Array<{
8
10
  nodeId: string;
9
11
  nodeName?: string;
@@ -31,11 +33,13 @@ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {
31
33
  update: (next: WorkflowTrigger | WorkflowNode) => any;
32
34
  remove: () => any;
33
35
  close: () => any;
36
+ "declare-state": (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
34
37
  navigate: (id: string) => any;
35
38
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
36
39
  onUpdate?: ((next: WorkflowTrigger | WorkflowNode) => any) | undefined;
37
40
  onRemove?: (() => any) | undefined;
38
41
  onClose?: (() => any) | undefined;
42
+ "onDeclare-state"?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
39
43
  onNavigate?: ((id: string) => any) | undefined;
40
44
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
41
45
  export default _default;