@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
@@ -28,7 +28,8 @@ import {
28
28
  ToggleGroupItem
29
29
  } from "@revenexx/studio";
30
30
  import EventTypeCombobox from "./EventTypeCombobox.vue";
31
- import { findManifest, nodeCategoryLabel, isVersionExecutable, newestExecutableVersion } from "../../data/integrationNodeCatalog";
31
+ import { findManifest, nodeCategoryLabel, isVersionExecutable, newestExecutableVersion, executableVersionsOf, newerExecutableVersions } from "../../data/integrationNodeCatalog";
32
+ import NodeVersionMoveDialog from "./NodeVersionMoveDialog.vue";
32
33
  import {
33
34
  collectUpstreamNodes,
34
35
  inputPortsForManifest,
@@ -39,6 +40,7 @@ import {
39
40
  } from "../../data/nodeInputData";
40
41
  import { portDotClass } from "../../data/portTone";
41
42
  import { computeLoopBodies, manifestIterates } from "../../data/loopBody";
43
+ import { applyingFields, staleKeys } from "../../utils/settingCondition";
42
44
  import { deepClone } from "../../utils/deepClone";
43
45
  import { copyToClipboard } from "../../utils/clipboard";
44
46
  import { nodeDisplayLabel } from "../../utils/validationFindings";
@@ -57,6 +59,7 @@ const props = defineProps({
57
59
  selection: { type: [Object, null], required: true },
58
60
  secretKeys: { type: Array, required: true },
59
61
  variables: { type: Object, required: false },
62
+ stateNamespaces: { type: Array, required: false },
60
63
  upstreamOutputs: { type: Array, required: false },
61
64
  autofocusName: { type: Boolean, required: false },
62
65
  nodes: { type: Array, required: false },
@@ -66,17 +69,35 @@ const props = defineProps({
66
69
  workflowId: { type: [Number, String], required: false },
67
70
  refusedFields: { type: Object, required: false }
68
71
  });
69
- const emit = defineEmits(["update", "close", "remove", "navigate"]);
72
+ const emit = defineEmits(["update", "close", "remove", "navigate", "declare-state"]);
70
73
  const draft = ref(deepClone(props.selection));
71
74
  watch(() => props.selection?.id, () => {
72
- if (props.selection && props.selection.id !== draft.value.id) draft.value = deepClone(props.selection);
75
+ if (props.selection && props.selection.id !== draft.value.id) {
76
+ draft.value = deepClone(props.selection);
77
+ pendingState.value = [];
78
+ }
73
79
  });
74
80
  const isNode = computed(() => "nodeSlug" in draft.value);
75
81
  const draftNode = computed(() => isNode.value ? draft.value : null);
76
82
  const draftTrigger = computed(() => isNode.value ? null : draft.value);
77
- const dirty = computed(() => JSON.stringify(draft.value) !== JSON.stringify(props.selection));
83
+ const pendingState = ref([]);
84
+ const offeredStateNamespaces = computed(() => [
85
+ ...props.stateNamespaces ?? [],
86
+ ...pendingState.value
87
+ ]);
88
+ function stageStateNamespace(namespace, role, visibility) {
89
+ if (offeredStateNamespaces.value.some((n) => n.namespace === namespace)) return;
90
+ pendingState.value = [...pendingState.value, { namespace, role, visibility }];
91
+ }
92
+ const draftChanged = computed(() => JSON.stringify(draft.value) !== JSON.stringify(props.selection));
93
+ const dirty = computed(() => draftChanged.value || pendingState.value.length > 0);
94
+ function commit() {
95
+ for (const ns of pendingState.value) emit("declare-state", ns.namespace, ns.role, ns.visibility);
96
+ pendingState.value = [];
97
+ if (draftChanged.value) emit("update", deepClone(draft.value));
98
+ }
78
99
  function save() {
79
- if (dirty.value) emit("update", deepClone(draft.value));
100
+ commit();
80
101
  emit("close");
81
102
  }
82
103
  const nameInput = ref(null);
@@ -99,6 +120,21 @@ const readyVersion = computed(() => {
99
120
  const wn = draftNode.value;
100
121
  return wn ? newestExecutableVersion(wn.nodeSlug) : null;
101
122
  });
123
+ const versionMoveOpen = ref(false);
124
+ const newerVersions = computed(() => {
125
+ const wn = draftNode.value;
126
+ return wn ? newerExecutableVersions(wn.nodeSlug, wn.nodeVersion) : [];
127
+ });
128
+ const canMoveVersion = computed(() => {
129
+ const wn = draftNode.value;
130
+ if (!wn) return false;
131
+ const runnable = executableVersionsOf(wn.nodeSlug);
132
+ if (!runnable.length) return false;
133
+ return runnable.length > 1 || runnable[0]?.version !== wn.nodeVersion;
134
+ });
135
+ function applyVersionMove(payload) {
136
+ patchNode({ nodeVersion: payload.version, config: payload.config });
137
+ }
102
138
  const TRIGGER_LABELS = computed(() => ({
103
139
  manual: t("integration.nodeInspector.trigger.type.manual"),
104
140
  schedule: t("integration.nodeInspector.trigger.type.schedule"),
@@ -150,39 +186,34 @@ watch(showSettingsTab, (hasThem) => {
150
186
  if (!hasThem) tab.value = "parameters";
151
187
  });
152
188
  const SETTINGS_FIELD_KEY = /(^|_)(timeout|retry|retries|backoff)|(timeout|retry|retries|backoff)(ms|sec|seconds|attempts)?$/i;
153
- const parameterFields = computed(() => (manifest.value?.config ?? []).filter((f) => !SETTINGS_FIELD_KEY.test(f.key)));
154
- const settingsFields = computed(() => (manifest.value?.config ?? []).filter((f) => SETTINGS_FIELD_KEY.test(f.key)));
155
- const requiredFields = computed(() => parameterFields.value.filter((f) => f.required));
156
- const optionalFields = computed(() => parameterFields.value.filter((f) => !f.required));
189
+ const applyingConfig = computed(
190
+ () => applyingFields(manifest.value?.config ?? [], draftNode.value?.config ?? {})
191
+ );
192
+ const parameterFields = computed(() => applyingConfig.value.filter((f) => !SETTINGS_FIELD_KEY.test(f.key)));
193
+ const settingsFields = computed(() => applyingConfig.value.filter((f) => SETTINGS_FIELD_KEY.test(f.key)));
194
+ const FOLD_ABOVE = 4;
195
+ const ALWAYS_DRAWN = 3;
196
+ const foldedFields = computed(
197
+ () => parameterFields.value.length > FOLD_ABOVE ? parameterFields.value.slice(ALWAYS_DRAWN).filter((f) => !f.required) : []
198
+ );
199
+ const foldedKeys = computed(() => new Set(foldedFields.value.map((f) => f.key)));
200
+ const foldAnchorKey = computed(() => foldedFields.value[0]?.key);
157
201
  function optionalShouldOpen() {
158
- if (!requiredFields.value.length) return true;
159
- return optionalFields.value.some((f) => props.refusedFields?.[f.key] !== void 0);
202
+ if (!parameterFields.value.some((f) => f.required)) return true;
203
+ return foldedFields.value.some((f) => props.refusedFields?.[f.key] !== void 0);
160
204
  }
161
205
  const optionalOpen = ref(optionalShouldOpen());
162
206
  watch([() => props.selection?.id, manifest], () => {
163
207
  optionalOpen.value = optionalShouldOpen();
164
208
  });
165
- function dependentKeys(changedKey) {
166
- const fields = manifest.value?.config ?? [];
167
- const out = /* @__PURE__ */ new Set();
168
- const queue = [changedKey];
169
- while (queue.length > 0) {
170
- const dep = queue.shift();
171
- for (const f of fields) {
172
- if ((f.dependsOn ?? []).includes(dep) && !out.has(f.key)) {
173
- out.add(f.key);
174
- queue.push(f.key);
175
- }
176
- }
177
- }
178
- out.delete(changedKey);
179
- return out;
209
+ function dependentKeys(changedKey, after) {
210
+ return staleKeys(manifest.value?.config ?? [], changedKey, after);
180
211
  }
181
212
  function writeConfig(key, value) {
182
213
  const wn = draftNode.value;
183
214
  if (!wn) return;
184
215
  const next = { ...wn.config, [key]: value };
185
- const cleared = dependentKeys(key);
216
+ const cleared = dependentKeys(key, next);
186
217
  for (const k of cleared) delete next[k];
187
218
  const clearedDynamicSchema = [...cleared].some(
188
219
  (k) => (manifest.value?.config ?? []).some((f) => f.key === k && f.type === "dynamic-schema")
@@ -760,7 +791,7 @@ const hasNext = computed(() => seqIndex.value >= 0 && seqIndex.value < sequence.
760
791
  function navigate(offset) {
761
792
  const target = sequence.value[seqIndex.value + offset];
762
793
  if (!target) return;
763
- if (dirty.value) emit("update", deepClone(draft.value));
794
+ commit();
764
795
  emit("navigate", target);
765
796
  }
766
797
  const outputView = ref("schema");
@@ -866,7 +897,16 @@ async function runTestStep() {
866
897
  try {
867
898
  const res = await api.post(
868
899
  `/nodes/${encodeURIComponent(wn.nodeSlug)}/${encodeURIComponent(wn.nodeVersion)}/execute:test`,
869
- { config: wn.config ?? {}, inputs }
900
+ {
901
+ config: wn.config ?? {},
902
+ inputs,
903
+ // Names the workflow being edited, which is what lets the test run READ
904
+ // the state namespaces this workflow declares (PO-374). Writes stay
905
+ // refused: a correlation created by a test click would be
906
+ // indistinguishable from one a real run made. Omitted for an unsaved
907
+ // workflow, which has nothing declared yet anyway.
908
+ ...props.workflowId ? { workflow_id: Number(props.workflowId) } : {}
909
+ }
870
910
  );
871
911
  testLogs.value = res.logs ?? [];
872
912
  testOutputs.value = {
@@ -959,6 +999,26 @@ async function runTestStep() {
959
999
  </PopoverContent>
960
1000
  </Popover>
961
1001
  </div>
1002
+ <!-- The version, once it is something the author can change (PO-339).
1003
+ Deliberately a line of its own and deliberately grey: where a newer
1004
+ version merely exists, nothing is wrong, and a coloured mark for a
1005
+ non-problem is what teaches people to stop reading marks. It is
1006
+ absent altogether for a node with one runnable version — there is no
1007
+ choice to offer — and for a node whose pinned version cannot run,
1008
+ where the notice below the tabs carries the same route in the louder
1009
+ tone that state has earned. -->
1010
+ <div v-if="isNode && canMoveVersion && versionExecutable" class="flex items-center gap-1 text-xs text-muted-foreground">
1011
+ <span v-if="newerVersions.length" class="tabular-nums">
1012
+ {{ t("integration.nodeInspector.version.available", { version: newerVersions[0]?.version ?? "" }) }}
1013
+ </span>
1014
+ <button
1015
+ type="button"
1016
+ class="rounded-sm underline decoration-dotted underline-offset-2 hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
1017
+ @click="versionMoveOpen = true"
1018
+ >
1019
+ {{ t("integration.nodeInspector.version.change") }}
1020
+ </button>
1021
+ </div>
962
1022
  </div>
963
1023
  <span class="shrink-0 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider text-slate-600">
964
1024
  {{ groupLabel }}
@@ -1298,6 +1358,18 @@ async function runTestStep() {
1298
1358
  class="mx-4 mb-1 shrink-0 rounded-md border border-rose-200 bg-rose-50 px-2.5 py-1.5 text-sm text-rose-700"
1299
1359
  >
1300
1360
  {{ readyVersion ? t("integration.nodeInspector.notice.versionNotReady.upgrade", { version: draftNode?.nodeVersion ?? "", ready: readyVersion }) : t("integration.nodeInspector.notice.versionNotReady.none", { version: draftNode?.nodeVersion ?? "" }) }}
1361
+ <!-- The sentence named a version and led nowhere (PO-339). It leads
1362
+ here now; where the node has no runnable version at all there is
1363
+ still nothing to offer, and the notice stays the statement it
1364
+ was. -->
1365
+ <button
1366
+ v-if="readyVersion && canMoveVersion"
1367
+ type="button"
1368
+ class="ml-1 rounded-sm font-medium underline underline-offset-2 hover:text-rose-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-400"
1369
+ @click="versionMoveOpen = true"
1370
+ >
1371
+ {{ t("integration.nodeInspector.version.moveTo", { version: readyVersion }) }}
1372
+ </button>
1301
1373
  </p>
1302
1374
 
1303
1375
  <div
@@ -1738,63 +1810,39 @@ async function runTestStep() {
1738
1810
  </button>
1739
1811
  </div>
1740
1812
  </div>
1741
- <IntegrationsNodeConfigField
1742
- v-for="field in requiredFields"
1743
- :key="field.key"
1744
- :field="field"
1745
- :config="draftNode.config ?? {}"
1746
- :slug="draftNode.nodeSlug"
1747
- :version="draftNode.nodeVersion"
1748
- :secret-keys="secretKeys"
1749
- :static-keys="(manifest.config ?? []).map((f2) => f2.key)"
1750
- :sibling-fields="manifest.config ?? []"
1751
- :depth="0"
1752
- :variables="variables"
1753
- :upstream-outputs="upstreamOutputs"
1754
- :refused="refusedFields?.[field.key]"
1755
- @set="(k, v) => writeConfig(k, v)"
1756
- />
1757
-
1758
- <!-- Optional settings, folded. The count is on the control on
1759
- purpose: "Show 9 optional settings" is a decision the user can
1760
- take without opening it, "Show more" is not. -->
1761
- <template v-if="optionalFields.length">
1813
+ <!-- One list, in the order the node declared it. The fold's
1814
+ control is drawn at the first setting it hides rather than
1815
+ under the form, so opening it inserts the tail where it
1816
+ belongs instead of moving anything. The count is on the
1817
+ control on purpose: "Show 9 optional settings" is a decision
1818
+ the user can take without opening it, "Show more" is not. -->
1819
+ <template v-for="field in parameterFields" :key="field.key">
1762
1820
  <button
1763
- v-if="!optionalOpen"
1821
+ v-if="field.key === foldAnchorKey"
1764
1822
  type="button"
1765
1823
  class="flex items-center gap-1.5 rounded px-1 py-1 text-sm text-muted-foreground transition hover:text-foreground"
1766
- @click="optionalOpen = true"
1824
+ @click="optionalOpen = !optionalOpen"
1767
1825
  >
1768
- <ChevronRight :size="13" />
1769
- {{ t("integration.nodeInspector.parameters.optional.show", optionalFields.length) }}
1826
+ <component :is="optionalOpen ? ChevronDown : ChevronRight" :size="13" />
1827
+ {{ optionalOpen ? t("integration.nodeInspector.parameters.optional.hide", foldedFields.length) : t("integration.nodeInspector.parameters.optional.show", foldedFields.length) }}
1770
1828
  </button>
1771
- <template v-else>
1772
- <button
1773
- v-if="requiredFields.length"
1774
- type="button"
1775
- class="flex items-center gap-1.5 rounded px-1 py-1 text-sm text-muted-foreground transition hover:text-foreground"
1776
- @click="optionalOpen = false"
1777
- >
1778
- <ChevronDown :size="13" />
1779
- {{ t("integration.nodeInspector.parameters.optional.hide", optionalFields.length) }}
1780
- </button>
1781
- <IntegrationsNodeConfigField
1782
- v-for="field in optionalFields"
1783
- :key="field.key"
1784
- :field="field"
1785
- :config="draftNode.config ?? {}"
1786
- :slug="draftNode.nodeSlug"
1787
- :version="draftNode.nodeVersion"
1788
- :secret-keys="secretKeys"
1789
- :static-keys="(manifest.config ?? []).map((f2) => f2.key)"
1790
- :sibling-fields="manifest.config ?? []"
1791
- :depth="0"
1792
- :variables="variables"
1793
- :upstream-outputs="upstreamOutputs"
1794
- :refused="refusedFields?.[field.key]"
1795
- @set="(k, v) => writeConfig(k, v)"
1796
- />
1797
- </template>
1829
+ <IntegrationsNodeConfigField
1830
+ v-if="optionalOpen || !foldedKeys.has(field.key)"
1831
+ :field="field"
1832
+ :config="draftNode.config ?? {}"
1833
+ :slug="draftNode.nodeSlug"
1834
+ :version="draftNode.nodeVersion"
1835
+ :secret-keys="secretKeys"
1836
+ :static-keys="(manifest.config ?? []).map((f2) => f2.key)"
1837
+ :sibling-fields="manifest.config ?? []"
1838
+ :depth="0"
1839
+ :variables="variables"
1840
+ :state-namespaces="offeredStateNamespaces"
1841
+ @declare-state="stageStateNamespace"
1842
+ :upstream-outputs="upstreamOutputs"
1843
+ :refused="refusedFields?.[field.key]"
1844
+ @set="(k, v) => writeConfig(k, v)"
1845
+ />
1798
1846
  </template>
1799
1847
 
1800
1848
  <p v-if="!parameterFields.length" class="text-sm text-muted-foreground italic">
@@ -1832,6 +1880,8 @@ async function runTestStep() {
1832
1880
  :sibling-fields="manifest?.config ?? []"
1833
1881
  :depth="0"
1834
1882
  :variables="variables"
1883
+ :state-namespaces="offeredStateNamespaces"
1884
+ @declare-state="stageStateNamespace"
1835
1885
  :upstream-outputs="upstreamOutputs"
1836
1886
  :refused="refusedFields?.[field.key]"
1837
1887
  @set="(k, v) => writeConfig(k, v)"
@@ -2121,6 +2171,11 @@ async function runTestStep() {
2121
2171
  </Button>
2122
2172
  </div>
2123
2173
  </div>
2174
+
2175
+ <!-- Asked outside the panes so it is not clipped by either, and mounted
2176
+ unconditionally: the two places that open it sit in different parts of
2177
+ the dialog and neither owns it. -->
2178
+ <NodeVersionMoveDialog v-model:open="versionMoveOpen" :node="draftNode" @move="applyVersionMove" />
2124
2179
  </div>
2125
2180
  </template>
2126
2181
 
@@ -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;
@@ -0,0 +1,21 @@
1
+ import type { WorkflowNode } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ node: WorkflowNode | null;
4
+ };
5
+ type __VLS_PublicProps = __VLS_Props & {
6
+ 'open'?: boolean;
7
+ };
8
+ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ "update:open": (value: boolean) => any;
10
+ move: (payload: {
11
+ version: string;
12
+ config: Record<string, unknown>;
13
+ }) => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
+ "onUpdate:open"?: ((value: boolean) => any) | undefined;
16
+ onMove?: ((payload: {
17
+ version: string;
18
+ config: Record<string, unknown>;
19
+ }) => any) | undefined;
20
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ export default _default;
@@ -0,0 +1,133 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogContent,
6
+ AlertDialogDescription,
7
+ AlertDialogFooter,
8
+ AlertDialogHeader,
9
+ AlertDialogTitle,
10
+ Button
11
+ } from "@revenexx/studio";
12
+ import { executableVersionsOf, findManifest, newestExecutableVersion } from "../../data/integrationNodeCatalog";
13
+ import { planVersionMove } from "../../data/nodeVersionMove";
14
+ import { resolveLocalized } from "../../utils/localize";
15
+ const open = defineModel("open", { type: Boolean, ...{ default: false } });
16
+ const props = defineProps({
17
+ node: { type: [Object, null], required: true }
18
+ });
19
+ const emit = defineEmits(["move"]);
20
+ const t = useIntegrationsT();
21
+ const versions = computed(() => props.node ? executableVersionsOf(props.node.nodeSlug) : []);
22
+ const target = ref("");
23
+ watch(open, (isOpen) => {
24
+ if (!isOpen || !props.node) return;
25
+ const newest = newestExecutableVersion(props.node.nodeSlug);
26
+ target.value = newest && newest !== props.node.nodeVersion ? newest : props.node.nodeVersion;
27
+ });
28
+ const plan = computed(() => {
29
+ const n = props.node;
30
+ if (!n) return null;
31
+ return planVersionMove(
32
+ findManifestExact(n.nodeSlug, n.nodeVersion),
33
+ findManifestExact(n.nodeSlug, target.value),
34
+ n.config
35
+ );
36
+ });
37
+ function findManifestExact(slug, version) {
38
+ const m = findManifest(slug, version);
39
+ return m && m.version === version ? m : null;
40
+ }
41
+ const unchanged = computed(() => !props.node || target.value === props.node.nodeVersion);
42
+ function labelOf(entry) {
43
+ return resolveLocalized(entry.field?.label) || entry.key;
44
+ }
45
+ function confirm() {
46
+ const p = plan.value;
47
+ if (!props.node || !p || unchanged.value) return;
48
+ emit("move", { version: target.value, config: p.nextConfig });
49
+ open.value = false;
50
+ }
51
+ </script>
52
+
53
+ <template>
54
+ <AlertDialog :open="open" @update:open="(v) => {
55
+ if (!v) open = false;
56
+ }">
57
+ <AlertDialogContent>
58
+ <AlertDialogHeader>
59
+ <AlertDialogTitle>{{ t("integration.nodeInspector.version.move.title") }}</AlertDialogTitle>
60
+ <AlertDialogDescription>
61
+ {{ t("integration.nodeInspector.version.move.description") }}
62
+ </AlertDialogDescription>
63
+ </AlertDialogHeader>
64
+
65
+ <div v-if="props.node && plan" class="space-y-3 text-sm">
66
+ <div class="flex items-center gap-2">
67
+ <span class="tabular-nums text-muted-foreground">v{{ props.node.nodeVersion }}</span>
68
+ <span aria-hidden="true">→</span>
69
+ <select
70
+ v-model="target"
71
+ class="rounded border border-black/10 bg-white px-1.5 py-0.5 tabular-nums"
72
+ :aria-label="t('integration.nodeInspector.version.move.targetAria')"
73
+ >
74
+ <option v-for="m in versions" :key="m.version" :value="m.version">
75
+ v{{ m.version }}
76
+ </option>
77
+ </select>
78
+ </div>
79
+
80
+ <!-- A version the catalogue no longer carries cannot be compared against,
81
+ so the lists below are what arrives rather than what was checked. Said
82
+ once, here, instead of marking every setting as suspect. -->
83
+ <p v-if="!plan.comparable" class="text-amber-600">
84
+ {{ t("integration.nodeInspector.version.move.notComparable", { version: props.node.nodeVersion }) }}
85
+ </p>
86
+
87
+ <p v-if="unchanged" class="text-muted-foreground">
88
+ {{ t("integration.nodeInspector.version.move.same") }}
89
+ </p>
90
+
91
+ <template v-else>
92
+ <p v-if="plan.lossless" class="text-muted-foreground">
93
+ {{ t("integration.nodeInspector.version.move.lossless") }}
94
+ </p>
95
+
96
+ <div v-if="plan.dropped.length">
97
+ <p class="font-medium">{{ t("integration.nodeInspector.version.move.dropped") }}</p>
98
+ <ul class="mt-0.5 list-disc pl-5 text-muted-foreground">
99
+ <li v-for="e in plan.dropped" :key="e.key">
100
+ {{ labelOf(e) }}
101
+ <!-- The key, where it is not already the whole label. A setting no
102
+ version declares any more has nothing but its key to go by, and
103
+ printing that twice reads as a bug rather than as precision. -->
104
+ <span v-if="labelOf(e) !== e.key" class="text-xs">({{ e.key }})</span>
105
+ </li>
106
+ </ul>
107
+ </div>
108
+
109
+ <div v-if="plan.review.length">
110
+ <p class="font-medium">{{ t("integration.nodeInspector.version.move.review") }}</p>
111
+ <ul class="mt-0.5 list-disc pl-5 text-muted-foreground">
112
+ <li v-for="e in plan.review" :key="e.key">
113
+ {{ labelOf(e) }}
114
+ <span class="text-xs">
115
+ ({{ e.reason === "nowRequired" ? t("integration.nodeInspector.version.move.reason.nowRequired") : t("integration.nodeInspector.version.move.reason.changed") }})
116
+ </span>
117
+ </li>
118
+ </ul>
119
+ </div>
120
+ </template>
121
+ </div>
122
+
123
+ <AlertDialogFooter>
124
+ <Button variant="outline" @click="open = false">
125
+ {{ t("integration.common.action.cancel") }}
126
+ </Button>
127
+ <Button :disabled="unchanged" @click="confirm">
128
+ {{ t("integration.nodeInspector.version.move.confirm") }}
129
+ </Button>
130
+ </AlertDialogFooter>
131
+ </AlertDialogContent>
132
+ </AlertDialog>
133
+ </template>
@@ -0,0 +1,21 @@
1
+ import type { WorkflowNode } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ node: WorkflowNode | null;
4
+ };
5
+ type __VLS_PublicProps = __VLS_Props & {
6
+ 'open'?: boolean;
7
+ };
8
+ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ "update:open": (value: boolean) => any;
10
+ move: (payload: {
11
+ version: string;
12
+ config: Record<string, unknown>;
13
+ }) => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
+ "onUpdate:open"?: ((value: boolean) => any) | undefined;
16
+ onMove?: ((payload: {
17
+ version: string;
18
+ config: Record<string, unknown>;
19
+ }) => any) | undefined;
20
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ export default _default;