@revenexx/studio-integrations 1.1.1 → 1.3.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 (119) hide show
  1. package/CHANGELOG.md +638 -0
  2. package/README.md +3 -1
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
  7. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
  8. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  9. package/dist/runtime/components/Integrations/ExpressionInput.vue +45 -19
  10. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  11. package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
  12. package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
  13. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
  14. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
  15. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
  16. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +5 -1
  17. package/dist/runtime/components/Integrations/NodeConfigField.vue +88 -1
  18. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +5 -1
  19. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +7 -3
  20. package/dist/runtime/components/Integrations/NodeInspector.vue +125 -54
  21. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +7 -3
  22. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
  23. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
  24. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
  25. package/dist/runtime/components/Integrations/RunActionsMenu.vue +2 -1
  26. package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
  27. package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
  28. package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
  29. package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
  30. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
  31. package/dist/runtime/components/Integrations/RunsTable.vue +129 -18
  32. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
  33. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
  34. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
  35. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
  36. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
  37. package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
  38. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
  39. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
  40. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
  41. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
  42. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
  43. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
  44. package/dist/runtime/components/Integrations/WorkflowEditor.vue +78 -30
  45. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
  46. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
  47. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
  48. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
  49. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
  50. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
  51. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
  52. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
  53. package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
  54. package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
  55. package/dist/runtime/components/Integrations/canvasSides.js +22 -0
  56. package/dist/runtime/composables/useCredentialTypes.js +2 -6
  57. package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
  58. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
  59. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
  60. package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
  61. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  62. package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
  63. package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
  64. package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
  65. package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
  66. package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
  67. package/dist/runtime/composables/useIntegrationsTemplates.js +7 -5
  68. package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
  69. package/dist/runtime/composables/useIntegrationsWorkflows.js +10 -7
  70. package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
  71. package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
  72. package/dist/runtime/composables/useOAuthConnect.js +33 -0
  73. package/dist/runtime/data/expressionEditor.d.ts +17 -5
  74. package/dist/runtime/data/expressionEditor.js +11 -5
  75. package/dist/runtime/data/handleId.d.ts +61 -1
  76. package/dist/runtime/data/handleId.js +8 -1
  77. package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
  78. package/dist/runtime/data/integrationNodeCatalog.js +8 -0
  79. package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
  80. package/dist/runtime/data/nodeVersionMove.js +65 -0
  81. package/dist/runtime/data/payloadForm.js +3 -2
  82. package/dist/runtime/i18n/messages/de.json +79 -12
  83. package/dist/runtime/i18n/messages/en.json +79 -12
  84. package/dist/runtime/pages/integrations/audit.vue +7 -6
  85. package/dist/runtime/pages/integrations/credentials.vue +9 -21
  86. package/dist/runtime/pages/integrations/index.vue +55 -8
  87. package/dist/runtime/pages/integrations/runs.vue +116 -7
  88. package/dist/runtime/pages/integrations/secrets.vue +2 -1
  89. package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
  90. package/dist/runtime/pages/integrations/templates/index.vue +18 -15
  91. package/dist/runtime/pages/integrations/workflows/[id].vue +169 -24
  92. package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
  93. package/dist/runtime/types/integrations.d.ts +49 -1
  94. package/dist/runtime/utils/apiError.d.ts +21 -1
  95. package/dist/runtime/utils/apiError.js +1 -1
  96. package/dist/runtime/utils/buildSettle.d.ts +68 -0
  97. package/dist/runtime/utils/buildSettle.js +29 -0
  98. package/dist/runtime/utils/clipboard.d.ts +33 -0
  99. package/dist/runtime/utils/clipboard.js +11 -0
  100. package/dist/runtime/utils/deepClone.d.ts +41 -0
  101. package/dist/runtime/utils/deepClone.js +3 -0
  102. package/dist/runtime/utils/formatDuration.d.ts +44 -0
  103. package/dist/runtime/utils/formatDuration.js +13 -0
  104. package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
  105. package/dist/runtime/utils/nodeDisplayName.js +6 -0
  106. package/dist/runtime/utils/notify.d.ts +1 -1
  107. package/dist/runtime/utils/runEnvelope.d.ts +31 -0
  108. package/dist/runtime/utils/runEnvelope.js +12 -0
  109. package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
  110. package/dist/runtime/utils/runFeedQuery.js +66 -0
  111. package/dist/runtime/utils/runOutputs.d.ts +33 -0
  112. package/dist/runtime/utils/runOutputs.js +10 -0
  113. package/dist/runtime/utils/settingCondition.d.ts +48 -0
  114. package/dist/runtime/utils/settingCondition.js +84 -0
  115. package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
  116. package/dist/runtime/utils/stateNamespaceName.js +8 -0
  117. package/dist/runtime/utils/validationFindings.d.ts +26 -3
  118. package/dist/runtime/utils/validationFindings.js +18 -3
  119. package/package.json +1 -1
@@ -1,16 +1,12 @@
1
1
  import { ref } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
- import { IntegrationsApiError } from "../utils/apiError.js";
4
+ import { errorReason } from "../utils/apiError.js";
5
5
  export function useIntegrationsRevisions() {
6
6
  const api = useIntegrationsApi();
7
7
  const t = useIntegrationsT();
8
8
  const loading = ref(false);
9
9
  const error = ref(null);
10
- function captureError(e, fallback) {
11
- if (e instanceof IntegrationsApiError) return e.message;
12
- return e?.message ?? fallback;
13
- }
14
10
  async function list(workflowId, page = 1, perPage = 25) {
15
11
  loading.value = true;
16
12
  error.value = null;
@@ -18,7 +14,7 @@ export function useIntegrationsRevisions() {
18
14
  const res = await api.get(`/workflows/${workflowId}/revisions?page=${page}&per_page=${perPage}`);
19
15
  return { revisions: res.data ?? [], meta: res.meta };
20
16
  } catch (e) {
21
- error.value = captureError(e, t("integration.revisions.error.loadFailed"));
17
+ error.value = errorReason(e, t("integration.revisions.error.loadFailed"));
22
18
  throw e;
23
19
  } finally {
24
20
  loading.value = false;
@@ -1,23 +1,11 @@
1
1
  import type { RunStatus, RunStepRow, WorkflowRun } from '../types/integrations.js';
2
+ import type { RunFeedQuery } from '../utils/runFeedQuery.js';
3
+ export type { RunFeedQuery };
2
4
  interface RunSummary {
3
5
  total: number;
4
6
  in_flight: number;
5
7
  by_status: Record<string, number>;
6
8
  }
7
- /**
8
- * Which page of the tenant's list to read.
9
- *
10
- * Only the page: `/runs` also narrows by status, trigger kind, workflow, window
11
- * and text, and none of that is driven from here yet. The shared DataTable
12
- * narrows and searches what it holds, and it cannot report a filter selection
13
- * back to the page under `serverMode` — so a narrowing sent to the platform
14
- * would have nothing to set it. That is the remaining half of PO-231, and the
15
- * parameters are waiting on the endpoint for it.
16
- */
17
- export interface RunFeedQuery {
18
- page: number;
19
- perPage: number;
20
- }
21
9
  export declare function useIntegrationsRuns(): {
22
10
  runs: any;
23
11
  loading: any;
@@ -29,6 +17,8 @@ export declare function useIntegrationsRuns(): {
29
17
  summary: any;
30
18
  hasMore: any;
31
19
  query: any;
20
+ setQuery: (patch: Partial<RunFeedQuery>) => Promise<WorkflowRun[]>;
21
+ narrowedTotal: any;
32
22
  load: (force?: boolean) => Promise<WorkflowRun[]>;
33
23
  loadSummary: () => Promise<RunSummary>;
34
24
  loadForWorkflow: (workflowId: number, workflowName: string, opts?: {
@@ -77,4 +67,3 @@ export declare function useIntegrationsRuns(): {
77
67
  }>;
78
68
  getStackTrace: (workflowId: number | string, runId: number | string) => Promise<string>;
79
69
  };
80
- export {};
@@ -1,31 +1,17 @@
1
1
  import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
- import { IntegrationsApiError } from "../utils/apiError.js";
4
+ import { errorReason } from "../utils/apiError.js";
5
5
  import { useLoadGate } from "./useLoadGate.js";
6
+ import {
7
+ DEFAULT_RUN_DIRECTION,
8
+ DEFAULT_RUN_SORT,
9
+ mapRunStatus,
10
+ runFeedNarrowingString,
11
+ runFeedQueryString,
12
+ withRunFeedPatch
13
+ } from "../utils/runFeedQuery.js";
6
14
  const EMPTY_SUMMARY = { total: 0, in_flight: 0, by_status: {} };
7
- function mapStatus(status) {
8
- switch (status) {
9
- case "completed":
10
- return "succeeded";
11
- case "pending":
12
- return "queued";
13
- case "terminated":
14
- return "failed";
15
- case "timed_out":
16
- return "failed";
17
- case "running":
18
- return "running";
19
- case "failed":
20
- return "failed";
21
- case "cancelled":
22
- return "cancelled";
23
- // An unknown/unexpected status must never surface as a stuck `running` —
24
- // fail closed so a run the backend can't classify is visibly terminal.
25
- default:
26
- return "failed";
27
- }
28
- }
29
15
  export function useIntegrationsRuns() {
30
16
  const runs = useState("integrations.runs", () => []);
31
17
  const loading = useState("integrations.runs.loading", () => false);
@@ -36,9 +22,20 @@ export function useIntegrationsRuns() {
36
22
  const hasMore = useState("integrations.runs.hasMore", () => false);
37
23
  const query = useState("integrations.runs.query", () => ({
38
24
  page: 1,
39
- perPage: 100
25
+ perPage: 25,
26
+ status: [],
27
+ triggerType: "",
28
+ search: "",
29
+ sort: DEFAULT_RUN_SORT,
30
+ direction: DEFAULT_RUN_DIRECTION
40
31
  }));
32
+ const narrowedTotal = useState("integrations.runs.narrowedTotal", () => 0);
33
+ const summaryWrite = useState("integrations.runs.summaryWrite", () => 0);
41
34
  const gate = useLoadGate("integrations.runs", { loading, loaded });
35
+ function claimSummary() {
36
+ const mine = ++summaryWrite.value;
37
+ return () => mine === summaryWrite.value;
38
+ }
42
39
  const SYNC_LIMIT = 5;
43
40
  const api = useIntegrationsApi();
44
41
  const t = useIntegrationsT();
@@ -49,7 +46,7 @@ export function useIntegrationsRuns() {
49
46
  id: String(r.id),
50
47
  workflowId: r.workflow_id,
51
48
  workflowName: r.workflow_name ?? workflowName,
52
- status: mapStatus(r.status),
49
+ status: mapRunStatus(r.status),
53
50
  temporalRunId: r.temporal_run_id ?? null,
54
51
  triggerId: r.trigger_handle ?? "",
55
52
  // The platform reports what started the run. This was hardcoded to
@@ -72,14 +69,24 @@ export function useIntegrationsRuns() {
72
69
  if (!await gate.claim(force)) return runs.value;
73
70
  error.value = null;
74
71
  try {
75
- const { page: pageNumber, perPage } = query.value;
72
+ const narrowing = runFeedNarrowingString(query.value);
73
+ const counted = claimSummary();
76
74
  const [page, counts] = await Promise.all([
77
- api.get(`/runs?page=${pageNumber}&per_page=${perPage}`),
78
- api.get("/runs/summary")
75
+ api.get(`/runs?${runFeedQueryString(query.value)}`),
76
+ api.get(`/runs/summary${narrowing ? `?${narrowing}` : ""}`)
79
77
  ]);
80
78
  runs.value = (page?.data ?? []).map((r) => mapRun(r));
81
79
  hasMore.value = page?.meta?.has_more ?? false;
82
- summary.value = counts?.data ?? { ...EMPTY_SUMMARY };
80
+ narrowedTotal.value = counts?.data?.total ?? 0;
81
+ if (narrowing) {
82
+ const refreshed = claimSummary();
83
+ void api.get("/runs/summary").then((tenantWide) => {
84
+ if (refreshed()) summary.value = tenantWide?.data ?? { ...EMPTY_SUMMARY };
85
+ }).catch(() => {
86
+ });
87
+ } else if (counted()) {
88
+ summary.value = counts?.data ?? { ...EMPTY_SUMMARY };
89
+ }
83
90
  if (!reconciling.value) {
84
91
  reconciling.value = true;
85
92
  try {
@@ -99,7 +106,7 @@ export function useIntegrationsRuns() {
99
106
  }
100
107
  loaded.value = true;
101
108
  } catch (e) {
102
- error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.runs.error.loadFailed");
109
+ error.value = errorReason(e, t("integration.runs.error.loadFailed"));
103
110
  loaded.value = false;
104
111
  } finally {
105
112
  gate.release();
@@ -139,11 +146,12 @@ export function useIntegrationsRuns() {
139
146
  return result;
140
147
  }
141
148
  async function loadSummary() {
149
+ const current = claimSummary();
142
150
  try {
143
151
  const res = await api.get("/runs/summary");
144
- summary.value = res?.data ?? { ...EMPTY_SUMMARY };
152
+ if (current()) summary.value = res?.data ?? { ...EMPTY_SUMMARY };
145
153
  } catch (e) {
146
- error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.runs.error.loadFailed");
154
+ if (current()) error.value = errorReason(e, t("integration.runs.error.loadFailed"));
147
155
  }
148
156
  return summary.value;
149
157
  }
@@ -161,7 +169,7 @@ export function useIntegrationsRuns() {
161
169
  latestPerWorkflow.value = (res?.data ?? []).map((r) => mapRun(r));
162
170
  latestLoaded.value = true;
163
171
  } catch (e) {
164
- error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.runs.error.loadFailed");
172
+ error.value = errorReason(e, t("integration.runs.error.loadFailed"));
165
173
  latestLoaded.value = false;
166
174
  } finally {
167
175
  latestGate.release();
@@ -208,8 +216,13 @@ export function useIntegrationsRuns() {
208
216
  };
209
217
  const getStackTrace = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}/stack-trace`).then((r) => r.stack_trace);
210
218
  const total = computed(() => summary.value.total);
211
- const failedCount = computed(() => Object.entries(summary.value.by_status).filter(([status]) => mapStatus(status) === "failed").reduce((sum, [, count]) => sum + count, 0));
219
+ const failedCount = computed(() => Object.entries(summary.value.by_status).filter(([status]) => mapRunStatus(status) === "failed").reduce((sum, [, count]) => sum + count, 0));
212
220
  const runningCount = computed(() => summary.value.in_flight);
221
+ async function setQuery(patch) {
222
+ const next = withRunFeedPatch(query.value, patch);
223
+ query.value = next;
224
+ return load(true);
225
+ }
213
226
  return {
214
227
  runs,
215
228
  loading,
@@ -221,6 +234,8 @@ export function useIntegrationsRuns() {
221
234
  summary,
222
235
  hasMore,
223
236
  query,
237
+ setQuery,
238
+ narrowedTotal,
224
239
  load,
225
240
  loadSummary,
226
241
  loadForWorkflow,
@@ -2,7 +2,7 @@ import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { useLoadGate } from "./useLoadGate.js";
5
- import { IntegrationsApiError } from "../utils/apiError.js";
5
+ import { errorReason } from "../utils/apiError.js";
6
6
  import { scheduleTimezone } from "../utils/schedule.js";
7
7
  export function useIntegrationsSchedules() {
8
8
  const api = useIntegrationsApi();
@@ -70,7 +70,7 @@ export function useIntegrationsSchedules() {
70
70
  meta.value = res?.meta ?? { total: 0, live_count: 0, timezones: [] };
71
71
  loaded.value = true;
72
72
  } catch (e) {
73
- error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.schedules.error.loadFailed");
73
+ error.value = errorReason(e, t("integration.schedules.error.loadFailed"));
74
74
  loaded.value = false;
75
75
  } finally {
76
76
  gate.release();
@@ -1,7 +1,7 @@
1
1
  import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
- import { IntegrationsApiError } from "../utils/apiError.js";
4
+ import { errorReason } from "../utils/apiError.js";
5
5
  import { useLoadGate } from "./useLoadGate.js";
6
6
  const KEY_PATTERN = /^[A-Z0-9_]+$/;
7
7
  export function useIntegrationsSecrets() {
@@ -26,10 +26,6 @@ export function useIntegrationsSecrets() {
26
26
  function toRecord(r) {
27
27
  return { key: r.key ?? "", created_at: r.created_at ?? "", updated_at: r.updated_at ?? "" };
28
28
  }
29
- function captureError(e, fallback) {
30
- if (e instanceof IntegrationsApiError) return e.message;
31
- return e?.message ?? fallback;
32
- }
33
29
  async function load(force = false) {
34
30
  if (!await gate.claim(force)) return secrets.value;
35
31
  error.value = null;
@@ -37,7 +33,7 @@ export function useIntegrationsSecrets() {
37
33
  secrets.value = normalize(await api.get("/secrets"));
38
34
  loaded.value = true;
39
35
  } catch (e) {
40
- error.value = captureError(e, t("integration.secrets.error.loadFailed"));
36
+ error.value = errorReason(e, t("integration.secrets.error.loadFailed"));
41
37
  } finally {
42
38
  gate.release();
43
39
  }
@@ -1,6 +1,6 @@
1
1
  import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
- import { IntegrationsApiError } from "../utils/apiError.js";
3
+ import { errorReason } from "../utils/apiError.js";
4
4
  import { resolveLocalized } from "../utils/localize.js";
5
5
  import { useLoadGate } from "./useLoadGate.js";
6
6
  const BLOB_VERSION = "v0-draft";
@@ -70,6 +70,11 @@ function applyDetail(base, api) {
70
70
  edges: def.edges ?? [],
71
71
  triggerPositions: def.triggerPositions ?? {},
72
72
  variables: def.variables,
73
+ // Enumerated key by key like every block above it, so one missing here is
74
+ // one lost in this direction. Harmless today — a template becomes a workflow
75
+ // server-side, so this blob only feeds the preview — but the preview is
76
+ // where an author would look to see what a template remembers.
77
+ state: def.state,
73
78
  triggers
74
79
  };
75
80
  const md = resolveLocalized(api.description ?? "") || base.descriptionMd;
@@ -100,9 +105,6 @@ export function useIntegrationsTemplates() {
100
105
  function unwrap(res) {
101
106
  return res?.data ?? res;
102
107
  }
103
- function captureError(e, fallback) {
104
- return e instanceof IntegrationsApiError ? e.message : e?.message ?? fallback;
105
- }
106
108
  async function load(force = false) {
107
109
  if (!await gate.claim(force)) return templates.value;
108
110
  error.value = null;
@@ -112,7 +114,7 @@ export function useIntegrationsTemplates() {
112
114
  rawDetails.value = {};
113
115
  loaded.value = true;
114
116
  } catch (e) {
115
- error.value = captureError(e, t("integration.templates.state.error.fallback"));
117
+ error.value = errorReason(e, t("integration.templates.state.error.fallback"));
116
118
  rawList.value = [];
117
119
  rawDetails.value = {};
118
120
  loaded.value = true;
@@ -1,6 +1,6 @@
1
1
  import { useIntegrationsApi } from "./integrationsApi.js";
2
2
  import { useIntegrationsT } from "./useIntegrationsT.js";
3
- import { IntegrationsApiError } from "../utils/apiError.js";
3
+ import { errorReason } from "../utils/apiError.js";
4
4
  import { useLoadGate } from "./useLoadGate.js";
5
5
  export function tenantTriggers(workflows, byWorkflow) {
6
6
  return workflows.flatMap((wf) => (byWorkflow[wf.id] ?? []).flatMap((t) => {
@@ -40,7 +40,7 @@ export function useIntegrationsTriggers() {
40
40
  error.value = workflowsError.value;
41
41
  loaded.value = workflowsLoaded.value && !workflowsError.value;
42
42
  } catch (e) {
43
- error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.triggers.error.loadFailed");
43
+ error.value = errorReason(e, t("integration.triggers.error.loadFailed"));
44
44
  } finally {
45
45
  gate.release();
46
46
  }
@@ -1,7 +1,8 @@
1
1
  import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
- import { IntegrationsApiError } from "../utils/apiError.js";
4
+ import { deepClone } from "../utils/deepClone.js";
5
+ import { errorReason } from "../utils/apiError.js";
5
6
  import { useLoadGate } from "./useLoadGate.js";
6
7
  const BLOB_VERSION = "v0-draft";
7
8
  function toCockpit(api) {
@@ -19,6 +20,7 @@ function toCockpit(api) {
19
20
  nodes: apiBlob.nodes ?? [],
20
21
  edges: apiBlob.edges ?? [],
21
22
  variables: apiBlob.variables ?? void 0,
23
+ state: apiBlob.state ?? void 0,
22
24
  triggerPositions: apiBlob.triggerPositions ?? {},
23
25
  stickies: apiBlob.stickies ?? [],
24
26
  groups: apiBlob.groups ?? [],
@@ -63,6 +65,11 @@ function toPayload(meta) {
63
65
  ...b.groups?.length ? { groups: b.groups } : {},
64
66
  // variables were silently dropped before — the schema always allowed them.
65
67
  ...b.variables && Object.keys(b.variables).length ? { variables: b.variables } : {},
68
+ // Same shape, and the same trap: both mappers enumerate blob keys, so a
69
+ // block missing from either one is lost in that direction. `state` is the
70
+ // declaration that makes the run grant reachable at all (PO-374) — losing
71
+ // it on save would take the workflow's memory away without a message.
72
+ ...b.state?.length ? { state: b.state } : {},
66
73
  ...b.tags?.length ? { tags: b.tags } : {},
67
74
  ...b.icon ? { icon: b.icon } : {}
68
75
  },
@@ -89,10 +96,6 @@ export function useIntegrationsWorkflows() {
89
96
  function unwrap(res) {
90
97
  return res?.data ?? res;
91
98
  }
92
- function captureError(e, fallback) {
93
- if (e instanceof IntegrationsApiError) return e.message;
94
- return e?.message ?? fallback;
95
- }
96
99
  function remember(api2, into) {
97
100
  if (api2.triggers !== void 0) into[Number(api2.id)] = api2.triggers;
98
101
  }
@@ -123,7 +126,7 @@ export function useIntegrationsWorkflows() {
123
126
  workflows.value = ingestList(list2);
124
127
  loaded.value = true;
125
128
  } catch (e) {
126
- error.value = captureError(e, t("integration.workflows.error.loadFailed"));
129
+ error.value = errorReason(e, t("integration.workflows.error.loadFailed"));
127
130
  loaded.value = false;
128
131
  } finally {
129
132
  gate.release();
@@ -157,7 +160,7 @@ export function useIntegrationsWorkflows() {
157
160
  async function duplicate(id, name) {
158
161
  const source = await get(id);
159
162
  if (!source) throw new Error(t("integration.workflows.error.notFound"));
160
- const blob = JSON.parse(JSON.stringify(source.blob));
163
+ const blob = deepClone(source.blob);
161
164
  const handleMap = /* @__PURE__ */ new Map();
162
165
  for (const trigger of blob.triggers ?? []) {
163
166
  const fresh = crypto.randomUUID();
@@ -1,7 +1,7 @@
1
1
  import { computed, onScopeDispose, ref, toValue, watch } from "vue";
2
2
  import { isVersionExecutable } from "../data/integrationNodeCatalog.js";
3
3
  import { useIntegrationsApi } from "./integrationsApi.js";
4
- import { IntegrationsApiError } from "../utils/apiError.js";
4
+ import { IntegrationsApiError, errorReason } from "../utils/apiError.js";
5
5
  export function isResolvableValue(v) {
6
6
  if (v === void 0 || v === null) return false;
7
7
  if (typeof v === "string") {
@@ -17,7 +17,7 @@ export function isResolvableValue(v) {
17
17
  function classifyError(err) {
18
18
  const status = err instanceof IntegrationsApiError ? err.status : void 0;
19
19
  const t = useIntegrationsT();
20
- const message = err instanceof IntegrationsApiError ? err.message : err?.message ?? t("integration.nodeConfig.resolve.error.generic");
20
+ const message = errorReason(err, t("integration.nodeConfig.resolve.error.generic"));
21
21
  let kind;
22
22
  switch (status) {
23
23
  case 409:
@@ -0,0 +1,42 @@
1
+ /**
2
+ * How an OAuth connect attempt ended.
3
+ *
4
+ * `completed` is the provider's own answer, relayed by the landing page. `closed` is
5
+ * the operator shutting the window — which says nothing about whether the
6
+ * authorisation went through, so the caller re-reads rather than announcing anything.
7
+ */
8
+ export type OAuthConnectOutcome = 'completed' | 'closed';
9
+ /**
10
+ * Connecting a credential by OAuth: the popup, and the two ways of finding out it is
11
+ * over.
12
+ *
13
+ * The studio opens the provider's authorisation page in a separate window rather than
14
+ * navigating, so that a half-filled credential form is still there afterwards. Two
15
+ * things can then end the attempt, and both have to be watched, because neither
16
+ * implies the other:
17
+ *
18
+ * - The integrations callback redirects to `/integrations/credentials/oauth-complete`,
19
+ * which posts `integrations-oauth-complete` back to us. This is the success path,
20
+ * and it is the only one that knows the authorisation actually happened.
21
+ * - The operator closes the window instead — cancelling, or completing it somewhere
22
+ * the message could not come back from. Nothing is posted, so the only signal is
23
+ * `popup.closed`, which cannot be subscribed to and has to be polled.
24
+ *
25
+ * **This needs the integrations-side `oauth_return_url` and the provider redirect to
26
+ * be configured**; without them the callback never reaches the landing page and only
27
+ * the close path ever fires. The prerequisite is written out in `oauth-complete.vue`.
28
+ * It lives on the composable rather than at a call site because it is the reason the
29
+ * handshake has two paths at all, and a reader who only opens one of the three callers
30
+ * would otherwise never meet it — which is exactly what happened while this was written
31
+ * out three times and the prerequisite recorded in one of them.
32
+ *
33
+ * `onSettled` is called once, whichever path fires, with what is known. The caller
34
+ * re-reads the credential and decides what to say, because that is where the three
35
+ * genuinely differ: the list refreshes the list, the detail dialog refreshes one
36
+ * credential and tells its parent, and the create dialog says nothing at all because the
37
+ * operator is still looking at the form. Only the waiting is shared.
38
+ */
39
+ export declare function useOAuthConnect(onSettled: (outcome: OAuthConnectOutcome) => void | Promise<void>): {
40
+ open: (authorizeUrl: string) => void;
41
+ cancel: () => void;
42
+ };
@@ -0,0 +1,33 @@
1
+ import { onUnmounted } from "vue";
2
+ export function useOAuthConnect(onSettled) {
3
+ let release = null;
4
+ function settle(outcome) {
5
+ if (!release) return;
6
+ release();
7
+ release = null;
8
+ void onSettled(outcome);
9
+ }
10
+ function open(authorizeUrl) {
11
+ release?.();
12
+ const popup = window.open(authorizeUrl, "oauth-connect", "width=600,height=720");
13
+ if (!popup) return;
14
+ const onMessage = (ev) => {
15
+ if (ev.origin !== window.location.origin) return;
16
+ if (ev.data?.type === "integrations-oauth-complete") settle("completed");
17
+ };
18
+ window.addEventListener("message", onMessage);
19
+ const poll = window.setInterval(() => {
20
+ if (popup.closed) settle("closed");
21
+ }, 1e3);
22
+ release = () => {
23
+ window.clearInterval(poll);
24
+ window.removeEventListener("message", onMessage);
25
+ };
26
+ }
27
+ function cancel() {
28
+ release?.();
29
+ release = null;
30
+ }
31
+ onUnmounted(cancel);
32
+ return { open, cancel };
33
+ }
@@ -12,11 +12,23 @@ export interface InlineNodeJSON {
12
12
  export declare const EXPR_TOKEN_NODE = "exprToken";
13
13
  /** Raw text → inline content: text nodes with one atom pill per token. */
14
14
  export declare function textToInlineContent(text: string): InlineNodeJSON[];
15
- /** Raw text → a full single-paragraph TipTap doc. */
16
- export declare function textToDoc(text: string): InlineNodeJSON;
15
+ /**
16
+ * One paragraph per line, or one paragraph for the whole value.
17
+ *
18
+ * A field the node declared `multiline` keeps its newlines: each line becomes a
19
+ * paragraph, which is the only structure a TipTap doc has for one. Every other
20
+ * field is single-line and stays a single paragraph — the editor swallows Enter
21
+ * there, so nothing can create a second one from inside.
22
+ */
23
+ type LineMode = {
24
+ multiline?: boolean;
25
+ };
26
+ /** Raw text → a full TipTap doc: one paragraph, or one per line when multiline. */
27
+ export declare function textToDoc(text: string, { multiline }?: LineMode): InlineNodeJSON;
17
28
  /** TipTap doc JSON → raw text (pills serialize back to their `${{ … }}` token).
18
- * Multiple paragraphs (pasted newlines) collapse to single spaces — the
19
- * fields are single-line. */
20
- export declare function docToText(doc: InlineNodeJSON | null | undefined): string;
29
+ * Multiple paragraphs are the lines of a multiline field; in a single-line
30
+ * field they can only have arrived by paste, and collapse to single spaces. */
31
+ export declare function docToText(doc: InlineNodeJSON | null | undefined, { multiline }?: LineMode): string;
21
32
  /** True when any TEXT node still carries a complete raw token (needs re-pilling). */
22
33
  export declare function hasRawTokenText(doc: InlineNodeJSON | null | undefined): boolean;
34
+ export {};
@@ -11,11 +11,17 @@ export function textToInlineContent(text) {
11
11
  if (cursor < text.length) out.push({ type: "text", text: text.slice(cursor) });
12
12
  return out;
13
13
  }
14
- export function textToDoc(text) {
15
- const content = textToInlineContent(text);
16
- return { type: "doc", content: [content.length ? { type: "paragraph", content } : { type: "paragraph" }] };
14
+ export function textToDoc(text, { multiline = false } = {}) {
15
+ const lines = multiline ? text.split("\n") : [text];
16
+ return {
17
+ type: "doc",
18
+ content: lines.map((line) => {
19
+ const content = textToInlineContent(line);
20
+ return content.length ? { type: "paragraph", content } : { type: "paragraph" };
21
+ })
22
+ };
17
23
  }
18
- export function docToText(doc) {
24
+ export function docToText(doc, { multiline = false } = {}) {
19
25
  if (!doc) return "";
20
26
  const inlineText = (p) => {
21
27
  let s = "";
@@ -26,7 +32,7 @@ export function docToText(doc) {
26
32
  }
27
33
  return s;
28
34
  };
29
- return (doc.content ?? []).filter((n) => n.type === "paragraph").map(inlineText).join(" ");
35
+ return (doc.content ?? []).filter((n) => n.type === "paragraph").map(inlineText).join(multiline ? "\n" : " ");
30
36
  }
31
37
  export function hasRawTokenText(doc) {
32
38
  if (!doc) return false;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Reading the logical port name out of a canvas handle id.
2
+ * The shape of a canvas handle id — writing one, and reading the port name back out.
3
3
  *
4
4
  * Each node on the canvas renders four connection points per port, one per
5
5
  * side, so that an edge can leave and arrive without crossing a card body. The
@@ -19,5 +19,65 @@
19
19
  * how to read an id, and because that is what makes it testable: the editor is
20
20
  * an SFC, out of reach of the unit suite, and the defect above shipped once
21
21
  * already without anything going red.
22
+ *
23
+ * **Both directions live here, and that is PO-258.** The reader was here alone while
24
+ * twelve places across six components assembled an id with a template string and five
25
+ * of them declared the side union again — so the two halves of one contract agreed by
26
+ * spelling the same thing the same way rather than by construction. Nothing was wrong
27
+ * on screen; what it cost is that adding a fifth dot position would have had the
28
+ * writers produce `paid__centre` while the reader, which never heard of it, handed
29
+ * `paid__centre` back as a port name. That is not nullish, so the `?? 'default'` behind
30
+ * every call site cannot catch it, and the editor would have persisted an output no
31
+ * node has.
32
+ *
33
+ * **What happens elsewhere when a fifth position is added, and that is PO-396.** This
34
+ * module owns what a side *is*; what the canvas does with one — where the library draws
35
+ * it, what faces it, where along an edge it sits — lives in
36
+ * `components/Integrations/canvasSides.ts`, which is separate for one reason worth
37
+ * keeping: `Position` is Vue Flow's, and this file has no imports, so its round trip can
38
+ * be proved without the canvas library.
39
+ *
40
+ * Those three answers used to be ternary chains inside the components, each ending in an
41
+ * `else`, and two of them were written more than once: the position map in the node card
42
+ * and the trigger card, and the opposite-side map in the editor and in both read-only
43
+ * canvases — where it was called `opposite` rather than `oppositeSide`, which is why a
44
+ * search by name found one of the three. An `else` absorbs a new name
45
+ * silently — a `centre` dot drawn on the left, with `right` called its opposite — and
46
+ * every one of those is a plausible answer for *some* side, so nothing would look wrong.
47
+ * They are now `Record<Side, …>` maps: adding a name to `SIDES` below stops all three
48
+ * compiling until they say what it means. Still more than one edit, and none of them
49
+ * can be forgotten, which is the better half of the trade.
22
50
  */
51
+ /**
52
+ * The side suffix, anchored at the end. The four sides are a closed set, which
53
+ * is what lets the name in front of them be read whole: a port name is not
54
+ * restricted to a safe alphabet: `portsForNode` builds one with `String(v)` over
55
+ * the config values a person types into a switch node's cases, and a
56
+ * server-resolved dynamic output is named whatever the remote system calls its
57
+ * fields — where a double underscore is a naming convention rather than an
58
+ * accident. So a name may well contain `__` itself, and taking the FIRST one as
59
+ * the separator would hand back a truncated name that no node has a port for.
60
+ * Truncated is worse than empty: it is not nullish, so a caller's
61
+ * `?? 'default'` cannot catch it either.
62
+ */
63
+ /** Every side a connection dot is rendered on. The closed set the parse anchors on. */
64
+ export declare const SIDES: readonly ["top", "right", "bottom", "left"];
65
+ export type Side = (typeof SIDES)[number];
23
66
  export declare function portFromHandleId(handleId: string | null | undefined): string | null;
67
+ /**
68
+ * The id of an output dot: the port's own name, then the side.
69
+ *
70
+ * The name is passed through as it is. A port name is not restricted to a safe
71
+ * alphabet — `portsForNode` builds one with `String(v)` over what a person types into a
72
+ * switch node's cases — and sanitising it here would produce an id the reader cannot
73
+ * turn back into the name the blob stores.
74
+ */
75
+ export declare function outputHandleId(port: string, side: Side): string;
76
+ /**
77
+ * The id of the single input dot on a given side.
78
+ *
79
+ * The separator sits at the front with no name before it, which is exactly what
80
+ * `portFromHandleId` reads as "no port": a node has one logical input, so there is no
81
+ * name to carry. A group frame's input is this on the top side.
82
+ */
83
+ export declare function inputHandleId(side: Side): string;
@@ -1,6 +1,13 @@
1
- const SIDE_SUFFIX = /__(?:top|right|bottom|left)$/;
1
+ export const SIDES = ["top", "right", "bottom", "left"];
2
+ const SIDE_SUFFIX = new RegExp(`__(?:${SIDES.join("|")})$`);
2
3
  export function portFromHandleId(handleId) {
3
4
  if (!handleId) return null;
4
5
  const name = handleId.replace(SIDE_SUFFIX, "");
5
6
  return name.startsWith("__") || !name ? null : name;
6
7
  }
8
+ export function outputHandleId(port, side) {
9
+ return `${port}__${side}`;
10
+ }
11
+ export function inputHandleId(side) {
12
+ return `__in__${side}`;
13
+ }