@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,8 +2,10 @@
2
2
  import { computed, ref } from "vue";
3
3
  import { Handle, Position, useVueFlow } from "@vue-flow/core";
4
4
  import { NodeToolbar } from "@vue-flow/node-toolbar";
5
- import { ClockCircle as Clock, Play, TrashBinMinimalistic as Trash2, LinkRound as Webhook, Bolt } from "#solar-icons";
5
+ import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
6
+ import { triggerGlyph as glyphForTrigger } from "../../data/triggerGlyphs";
6
7
  import { resolveDisplayName } from "../../utils/localize";
8
+ import { triggerTypeLabel } from "../../data/integrationNodeCatalog";
7
9
  import { describeCron } from "../../utils/schedule";
8
10
  const props = defineProps({
9
11
  id: { type: String, required: true },
@@ -12,19 +14,18 @@ const props = defineProps({
12
14
  });
13
15
  defineOptions({ inheritAttrs: false });
14
16
  defineEmits(["updateNodeInternals"]);
15
- const icon = computed(() => {
16
- return props.data.type === "schedule" ? Clock : props.data.type === "webhook" ? Webhook : props.data.type === "event" ? Bolt : Play;
17
- });
17
+ const t = useIntegrationsT();
18
+ const icon = computed(() => glyphForTrigger(props.data.type));
18
19
  const subtitle = computed(() => {
19
20
  const c = props.data.config;
20
- if (props.data.type === "schedule") return describeCron(c.cron) ?? c.cron ?? "no schedule set";
21
+ if (props.data.type === "schedule") return describeCron(c.cron) ?? c.cron ?? t("integration.canvas.trigger.subtitle.noSchedule");
21
22
  if (props.data.type === "webhook") return `${c.method ?? "POST"} ${c.path ?? "/"}`;
22
- if (props.data.type === "event") return c.event_type ? `on ${c.event_type}` : "no event set";
23
- return "On demand";
23
+ if (props.data.type === "event") return c.event_type ? t("integration.canvas.trigger.subtitle.onEvent", { event: c.event_type }) : t("integration.canvas.trigger.subtitle.noEvent");
24
+ return t("integration.canvas.trigger.subtitle.onDemand");
24
25
  });
25
26
  const title = computed(() => {
26
27
  const n = props.data.name;
27
- if (!n) return props.data.type.charAt(0).toUpperCase() + props.data.type.slice(1);
28
+ if (!n) return triggerTypeLabel(props.data.type);
28
29
  return resolveDisplayName(n);
29
30
  });
30
31
  const hovered = ref(false);
@@ -54,10 +55,11 @@ function sideToPosition(s) {
54
55
  <button
55
56
  type="button"
56
57
  class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-rose-600 hover:bg-rose-50"
57
- title="Delete"
58
+ :title="t('integration.common.action.delete')"
59
+ :aria-label="t('integration.canvas.trigger.action.deleteAria')"
58
60
  @click="remove"
59
61
  >
60
- <Trash2 :size="11" />
62
+ <TrashBin :size="11" />
61
63
  </button>
62
64
  </NodeToolbar>
63
65
 
@@ -74,7 +76,7 @@ function sideToPosition(s) {
74
76
  identically (icon chip size-8/15, subtitle plain text-sm, mt-0.5). -->
75
77
  <div class="flex items-center gap-2.5 px-3 py-2.5">
76
78
  <span class="flex size-8 shrink-0 items-center justify-center rounded-md bg-emerald-500 text-white shadow-sm">
77
- <component :is="icon" :size="15" :stroke-width="2.5" />
79
+ <component :is="icon" :size="15" />
78
80
  </span>
79
81
  <div class="min-w-0 flex-1">
80
82
  <div class="text-sm font-semibold text-foreground leading-tight truncate">
@@ -86,8 +88,8 @@ function sideToPosition(s) {
86
88
  </div>
87
89
  </div>
88
90
  <div class="flex items-center justify-between border-t border-emerald-500/15 bg-emerald-500/[0.04] rounded-b-md px-3 py-1 text-mono text-emerald-700/80 font-semibold">
89
- <span>Trigger · {{ data.type }}</span>
90
- <span v-if="data.active === false" class="text-amber-600">Inactive</span>
91
+ <span>{{ t("integration.canvas.badge.trigger") }} · {{ data.type }}</span>
92
+ <span v-if="data.active === false" class="text-amber-600">{{ t("integration.canvas.trigger.badge.inactive") }}</span>
91
93
  </div>
92
94
  </div>
93
95
 
@@ -1,11 +1,12 @@
1
1
  <script setup>
2
2
  import { computed, reactive, ref, watch } from "vue";
3
- import { AddCircle as Plus, Copy, TrashBinMinimalistic as Trash2 } from "#solar-icons";
3
+ import { AddCircle as Plus, Copy, TrashBinMinimalistic as TrashBin } from "#solar-icons";
4
4
  import {
5
5
  Button,
6
6
  Dialog,
7
7
  DialogContent,
8
8
  DialogDescription,
9
+ DialogFooter,
9
10
  DialogHeader,
10
11
  DialogTitle,
11
12
  Input,
@@ -18,40 +19,59 @@ import {
18
19
  SelectValue
19
20
  } from "@revenexx/studio";
20
21
  import { toast } from "vue-sonner";
21
- import { resolveDisplayName } from "../../utils/localize";
22
+ import { languageKeyRead, languageKeyToWrite, resolveDisplayName } from "../../utils/localize";
22
23
  const props = defineProps({
23
24
  open: { type: Boolean, required: true },
24
25
  variables: { type: Object, required: true }
25
26
  });
26
27
  const emit = defineEmits(["update:open", "update:variables"]);
28
+ const t = useIntegrationsT();
27
29
  const KEY_PATTERN = /^[a-z_][a-z0-9_]*$/i;
28
30
  const TYPES = ["string", "number", "boolean", "object", "array"];
31
+ const working = ref({});
29
32
  const jsonDrafts = reactive({});
30
33
  const jsonErrors = reactive({});
34
+ const descDrafts = reactive({});
31
35
  watch(() => props.open, (o) => {
32
36
  if (!o) return;
37
+ working.value = JSON.parse(JSON.stringify(props.variables));
33
38
  for (const k of Object.keys(jsonDrafts)) delete jsonDrafts[k];
34
39
  for (const k of Object.keys(jsonErrors)) delete jsonErrors[k];
35
- for (const [key, v] of Object.entries(props.variables)) {
40
+ for (const k of Object.keys(descDrafts)) delete descDrafts[k];
41
+ for (const [key, v] of Object.entries(working.value)) {
42
+ descDrafts[key] = descriptionText(v);
36
43
  if (v.type === "object" || v.type === "array") {
37
44
  jsonDrafts[key] = v.default === void 0 ? "" : JSON.stringify(v.default, null, 2);
38
45
  jsonErrors[key] = null;
39
46
  }
40
47
  }
41
48
  });
42
- const entries = computed(() => Object.entries(props.variables));
49
+ const entries = computed(() => Object.entries(working.value));
50
+ const changed = computed(() => JSON.stringify(working.value) !== JSON.stringify(props.variables));
51
+ const hasJsonError = computed(() => Object.values(jsonErrors).some(Boolean));
43
52
  function commit(next) {
44
- emit("update:variables", next);
53
+ working.value = next;
54
+ }
55
+ function applyAndClose() {
56
+ for (const [key, v] of Object.entries(working.value)) {
57
+ if ((v.type === "object" || v.type === "array") && jsonDrafts[key] !== void 0) {
58
+ applyJsonDraft(key, v.type);
59
+ }
60
+ }
61
+ if (hasJsonError.value) return;
62
+ if (changed.value) emit("update:variables", working.value);
63
+ emit("update:open", false);
45
64
  }
46
65
  function patchVar(key, patch) {
47
- const cur = props.variables[key];
66
+ const cur = working.value[key];
48
67
  if (!cur) return;
49
- commit({ ...props.variables, [key]: { ...cur, ...patch } });
68
+ commit({ ...working.value, [key]: { ...cur, ...patch } });
50
69
  }
51
70
  function removeVar(key) {
52
- const next = { ...props.variables };
71
+ const next = { ...working.value };
53
72
  delete next[key];
54
73
  delete jsonDrafts[key];
74
+ delete descDrafts[key];
55
75
  delete jsonErrors[key];
56
76
  commit(next);
57
77
  }
@@ -64,6 +84,8 @@ function changeType(key, type) {
64
84
  array: void 0
65
85
  };
66
86
  patchVar(key, { type, default: blank[type] });
87
+ delete jsonDrafts[key];
88
+ delete jsonErrors[key];
67
89
  if (type === "object" || type === "array") {
68
90
  jsonDrafts[key] = "";
69
91
  jsonErrors[key] = null;
@@ -80,49 +102,55 @@ function applyJsonDraft(key, type) {
80
102
  const parsed = JSON.parse(raw);
81
103
  const ok = type === "array" ? Array.isArray(parsed) : parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
82
104
  if (!ok) {
83
- jsonErrors[key] = type === "array" ? "Must be a JSON array." : "Must be a JSON object.";
105
+ jsonErrors[key] = type === "array" ? t("integration.workflows.variables.error.mustBeArray") : t("integration.workflows.variables.error.mustBeObject");
84
106
  return;
85
107
  }
86
108
  jsonErrors[key] = null;
87
109
  patchVar(key, { default: parsed });
88
110
  } catch {
89
- jsonErrors[key] = "Not valid JSON.";
111
+ jsonErrors[key] = t("integration.workflows.variables.error.invalidJson");
90
112
  }
91
113
  }
92
114
  function descriptionText(v) {
93
115
  return resolveDisplayName(v.description);
94
116
  }
95
117
  function writeDescription(key, text) {
96
- const cur = props.variables[key];
118
+ const cur = working.value[key];
97
119
  if (!cur) return;
120
+ descDrafts[key] = text;
121
+ const existing = typeof cur.description === "object" && cur.description !== null ? { ...cur.description } : {};
98
122
  if (!text.trim()) {
123
+ delete existing[languageKeyRead(cur.description)];
124
+ if (Object.keys(existing).length) {
125
+ patchVar(key, { description: existing });
126
+ return;
127
+ }
99
128
  const { description: _drop, ...rest } = cur;
100
- commit({ ...props.variables, [key]: rest });
129
+ commit({ ...working.value, [key]: rest });
101
130
  return;
102
131
  }
103
- const existing = typeof cur.description === "object" && cur.description !== null ? cur.description : {};
104
- patchVar(key, { description: { ...existing, en: text } });
132
+ patchVar(key, { description: { ...existing, [languageKeyToWrite(cur.description)]: text } });
105
133
  }
106
134
  const newKey = ref("");
107
135
  const newKeyError = computed(() => {
108
136
  const k = newKey.value.trim();
109
137
  if (!k) return null;
110
- if (!KEY_PATTERN.test(k)) return "Letters, digits and _ only; must not start with a digit.";
111
- if (props.variables[k]) return "Key already exists.";
138
+ if (!KEY_PATTERN.test(k)) return t("integration.workflows.variables.error.invalidKey");
139
+ if (working.value[k]) return t("integration.workflows.variables.error.keyExists");
112
140
  return null;
113
141
  });
114
142
  function addVariable() {
115
143
  const k = newKey.value.trim();
116
144
  if (!k || newKeyError.value) return;
117
- commit({ ...props.variables, [k]: { type: "string", default: "" } });
145
+ commit({ ...working.value, [k]: { type: "string", default: "" } });
118
146
  newKey.value = "";
119
147
  }
120
148
  async function copyUsage(key) {
121
149
  try {
122
150
  await navigator.clipboard.writeText(`\${{ vars.${key} }}`);
123
- toast.success("Expression copied");
151
+ toast.success(t("integration.workflows.variables.toast.copied"));
124
152
  } catch {
125
- toast.error("Could not copy");
153
+ toast.error(t("integration.workflows.variables.toast.copyFailed"));
126
154
  }
127
155
  }
128
156
  </script>
@@ -131,24 +159,24 @@ async function copyUsage(key) {
131
159
  <Dialog :open="open" @update:open="(v2) => emit('update:open', v2)">
132
160
  <DialogContent class="max-w-[94vw] sm:max-w-3xl">
133
161
  <DialogHeader>
134
- <DialogTitle>Workflow variables</DialogTitle>
162
+ <DialogTitle>{{ t("integration.workflows.variables.dialog.title") }}</DialogTitle>
135
163
  <DialogDescription class="text-sm">
136
- Reusable values for this workflow — reference them in any config field as
164
+ {{ t("integration.workflows.variables.dialog.description.before") }}
137
165
  <code v-pre class="rounded bg-black/[0.05] px-1 py-0.5 font-mono text-xs">${{ vars.key }}</code>.
138
- Changes persist with the workflow Save.
166
+ {{ t("integration.workflows.variables.dialog.description.after") }}
139
167
  </DialogDescription>
140
168
  </DialogHeader>
141
169
 
142
170
  <div class="max-h-[62vh] space-y-3 overflow-y-auto pr-1">
143
171
  <p v-if="!entries.length" class="rounded-lg border border-dashed border-black/[0.12] px-4 py-6 text-center text-sm text-muted-foreground">
144
- No variables yet — add the first one below.
172
+ {{ t("integration.workflows.variables.state.empty") }}
145
173
  </p>
146
174
 
147
175
  <div
148
176
  v-for="[key, v] in entries"
149
177
  :key="key"
150
178
  role="group"
151
- :aria-label="`Variable ${key}`"
179
+ :aria-label="t('integration.workflows.variables.row.aria', { key })"
152
180
  class="rounded-lg border border-black/[0.06] bg-white p-3"
153
181
  >
154
182
  <div class="flex items-center gap-2">
@@ -156,7 +184,7 @@ async function copyUsage(key) {
156
184
  <button
157
185
  type="button"
158
186
  class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
159
- :title="`Copy \${{ vars.${key} }}`"
187
+ :title="t('integration.workflows.variables.action.copyUsage.title', { key })"
160
188
  @click="copyUsage(key)"
161
189
  >
162
190
  <Copy :size="12" />
@@ -165,10 +193,10 @@ async function copyUsage(key) {
165
193
  <button
166
194
  type="button"
167
195
  class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
168
- title="Delete variable"
196
+ :title="t('integration.workflows.variables.action.delete')"
169
197
  @click="removeVar(key)"
170
198
  >
171
- <Trash2 :size="13" />
199
+ <TrashBin :size="13" />
172
200
  </button>
173
201
  </div>
174
202
 
@@ -176,12 +204,12 @@ async function copyUsage(key) {
176
204
  <!-- One row per variable, so every id carries its key — otherwise
177
205
  the labels of the second row point at the first row's fields. -->
178
206
  <div class="space-y-1">
179
- <Label :for="`var-${key}-type`" class="text-mono text-muted-foreground">Type</Label>
180
- <Select :model-value="v.type" @update:model-value="(t) => changeType(key, t)">
207
+ <Label :for="`var-${key}-type`" class="text-mono text-muted-foreground">{{ t("integration.workflows.variables.field.type.label") }}</Label>
208
+ <Select :model-value="v.type" @update:model-value="(vt) => changeType(key, vt)">
181
209
  <!-- Both: the `id` lets the visible Label's `for` resolve, and the
182
210
  `aria-label` names the trigger without relying on an engine
183
211
  naming a button from a <label> — see the sweep's docblock. -->
184
- <SelectTrigger :id="`var-${key}-type`" aria-label="Type" class="h-8 bg-white text-sm">
212
+ <SelectTrigger :id="`var-${key}-type`" :aria-label="t('integration.workflows.variables.field.type.label')" class="h-8 bg-white text-sm">
185
213
  <SelectValue />
186
214
  </SelectTrigger>
187
215
  <SelectContent>
@@ -193,7 +221,7 @@ async function copyUsage(key) {
193
221
  <div class="space-y-1">
194
222
  <!-- "Value", not "Default": PO-206. The `for`/`id` still name the
195
223
  blob field, which is called `default` and stays that way. -->
196
- <Label :for="`var-${key}-default`" class="text-mono text-muted-foreground">Value</Label>
224
+ <Label :for="`var-${key}-default`" class="text-mono text-muted-foreground">{{ t("integration.workflows.variables.field.value.label") }}</Label>
197
225
 
198
226
  <!-- Only one of these renders, so they share the one `id` the Label
199
227
  points at. The labelable ones take their name from that Label —
@@ -221,7 +249,7 @@ async function copyUsage(key) {
221
249
  <Switch
222
250
  :id="`var-${key}-default`"
223
251
  :model-value="v.default === true"
224
- aria-label="Value"
252
+ :aria-label="t('integration.workflows.variables.field.value.label')"
225
253
  @update:model-value="(val) => patchVar(key, { default: val })"
226
254
  />
227
255
  </div>
@@ -233,7 +261,7 @@ async function copyUsage(key) {
233
261
  spellcheck="false"
234
262
  class="w-full rounded-md border bg-white p-2 font-mono text-xs focus:outline-none focus:ring-2 focus:ring-primary/40"
235
263
  :class="jsonErrors[key] ? 'border-rose-400' : 'border-black/[0.08]'"
236
- :placeholder="v.type === 'array' ? 'JSON array, e.g. [1, 2]' : 'JSON object, e.g. { \u2026 }'"
264
+ :placeholder="v.type === 'array' ? t('integration.workflows.variables.field.value.placeholder.array') : t('integration.workflows.variables.field.value.placeholder.object')"
237
265
  @input="(e) => jsonDrafts[key] = e.target.value"
238
266
  @blur="applyJsonDraft(key, v.type)"
239
267
  />
@@ -243,12 +271,12 @@ async function copyUsage(key) {
243
271
  </div>
244
272
 
245
273
  <div class="mt-2 space-y-1">
246
- <Label :for="`var-${key}-desc`" class="text-mono text-muted-foreground">Description</Label>
274
+ <Label :for="`var-${key}-desc`" class="text-mono text-muted-foreground">{{ t("integration.workflows.variables.field.description.label") }}</Label>
247
275
  <Input
248
276
  :id="`var-${key}-desc`"
249
- :model-value="descriptionText(v)"
277
+ :model-value="descDrafts[key] ?? descriptionText(v)"
250
278
  class="h-8 bg-white text-sm"
251
- placeholder="What this variable is for"
279
+ :placeholder="t('integration.workflows.variables.field.description.placeholder')"
252
280
  @update:model-value="(val) => writeDescription(key, String(val))"
253
281
  />
254
282
  </div>
@@ -256,23 +284,35 @@ async function copyUsage(key) {
256
284
 
257
285
  <!-- Add row -->
258
286
  <div class="rounded-lg border border-dashed border-black/[0.12] p-3">
259
- <Label for="var-new-key" class="text-mono text-muted-foreground">New variable</Label>
287
+ <Label for="var-new-key" class="text-mono text-muted-foreground">{{ t("integration.workflows.variables.field.newKey.label") }}</Label>
260
288
  <div class="mt-1 flex items-center gap-2">
261
289
  <Input
262
290
  id="var-new-key"
263
291
  v-model="newKey"
264
292
  class="h-8 flex-1 bg-white font-mono text-sm"
265
- placeholder="e.g. base_url"
293
+ :placeholder="t('integration.workflows.variables.field.newKey.placeholder')"
266
294
  @keydown.enter.prevent="addVariable"
267
295
  />
268
296
  <Button size="sm" class="h-8" :disabled="!newKey.trim() || !!newKeyError" @click="addVariable">
269
297
  <Plus :size="14" class="mr-1" />
270
- Add
298
+ {{ t("integration.workflows.variables.action.add") }}
271
299
  </Button>
272
300
  </div>
273
301
  <p v-if="newKeyError" class="mt-1 text-xs text-rose-600">{{ newKeyError }}</p>
274
302
  </div>
275
303
  </div>
304
+
305
+ <!-- Cancel / Done, the same pair every dialog in the editor closes on. Done
306
+ reaches the workflow's unsaved state; only the header's Save reaches the
307
+ platform. -->
308
+ <DialogFooter>
309
+ <Button variant="outline" size="sm" class="h-8" @click="emit('update:open', false)">
310
+ {{ t("integration.common.action.cancel") }}
311
+ </Button>
312
+ <Button size="sm" class="h-8" :disabled="hasJsonError" @click="applyAndClose">
313
+ {{ t("integration.workflows.variables.action.done") }}
314
+ </Button>
315
+ </DialogFooter>
276
316
  </DialogContent>
277
317
  </Dialog>
278
318
  </template>