@revenexx/studio-integrations 0.6.0 → 1.0.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 (137) hide show
  1. package/CHANGELOG.md +643 -0
  2. package/README.md +31 -0
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +12 -3
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +32 -28
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +45 -38
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +21 -15
  13. package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
  14. package/dist/runtime/components/Integrations/Layout.vue +28 -3
  15. package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +30 -16
  17. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
  18. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -8
  19. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
  20. package/dist/runtime/components/Integrations/NodeConfigField.vue +45 -42
  21. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
  22. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
  23. package/dist/runtime/components/Integrations/NodeInspector.vue +315 -221
  24. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
  25. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
  26. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +37 -10
  27. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
  28. package/dist/runtime/components/Integrations/NodePalette.vue +40 -35
  29. package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
  30. package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
  31. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  32. package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
  33. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  34. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  35. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  36. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  37. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  38. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  39. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  40. package/dist/runtime/components/Integrations/StickyNoteView.vue +82 -34
  41. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  42. package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
  43. package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
  44. package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
  45. package/dist/runtime/components/Integrations/TemplateCard.vue +18 -8
  46. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +22 -12
  47. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  48. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +41 -39
  49. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  50. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
  51. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
  52. package/dist/runtime/components/Integrations/WorkflowEditor.vue +131 -87
  53. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
  54. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
  55. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +27 -24
  56. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
  57. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
  58. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +76 -46
  59. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +55 -43
  60. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
  61. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +55 -43
  62. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
  63. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
  64. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -13
  65. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +79 -39
  66. package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
  67. package/dist/runtime/composables/integrationsApi.js +4 -2
  68. package/dist/runtime/composables/useCredentialTypes.js +4 -2
  69. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
  70. package/dist/runtime/composables/useIntegrationsCredentials.js +8 -6
  71. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  72. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  73. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  74. package/dist/runtime/composables/useIntegrationsNodes.js +14 -2
  75. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  76. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  77. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  78. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  79. package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
  80. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +3 -2
  81. package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
  82. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  83. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  84. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  85. package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
  86. package/dist/runtime/composables/useIntegrationsTriggers.d.ts +23 -3
  87. package/dist/runtime/composables/useIntegrationsTriggers.js +21 -8
  88. package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
  89. package/dist/runtime/composables/useIntegrationsWorkflows.js +30 -7
  90. package/dist/runtime/composables/useNodeConfigResolve.js +4 -2
  91. package/dist/runtime/data/integrationNodeCatalog.d.ts +63 -1
  92. package/dist/runtime/data/integrationNodeCatalog.js +22 -4
  93. package/dist/runtime/data/integrationTemplates.d.ts +9 -0
  94. package/dist/runtime/data/integrationTemplates.js +7 -1
  95. package/dist/runtime/data/nodeInputData.d.ts +58 -1
  96. package/dist/runtime/data/nodeInputData.js +30 -5
  97. package/dist/runtime/data/payloadForm.js +2 -1
  98. package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
  99. package/dist/runtime/data/triggerGlyphs.js +11 -0
  100. package/dist/runtime/data/workflowIconCatalog.js +2 -1
  101. package/dist/runtime/i18n/messages/de.json +1224 -0
  102. package/dist/runtime/i18n/messages/en.json +1224 -0
  103. package/dist/runtime/i18n/translate.d.ts +83 -0
  104. package/dist/runtime/i18n/translate.js +41 -0
  105. package/dist/runtime/pages/integrations/audit.vue +52 -34
  106. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
  107. package/dist/runtime/pages/integrations/credentials.vue +73 -52
  108. package/dist/runtime/pages/integrations/index.vue +66 -73
  109. package/dist/runtime/pages/integrations/runs.vue +33 -21
  110. package/dist/runtime/pages/integrations/schedules.vue +49 -28
  111. package/dist/runtime/pages/integrations/secrets.vue +34 -34
  112. package/dist/runtime/pages/integrations/templates/[slug].vue +89 -72
  113. package/dist/runtime/pages/integrations/templates/index.vue +35 -27
  114. package/dist/runtime/pages/integrations/workflows/[id].vue +351 -197
  115. package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
  116. package/dist/runtime/plugins/locale.d.ts +10 -0
  117. package/dist/runtime/plugins/locale.js +6 -0
  118. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  119. package/dist/runtime/plugins/realtime.client.js +4 -0
  120. package/dist/runtime/types/integrations.d.ts +25 -1
  121. package/dist/runtime/utils/cockpitSources.d.ts +22 -3
  122. package/dist/runtime/utils/credentialState.d.ts +30 -2
  123. package/dist/runtime/utils/credentialState.js +26 -11
  124. package/dist/runtime/utils/localize.d.ts +52 -5
  125. package/dist/runtime/utils/localize.js +47 -9
  126. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  127. package/dist/runtime/utils/pollBudget.js +17 -0
  128. package/dist/runtime/utils/schedule.d.ts +0 -25
  129. package/dist/runtime/utils/schedule.js +37 -25
  130. package/dist/runtime/utils/servedPaths.d.ts +22 -0
  131. package/dist/runtime/utils/servedPaths.js +3 -0
  132. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  133. package/dist/runtime/utils/statusStyle.js +9 -1
  134. package/dist/runtime/utils/validationFindings.d.ts +2 -2
  135. package/dist/runtime/utils/validationFindings.js +4 -3
  136. package/dist/types.d.mts +2 -6
  137. package/package.json +3 -2
@@ -2,7 +2,8 @@
2
2
  import { computed, nextTick, ref } from "vue";
3
3
  import { Handle, Position, useVueFlow } from "@vue-flow/core";
4
4
  import { NodeToolbar } from "@vue-flow/node-toolbar";
5
- import { ChevronsDownUp, ChevronsUpDown, Trash2, Ungroup } from "lucide-vue-next";
5
+ import { ChevronsDownUp, ChevronsUpDown, Ungroup } from "lucide-vue-next";
6
+ import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
6
7
  import { resolveDisplayName } from "../../utils/localize";
7
8
  const props = defineProps({
8
9
  id: { type: String, required: true },
@@ -10,22 +11,28 @@ const props = defineProps({
10
11
  selected: { type: Boolean, required: false }
11
12
  });
12
13
  defineEmits(["updateNodeInternals"]);
14
+ const t = useIntegrationsT();
13
15
  const GROUP_COLORS = [
14
- { name: "Amber", bg: "rgba(254,243,199,0.45)", border: "#fcd34d", chip: "#f59e0b" },
15
- { name: "Orange", bg: "rgba(255,237,213,0.45)", border: "#fdba74", chip: "#f97316" },
16
- { name: "Rose", bg: "rgba(255,228,230,0.45)", border: "#fda4af", chip: "#f43f5e" },
17
- { name: "Green", bg: "rgba(220,252,231,0.45)", border: "#86efac", chip: "#10b981" },
18
- { name: "Blue", bg: "rgba(219,234,254,0.45)", border: "#93c5fd", chip: "#3b82f6" },
19
- { name: "Violet", bg: "rgba(237,233,254,0.45)", border: "#c4b5fd", chip: "#8b5cf6" },
20
- { name: "Gray", bg: "rgba(245,245,244,0.6)", border: "#d6d3d1", chip: "#78716c" }
16
+ { nameKey: "integration.canvas.color.amber", bg: "rgba(254,243,199,0.45)", border: "#fcd34d", chip: "#f59e0b" },
17
+ { nameKey: "integration.canvas.color.orange", bg: "rgba(255,237,213,0.45)", border: "#fdba74", chip: "#f97316" },
18
+ { nameKey: "integration.canvas.color.rose", bg: "rgba(255,228,230,0.45)", border: "#fda4af", chip: "#f43f5e" },
19
+ { nameKey: "integration.canvas.color.green", bg: "rgba(220,252,231,0.45)", border: "#86efac", chip: "#10b981" },
20
+ { nameKey: "integration.canvas.color.blue", bg: "rgba(219,234,254,0.45)", border: "#93c5fd", chip: "#3b82f6" },
21
+ { nameKey: "integration.canvas.color.violet", bg: "rgba(237,233,254,0.45)", border: "#c4b5fd", chip: "#8b5cf6" },
22
+ { nameKey: "integration.canvas.color.gray", bg: "rgba(245,245,244,0.6)", border: "#d6d3d1", chip: "#78716c" }
21
23
  ];
22
24
  const color = computed(() => GROUP_COLORS[props.data.group.color ?? 4] ?? GROUP_COLORS[4]);
23
- const label = computed(() => resolveDisplayName(props.data.group.label) || "Group");
25
+ const label = computed(() => resolveDisplayName(props.data.group.label) || t("integration.canvas.group.defaultLabel"));
24
26
  const collapsed = computed(() => props.data.group.collapsed === true);
25
27
  const editable = computed(() => !!props.data.actions);
26
28
  const editing = ref(false);
27
29
  const draft = ref("");
28
30
  const inputEl = ref(null);
31
+ function onLabelDoubleClick(event) {
32
+ if (!editable.value) return;
33
+ event.stopPropagation();
34
+ startRename();
35
+ }
29
36
  function startRename() {
30
37
  if (!editable.value) return;
31
38
  draft.value = label.value;
@@ -51,7 +58,7 @@ function onHoverLeave() {
51
58
  }, 250);
52
59
  }
53
60
  const toolbarVisible = computed(() => editable.value && !editing.value && (hovered.value || !!props.selected));
54
- const memberCountText = computed(() => `${props.data.memberCount} node${props.data.memberCount === 1 ? "" : "s"}`);
61
+ const memberCountText = computed(() => t("integration.canvas.group.memberCount", props.data.memberCount));
55
62
  const flow = useVueFlow();
56
63
  const resizing = ref(false);
57
64
  const liveW = ref(0);
@@ -96,15 +103,15 @@ function onResizeEnd() {
96
103
  class="size-4.5 rounded-full border transition hover:scale-110"
97
104
  :class="(data.group.color ?? 4) === i ? 'ring-2 ring-black/25 ring-offset-1' : ''"
98
105
  :style="{ backgroundColor: c.chip, borderColor: c.border, width: '16px', height: '16px' }"
99
- :title="c.name"
100
- :aria-label="c.name"
106
+ :title="t(c.nameKey)"
107
+ :aria-label="t(c.nameKey)"
101
108
  @click="data.actions?.setColor(i)"
102
109
  />
103
110
  <div class="mx-0.5 h-4 w-px bg-black/10" />
104
111
  <button
105
112
  type="button"
106
113
  class="flex size-6 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
107
- :title="collapsed ? 'Expand group' : 'Collapse group'"
114
+ :title="collapsed ? t('integration.canvas.group.action.expand') : t('integration.canvas.group.action.collapse')"
108
115
  @click="data.actions?.toggleCollapse()"
109
116
  >
110
117
  <component :is="collapsed ? ChevronsUpDown : ChevronsDownUp" :size="13" />
@@ -112,7 +119,7 @@ function onResizeEnd() {
112
119
  <button
113
120
  type="button"
114
121
  class="flex size-6 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
115
- title="Ungroup (keep the nodes)"
122
+ :title="t('integration.canvas.group.action.ungroup')"
116
123
  @click="data.actions?.ungroup()"
117
124
  >
118
125
  <Ungroup :size="13" />
@@ -120,10 +127,10 @@ function onResizeEnd() {
120
127
  <button
121
128
  type="button"
122
129
  class="flex size-6 items-center justify-center rounded-md text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
123
- title="Remove group frame"
130
+ :title="t('integration.canvas.group.action.removeFrame')"
124
131
  @click="data.actions?.ungroup()"
125
132
  >
126
- <Trash2 :size="13" />
133
+ <TrashBin :size="13" />
127
134
  </button>
128
135
  </div>
129
136
  </NodeToolbar>
@@ -136,6 +143,9 @@ function onResizeEnd() {
136
143
  :position="Position.Top"
137
144
  class="!size-3 !rounded-full !border-2 !border-white !bg-slate-500 !shadow-sm"
138
145
  />
146
+ <!-- The collapsed card stops its own double-click unconditionally, which is
147
+ safe: both read-only panels build every frame with `collapsed: false`, so
148
+ this surface is the editor's alone. -->
139
149
  <div
140
150
  class="w-[220px] rounded-lg border-2 bg-white transition-all duration-150"
141
151
  :style="{ borderColor: color.border }"
@@ -183,7 +193,7 @@ function onResizeEnd() {
183
193
  v-if="editing"
184
194
  ref="inputEl"
185
195
  v-model="draft"
186
- aria-label="Group name"
196
+ :aria-label="t('integration.canvas.group.input.nameAria')"
187
197
  class="nodrag w-40 rounded border border-black/[0.1] bg-white px-1 py-0.5 text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-primary/40"
188
198
  @blur="commitRename"
189
199
  @keydown.enter.prevent="commitRename"
@@ -194,7 +204,7 @@ function onResizeEnd() {
194
204
  v-else
195
205
  class="select-none text-sm font-semibold"
196
206
  :style="{ color: color.chip }"
197
- @dblclick.stop="startRename"
207
+ @dblclick="onLabelDoubleClick"
198
208
  >
199
209
  {{ label }}
200
210
  </span>
@@ -206,7 +216,7 @@ function onResizeEnd() {
206
216
  v-if="editable"
207
217
  class="nodrag pointer-events-auto absolute bottom-0 right-0 flex size-5 cursor-se-resize items-end justify-end p-1"
208
218
  :style="{ color: color.chip }"
209
- title="Resize group"
219
+ :title="t('integration.canvas.group.action.resize')"
210
220
  @pointerdown="onResizeStart"
211
221
  @pointermove="onResizeMove"
212
222
  @pointerup="onResizeEnd"
@@ -1,6 +1,12 @@
1
1
  <script setup>
2
+ import { nextTick, onMounted, ref } from "vue";
2
3
  import Sidebar from "./Sidebar.vue";
4
+ const t = useIntegrationsT();
3
5
  const { focused } = useIntegrationsFocus();
6
+ const animate = ref(false);
7
+ onMounted(() => nextTick(() => {
8
+ animate.value = true;
9
+ }));
4
10
  </script>
5
11
 
6
12
  <template>
@@ -10,11 +16,30 @@ const { focused } = useIntegrationsFocus();
10
16
  href="#integrations-main"
11
17
  class="sr-only focus:not-sr-only focus:absolute focus:left-3 focus:top-3 focus:z-50 focus:rounded-md focus:bg-white focus:px-3 focus:py-1.5 focus:text-sm focus:font-medium focus:ring-2 focus:ring-primary/40"
12
18
  >
13
- Skip to content
19
+ {{ t("integration.shell.action.skipToContent") }}
14
20
  </a>
21
+ <!-- `inert` + `aria-hidden` are what make the fold real. The inner wrapper
22
+ below keeps its own `w-64` so the slide-out animates, so a folded rail
23
+ is merely clipped by `overflow-hidden`: its links keep their size and
24
+ stay focusable at a negative x, and a keyboard walks into a rail nobody
25
+ can see (PO-273). `display: none` would fix that too and take the
26
+ animation with it.
27
+
28
+ Both attributes are written to be *absent* rather than false when the
29
+ rail stands: `inert` is presence-based, so a renderer that emitted
30
+ `inert="false"` as an attribute would hold the standing rail back
31
+ instead. That the standing rail really does take focus is asserted in
32
+ its own right — specs/studio-navigation.md AC-1 is the control for AC-5.
33
+
34
+ The id is how a test reaches the rail while it is folded: out of the
35
+ accessibility tree, it can no longer be addressed by its role, and
36
+ `nav` alone would catch any navigation the page grows later. -->
15
37
  <div
16
- class="shrink-0 overflow-hidden transition-[width] duration-200 ease-out border-r border-black/[0.06] bg-white"
17
- :class="focused ? 'w-0 !border-r-0' : 'w-64'"
38
+ id="integrations-nav"
39
+ class="shrink-0 overflow-hidden border-r border-black/[0.06] bg-white"
40
+ :class="[focused ? 'w-0 !border-r-0' : 'w-64', animate ? 'transition-[width] duration-200 ease-out' : '']"
41
+ :inert="focused || void 0"
42
+ :aria-hidden="focused ? 'true' : void 0"
18
43
  >
19
44
  <div class="h-full w-64 overflow-y-auto">
20
45
  <Sidebar />
@@ -5,6 +5,9 @@ type __VLS_Props = {
5
5
  field: ConfigField;
6
6
  config: Record<string, unknown>;
7
7
  modelValue: unknown;
8
+ /** The settings this one sits among, so a dependency can be named as the
9
+ * operator sees it rather than by its config key. */
10
+ siblingFields?: ConfigField[];
8
11
  };
9
12
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
13
  "update:modelValue": (value: unknown) => any;
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import { computed, ref } from "vue";
3
+ import { isVersionExecutable } from "../../data/integrationNodeCatalog";
3
4
  import { ChevronDown } from "lucide-vue-next";
4
5
  import { Input } from "@revenexx/studio";
5
6
  import { resolveLocalized } from "../../utils/localize";
@@ -8,11 +9,14 @@ const props = defineProps({
8
9
  version: { type: String, required: true },
9
10
  field: { type: Object, required: true },
10
11
  config: { type: Object, required: true },
11
- modelValue: { type: null, required: true }
12
+ modelValue: { type: null, required: true },
13
+ siblingFields: { type: Array, required: false }
12
14
  });
13
15
  const emit = defineEmits(["update:modelValue"]);
16
+ const t = useIntegrationsT();
14
17
  const search = ref("");
15
18
  const focused = ref(false);
19
+ const versionNotReady = computed(() => !isVersionExecutable(props.slug, props.version));
16
20
  const q = useNodeConfigResolve({
17
21
  slug: () => props.slug,
18
22
  version: () => props.version,
@@ -21,6 +25,10 @@ const q = useNodeConfigResolve({
21
25
  dependsOn: () => props.field.dependsOn,
22
26
  search: () => focused.value ? search.value : ""
23
27
  });
28
+ const unsatisfiedLabels = computed(() => q.unsatisfied.value.map((key) => {
29
+ const sibling = props.siblingFields?.find((f) => f.key === key);
30
+ return sibling && resolveLocalized(sibling.label) || key;
31
+ }));
24
32
  const options = computed(() => q.data.value?.options ?? []);
25
33
  const isMulti = computed(() => props.field.type === "multiselect");
26
34
  const disabledControlClass = "flex h-8 items-center rounded-md border border-black/[0.08] bg-white px-2 text-sm text-muted-foreground opacity-50";
@@ -75,28 +83,34 @@ function toggleMulti(optValue) {
75
83
 
76
84
  <template>
77
85
  <div>
86
+ <!-- the pinned version cannot be run, so there is nothing to resolve -->
87
+ <template v-if="versionNotReady">
88
+ <div :class="disabledControlClass">—</div>
89
+ <p class="mt-1 text-sm text-amber-600">{{ t("integration.nodeConfig.dynamicField.versionNotReady") }}</p>
90
+ </template>
91
+
78
92
  <!-- gated: required dependencies still unset -->
79
- <template v-if="q.gated.value">
93
+ <template v-else-if="q.gated.value">
80
94
  <div :class="disabledControlClass">—</div>
81
- <p class="mt-1 text-sm text-amber-600">Set {{ q.unsatisfied.value.join(", ") }} first</p>
95
+ <p class="mt-1 text-sm text-amber-600">{{ t("integration.nodeConfig.dynamicField.gated", { keys: unsatisfiedLabels.join(", ") }) }}</p>
82
96
  </template>
83
97
 
84
98
  <!-- first load: no data yet -->
85
99
  <template v-else-if="q.isFetching.value && options.length === 0">
86
- <div :class="disabledControlClass">resolving options…</div>
100
+ <div :class="disabledControlClass">{{ t("integration.nodeConfig.dynamicField.resolving") }}</div>
87
101
  </template>
88
102
 
89
103
  <!-- error: build/runtime/etc -->
90
104
  <template v-else-if="q.error.value">
91
105
  <div :class="disabledControlClass">—</div>
92
106
  <div class="mt-1 flex items-center gap-2 text-sm text-amber-600">
93
- <span>{{ q.error.value.kind === "building" ? "Node bundle still building." : q.error.value.message }}</span>
107
+ <span>{{ q.error.value.kind === "building" ? t("integration.nodeConfig.resolve.error.building") : q.error.value.message }}</span>
94
108
  <button
95
109
  type="button"
96
110
  class="rounded border border-black/[0.08] px-1.5 py-0.5 text-mono text-muted-foreground transition hover:text-foreground"
97
111
  @click="q.refetch()"
98
112
  >
99
- RETRY
113
+ {{ t("integration.nodeConfig.resolve.action.retry") }}
100
114
  </button>
101
115
  </div>
102
116
  </template>
@@ -106,13 +120,13 @@ function toggleMulti(optValue) {
106
120
  <Input
107
121
  v-if="options.length > 10"
108
122
  v-model="multiFilter"
109
- :aria-label="`Filter the options for ${resolveLocalized(field.label)}`"
110
- placeholder="Filter options"
123
+ :aria-label="t('integration.nodeConfig.dynamicField.multiselect.filterAria', { label: resolveLocalized(field.label) })"
124
+ :placeholder="t('integration.nodeConfig.dynamicField.multiselect.filterPlaceholder')"
111
125
  class="mb-1.5 h-7 bg-white text-sm"
112
126
  />
113
127
  <div role="group" :aria-label="resolveLocalized(field.label)" class="flex flex-wrap gap-1">
114
128
  <span v-if="visibleMultiOptions.length === 0" class="text-sm text-muted-foreground">
115
- {{ options.length === 0 ? "no options available" : "no options match the filter" }}
129
+ {{ options.length === 0 ? t("integration.nodeConfig.dynamicField.empty") : t("integration.nodeConfig.dynamicField.emptyFiltered") }}
116
130
  </span>
117
131
  <button
118
132
  v-for="opt in visibleMultiOptions"
@@ -131,10 +145,10 @@ function toggleMulti(optValue) {
131
145
  :key="m"
132
146
  class="rounded-md border border-black/[0.08] bg-white px-1.5 py-0.5 text-sm text-muted-foreground opacity-60"
133
147
  >
134
- {{ m }} (not in list)
148
+ {{ t("integration.nodeConfig.dynamicField.notInList", { value: m }) }}
135
149
  </span>
136
150
  </div>
137
- <p v-if="q.isFetching.value && q.hasData.value" class="mt-1 text-sm text-muted-foreground">reloading…</p>
151
+ <p v-if="q.isFetching.value && q.hasData.value" class="mt-1 text-sm text-muted-foreground">{{ t("integration.nodeConfig.dynamicField.reloading") }}</p>
138
152
  </template>
139
153
 
140
154
  <!-- single select: searchable combobox; typing re-resolves with the term -->
@@ -143,7 +157,7 @@ function toggleMulti(optValue) {
143
157
  <Input
144
158
  :model-value="focused ? search : currentLabel"
145
159
  :aria-label="resolveLocalized(field.label)"
146
- :placeholder="resolveLocalized(field.placeholder) || 'Pick one'"
160
+ :placeholder="resolveLocalized(field.placeholder) || t('integration.nodeConfig.select.placeholder')"
147
161
  class="h-8 bg-white pr-7 text-sm"
148
162
  @update:model-value="(v) => {
149
163
  search = String(v);
@@ -157,7 +171,7 @@ function toggleMulti(optValue) {
157
171
  class="absolute left-0 right-0 top-full z-30 mt-1 max-h-56 overflow-y-auto rounded-md border border-black/[0.08] bg-white py-1 shadow-md"
158
172
  >
159
173
  <div v-if="q.isFetching.value" class="px-2.5 py-1.5 text-sm text-muted-foreground">
160
- {{ search ? "searching\u2026" : "resolving options\u2026" }}
174
+ {{ search ? t("integration.nodeConfig.dynamicField.searching") : t("integration.nodeConfig.dynamicField.resolving") }}
161
175
  </div>
162
176
  <button
163
177
  v-for="opt in visibleOptions"
@@ -170,14 +184,14 @@ function toggleMulti(optValue) {
170
184
  <span class="min-w-0 truncate">{{ resolveLocalized(opt.label) || String(opt.value) }}</span>
171
185
  </button>
172
186
  <div v-if="!q.isFetching.value && visibleOptions.length === 0" class="px-2.5 py-1.5 text-sm text-muted-foreground">
173
- {{ search ? 'nothing matches "' + search + '"' : "no options available" }}
187
+ {{ search ? t("integration.nodeConfig.dynamicField.noMatch", { query: search }) : t("integration.nodeConfig.dynamicField.empty") }}
174
188
  </div>
175
189
  </div>
176
190
  </div>
177
191
  <p v-if="currentValue !== '' && !currentKnown" class="mt-1 text-sm text-amber-600">
178
- current value not in resolved options
192
+ {{ t("integration.nodeConfig.dynamicField.currentNotResolved") }}
179
193
  </p>
180
- <p v-if="q.isFetching.value && q.hasData.value && !focused" class="mt-1 text-sm text-muted-foreground">reloading…</p>
194
+ <p v-if="q.isFetching.value && q.hasData.value && !focused" class="mt-1 text-sm text-muted-foreground">{{ t("integration.nodeConfig.dynamicField.reloading") }}</p>
181
195
  </template>
182
196
  </div>
183
197
  </template>
@@ -5,6 +5,9 @@ type __VLS_Props = {
5
5
  field: ConfigField;
6
6
  config: Record<string, unknown>;
7
7
  modelValue: unknown;
8
+ /** The settings this one sits among, so a dependency can be named as the
9
+ * operator sees it rather than by its config key. */
10
+ siblingFields?: ConfigField[];
8
11
  };
9
12
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
13
  "update:modelValue": (value: unknown) => any;
@@ -13,6 +13,7 @@ const props = defineProps({
13
13
  depth: { type: Number, required: false }
14
14
  });
15
15
  const emit = defineEmits(["set"]);
16
+ const t = useIntegrationsT();
16
17
  void IntegrationsApiError;
17
18
  const MAX_DEPTH = 5;
18
19
  const q = useNodeConfigResolve({
@@ -29,17 +30,17 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
29
30
  <div>
30
31
  <!-- gated: required dependencies still unset -->
31
32
  <p v-if="q.gated.value" class="text-sm text-amber-600">
32
- Set {{ q.unsatisfied.value.join(", ") }} first
33
+ {{ t("integration.nodeConfig.dynamicField.gated", { keys: q.unsatisfied.value.join(", ") }) }}
33
34
  </p>
34
35
 
35
36
  <!-- first load: no fields yet -->
36
37
  <p v-else-if="q.isFetching.value && !resolvedFields.length" class="text-sm text-muted-foreground">
37
- Resolving fields…
38
+ {{ t("integration.nodeConfig.dynamicSchema.resolving") }}
38
39
  </p>
39
40
 
40
41
  <!-- error: build/runtime/etc -->
41
42
  <div v-else-if="q.error.value" class="flex items-center gap-2 text-sm text-amber-600">
42
- <span>{{ q.error.value.kind === "building" ? "Node bundle still building." : q.error.value.message }}</span>
43
+ <span>{{ q.error.value.kind === "building" ? t("integration.nodeConfig.resolve.error.building") : q.error.value.message }}</span>
43
44
  <Button
44
45
  type="button"
45
46
  variant="outline"
@@ -47,13 +48,13 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
47
48
  class="h-6 px-1.5 text-mono text-muted-foreground"
48
49
  @click="q.refetch()"
49
50
  >
50
- RETRY
51
+ {{ t("integration.nodeConfig.resolve.action.retry") }}
51
52
  </Button>
52
53
  </div>
53
54
 
54
55
  <!-- empty: resolved but no fields -->
55
56
  <p v-else-if="!resolvedFields.length" class="text-sm text-muted-foreground">
56
- No fields resolved.
57
+ {{ t("integration.nodeConfig.dynamicSchema.empty") }}
57
58
  </p>
58
59
 
59
60
  <!-- fieldset: the resolved child fields, rendered into the flat node config -->
@@ -69,12 +70,12 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
69
70
  <template v-for="f in resolvedFields" :key="f.key">
70
71
  <!-- collision guard: a resolved key that shadows a static config field is skipped -->
71
72
  <p v-if="staticKeys.includes(f.key)" class="text-sm text-amber-600">
72
- resolved field '{{ f.key }}' collides with a static config field — skipped
73
+ {{ t("integration.nodeConfig.dynamicSchema.fieldCollision", { key: f.key }) }}
73
74
  </p>
74
75
 
75
76
  <!-- depth cap: don't expand nested dynamic-schema past MAX_DEPTH -->
76
77
  <p v-else-if="f.type === 'dynamic-schema' && (depth ?? 0) >= MAX_DEPTH" class="text-sm text-muted-foreground">
77
- nested dynamic-schema is not expanded further
78
+ {{ t("integration.nodeConfig.dynamicSchema.depthCapped") }}
78
79
  </p>
79
80
 
80
81
  <!-- a resolved field, rendered exactly like a static node config field -->
@@ -86,13 +87,14 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
86
87
  :version="version"
87
88
  :secret-keys="secretKeys"
88
89
  :static-keys="staticKeys"
90
+ :sibling-fields="resolvedFields"
89
91
  :depth="(depth ?? 0) + 1"
90
92
  @set="(k, v) => emit('set', k, v)"
91
93
  />
92
94
  </template>
93
95
 
94
96
  <p v-if="q.isFetching.value && q.hasData.value" class="text-sm text-muted-foreground">
95
- reloading fields…
97
+ {{ t("integration.nodeConfig.dynamicSchema.reloading") }}
96
98
  </p>
97
99
  </fieldset>
98
100
  </div>
@@ -6,6 +6,9 @@ type __VLS_Props = {
6
6
  version: string;
7
7
  secretKeys: string[];
8
8
  staticKeys?: string[];
9
+ /** The whole set of settings this one belongs to — a `dynamic` field names the
10
+ * ones it waits for out of it (NodeConfigDynamicField). */
11
+ siblingFields?: ConfigField[];
9
12
  depth?: number;
10
13
  variables?: Record<string, WorkflowVariable>;
11
14
  upstreamOutputs?: Array<{