@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
@@ -1,40 +1,45 @@
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";
5
+ import { useLoadGate } from "./useLoadGate.js";
4
6
  const KEY_PATTERN = /^[A-Z0-9_]+$/;
5
7
  export function useIntegrationsSecrets() {
6
8
  const secrets = useState("integrations.secrets", () => []);
7
9
  const loading = useState("integrations.secrets.loading", () => false);
8
10
  const error = useState("integrations.secrets.error", () => null);
9
11
  const loaded = useState("integrations.secrets.loaded", () => false);
12
+ const gate = useLoadGate("integrations.secrets", { loading, loaded });
10
13
  const api = useIntegrationsApi();
14
+ const t = useIntegrationsT();
11
15
  function isValidKey(key) {
12
16
  return KEY_PATTERN.test(key);
13
17
  }
14
18
  function normalize(res) {
15
19
  const obj = res;
20
+ if (Array.isArray(obj?.data)) return obj.data.map(toRecord);
21
+ if (Array.isArray(obj?.secrets)) return obj.secrets.map(toRecord);
16
22
  if (Array.isArray(obj?.keys)) return obj.keys.map((key) => ({ key, created_at: "", updated_at: "" }));
17
- if (Array.isArray(obj?.data)) return obj.data;
18
- if (Array.isArray(obj?.secrets)) return obj.secrets;
19
- if (Array.isArray(res)) return res;
23
+ if (Array.isArray(res)) return res.map(toRecord);
20
24
  return [];
21
25
  }
26
+ function toRecord(r) {
27
+ return { key: r.key ?? "", created_at: r.created_at ?? "", updated_at: r.updated_at ?? "" };
28
+ }
22
29
  function captureError(e, fallback) {
23
30
  if (e instanceof IntegrationsApiError) return e.message;
24
31
  return e?.message ?? fallback;
25
32
  }
26
33
  async function load(force = false) {
27
- if (loading.value) return secrets.value;
28
- if (loaded.value && !force) return secrets.value;
29
- loading.value = true;
34
+ if (!await gate.claim(force)) return secrets.value;
30
35
  error.value = null;
31
36
  try {
32
37
  secrets.value = normalize(await api.get("/secrets"));
33
38
  loaded.value = true;
34
39
  } catch (e) {
35
- error.value = captureError(e, "Could not load secrets.");
40
+ error.value = captureError(e, t("integration.secrets.error.loadFailed"));
36
41
  } finally {
37
- loading.value = false;
42
+ gate.release();
38
43
  }
39
44
  return secrets.value;
40
45
  }
@@ -44,7 +49,7 @@ export function useIntegrationsSecrets() {
44
49
  }
45
50
  async function upsert(key, value) {
46
51
  if (!isValidKey(key)) {
47
- throw new Error("Key must match ^[A-Z0-9_]+$");
52
+ throw new Error(t("integration.secrets.error.invalidKey"));
48
53
  }
49
54
  const created = !secrets.value.some((s) => s.key === key);
50
55
  await api.post("/secrets", { key, value });
@@ -0,0 +1,35 @@
1
+ import type { ComputedRef } from 'vue';
2
+ import type { Translate } from '../i18n/translate.js';
3
+ /**
4
+ * `useIntegrationsT()` — the studio's own interface strings.
5
+ *
6
+ * const t = useIntegrationsT()
7
+ * t('integration.dashboard.runs.title')
8
+ * t('integration.dashboard.attention.missedRuns', missed.length)
9
+ * t('integration.credentials.toast.deleted', { name: credential.name })
10
+ *
11
+ * Returns `t` directly rather than `{ t }`, matching `useT()` in
12
+ * `@revenexx/studio-shared`. It is **not** that function: `useT()` asks the
13
+ * host's catalogue, which has no `integration.*` keys in it, so this module
14
+ * reads its own (`runtime/i18n/messages/*.json`) instead. The names differ on
15
+ * purpose — studio-shared registers a global auto-import called `useT`, and two
16
+ * modules claiming one name is a silent, one-wins collision.
17
+ *
18
+ * Not a composable in the reactive-state sense: no component instance is
19
+ * needed, so it may be called from a plain helper or outside `setup()`.
20
+ *
21
+ * **Reactivity is on the caller.** `t()` reads the language at call time, so
22
+ * anything evaluated once — a `const TABS = [...]` at module or setup scope —
23
+ * keeps whatever language was current when the file loaded. Put it in a
24
+ * `computed()`. That is the one way to ship this looking finished and have it
25
+ * not switch.
26
+ */
27
+ export declare function useIntegrationsT(): Translate;
28
+ /**
29
+ * The language the studio's chrome is rendering in, as a `ComputedRef`.
30
+ *
31
+ * For the rare case that needs the tag itself — an `Intl` format, a `lang`
32
+ * attribute — rather than a translated string. To *set* it, that is
33
+ * `useIntegrationsLocale()`, which is the host's seam.
34
+ */
35
+ export declare function useIntegrationsUiLocale(): ComputedRef<string>;
@@ -0,0 +1,9 @@
1
+ import { computed } from "vue";
2
+ import { studioLocale } from "../utils/localize.js";
3
+ import { translate } from "../i18n/translate.js";
4
+ export function useIntegrationsT() {
5
+ return translate;
6
+ }
7
+ export function useIntegrationsUiLocale() {
8
+ return computed(() => studioLocale());
9
+ }
@@ -1,11 +1,11 @@
1
1
  import type { TemplateRequirements, WorkflowTemplate } from '../types/integrations.js';
2
2
  export declare function useIntegrationsTemplates(): {
3
- templates: any;
3
+ templates: import("vue").ComputedRef<WorkflowTemplate[]>;
4
4
  loaded: any;
5
5
  loading: any;
6
6
  error: any;
7
- total: import("vue").ComputedRef<any>;
8
- popular: import("vue").ComputedRef<any[]>;
7
+ total: import("vue").ComputedRef<number>;
8
+ startingPoints: import("vue").ComputedRef<WorkflowTemplate[]>;
9
9
  load: (force?: boolean) => Promise<WorkflowTemplate[]>;
10
10
  list: () => WorkflowTemplate[];
11
11
  get: (slug: string) => WorkflowTemplate | null;
@@ -2,6 +2,7 @@ import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { IntegrationsApiError } from "../utils/apiError.js";
4
4
  import { resolveLocalized } from "../utils/localize.js";
5
+ import { useLoadGate } from "./useLoadGate.js";
5
6
  const BLOB_VERSION = "v0-draft";
6
7
  const INDUSTRY_VALUES = ["mechanical-engineering", "chemicals", "wholesale", "manufacturing", "retail", "any"];
7
8
  function plainExcerpt(md) {
@@ -34,14 +35,18 @@ function mapListItem(api) {
34
35
  triggerTypes: api.triggerTypes ?? [],
35
36
  images: api.images ?? [],
36
37
  package: api.package ? { name: api.package.name, label: api.package.label ?? null } : null,
37
- category: api.category ?? "sync",
38
+ // The registry documents this field as free-form and returns it as nullable,
39
+ // so "no category" is a state and not an error. It used to become `sync` —
40
+ // a category nothing publishes, so the studio was badging a template with a
41
+ // name no publisher had chosen. Empty travels instead, and every surface that
42
+ // draws a chip leaves it out rather than filling it in (PO-243).
43
+ category: api.category || "",
38
44
  industries: mapIndustries(api.industries),
39
45
  vendors: api.vendors ?? [],
40
46
  requiredSecrets: [],
41
47
  level: api.level ?? "beginner",
42
48
  outline: [],
43
49
  nodeCount: 0,
44
- installs: 0,
45
50
  publisher: publisherFor(api.slug, api.vendors ?? []),
46
51
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
47
52
  blob: emptyBlob(resolveLocalized(api.name)),
@@ -79,10 +84,18 @@ function applyDetail(base, api) {
79
84
  };
80
85
  }
81
86
  export function useIntegrationsTemplates() {
82
- const templates = useState("integrations.templates", () => []);
87
+ const t = useIntegrationsT();
88
+ const rawList = useState("integrations.templates.raw", () => []);
89
+ const rawDetails = useState("integrations.templates.rawDetails", () => ({}));
90
+ const templates = computed(() => rawList.value.map((item) => {
91
+ const base = mapListItem(item);
92
+ const detail = rawDetails.value[item.slug];
93
+ return detail ? applyDetail(base, detail) : base;
94
+ }));
83
95
  const loaded = useState("integrations.templates.loaded", () => false);
84
96
  const loading = useState("integrations.templates.loading", () => false);
85
97
  const error = useState("integrations.templates.error", () => null);
98
+ const gate = useLoadGate("integrations.templates", { loading, loaded });
86
99
  const api = useIntegrationsApi();
87
100
  function unwrap(res) {
88
101
  return res?.data ?? res;
@@ -91,20 +104,20 @@ export function useIntegrationsTemplates() {
91
104
  return e instanceof IntegrationsApiError ? e.message : e?.message ?? fallback;
92
105
  }
93
106
  async function load(force = false) {
94
- if (loading.value) return templates.value;
95
- if (loaded.value && !force) return templates.value;
96
- loading.value = true;
107
+ if (!await gate.claim(force)) return templates.value;
97
108
  error.value = null;
98
109
  try {
99
110
  const list2 = unwrap(await api.get("/templates")) ?? [];
100
- templates.value = list2.map(mapListItem);
111
+ rawList.value = list2;
112
+ rawDetails.value = {};
101
113
  loaded.value = true;
102
114
  } catch (e) {
103
- error.value = captureError(e, "Could not load templates.");
104
- templates.value = [];
115
+ error.value = captureError(e, t("integration.templates.state.error.fallback"));
116
+ rawList.value = [];
117
+ rawDetails.value = {};
105
118
  loaded.value = true;
106
119
  } finally {
107
- loading.value = false;
120
+ gate.release();
108
121
  }
109
122
  return templates.value;
110
123
  }
@@ -112,16 +125,15 @@ export function useIntegrationsTemplates() {
112
125
  return templates.value;
113
126
  }
114
127
  function get(slug) {
115
- return templates.value.find((t) => t.slug === slug) ?? null;
128
+ return templates.value.find((t2) => t2.slug === slug) ?? null;
116
129
  }
117
130
  async function fetchDetail(slug) {
118
131
  const base = get(slug);
119
132
  if (!base?.fromApi) return base;
120
133
  try {
121
134
  const detail = unwrap(await api.get(`/templates/${slug}`));
122
- const merged = applyDetail(base, detail);
123
- templates.value = templates.value.map((t) => t.slug === slug ? merged : t);
124
- return merged;
135
+ rawDetails.value = { ...rawDetails.value, [slug]: detail };
136
+ return get(slug);
125
137
  } catch {
126
138
  return base;
127
139
  }
@@ -165,6 +177,6 @@ export function useIntegrationsTemplates() {
165
177
  return instantiate(slug);
166
178
  }
167
179
  const total = computed(() => templates.value.length);
168
- const popular = computed(() => [...templates.value].sort((a, b) => b.installs - a.installs).slice(0, 4));
169
- return { templates, loaded, loading, error, total, popular, load, list, get, fetchDetail, ensureDetails, requirements, instantiate, install };
180
+ const startingPoints = computed(() => templates.value.slice(0, 4));
181
+ return { templates, loaded, loading, error, total, startingPoints, load, list, get, fetchDetail, ensureDetails, requirements, instantiate, install };
170
182
  }
@@ -1,12 +1,16 @@
1
1
  import { useIntegrationsApi } from "./integrationsApi.js";
2
+ import { useIntegrationsT } from "./useIntegrationsT.js";
2
3
  import { IntegrationsApiError } from "../utils/apiError.js";
4
+ import { useLoadGate } from "./useLoadGate.js";
3
5
  export function useIntegrationsTriggers() {
4
6
  const all = useState("integrations.triggers.all", () => []);
5
7
  const loading = useState("integrations.triggers.loading", () => false);
6
8
  const error = useState("integrations.triggers.error", () => null);
7
9
  const loaded = useState("integrations.triggers.loaded", () => false);
8
10
  const api = useIntegrationsApi();
9
- const { workflows, load: loadWorkflows } = useIntegrationsWorkflows();
11
+ const t = useIntegrationsT();
12
+ const { workflows, load: loadWorkflows, loaded: workflowsLoaded } = useIntegrationsWorkflows();
13
+ const gate = useLoadGate("integrations.triggers", { loading, loaded });
10
14
  function unwrapList(res) {
11
15
  if (Array.isArray(res)) return res;
12
16
  return res?.data ?? [];
@@ -15,21 +19,19 @@ export function useIntegrationsTriggers() {
15
19
  return unwrapList(await api.get(`/workflows/${workflowId}/triggers`));
16
20
  }
17
21
  async function loadAll(force = false) {
18
- if (loading.value) return all.value;
19
- if (loaded.value && !force) return all.value;
20
- loading.value = true;
22
+ if (!await gate.claim(force)) return all.value;
21
23
  error.value = null;
22
24
  try {
23
25
  await loadWorkflows();
24
26
  const settled = await Promise.allSettled(
25
- workflows.value.map(async (wf) => (await list(wf.id)).map((t) => ({ ...t, workflowName: wf.name })))
27
+ workflows.value.map(async (wf) => (await list(wf.id)).map((t2) => ({ ...t2, workflowName: wf.name })))
26
28
  );
27
29
  all.value = settled.flatMap((p) => p.status === "fulfilled" ? p.value : []);
28
- loaded.value = true;
30
+ loaded.value = workflowsLoaded.value && settled.every((p) => p.status === "fulfilled");
29
31
  } catch (e) {
30
- error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? "Could not load triggers.";
32
+ error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.triggers.error.loadFailed");
31
33
  } finally {
32
- loading.value = false;
34
+ gate.release();
33
35
  }
34
36
  return all.value;
35
37
  }
@@ -1,6 +1,8 @@
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";
5
+ import { useLoadGate } from "./useLoadGate.js";
4
6
  const BLOB_VERSION = "v0-draft";
5
7
  function toCockpit(api) {
6
8
  const apiBlob = api.blob ?? {};
@@ -35,6 +37,10 @@ function toCockpit(api) {
35
37
  execution_mode: api.execution_mode,
36
38
  buildStatus: api.build_status ?? null,
37
39
  buildError: api.build_error ?? null,
40
+ // Carried through as-is: `undefined` (e.g. the list response, which omits
41
+ // it) means "unknown", NOT "valid" — callers must fail open and only gate
42
+ // activation on a non-empty array.
43
+ warnings: api.warnings,
38
44
  created_at: api.created_at ?? "",
39
45
  updated_at: api.updated_at ?? ""
40
46
  };
@@ -76,7 +82,9 @@ export function useIntegrationsWorkflows() {
76
82
  const loading = useState("integrations.workflows.loading", () => false);
77
83
  const error = useState("integrations.workflows.error", () => null);
78
84
  const loaded = useState("integrations.workflows.loaded", () => false);
85
+ const gate = useLoadGate("integrations.workflows", { loading, loaded });
79
86
  const api = useIntegrationsApi();
87
+ const t = useIntegrationsT();
80
88
  function unwrap(res) {
81
89
  return res?.data ?? res;
82
90
  }
@@ -89,18 +97,16 @@ export function useIntegrationsWorkflows() {
89
97
  workflows.value = idx < 0 ? [...workflows.value, wf] : workflows.value.map((w) => w.id === wf.id ? wf : w);
90
98
  }
91
99
  async function load(force = false) {
92
- if (loading.value) return workflows.value;
93
- if (loaded.value && !force) return workflows.value;
94
- loading.value = true;
100
+ if (!await gate.claim(force)) return workflows.value;
95
101
  error.value = null;
96
102
  try {
97
103
  const list2 = unwrap(await api.get("/workflows")) ?? [];
98
104
  workflows.value = list2.map(toCockpit);
99
105
  loaded.value = true;
100
106
  } catch (e) {
101
- error.value = captureError(e, "Could not load workflows.");
107
+ error.value = captureError(e, t("integration.workflows.error.loadFailed"));
102
108
  } finally {
103
- loading.value = false;
109
+ gate.release();
104
110
  }
105
111
  return workflows.value;
106
112
  }
@@ -130,7 +136,7 @@ export function useIntegrationsWorkflows() {
130
136
  }
131
137
  async function duplicate(id, name) {
132
138
  const source = await get(id);
133
- if (!source) throw new Error("Workflow not found.");
139
+ if (!source) throw new Error(t("integration.workflows.error.notFound"));
134
140
  const blob = JSON.parse(JSON.stringify(source.blob));
135
141
  const handleMap = /* @__PURE__ */ new Map();
136
142
  for (const trigger of blob.triggers ?? []) {
@@ -166,7 +172,7 @@ export function useIntegrationsWorkflows() {
166
172
  lastError = err;
167
173
  }
168
174
  }
169
- throw lastError instanceof Error ? lastError : new Error("Could not duplicate the workflow.");
175
+ throw lastError instanceof Error ? lastError : new Error(t("integration.workflows.error.duplicateFailed"));
170
176
  }
171
177
  async function update(id, patch) {
172
178
  const wf = toCockpit(unwrap(await api.put(`/workflows/${id}`, toPayload({
@@ -0,0 +1,17 @@
1
+ import type { Ref } from 'vue';
2
+ import type { LoadGate } from '../utils/loadGate.js';
3
+ /**
4
+ * A composable's load gate, keyed by the namespace its own state already uses:
5
+ * `useLoadGate('integrations.triggers', { loading, loaded })`.
6
+ *
7
+ * The rule lives in `utils/loadGate.ts`; this is the wiring, and the wiring is the
8
+ * reason it is a composable. The counters have to share the flag's fate — one per
9
+ * Nuxt app on the client, one per request on the server — so they are `useState`
10
+ * like the flag itself. A module-level counter would be shared between two
11
+ * concurrent server-rendered requests, and then one tenant's finished load would
12
+ * answer another tenant's forced caller.
13
+ */
14
+ export declare function useLoadGate(key: string, flags: {
15
+ loading: Ref<boolean>;
16
+ loaded?: Ref<boolean>;
17
+ }): LoadGate;
@@ -0,0 +1,8 @@
1
+ import { createLoadGate } from "../utils/loadGate.js";
2
+ export function useLoadGate(key, flags) {
3
+ return createLoadGate({
4
+ ...flags,
5
+ started: useState(`${key}.started`, () => 0),
6
+ finished: useState(`${key}.finished`, () => 0)
7
+ });
8
+ }
@@ -15,7 +15,8 @@ export function isResolvableValue(v) {
15
15
  }
16
16
  function classifyError(err) {
17
17
  const status = err instanceof IntegrationsApiError ? err.status : void 0;
18
- const message = err instanceof IntegrationsApiError ? err.message : err?.message ?? "Resolve failed";
18
+ const t = useIntegrationsT();
19
+ const message = err instanceof IntegrationsApiError ? err.message : err?.message ?? t("integration.nodeConfig.resolve.error.generic");
19
20
  let kind;
20
21
  switch (status) {
21
22
  case 409:
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Pure core of the tidy-up branch reorder (see WorkflowEditor's
3
+ * `reorderBranchesByHandle`): given laid-out node positions and the outgoing
4
+ * edges tagged with their output-port index, compute the x moves that put
5
+ * each source's branch subtrees into handle order.
6
+ *
7
+ * dagre places nodes as points and is blind to which output HANDLE an edge
8
+ * leaves from, so a node's branches can land in the opposite left-to-right
9
+ * order to their handles and cross right under the node. Only self-contained,
10
+ * pairwise-disjoint subtrees are moved — a reconverging branch (diamond,
11
+ * shared descendant, outside parent) leaves the whole node untouched rather
12
+ * than corrupted. Idempotent: an already-ordered node yields no moves.
13
+ *
14
+ * The crossing is owned by the direct CHILD NODE's own x, not the subtree's
15
+ * extent: dagre centres a node over its (possibly wide) subtree, so a spine —
16
+ * a long main chain where each step also hangs off a short error/leaf branch —
17
+ * puts the leaf to the LEFT while the continuation node sits at its subtree's
18
+ * centre to the right, crossing out/error even though the two subtrees never
19
+ * share a node. So we reorder by the child nodes' positions: the children keep
20
+ * exactly the set of x slots they already occupy, re-slotted into handle order.
21
+ * That's a pure permutation — the rank's footprint (and the source's centre
22
+ * over it) is preserved and no branch is flung wide, whatever its subtree size.
23
+ *
24
+ * Kept free of vue-flow/component state so it can be unit-tested against
25
+ * hand-built graphs.
26
+ */
27
+ export interface ReorderNode {
28
+ x: number;
29
+ }
30
+ export interface ReorderEdge {
31
+ source: string;
32
+ target: string;
33
+ /** Left-to-right index of the source's output handle this edge leaves from. */
34
+ portIndex: number;
35
+ }
36
+ /** Node id → new x, for exactly the nodes that need to move. */
37
+ export declare function reorderBranchSubtrees(nodes: Map<string, ReorderNode>, edges: ReorderEdge[]): Map<string, number>;
@@ -0,0 +1,71 @@
1
+ export function reorderBranchSubtrees(nodes, edges) {
2
+ const inGraph = (e) => nodes.has(e.source) && nodes.has(e.target);
3
+ const children = /* @__PURE__ */ new Map();
4
+ const parents = /* @__PURE__ */ new Map();
5
+ for (const e of edges) {
6
+ if (!inGraph(e)) continue;
7
+ (children.get(e.source) ?? (children.set(e.source, []), children.get(e.source))).push(e.target);
8
+ (parents.get(e.target) ?? (parents.set(e.target, []), parents.get(e.target))).push(e.source);
9
+ }
10
+ const subtreeOf = (root) => {
11
+ const seen = /* @__PURE__ */ new Set();
12
+ const stack = [root];
13
+ while (stack.length) {
14
+ const v = stack.pop();
15
+ if (seen.has(v)) continue;
16
+ seen.add(v);
17
+ for (const c of children.get(v) ?? []) if (!seen.has(c)) stack.push(c);
18
+ }
19
+ return seen;
20
+ };
21
+ const branchesBySource = /* @__PURE__ */ new Map();
22
+ for (const e of edges) {
23
+ if (!inGraph(e)) continue;
24
+ const byTarget = branchesBySource.get(e.source) ?? (branchesBySource.set(e.source, /* @__PURE__ */ new Map()), branchesBySource.get(e.source));
25
+ byTarget.set(e.target, Math.min(byTarget.get(e.target) ?? Infinity, e.portIndex));
26
+ }
27
+ const moved = /* @__PURE__ */ new Map();
28
+ const xOf = (id) => moved.get(id) ?? nodes.get(id).x;
29
+ for (const [source, byTarget] of branchesBySource) {
30
+ const targets = [...byTarget].map(([target, idx]) => ({ target, idx }));
31
+ if (targets.length < 2) continue;
32
+ const subtrees = new Map(targets.map((t) => [t.target, subtreeOf(t.target)]));
33
+ let safe = true;
34
+ const union = /* @__PURE__ */ new Set();
35
+ outer: for (const t of targets) {
36
+ const st = subtrees.get(t.target);
37
+ if (st.has(source)) {
38
+ safe = false;
39
+ break;
40
+ }
41
+ for (const m of st) {
42
+ if (union.has(m)) {
43
+ safe = false;
44
+ break outer;
45
+ }
46
+ union.add(m);
47
+ for (const p of parents.get(m) ?? []) {
48
+ if (p !== source && !st.has(p)) {
49
+ safe = false;
50
+ break outer;
51
+ }
52
+ }
53
+ }
54
+ }
55
+ if (!safe) continue;
56
+ const desired = [...targets].sort((a, b) => a.idx - b.idx || a.target.localeCompare(b.target));
57
+ const current = [...targets].sort((a, b) => xOf(a.target) - xOf(b.target) || a.target.localeCompare(b.target));
58
+ if (current.every((t, i) => t.target === desired[i].target)) continue;
59
+ const slots = current.map((t) => xOf(t.target));
60
+ for (let i = 0; i < desired.length; i++) {
61
+ const target = desired[i].target;
62
+ const dx = slots[i] - xOf(target);
63
+ if (dx !== 0) {
64
+ for (const m of subtrees.get(target)) {
65
+ moved.set(m, xOf(m) + dx);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ return moved;
71
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Reading the logical port name out of a canvas handle id.
3
+ *
4
+ * Each node on the canvas renders four connection points per port, one per
5
+ * side, so that an edge can leave and arrive without crossing a card body. The
6
+ * side rides along in the handle id as a `__<side>` suffix; the workflow blob
7
+ * stores only the port name. Everything that persists an edge, or reasons about
8
+ * which port an edge left from, has to read the name back out — and an id whose
9
+ * name is missing has to be told apart from one whose name is unusual.
10
+ *
11
+ * An output dot is `<port>__<side>`. A node's single input dot is `__in__<side>`
12
+ * and a group frame's is `__in__top`: the separator sits at the very front, so
13
+ * there is no port name in front of it. Those answer `null` — **not** the empty
14
+ * string, which is what a `slice(0, 0)` would hand back. Every caller writes
15
+ * `portFromHandleId(id) ?? 'default'`, and `??` does not fire on `''`, so an
16
+ * empty name would travel on past the fallback that exists to catch it.
17
+ *
18
+ * It lives here rather than in the editor because it is the one place that knows
19
+ * how to read an id, and because that is what makes it testable: the editor is
20
+ * an SFC, out of reach of the unit suite, and the defect above shipped once
21
+ * already without anything going red.
22
+ */
23
+ export declare function portFromHandleId(handleId: string | null | undefined): string | null;
@@ -0,0 +1,6 @@
1
+ const SIDE_SUFFIX = /__(?:top|right|bottom|left)$/;
2
+ export function portFromHandleId(handleId) {
3
+ if (!handleId) return null;
4
+ const name = handleId.replace(SIDE_SUFFIX, "");
5
+ return name.startsWith("__") || !name ? null : name;
6
+ }
@@ -13,11 +13,52 @@ 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;
21
62
  export declare function findManifest(slug: string, version?: string): NodeManifest | null;
22
63
  export declare const TRIGGER_OUTPUT_PORT = "default";
23
64
  export interface TriggerTemplate {
@@ -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,19 @@ 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
+ }
15
29
  export function findManifest(slug, version) {
16
30
  const matches = INTEGRATION_NODE_CATALOG.filter((m) => m.slug === slug);
17
31
  if (!matches.length) return null;