@revenexx/studio-integrations 0.5.1 → 0.7.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 (171) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/README.md +48 -4
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +10 -2
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  12. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
  13. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  14. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  15. package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
  16. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  17. package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
  18. package/dist/runtime/components/Integrations/Layout.vue +48 -4
  19. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
  20. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
  21. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  22. package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
  23. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  24. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  25. package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
  26. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  27. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  28. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  29. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  30. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  31. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  32. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  33. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  34. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
  35. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  36. package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
  37. package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
  38. package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
  39. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  40. package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
  41. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  42. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  43. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  44. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  45. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  46. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  47. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  48. package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
  49. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  50. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  51. package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
  52. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  53. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  54. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
  55. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  56. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  57. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
  58. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  59. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  60. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
  61. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  62. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
  63. package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
  64. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
  65. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
  66. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  67. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
  68. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  69. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  70. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
  71. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  72. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
  73. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
  74. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  75. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
  76. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  77. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
  78. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
  79. package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
  80. package/dist/runtime/composables/integrationsApi.js +4 -2
  81. package/dist/runtime/composables/useCredentialTypes.js +8 -6
  82. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  83. package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
  84. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  85. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  86. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  87. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  88. package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
  89. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  90. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  91. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  92. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  93. package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
  94. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  95. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  96. package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
  97. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  98. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  99. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  100. package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
  101. package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
  102. package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
  103. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  104. package/dist/runtime/composables/useLoadGate.js +8 -0
  105. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  106. package/dist/runtime/data/branchReorder.d.ts +37 -0
  107. package/dist/runtime/data/branchReorder.js +71 -0
  108. package/dist/runtime/data/handleId.d.ts +23 -0
  109. package/dist/runtime/data/handleId.js +6 -0
  110. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  111. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  112. package/dist/runtime/data/integrationTemplates.d.ts +48 -0
  113. package/dist/runtime/data/integrationTemplates.js +21 -0
  114. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  115. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  116. package/dist/runtime/data/nodeIcons.js +73 -0
  117. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  118. package/dist/runtime/data/nodeInputData.js +4 -0
  119. package/dist/runtime/data/payloadForm.d.ts +9 -2
  120. package/dist/runtime/data/payloadForm.js +29 -4
  121. package/dist/runtime/data/portTone.d.ts +17 -0
  122. package/dist/runtime/data/portTone.js +7 -0
  123. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  124. package/dist/runtime/data/webhookAuth.js +19 -0
  125. package/dist/runtime/data/workflowIconCatalog.js +4 -6
  126. package/dist/runtime/i18n/messages/de.json +1207 -0
  127. package/dist/runtime/i18n/messages/en.json +1207 -0
  128. package/dist/runtime/i18n/translate.d.ts +83 -0
  129. package/dist/runtime/i18n/translate.js +41 -0
  130. package/dist/runtime/pages/integrations/audit.vue +140 -37
  131. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
  132. package/dist/runtime/pages/integrations/credentials.vue +180 -64
  133. package/dist/runtime/pages/integrations/index.vue +91 -92
  134. package/dist/runtime/pages/integrations/runs.vue +33 -21
  135. package/dist/runtime/pages/integrations/schedules.vue +88 -23
  136. package/dist/runtime/pages/integrations/secrets.vue +57 -35
  137. package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
  138. package/dist/runtime/pages/integrations/templates/index.vue +82 -103
  139. package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
  140. package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
  141. package/dist/runtime/plugins/locale.d.ts +10 -0
  142. package/dist/runtime/plugins/locale.js +6 -0
  143. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  144. package/dist/runtime/plugins/realtime.client.js +4 -0
  145. package/dist/runtime/types/integrations.d.ts +65 -6
  146. package/dist/runtime/utils/apiError.d.ts +47 -0
  147. package/dist/runtime/utils/apiError.js +32 -0
  148. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  149. package/dist/runtime/utils/cockpitSources.js +14 -0
  150. package/dist/runtime/utils/credentialState.d.ts +24 -1
  151. package/dist/runtime/utils/credentialState.js +26 -11
  152. package/dist/runtime/utils/loadGate.d.ts +61 -0
  153. package/dist/runtime/utils/loadGate.js +22 -0
  154. package/dist/runtime/utils/localize.d.ts +46 -4
  155. package/dist/runtime/utils/localize.js +34 -7
  156. package/dist/runtime/utils/notify.d.ts +59 -0
  157. package/dist/runtime/utils/notify.js +21 -0
  158. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  159. package/dist/runtime/utils/pollBudget.js +17 -0
  160. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  161. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  162. package/dist/runtime/utils/schedule.d.ts +116 -0
  163. package/dist/runtime/utils/schedule.js +219 -0
  164. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  165. package/dist/runtime/utils/statusStyle.js +9 -1
  166. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  167. package/dist/runtime/utils/validationFindings.js +54 -0
  168. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  169. package/dist/runtime/utils/whenIdle.js +12 -0
  170. package/dist/types.d.mts +2 -6
  171. package/package.json +10 -3
@@ -20,9 +20,11 @@ import {
20
20
  SelectValue
21
21
  } from "@revenexx/studio";
22
22
  import TemplateFlowPreview from "../../../components/Integrations/TemplateFlowPreview.vue";
23
- import NodeLogo from "../../../components/Integrations/NodeLogo.vue";
24
- import { TEMPLATE_CATEGORIES } from "../../../data/integrationTemplates";
23
+ import NodeMark from "../../../components/Integrations/NodeMark.vue";
24
+ import { templateCategoryLabel, templatePackageLabel } from "../../../data/integrationTemplates";
25
+ import { describeCron } from "../../../utils/schedule";
25
26
  const SELECT_LATER = "__later__";
27
+ const t = useIntegrationsT();
26
28
  const route = useRoute();
27
29
  const router = useRouter();
28
30
  const { get, load, loaded, fetchDetail, requirements, instantiate } = useIntegrationsTemplates();
@@ -31,7 +33,7 @@ const { keys: secretKeys, load: loadSecrets } = useIntegrationsSecrets();
31
33
  const { ofType, load: loadCredentials } = useIntegrationsCredentials();
32
34
  const slug = computed(() => String(route.params.slug));
33
35
  const tpl = computed(() => get(slug.value));
34
- useHead({ title: () => `${tpl.value?.name ?? "Template"} \xB7 Integration Studio` });
36
+ useHead({ title: () => t("integration.templates.detail.head.title", { name: tpl.value?.name ?? t("integration.templates.detail.head.fallbackName") }) });
35
37
  const reqs = ref(null);
36
38
  onMounted(async () => {
37
39
  await load();
@@ -49,7 +51,7 @@ const steps = computed(() => {
49
51
  if (!blob?.nodes.length) return [];
50
52
  const nodeById = new Map(blob.nodes.map((n) => [n.id, n]));
51
53
  const happyEdges = (blob.edges ?? []).filter((e) => e.from.output !== "error");
52
- const triggerHandles = new Set((blob.triggers ?? []).map((t) => t.id));
54
+ const triggerHandles = new Set((blob.triggers ?? []).map((t2) => t2.id));
53
55
  const incoming = /* @__PURE__ */ new Map();
54
56
  for (const e of happyEdges) {
55
57
  if (!triggerHandles.has(e.from.nodeId)) incoming.set(e.to.nodeId, (incoming.get(e.to.nodeId) ?? 0) + 1);
@@ -89,23 +91,28 @@ function firstSentence(text) {
89
91
  return idx === -1 ? text : text.slice(0, idx + 1);
90
92
  }
91
93
  const TRIGGER_ICON = { schedule: Clock, webhook: Webhook, manual: Play, event: Zap };
92
- const triggerChips = computed(() => (tpl.value?.blob.triggers ?? []).map((t) => {
93
- let label = t.type.charAt(0).toUpperCase() + t.type.slice(1);
94
- if (t.type === "schedule" && t.config?.cron) label = `Schedule \xB7 ${t.config.cron}${t.config.timezone ? ` (${t.config.timezone})` : ""}`;
95
- if (t.type === "webhook" && t.config?.path) label = `Webhook \xB7 ${t.config.path}`;
96
- if (t.type === "manual") label = "Run on demand";
97
- return { key: t.id, icon: TRIGGER_ICON[t.type] ?? Zap, label };
94
+ const triggerChips = computed(() => (tpl.value?.blob.triggers ?? []).map((trig) => {
95
+ let label = trig.type.charAt(0).toUpperCase() + trig.type.slice(1);
96
+ if (trig.type === "schedule" && trig.config?.cron) {
97
+ const when = describeCron(String(trig.config.cron)) ?? String(trig.config.cron);
98
+ label = `${t("integration.templates.detail.trigger.schedule")} \xB7 ${when}${trig.config.timezone ? ` (${trig.config.timezone})` : ""}`;
99
+ }
100
+ if (trig.type === "webhook" && trig.config?.path) label = `${t("integration.templates.trigger.webhook")} \xB7 ${trig.config.path}`;
101
+ if (trig.type === "manual") label = t("integration.templates.trigger.manual");
102
+ return { key: trig.id, icon: TRIGGER_ICON[trig.type] ?? Zap, label };
98
103
  }));
99
104
  const variables = computed(() => Object.entries(tpl.value?.blob.variables ?? {}).map(([key, v]) => ({
100
105
  key,
101
106
  default: String(v?.default ?? ""),
102
107
  description: resolveLocalized(v?.description ?? "")
103
108
  })));
104
- const variableColumns = [
105
- { key: "key", label: "Variable", width: "minmax(0,180px)", sortable: true },
106
- { key: "description", label: "Description" },
107
- { key: "default", label: "Default", width: "minmax(0,200px)" }
108
- ];
109
+ const variableColumns = computed(() => [
110
+ { key: "key", label: t("integration.templates.detail.variables.column.key"), width: "minmax(0,180px)", sortable: true },
111
+ { key: "description", label: t("integration.templates.detail.variables.column.description") },
112
+ // Stored as `default` in the blob, but it *is* the value the workflow runs
113
+ // with — see `specs/workflow-variables.md`.
114
+ { key: "default", label: t("integration.templates.detail.variables.column.value"), width: "minmax(0,200px)" }
115
+ ]);
109
116
  const credentialReqs = computed(() => reqs.value?.credentialTypes ?? []);
110
117
  const secretReqs = computed(() => reqs.value?.secrets ?? []);
111
118
  const secretChecks = computed(() => {
@@ -113,7 +120,7 @@ const secretChecks = computed(() => {
113
120
  if (fromApi.length) {
114
121
  return fromApi.map((s) => ({
115
122
  key: s.key,
116
- label: s.key ?? "(chosen at install)",
123
+ label: s.key ?? t("integration.templates.detail.secrets.chosenAtInstall"),
117
124
  present: s.exists || (s.key ? secretKeys.value.includes(s.key) : false)
118
125
  }));
119
126
  }
@@ -126,17 +133,8 @@ const secretChecks = computed(() => {
126
133
  const missingSecrets = computed(() => secretChecks.value.filter((s) => !s.present).length);
127
134
  const missingCredentials = computed(() => credentialReqs.value.filter((c) => !ofType(c.slug).length).length);
128
135
  const setupReady = computed(() => missingSecrets.value === 0 && missingCredentials.value === 0);
129
- const packageLabel = computed(() => {
130
- const pkg = tpl.value?.package;
131
- if (!pkg) return null;
132
- const tail = (pkg.name.split("/").pop() ?? pkg.name).replace(/^integrations-nodes-/, "");
133
- if (tail === "core") return null;
134
- return pkg.label ?? tail.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
135
- });
136
- const categoryLabel = computed(() => {
137
- const cat = tpl.value?.category;
138
- return TEMPLATE_CATEGORIES.find((c) => c.key === cat)?.label ?? (cat ? cat.charAt(0).toUpperCase() + cat.slice(1) : "");
139
- });
136
+ const packageLabel = computed(() => tpl.value ? templatePackageLabel(tpl.value) : null);
137
+ const categoryLabel = computed(() => templateCategoryLabel(tpl.value?.category ?? ""));
140
138
  const installOpen = ref(false);
141
139
  const installing = ref(false);
142
140
  const installError = ref(null);
@@ -167,12 +165,20 @@ async function confirmInstall() {
167
165
  });
168
166
  if (id) await router.push(`/integrations/workflows/${id}`);
169
167
  } catch (e) {
170
- installError.value = e.message || "Install failed.";
168
+ installError.value = e.message || t("integration.templates.detail.install.error.fallback");
171
169
  } finally {
172
170
  installing.value = false;
173
171
  }
174
172
  }
175
- const PUBLISHER_KIND_LABEL = { revenexx: "Official", partner: "Partner", community: "Community" };
173
+ const PUBLISHER_KIND_LABEL_KEY = {
174
+ revenexx: "integration.templates.detail.publisher.kind.revenexx",
175
+ partner: "integration.templates.detail.publisher.kind.partner",
176
+ community: "integration.templates.detail.publisher.kind.community"
177
+ };
178
+ function publisherKindLabel(kind) {
179
+ const key = PUBLISHER_KIND_LABEL_KEY[kind];
180
+ return key ? t(key) : kind;
181
+ }
176
182
  function publisherBadgeClass(kind) {
177
183
  return kind === "revenexx" ? "bg-violet-500/10 text-violet-700 border-violet-500/20" : kind === "partner" ? "bg-amber-500/10 text-amber-700 border-amber-500/20" : "bg-slate-500/10 text-slate-700 border-slate-500/20";
178
184
  }
@@ -187,14 +193,14 @@ function levelDot(level) {
187
193
  <PageHeader :title="tpl.name">
188
194
  <template #title>
189
195
  <span class="inline-flex items-center gap-1.5">
190
- <NuxtLink to="/integrations/templates" class="font-normal text-muted-foreground/80 hover:text-primary transition-colors">Templates</NuxtLink>
196
+ <NuxtLink to="/integrations/templates" class="font-normal text-muted-foreground/80 hover:text-primary transition-colors">{{ t("integration.templates.detail.breadcrumb.templates") }}</NuxtLink>
191
197
  <Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
192
198
  <span class="truncate">{{ tpl.name }}</span>
193
199
  </span>
194
200
  </template>
195
201
  <template #actions>
196
202
  <Button class="h-7 text-sm" @click="openInstall">
197
- Use template
203
+ {{ t("integration.templates.detail.action.useTemplate") }}
198
204
  </Button>
199
205
  </template>
200
206
  </PageHeader>
@@ -207,7 +213,7 @@ function levelDot(level) {
207
213
  <Loader2 :size="18" class="animate-spin text-muted-foreground/50" />
208
214
  </div>
209
215
  <div class="absolute left-3 top-3 flex items-center gap-1.5">
210
- <span class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
216
+ <span v-if="categoryLabel" class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
211
217
  {{ categoryLabel }}
212
218
  </span>
213
219
  <span class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
@@ -233,8 +239,13 @@ function levelDot(level) {
233
239
  <div class="grid gap-4 lg:grid-cols-3">
234
240
  <div class="lg:col-span-2 space-y-4">
235
241
  <!-- About: the registry description, rendered -->
236
- <div v-if="descriptionHtml" class="rounded-lg border border-black/[0.06] bg-white p-4">
237
- <h3 class="text-sm font-semibold mb-2">About this template</h3>
242
+ <div
243
+ v-if="descriptionHtml"
244
+ role="region"
245
+ aria-labelledby="tpl-about-heading"
246
+ class="rounded-lg border border-black/[0.06] bg-white p-4"
247
+ >
248
+ <h3 id="tpl-about-heading" class="text-sm font-semibold mb-2">{{ t("integration.templates.detail.about.title") }}</h3>
238
249
  <div
239
250
  class="text-sm leading-relaxed text-foreground/90 space-y-2 [&_h1]:text-sm [&_h1]:font-semibold [&_h2]:text-sm [&_h2]:font-semibold [&_h3]:text-sm [&_h3]:font-semibold [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5 [&_li]:mt-1 [&_code]:rounded [&_code]:bg-slate-100 [&_code]:px-1 [&_code]:py-px [&_code]:font-mono [&_code]:text-[13px] [&_a]:text-primary [&_a]:underline [&_strong]:font-semibold"
240
251
  v-html="descriptionHtml"
@@ -242,8 +253,12 @@ function levelDot(level) {
242
253
  </div>
243
254
 
244
255
  <!-- Step by step, humanized from the actual nodes -->
245
- <div class="rounded-lg border border-black/[0.06] bg-white p-4">
246
- <h3 class="text-sm font-semibold mb-3">Step by step</h3>
256
+ <div
257
+ role="region"
258
+ aria-labelledby="tpl-steps-heading"
259
+ class="rounded-lg border border-black/[0.06] bg-white p-4"
260
+ >
261
+ <h3 id="tpl-steps-heading" class="text-sm font-semibold mb-3">{{ t("integration.templates.detail.steps.title") }}</h3>
247
262
  <ol v-if="steps.length" class="space-y-2.5">
248
263
  <li
249
264
  v-for="(step, i) in steps"
@@ -254,7 +269,7 @@ function levelDot(level) {
254
269
  {{ i + 1 }}
255
270
  </span>
256
271
  <span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-white ring-1 ring-black/[0.08]">
257
- <NodeLogo :manifest="step.manifest" :size="15" />
272
+ <NodeMark :manifest="step.manifest" :size="15" :badge="false" />
258
273
  </span>
259
274
  <span class="min-w-0 pt-0.5">
260
275
  <span class="block text-sm font-medium text-foreground">{{ step.label }}</span>
@@ -263,14 +278,23 @@ function levelDot(level) {
263
278
  </li>
264
279
  </ol>
265
280
  <p v-else class="text-sm text-muted-foreground italic">
266
- No step-by-step outline for this template yet.
281
+ {{ t("integration.templates.detail.steps.empty") }}
267
282
  </p>
268
283
  </div>
269
284
 
270
285
  <!-- Variables the operator can tune after install -->
271
- <div v-if="variables.length" class="rounded-lg border border-black/[0.06] bg-white p-4">
272
- <h3 class="text-sm font-semibold mb-1">Variables you can tune</h3>
273
- <p class="text-sm text-muted-foreground mb-3">Set once after install, used across the workflow.</p>
286
+ <!-- Named by its own heading rather than by a copy of it: two strings
287
+ saying the same thing drift, and the test only holds one of them.
288
+ `role` stays because this is a div — a bare <section> would be a
289
+ region on its own, this one has to say so. -->
290
+ <div
291
+ v-if="variables.length"
292
+ role="region"
293
+ aria-labelledby="tpl-variables-heading"
294
+ class="rounded-lg border border-black/[0.06] bg-white p-4"
295
+ >
296
+ <h3 id="tpl-variables-heading" class="text-sm font-semibold mb-1">{{ t("integration.templates.detail.variables.title") }}</h3>
297
+ <p class="text-sm text-muted-foreground mb-3">{{ t("integration.templates.detail.variables.description") }}</p>
274
298
  <DataTable
275
299
  :data="variables"
276
300
  :columns="variableColumns"
@@ -296,51 +320,55 @@ function levelDot(level) {
296
320
  <!-- Setup rail -->
297
321
  <div class="space-y-3">
298
322
  <!-- Readiness + CTA -->
299
- <div class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2.5">
323
+ <!-- `aria-label` deliberately, unlike the two panels below: this one
324
+ shows no heading of its own, so there is no visible string to
325
+ point `aria-labelledby` at. Copy this pattern only for a panel
326
+ that genuinely has none. -->
327
+ <div role="group" :aria-label="t('integration.templates.detail.setup.ariaLabel')" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2.5">
300
328
  <div v-if="setupReady" class="flex items-center gap-1.5 text-sm text-emerald-700">
301
329
  <CheckCircle2 :size="14" />
302
- Everything is ready to install.
330
+ {{ t("integration.templates.detail.setup.ready") }}
303
331
  </div>
304
332
  <div v-else class="text-sm text-amber-700 space-y-0.5">
305
333
  <div v-if="missingCredentials" class="flex items-center gap-1.5">
306
334
  <Key :size="12" />
307
- {{ missingCredentials }} credential{{ missingCredentials === 1 ? "" : "s" }} to connect
335
+ {{ t("integration.templates.detail.setup.missingCredentials", missingCredentials) }}
308
336
  </div>
309
337
  <div v-if="missingSecrets" class="flex items-center gap-1.5">
310
338
  <ShieldAlert :size="12" />
311
- {{ missingSecrets }} secret{{ missingSecrets === 1 ? "" : "s" }} missing
339
+ {{ t("integration.templates.detail.setup.missingSecrets", missingSecrets) }}
312
340
  </div>
313
341
  </div>
314
342
  <Button class="w-full h-8 text-sm" @click="openInstall">
315
- Use template
343
+ {{ t("integration.templates.detail.action.useTemplate") }}
316
344
  </Button>
317
345
  <p class="text-xs text-muted-foreground">
318
- Creates a paused copy in your workflows. Nothing runs until you activate it.
346
+ {{ t("integration.templates.detail.setup.hint") }}
319
347
  </p>
320
348
  </div>
321
349
 
322
350
  <div class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
323
- <div class="text-mono text-muted-foreground">Publisher</div>
351
+ <div class="text-mono text-muted-foreground">{{ t("integration.templates.detail.publisher.title") }}</div>
324
352
  <div class="flex items-center gap-2">
325
353
  <span class="flex size-6 items-center justify-center rounded-full bg-muted text-muted-foreground">
326
354
  <User :size="12" />
327
355
  </span>
328
356
  <div class="text-sm font-medium truncate">{{ tpl.publisher.name }}</div>
329
357
  <Badge variant="outline" :class="['text-sm border', publisherBadgeClass(tpl.publisher.kind)]">
330
- {{ PUBLISHER_KIND_LABEL[tpl.publisher.kind] ?? tpl.publisher.kind }}
358
+ {{ publisherKindLabel(tpl.publisher.kind) }}
331
359
  </Badge>
332
360
  </div>
333
361
  <div v-if="packageLabel" class="border-t border-black/[0.06] pt-2 text-sm">
334
- <div class="text-muted-foreground">Ships with</div>
362
+ <div class="text-muted-foreground">{{ t("integration.templates.detail.publisher.shipsWith") }}</div>
335
363
  <div class="font-medium">{{ packageLabel }}</div>
336
364
  </div>
337
365
  <div class="border-t border-black/[0.06] pt-2 grid grid-cols-2 gap-2 text-sm">
338
366
  <div>
339
- <div class="text-muted-foreground">Steps</div>
367
+ <div class="text-muted-foreground">{{ t("integration.templates.detail.rail.stepsLabel") }}</div>
340
368
  <div class="font-medium tabular-nums">{{ steps.length || tpl.nodeCount }}</div>
341
369
  </div>
342
370
  <div>
343
- <div class="text-muted-foreground">Level</div>
371
+ <div class="text-muted-foreground">{{ t("integration.templates.detail.rail.levelLabel") }}</div>
344
372
  <span class="mt-0.5 inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
345
373
  <span class="size-1.5 rounded-full" :class="levelDot(tpl.level)" />
346
374
  {{ tpl.level }}
@@ -349,9 +377,12 @@ function levelDot(level) {
349
377
  </div>
350
378
  </div>
351
379
 
352
- <div v-if="credentialReqs.length" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
353
- <div class="flex items-center gap-1.5 text-mono text-muted-foreground">
354
- <Key :size="11" /> Credentials
380
+ <!-- Named by the label it shows, like the variables panel above: an
381
+ invented name is a second string saying the same thing, and it is
382
+ what a screen reader hears instead of what is written. -->
383
+ <div v-if="credentialReqs.length" role="group" aria-labelledby="tpl-credentials-label" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
384
+ <div id="tpl-credentials-label" class="flex items-center gap-1.5 text-mono text-muted-foreground">
385
+ <Key :size="11" /> {{ t("integration.templates.detail.credentials.title") }}
355
386
  </div>
356
387
  <ul class="space-y-1">
357
388
  <li
@@ -368,22 +399,22 @@ function levelDot(level) {
368
399
  class="shrink-0 bg-emerald-500/10 border-emerald-500/20 text-emerald-700 text-sm"
369
400
  >
370
401
  <CheckCircle2 :size="10" class="mr-0.5" />
371
- {{ ofType(c.slug).length }} ready
402
+ {{ t("integration.templates.detail.credentials.readyCount", ofType(c.slug).length) }}
372
403
  </Badge>
373
404
  <Badge v-else variant="outline" class="shrink-0 bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm">
374
- Connect first
405
+ {{ t("integration.templates.detail.credentials.connectFirst") }}
375
406
  </Badge>
376
407
  </li>
377
408
  </ul>
378
- <NuxtLink to="/integrations/credentials" class="block text-sm text-primary hover:underline">Manage credentials →</NuxtLink>
409
+ <NuxtLink to="/integrations/credentials" class="block text-sm text-primary hover:underline">{{ t("integration.templates.detail.credentials.manageLink") }} →</NuxtLink>
379
410
  </div>
380
411
 
381
- <div v-if="secretChecks.length" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
412
+ <div v-if="secretChecks.length" role="group" aria-labelledby="tpl-secrets-label" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
382
413
  <div class="flex items-center gap-2">
383
- <span class="text-mono text-muted-foreground">Required secrets</span>
414
+ <span id="tpl-secrets-label" class="text-mono text-muted-foreground">{{ t("integration.templates.detail.secrets.title") }}</span>
384
415
  <span v-if="missingSecrets > 0" class="flex items-center gap-1 text-sm text-amber-700">
385
416
  <ShieldAlert :size="11" />
386
- {{ missingSecrets }} missing
417
+ {{ t("integration.templates.detail.secrets.missingBadge", missingSecrets) }}
387
418
  </span>
388
419
  </div>
389
420
  <ul class="space-y-1">
@@ -399,20 +430,20 @@ function levelDot(level) {
399
430
  class="bg-emerald-500/10 border-emerald-500/20 text-emerald-700 text-sm"
400
431
  >
401
432
  <CheckCircle2 :size="10" class="mr-0.5" />
402
- Set
433
+ {{ t("integration.templates.detail.secrets.set") }}
403
434
  </Badge>
404
435
  <Badge v-else variant="outline" class="bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm">
405
- Missing
436
+ {{ t("integration.templates.detail.secrets.missing") }}
406
437
  </Badge>
407
438
  </li>
408
439
  </ul>
409
440
  </div>
410
441
 
411
442
  <div v-if="tpl.industries.length || tpl.vendors.length" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
412
- <div class="text-mono text-muted-foreground">Good fit for</div>
443
+ <div class="text-mono text-muted-foreground">{{ t("integration.templates.detail.fit.title") }}</div>
413
444
  <div class="flex flex-wrap gap-1">
414
445
  <span v-for="ind in tpl.industries" :key="`i-${ind}`" class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
415
- {{ ind === "any" ? "Any industry" : ind }}
446
+ {{ ind === "any" ? t("integration.templates.detail.fit.anyIndustry") : ind }}
416
447
  </span>
417
448
  <span v-for="v in tpl.vendors" :key="`v-${v}`" class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
418
449
  {{ v }}
@@ -425,65 +456,65 @@ function levelDot(level) {
425
456
  </template>
426
457
  <div v-else-if="!loaded" class="p-12 flex items-center justify-center gap-2 text-sm text-muted-foreground">
427
458
  <Loader2 :size="14" class="animate-spin" />
428
- Loading template…
459
+ {{ t("integration.templates.detail.state.loading") }}
429
460
  </div>
430
461
  <div v-else class="p-12 text-center text-sm text-muted-foreground">
431
- Template not found.
462
+ {{ t("integration.templates.detail.state.notFound") }}
432
463
  </div>
433
464
 
434
465
  <!-- Install: name + per-node credential binding ------------------------- -->
435
466
  <Dialog v-model:open="installOpen">
436
467
  <DialogContent class="sm:max-w-lg">
437
468
  <DialogHeader>
438
- <DialogTitle>Use template</DialogTitle>
469
+ <DialogTitle>{{ t("integration.templates.detail.action.useTemplate") }}</DialogTitle>
439
470
  <DialogDescription class="text-sm">
440
- Pre-wire credentials, then we create the workflow. It starts inactive, so you can finish wiring in the editor and activate when ready.
471
+ {{ t("integration.templates.detail.install.description") }}
441
472
  </DialogDescription>
442
473
  </DialogHeader>
443
474
 
444
475
  <div class="space-y-4 py-1 max-h-[28rem] overflow-y-auto pr-1">
445
476
  <div class="space-y-1.5">
446
- <Label for="wf-name">Workflow name</Label>
447
- <Input id="wf-name" v-model="name" class="bg-white" placeholder="Workflow name" />
477
+ <Label for="wf-name">{{ t("integration.templates.detail.install.nameLabel") }}</Label>
478
+ <Input id="wf-name" v-model="name" class="bg-white" :placeholder="t('integration.templates.detail.install.namePlaceholder')" />
448
479
  </div>
449
480
 
450
481
  <!-- Credentials -->
451
482
  <div class="space-y-2">
452
483
  <div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
453
- Credentials
484
+ {{ t("integration.templates.detail.credentials.title") }}
454
485
  </div>
455
486
  <p v-if="!credentialReqs.length" class="text-sm text-muted-foreground italic">
456
- This template needs no credentials.
487
+ {{ t("integration.templates.detail.install.noCredentials") }}
457
488
  </p>
458
489
  <div v-for="req in credentialReqs" :key="req.slug" class="space-y-2">
459
490
  <div class="flex items-center gap-2">
460
491
  <span class="text-sm font-medium">{{ req.name ?? req.slug }}</span>
461
492
  <Badge v-if="!req.registered" variant="outline" class="bg-rose-500/10 border-rose-500/20 text-rose-700 text-sm">
462
- not registered
493
+ {{ t("integration.templates.detail.install.notRegistered") }}
463
494
  </Badge>
464
495
  <Badge v-else-if="!ofType(req.slug).length" variant="outline" class="bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm">
465
- none yet
496
+ {{ t("integration.templates.detail.install.noneYet") }}
466
497
  </Badge>
467
498
  </div>
468
499
 
469
500
  <p v-if="!ofType(req.slug).length" class="rounded-md border-l-2 border-black/[0.08] bg-muted/30 px-2.5 py-1.5 text-sm text-muted-foreground">
470
- No credentials of this type yet.
471
- <NuxtLink to="/integrations/credentials" class="text-primary hover:underline">Create one</NuxtLink>
472
- first, or install now and wire it up in the editor.
501
+ {{ t("integration.templates.detail.install.noCredentialsOfType") }}
502
+ <NuxtLink to="/integrations/credentials" class="text-primary hover:underline">{{ t("integration.templates.detail.install.createOneLink") }}</NuxtLink>
503
+ {{ t("integration.templates.detail.install.orInstallLater") }}
473
504
  </p>
474
505
  <div v-for="nodeId in nodesFor(req.usedBy)" v-else :key="nodeId" class="space-y-1">
475
- <Label class="text-xs text-muted-foreground font-mono">node {{ nodeId }}</Label>
506
+ <Label :for="`tpl-cred-${nodeId}`" class="text-xs text-muted-foreground font-mono">{{ t("integration.templates.detail.install.nodeLabel", { id: nodeId }) }}</Label>
476
507
  <Select
477
508
  :model-value="bindings[nodeId] ?? SELECT_LATER"
478
509
  @update:model-value="(v2) => {
479
510
  bindings[nodeId] = v2 ?? SELECT_LATER;
480
511
  }"
481
512
  >
482
- <SelectTrigger class="bg-white">
483
- <SelectValue placeholder="Select later" />
513
+ <SelectTrigger :id="`tpl-cred-${nodeId}`" class="bg-white">
514
+ <SelectValue :placeholder="t('integration.templates.detail.install.selectLater')" />
484
515
  </SelectTrigger>
485
516
  <SelectContent>
486
- <SelectItem :value="SELECT_LATER">Select later</SelectItem>
517
+ <SelectItem :value="SELECT_LATER">{{ t("integration.templates.detail.install.selectLater") }}</SelectItem>
487
518
  <SelectItem v-for="c in ofType(req.slug)" :key="c.id" :value="c.id">
488
519
  {{ c.name }}
489
520
  </SelectItem>
@@ -496,11 +527,11 @@ function levelDot(level) {
496
527
  <!-- Secrets -->
497
528
  <div v-if="secretChecks.length" class="space-y-2">
498
529
  <div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
499
- Secrets
530
+ {{ t("integration.templates.detail.install.secretsHeader") }}
500
531
  </div>
501
532
  <p class="text-sm text-muted-foreground">
502
- Referenced secret keys must exist before the workflow runs. Manage them on the
503
- <NuxtLink to="/integrations/secrets" class="text-primary hover:underline">Secrets</NuxtLink> page.
533
+ {{ t("integration.templates.detail.install.secretsDescription") }}
534
+ <NuxtLink to="/integrations/secrets" class="text-primary hover:underline">{{ t("integration.nav.item.secrets") }}</NuxtLink> {{ t("integration.templates.detail.install.secretsDescriptionSuffix") }}
504
535
  </p>
505
536
  <ul class="space-y-1">
506
537
  <li
@@ -510,7 +541,7 @@ function levelDot(level) {
510
541
  >
511
542
  <code class="font-mono">{{ s.label }}</code>
512
543
  <Badge variant="outline" :class="s.present ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-700 text-sm' : 'bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm'">
513
- {{ s.present ? "Stored" : "Missing" }}
544
+ {{ s.present ? t("integration.templates.detail.install.secretStored") : t("integration.templates.detail.secrets.missing") }}
514
545
  </Badge>
515
546
  </li>
516
547
  </ul>
@@ -523,11 +554,11 @@ function levelDot(level) {
523
554
 
524
555
  <DialogFooter>
525
556
  <Button variant="outline" :disabled="installing" @click="installOpen = false">
526
- Cancel
557
+ {{ t("integration.common.action.cancel") }}
527
558
  </Button>
528
559
  <Button :disabled="installing || !name.trim()" @click="confirmInstall">
529
560
  <Loader2 v-if="installing" :size="14" class="mr-1 animate-spin" />
530
- {{ installing ? "Setting up\u2026" : "Install" }}
561
+ {{ installing ? t("integration.templates.detail.install.submitting") : t("integration.templates.detail.install.submit") }}
531
562
  </Button>
532
563
  </DialogFooter>
533
564
  </DialogContent>