@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
@@ -1,5 +1,6 @@
1
1
  import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
+ import { useIntegrationsT } from "./useIntegrationsT.js";
3
4
  import { IntegrationsApiError } from "../utils/apiError.js";
4
5
  import { useLoadGate } from "./useLoadGate.js";
5
6
  const BLOB_VERSION = "v0-draft";
@@ -81,8 +82,10 @@ export function useIntegrationsWorkflows() {
81
82
  const loading = useState("integrations.workflows.loading", () => false);
82
83
  const error = useState("integrations.workflows.error", () => null);
83
84
  const loaded = useState("integrations.workflows.loaded", () => false);
85
+ const apiTriggers = useState("integrations.workflows.apiTriggers", () => ({}));
84
86
  const gate = useLoadGate("integrations.workflows", { loading, loaded });
85
87
  const api = useIntegrationsApi();
88
+ const t = useIntegrationsT();
86
89
  function unwrap(res) {
87
90
  return res?.data ?? res;
88
91
  }
@@ -90,6 +93,24 @@ export function useIntegrationsWorkflows() {
90
93
  if (e instanceof IntegrationsApiError) return e.message;
91
94
  return e?.message ?? fallback;
92
95
  }
96
+ function remember(api2, into) {
97
+ if (api2.triggers !== void 0) into[Number(api2.id)] = api2.triggers;
98
+ }
99
+ function ingest(api2) {
100
+ const next = { ...apiTriggers.value };
101
+ remember(api2, next);
102
+ apiTriggers.value = next;
103
+ return toCockpit(api2);
104
+ }
105
+ function ingestList(list2) {
106
+ const next = { ...apiTriggers.value };
107
+ const out = list2.map((api2) => {
108
+ remember(api2, next);
109
+ return toCockpit(api2);
110
+ });
111
+ apiTriggers.value = next;
112
+ return out;
113
+ }
93
114
  function upsertCache(wf) {
94
115
  const idx = workflows.value.findIndex((w) => w.id === wf.id);
95
116
  workflows.value = idx < 0 ? [...workflows.value, wf] : workflows.value.map((w) => w.id === wf.id ? wf : w);
@@ -99,10 +120,11 @@ export function useIntegrationsWorkflows() {
99
120
  error.value = null;
100
121
  try {
101
122
  const list2 = unwrap(await api.get("/workflows")) ?? [];
102
- workflows.value = list2.map(toCockpit);
123
+ workflows.value = ingestList(list2);
103
124
  loaded.value = true;
104
125
  } catch (e) {
105
- error.value = captureError(e, "Could not load workflows.");
126
+ error.value = captureError(e, t("integration.workflows.error.loadFailed"));
127
+ loaded.value = false;
106
128
  } finally {
107
129
  gate.release();
108
130
  }
@@ -114,7 +136,7 @@ export function useIntegrationsWorkflows() {
114
136
  }
115
137
  async function get(id) {
116
138
  try {
117
- const wf = toCockpit(unwrap(await api.get(`/workflows/${id}`)));
139
+ const wf = ingest(unwrap(await api.get(`/workflows/${id}`)));
118
140
  upsertCache(wf);
119
141
  return wf;
120
142
  } catch {
@@ -122,7 +144,7 @@ export function useIntegrationsWorkflows() {
122
144
  }
123
145
  }
124
146
  async function create(input) {
125
- const wf = toCockpit(unwrap(await api.post("/workflows", toPayload({
147
+ const wf = ingest(unwrap(await api.post("/workflows", toPayload({
126
148
  name: input.name,
127
149
  description: input.description,
128
150
  blob: input.blob,
@@ -134,7 +156,7 @@ export function useIntegrationsWorkflows() {
134
156
  }
135
157
  async function duplicate(id, name) {
136
158
  const source = await get(id);
137
- if (!source) throw new Error("Workflow not found.");
159
+ if (!source) throw new Error(t("integration.workflows.error.notFound"));
138
160
  const blob = JSON.parse(JSON.stringify(source.blob));
139
161
  const handleMap = /* @__PURE__ */ new Map();
140
162
  for (const trigger of blob.triggers ?? []) {
@@ -170,10 +192,10 @@ export function useIntegrationsWorkflows() {
170
192
  lastError = err;
171
193
  }
172
194
  }
173
- throw lastError instanceof Error ? lastError : new Error("Could not duplicate the workflow.");
195
+ throw lastError instanceof Error ? lastError : new Error(t("integration.workflows.error.duplicateFailed"));
174
196
  }
175
197
  async function update(id, patch) {
176
- const wf = toCockpit(unwrap(await api.put(`/workflows/${id}`, toPayload({
198
+ const wf = ingest(unwrap(await api.put(`/workflows/${id}`, toPayload({
177
199
  name: patch.name,
178
200
  description: patch.description,
179
201
  blob: patch.blob,
@@ -199,6 +221,7 @@ export function useIntegrationsWorkflows() {
199
221
  loading,
200
222
  error,
201
223
  loaded,
224
+ apiTriggers,
202
225
  total,
203
226
  activeCount,
204
227
  load,
@@ -1,4 +1,5 @@
1
1
  import { computed, onScopeDispose, ref, toValue, watch } from "vue";
2
+ import { isVersionExecutable } from "../data/integrationNodeCatalog.js";
2
3
  import { useIntegrationsApi } from "./integrationsApi.js";
3
4
  import { IntegrationsApiError } from "../utils/apiError.js";
4
5
  export function isResolvableValue(v) {
@@ -15,7 +16,8 @@ export function isResolvableValue(v) {
15
16
  }
16
17
  function classifyError(err) {
17
18
  const status = err instanceof IntegrationsApiError ? err.status : void 0;
18
- const message = err instanceof IntegrationsApiError ? err.message : err?.message ?? "Resolve failed";
19
+ const t = useIntegrationsT();
20
+ const message = err instanceof IntegrationsApiError ? err.message : err?.message ?? t("integration.nodeConfig.resolve.error.generic");
19
21
  let kind;
20
22
  switch (status) {
21
23
  case 409:
@@ -53,7 +55,7 @@ export function useNodeConfigResolve(opts) {
53
55
  });
54
56
  const gated = computed(() => unsatisfied.value.length > 0);
55
57
  const isEnabled = computed(
56
- () => (toValue(opts.enabled) ?? true) && !gated.value && !!toValue(opts.slug) && !!toValue(opts.version)
58
+ () => (toValue(opts.enabled) ?? true) && isVersionExecutable(toValue(opts.slug), toValue(opts.version)) && !gated.value && !!toValue(opts.slug) && !!toValue(opts.version)
57
59
  );
58
60
  const subsetKey = computed(() => {
59
61
  const cfg = toValue(opts.config);
@@ -13,11 +13,74 @@ export declare function applyNodeCatalog(manifests: NodeManifest[]): void;
13
13
  * Categories rendered in the picker. Colors map to the L.I.D.F.
14
14
  * domain palette we use across the cockpit.
15
15
  */
16
+ /**
17
+ * The catalogue's five top-level groups.
18
+ *
19
+ * `label` is the group's **stable English identity**, not display text: the
20
+ * palette matches a stack path back to a category by comparing against it, so
21
+ * translating it in place would break navigation the moment the language
22
+ * changed. Anything that puts a group name on screen calls
23
+ * {@link nodeCategoryLabel} instead.
24
+ */
16
25
  export declare const NODE_CATEGORIES: Array<{
17
26
  key: NodeManifest['category'];
18
27
  label: string;
19
28
  color: string;
20
29
  }>;
30
+ /**
31
+ * A category's name for the reader, read at call time so it follows a language
32
+ * switch. Falls back to the English identity for a category the catalogue has
33
+ * no wording for — visible and usable, rather than a raw key.
34
+ */
35
+ export declare function nodeCategoryLabel(key: NodeManifest['category'] | string | null | undefined): string;
36
+ /** The trigger kinds the catalogue carries a word for, as a runtime value —
37
+ * `TriggerType` is a type and cannot be asked at runtime what it contains. */
38
+ /**
39
+ * The trigger kinds the studio has a word for, in the order a reader is offered
40
+ * them. Exported for the same reason `RUN_STATUS_WORDS` is: the runs list narrows
41
+ * by trigger kind and needs the vocabulary it labels, not a second copy of it.
42
+ */
43
+ export declare const TRIGGER_TYPE_WORDS: readonly ["manual", "schedule", "webhook", "event"];
44
+ /**
45
+ * What a trigger's kind is called for the reader — `manual` as "Manual", and in
46
+ * German as "Manuell".
47
+ *
48
+ * Here rather than in a component because two surfaces say it: the trigger card
49
+ * on the canvas and the Trigger column of the runs list, which used to print the
50
+ * raw field and so read English under a German heading. One function is the same
51
+ * lesson `resolveLocalized` was built on — a word said in two places gets
52
+ * translated in one of them.
53
+ *
54
+ * The keys are `integration.common.triggerType.*`: they belong to neither
55
+ * surface, and the catalogue puts a word shared across surfaces under `common`.
56
+ *
57
+ * A kind the catalogue has no word for is returned as it came, the way
58
+ * `runStatusLabel` returns an unknown status: asking for the key would put the
59
+ * key itself on screen, because a missing message resolves to its own name.
60
+ */
61
+ export declare function triggerTypeLabel(type: TriggerType | string | null | undefined): string;
62
+ /**
63
+ * Whether this exact node version can be executed — its code was built, so
64
+ * `execute:test` and `config:resolve` will not be refused (PO-338).
65
+ *
66
+ * Deliberately NOT `findManifest(...)?.__hasBundle !== false`: that helper falls
67
+ * back to the newest registered version when the pinned one is absent, so a node
68
+ * pinned to a withdrawn version would inherit a stranger's build state and every
69
+ * surface would then say something about a version the catalogue does not have.
70
+ * Only an exact match may answer, and anything else — unknown version, catalogue
71
+ * not loaded yet, a host whose API does not report the field — reads as
72
+ * executable, because the alternative is a studio that hides working nodes.
73
+ */
74
+ export declare function isVersionExecutable(slug: string, version: string): boolean;
75
+ /**
76
+ * The newest version of this node that can be executed, or `null` when none can.
77
+ *
78
+ * What a surface tells the operator to move to. The catalogue is semver-descending
79
+ * within a slug, so the first executable entry is the newest one. `null` where the
80
+ * whole node is unusable — then there is nothing to point at and the surface says
81
+ * only what is wrong.
82
+ */
83
+ export declare function newestExecutableVersion(slug: string): string | null;
21
84
  export declare function findManifest(slug: string, version?: string): NodeManifest | null;
22
85
  export declare const TRIGGER_OUTPUT_PORT = "default";
23
86
  export interface TriggerTemplate {
@@ -34,7 +97,6 @@ export interface TriggerTemplate {
34
97
  en: string;
35
98
  de: string;
36
99
  };
37
- icon: string;
38
100
  /** Default config the canvas seeds when the user drops the tile. */
39
101
  defaultConfig: Record<string, unknown>;
40
102
  }
@@ -1,4 +1,5 @@
1
1
  import { reactive } from "vue";
2
+ import { useIntegrationsT } from "../composables/useIntegrationsT.js";
2
3
  export const INTEGRATION_NODE_CATALOG = reactive([]);
3
4
  export function applyNodeCatalog(manifests) {
4
5
  INTEGRATION_NODE_CATALOG.splice(0, INTEGRATION_NODE_CATALOG.length, ...manifests);
@@ -12,6 +13,27 @@ export const NODE_CATEGORIES = [
12
13
  { key: "transform", label: "Transforms", color: "bg-slate-100 text-amber-700/80" },
13
14
  { key: "control", label: "Control flow", color: "bg-slate-100 text-rose-600/80" }
14
15
  ];
16
+ export function nodeCategoryLabel(key) {
17
+ const category = NODE_CATEGORIES.find((c) => c.key === key);
18
+ if (!category) return "";
19
+ return useIntegrationsT()(`integration.nodes.category.${category.key}`) || category.label;
20
+ }
21
+ export const TRIGGER_TYPE_WORDS = ["manual", "schedule", "webhook", "event"];
22
+ const TRIGGER_TYPE_WORD_SET = new Set(TRIGGER_TYPE_WORDS);
23
+ export function triggerTypeLabel(type) {
24
+ if (!type) return "";
25
+ const kind = String(type);
26
+ if (!TRIGGER_TYPE_WORD_SET.has(kind)) return kind;
27
+ return useIntegrationsT()(`integration.common.triggerType.${kind}`);
28
+ }
29
+ export function isVersionExecutable(slug, version) {
30
+ const exact = INTEGRATION_NODE_CATALOG.find((m) => m.slug === slug && m.version === version);
31
+ return exact ? exact.__hasBundle !== false : true;
32
+ }
33
+ export function newestExecutableVersion(slug) {
34
+ const hit = INTEGRATION_NODE_CATALOG.find((m) => m.slug === slug && m.__hasBundle !== false);
35
+ return hit?.version ?? null;
36
+ }
15
37
  export function findManifest(slug, version) {
16
38
  const matches = INTEGRATION_NODE_CATALOG.filter((m) => m.slug === slug);
17
39
  if (!matches.length) return null;
@@ -28,7 +50,6 @@ export const TRIGGER_TEMPLATES = [
28
50
  en: "Start the workflow yourself with the Run button or an API call.",
29
51
  de: "Starte den Workflow selbst \xFCber den Run-Button oder einen API-Aufruf."
30
52
  },
31
- icon: "Play",
32
53
  defaultConfig: {}
33
54
  },
34
55
  {
@@ -39,7 +60,6 @@ export const TRIGGER_TEMPLATES = [
39
60
  en: "Runs automatically on a schedule you define, for example every night.",
40
61
  de: "L\xE4uft automatisch nach deinem Zeitplan, zum Beispiel jede Nacht."
41
62
  },
42
- icon: "Clock",
43
63
  defaultConfig: { cron: "0 * * * *", timezone: "Europe/Berlin" }
44
64
  },
45
65
  {
@@ -50,7 +70,6 @@ export const TRIGGER_TEMPLATES = [
50
70
  en: "Starts when an external system calls this workflow's URL.",
51
71
  de: "Startet, wenn ein externes System die URL dieses Workflows aufruft."
52
72
  },
53
- icon: "Webhook",
54
73
  defaultConfig: { path: "/incoming", method: "POST" }
55
74
  },
56
75
  {
@@ -61,7 +80,6 @@ export const TRIGGER_TEMPLATES = [
61
80
  en: "Starts when something happens on the platform, for example a new customer.",
62
81
  de: "Startet, wenn auf der Plattform etwas passiert, zum Beispiel ein neuer Kunde."
63
82
  },
64
- icon: "Zap",
65
83
  defaultConfig: { event_type: "" }
66
84
  }
67
85
  ];
@@ -35,6 +35,15 @@ export declare const TEMPLATE_CATEGORIES: Array<{
35
35
  * category the studio has never been taught still gets a chip, named as well as it
36
36
  * can be rather than dropped. That guarantee held on the gallery alone before; it
37
37
  * now holds on all four.
38
+ *
39
+ * `specs/templates.md` AC-8 splits this in two: the first half — a category the
40
+ * studio *does* have a name for — is the studio's own word, and can follow the
41
+ * reader's language; the second half — the registry's own key, capitalised — never
42
+ * can, because a bare key carries no text to read in any language. Only the first
43
+ * half goes through `useIntegrationsT()`. A message missing from the catalogue
44
+ * (this key not merged into `messages/*.json` yet) falls back to the same English
45
+ * word `TEMPLATE_CATEGORIES` always carried, so the label degrades to "correct but
46
+ * unlocalised" rather than to the raw key.
38
47
  */
39
48
  export declare function templateCategoryLabel(key: string): string;
40
49
  /**
@@ -1,3 +1,4 @@
1
+ import { useIntegrationsT } from "../composables/useIntegrationsT.js";
1
2
  export const TEMPLATE_CATEGORIES = [
2
3
  // Uniform muted pill — the label carries the meaning, no rainbow needed.
3
4
  { key: "commerce", label: "Commerce", color: "bg-slate-100 text-slate-700" },
@@ -12,7 +13,12 @@ export const TEMPLATE_CATEGORIES = [
12
13
  export function templateCategoryLabel(key) {
13
14
  if (!key) return "";
14
15
  const known = TEMPLATE_CATEGORIES.find((c) => c.key === key);
15
- if (known) return known.label;
16
+ if (known) {
17
+ const t = useIntegrationsT();
18
+ const messageKey = `integration.templates.category.${known.key}`;
19
+ const translated = t(messageKey);
20
+ return translated === messageKey ? known.label : translated;
21
+ }
16
22
  return key.charAt(0).toUpperCase() + key.slice(1);
17
23
  }
18
24
  export function templatePackageLabel(tpl) {
@@ -1,4 +1,4 @@
1
- import type { NodeManifest, NodeManifestOutput, OutputKind, WorkflowEdge, WorkflowNode } from '../types/integrations.js';
1
+ import type { LocalizedString, NodeManifest, NodeManifestOutput, OutputKind, WorkflowEdge, WorkflowNode } from '../types/integrations.js';
2
2
  /** `${{ nodes.<id>.outputs.<port> }}`, optionally narrowed to one field. */
3
3
  export declare function nodeOutputExpr(nodeId: string, port: string, field?: string): string;
4
4
  /** `${{ trigger.payload }}`, optionally narrowed to a path inside it. */
@@ -20,13 +20,70 @@ export interface FieldEntry {
20
20
  name: string;
21
21
  /** Declared data type (`string`, `number`, `boolean`, …) when the manifest carries one. */
22
22
  dataType: string | null;
23
+ /**
24
+ * What the publisher says this field holds, unresolved. Kept in the shape the
25
+ * manifest sent it so the language is chosen where it is drawn — see the note
26
+ * on {@link PortEntry.description}.
27
+ */
28
+ description?: LocalizedString;
23
29
  }
24
30
  export interface PortEntry {
25
31
  name: string;
26
32
  kind: OutputKind;
27
33
  /** Declared payload fields of this port (schema; may be empty). */
28
34
  fields: FieldEntry[];
35
+ /**
36
+ * What the publisher says flows through this port, **unresolved** — the
37
+ * localised map as the manifest sent it, not a string.
38
+ *
39
+ * Resolving here would pick the language at the moment the ports are derived,
40
+ * and these helpers are pure by design: their tests would then depend on the
41
+ * studio's locale, and a language switched afterwards would not reach a port
42
+ * list already computed. So the raw value travels and whoever draws it calls
43
+ * the resolver, which is how every other manifest string in the studio is
44
+ * read. Absent rather than null when the publisher wrote none: most do not,
45
+ * and an optional key keeps the derivation's shape honest about that.
46
+ */
47
+ description?: LocalizedString;
29
48
  }
49
+ export interface InputPortEntry {
50
+ name: string;
51
+ /** Declared data type of what the node accepts here. */
52
+ dataType: string | null;
53
+ /**
54
+ * Whether an upstream connection is mandatory.
55
+ *
56
+ * Most manifests leave the key out, so the default decides what nearly every
57
+ * node claims. It is **the service's**, not ours: the node-manifest schema
58
+ * documents `required` as *"Whether an upstream connection is mandatory.
59
+ * Defaults to true."*, so an absent key is read as required and the chip says
60
+ * so. Reading it the other way would have the studio call a mandatory input
61
+ * optional on the strength of a key nobody wrote.
62
+ *
63
+ * It is a statement about what the type **declares**, not a rule anything
64
+ * enforces — nothing in the studio blocks a save over it, and the service's own
65
+ * blob validation checks that a named port exists rather than that it is fed.
66
+ */
67
+ required: boolean;
68
+ /** What the publisher says the node expects, unresolved — see {@link PortEntry.description}. */
69
+ description?: LocalizedString;
70
+ }
71
+ /**
72
+ * What a node accepts, read from the manifest's `inputs` map.
73
+ *
74
+ * **Two wire shapes, one of them a trap.** A node with named ports arrives as an
75
+ * object; a node that accepts nothing arrives as `[]`, because the service
76
+ * encodes an empty PHP array as a JSON array rather than an object. So the empty
77
+ * case has a different JSON *type* from the populated one, and reading it with
78
+ * `Object.entries` alone would work by luck rather than by intent. Both land here
79
+ * as an empty list.
80
+ *
81
+ * The studio read this under a singular `input` key until PO-216 — a shape the
82
+ * service has never sent and its schema forbids, so every input a node declared
83
+ * was invisible: the sentence, the data type, and whether a connection was
84
+ * mandatory at all.
85
+ */
86
+ export declare function inputPortsForManifest(manifest: NodeManifest | null | undefined): InputPortEntry[];
30
87
  /**
31
88
  * A manifest declaring zero output ports describes a TERMINAL node (PO-201):
32
89
  * the run ends there, so it gets no output handle and no edge may leave it.
@@ -37,13 +37,38 @@ export function collectUpstreamNodes(selectedId, nodes, edges) {
37
37
  function fieldEntries(raw) {
38
38
  if (Array.isArray(raw)) return raw.map((v) => ({ name: String(v), dataType: null }));
39
39
  if (raw && typeof raw === "object") {
40
- return Object.entries(raw).map(([name, spec]) => ({
41
- name,
42
- dataType: spec && typeof spec === "object" && typeof spec.dataType === "string" ? spec.dataType : null
43
- }));
40
+ return Object.entries(raw).map(([name, spec]) => {
41
+ const s = spec && typeof spec === "object" ? spec : null;
42
+ return {
43
+ name,
44
+ dataType: typeof s?.dataType === "string" ? s.dataType : null,
45
+ ...describedBy(s?.description)
46
+ };
47
+ });
44
48
  }
45
49
  return [];
46
50
  }
51
+ function describedBy(raw) {
52
+ if (typeof raw === "string") return raw.trim() ? { description: raw.trim() } : {};
53
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
54
+ const entries = Object.entries(raw).filter((e) => typeof e[1] === "string" && e[1].trim() !== "").map(([k, v]) => [k, v.trim()]);
55
+ return entries.length ? { description: Object.fromEntries(entries) } : {};
56
+ }
57
+ return {};
58
+ }
59
+ export function inputPortsForManifest(manifest) {
60
+ const raw = manifest?.inputs;
61
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
62
+ return Object.entries(raw).map(([name, spec]) => {
63
+ const s = spec && typeof spec === "object" ? spec : null;
64
+ return {
65
+ name,
66
+ dataType: typeof s?.dataType === "string" ? s.dataType : null,
67
+ required: s?.required !== false,
68
+ ...describedBy(s?.description)
69
+ };
70
+ });
71
+ }
47
72
  export function isTerminalManifest(manifest) {
48
73
  return !!manifest && Array.isArray(manifest.outputs) && !manifest.outputs.length;
49
74
  }
@@ -53,7 +78,7 @@ export function portsForNode(node, manifest, resolvedOutputs) {
53
78
  const ports = [];
54
79
  for (const o of list) {
55
80
  if (o.name) {
56
- ports.push({ name: o.name, kind: o.kind, fields: fieldEntries(o.fields) });
81
+ ports.push({ name: o.name, kind: o.kind, fields: fieldEntries(o.fields), ...describedBy(o.description) });
57
82
  } else if (o.sourceFromConfig) {
58
83
  const raw = node.config?.[o.sourceFromConfig];
59
84
  const values = Array.isArray(raw) ? raw : [];
@@ -1,3 +1,4 @@
1
+ import { useIntegrationsT } from "../composables/useIntegrationsT.js";
1
2
  const PAYLOAD_REF_RE = /\$\{\{\s*trigger\.payload\.([a-zA-Z0-9_.-]+)/g;
2
3
  const FILE_KEYS = /* @__PURE__ */ new Set(["data", "filename", "contentType", "size"]);
3
4
  function collectRefs(value, out) {
@@ -49,7 +50,7 @@ export function payloadFieldsFromBlob(blob) {
49
50
  }
50
51
  continue;
51
52
  }
52
- fields.push({ kind: "file", path: prefix, label: prefix === "" ? "File" : labelFor(prefix) });
53
+ fields.push({ kind: "file", path: prefix, label: prefix === "" ? useIntegrationsT()("integration.common.label.file") : labelFor(prefix) });
53
54
  for (const leaf of leaves) {
54
55
  if (FILE_KEYS.has(leaf)) consumed.add(prefix === "" ? leaf : `${prefix}.${leaf}`);
55
56
  }
@@ -0,0 +1,47 @@
1
+ import type { Component } from 'vue';
2
+ import type { TriggerType } from '../types/integrations.js';
3
+ /**
4
+ * Trigger type → glyph, for every surface that draws one.
5
+ *
6
+ * Eight files drew a trigger's glyph, and they did it two ways. Five spelled the
7
+ * mapping out — the canvas card and the run replay panel as ternaries, the node
8
+ * inspector's header, the template card and the template detail page as records —
9
+ * and three did not carry a mapping at all: they resolved a glyph from Lucide by
10
+ * *name*, so no import mentioned one, in the node palette's trigger tiles (through
11
+ * `nodeIcon`), the create dialog's trigger select and the template flow preview
12
+ * (each through a local `iconFor`).
13
+ *
14
+ * They did not agree: a webhook was a globe on the templates page, a link on the
15
+ * canvas and Lucide's pretzel in the palette, so the same trigger changed shape
16
+ * three times on the way from picking it to seeing it. The replay panel's ternary
17
+ * had no `event` branch at all, so an event-triggered run drew the manual play
18
+ * button — wrong information, not a mismatch (PO-329).
19
+ *
20
+ * One map instead, and the reason it can be Solar's: `TRIGGER_TEMPLATES` is
21
+ * this repository's own catalogue, not a node manifest, so its `icon` strings
22
+ * ('Play', 'Clock', 'Webhook', 'Zap') were Lucide names by habit rather than by
23
+ * contract. The node-type icon map (`nodeIcons.ts`) is a different thing and
24
+ * stays Lucide deliberately — see `docs/icons.md`.
25
+ */
26
+ export declare const TRIGGER_GLYPHS: Record<TriggerType, Component>;
27
+ /**
28
+ * The glyph for a trigger type.
29
+ *
30
+ * The parameter is deliberately wider than `TriggerType`: some callers read the
31
+ * type out of a template blob or a run record, where the service may name a kind
32
+ * the studio has no word for yet — `templates/[slug].vue` is built for exactly
33
+ * that case and says so.
34
+ *
35
+ * Such a kind falls back to the **bolt**, not to the manual play button, which is
36
+ * what the templates surfaces did before this map existed. A play button asserts
37
+ * "you start this yourself", and for an unknown kind that is a claim rather than a
38
+ * gap; the bolt says only that something fires it.
39
+ *
40
+ * `Object.hasOwn`, not `?? Bolt` on its own: a plain object inherits
41
+ * `Object.prototype`, so a service that named a trigger kind `constructor` or
42
+ * `toString` would resolve to a function, the `??` would never fire, and the card
43
+ * would try to render `<component :is="Object">`. The `Lucide[name]` lookups this
44
+ * map replaced could not do that — a module namespace object has a null prototype
45
+ * — so the guard is what keeps the docstring above true rather than nearly true.
46
+ */
47
+ export declare function triggerGlyph(type?: string | null): Component;
@@ -0,0 +1,11 @@
1
+ import { Bolt, ClockCircle, LinkRound, Play } from "#solar-icons";
2
+ export const TRIGGER_GLYPHS = {
3
+ manual: Play,
4
+ schedule: ClockCircle,
5
+ webhook: LinkRound,
6
+ event: Bolt
7
+ };
8
+ export function triggerGlyph(type) {
9
+ const key = String(type);
10
+ return Object.hasOwn(TRIGGER_GLYPHS, key) ? TRIGGER_GLYPHS[key] : Bolt;
11
+ }
@@ -99,6 +99,7 @@ export function freshImageSrc(stored) {
99
99
  return null;
100
100
  }
101
101
  export function nodeLogoOptions() {
102
+ const t = useIntegrationsT();
102
103
  const byId = /* @__PURE__ */ new Map();
103
104
  for (const m of INTEGRATION_NODE_CATALOG) {
104
105
  const imgs = m.images ?? [];
@@ -107,7 +108,7 @@ export function nodeLogoOptions() {
107
108
  const id = imageIdFromSrc(logo.src) ?? logo.src;
108
109
  if (byId.has(id)) continue;
109
110
  const label = m.__package?.label || (m.groups?.length ? resolveLocalized(m.groups[0]) : "") || resolveLocalized(m.name);
110
- byId.set(id, { label: label || "Integration", src: logo.src });
111
+ byId.set(id, { label: label || t("integration.workflows.iconCatalog.label.fallback"), src: logo.src });
111
112
  }
112
113
  return [...byId.entries()].map(([id, v]) => ({ id, src: v.src, label: v.label })).sort((a, b) => a.label.localeCompare(b.label));
113
114
  }