@revenexx/studio-integrations 1.1.1 → 1.2.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 (96) hide show
  1. package/CHANGELOG.md +483 -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/NodeConfigField.vue +23 -0
  16. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
  17. package/dist/runtime/components/Integrations/NodeInspector.vue +25 -32
  18. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
  19. package/dist/runtime/components/Integrations/RunActionsMenu.vue +2 -1
  20. package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
  21. package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
  22. package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
  23. package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
  24. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
  25. package/dist/runtime/components/Integrations/RunsTable.vue +129 -18
  26. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
  27. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
  28. package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
  29. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
  30. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
  31. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
  32. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
  33. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
  34. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
  35. package/dist/runtime/components/Integrations/WorkflowEditor.vue +39 -28
  36. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
  37. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
  38. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
  39. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
  40. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
  41. package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
  42. package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
  43. package/dist/runtime/components/Integrations/canvasSides.js +22 -0
  44. package/dist/runtime/composables/useCredentialTypes.js +2 -6
  45. package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
  46. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
  47. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
  48. package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
  49. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  50. package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
  51. package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
  52. package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
  53. package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
  54. package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
  55. package/dist/runtime/composables/useIntegrationsTemplates.js +2 -5
  56. package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
  57. package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -7
  58. package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
  59. package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
  60. package/dist/runtime/composables/useOAuthConnect.js +33 -0
  61. package/dist/runtime/data/expressionEditor.d.ts +17 -5
  62. package/dist/runtime/data/expressionEditor.js +11 -5
  63. package/dist/runtime/data/handleId.d.ts +61 -1
  64. package/dist/runtime/data/handleId.js +8 -1
  65. package/dist/runtime/data/payloadForm.js +3 -2
  66. package/dist/runtime/i18n/messages/de.json +26 -8
  67. package/dist/runtime/i18n/messages/en.json +26 -8
  68. package/dist/runtime/pages/integrations/audit.vue +7 -6
  69. package/dist/runtime/pages/integrations/credentials.vue +9 -21
  70. package/dist/runtime/pages/integrations/index.vue +55 -8
  71. package/dist/runtime/pages/integrations/runs.vue +116 -7
  72. package/dist/runtime/pages/integrations/secrets.vue +2 -1
  73. package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
  74. package/dist/runtime/pages/integrations/templates/index.vue +18 -15
  75. package/dist/runtime/pages/integrations/workflows/[id].vue +155 -20
  76. package/dist/runtime/types/integrations.d.ts +5 -0
  77. package/dist/runtime/utils/apiError.d.ts +21 -1
  78. package/dist/runtime/utils/apiError.js +1 -1
  79. package/dist/runtime/utils/clipboard.d.ts +33 -0
  80. package/dist/runtime/utils/clipboard.js +11 -0
  81. package/dist/runtime/utils/deepClone.d.ts +41 -0
  82. package/dist/runtime/utils/deepClone.js +3 -0
  83. package/dist/runtime/utils/formatDuration.d.ts +44 -0
  84. package/dist/runtime/utils/formatDuration.js +13 -0
  85. package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
  86. package/dist/runtime/utils/nodeDisplayName.js +6 -0
  87. package/dist/runtime/utils/notify.d.ts +1 -1
  88. package/dist/runtime/utils/runEnvelope.d.ts +31 -0
  89. package/dist/runtime/utils/runEnvelope.js +12 -0
  90. package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
  91. package/dist/runtime/utils/runFeedQuery.js +66 -0
  92. package/dist/runtime/utils/runOutputs.d.ts +33 -0
  93. package/dist/runtime/utils/runOutputs.js +10 -0
  94. package/dist/runtime/utils/validationFindings.d.ts +26 -3
  95. package/dist/runtime/utils/validationFindings.js +18 -3
  96. package/package.json +1 -1
@@ -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";
@@ -100,9 +100,6 @@ export function useIntegrationsTemplates() {
100
100
  function unwrap(res) {
101
101
  return res?.data ?? res;
102
102
  }
103
- function captureError(e, fallback) {
104
- return e instanceof IntegrationsApiError ? e.message : e?.message ?? fallback;
105
- }
106
103
  async function load(force = false) {
107
104
  if (!await gate.claim(force)) return templates.value;
108
105
  error.value = null;
@@ -112,7 +109,7 @@ export function useIntegrationsTemplates() {
112
109
  rawDetails.value = {};
113
110
  loaded.value = true;
114
111
  } catch (e) {
115
- error.value = captureError(e, t("integration.templates.state.error.fallback"));
112
+ error.value = errorReason(e, t("integration.templates.state.error.fallback"));
116
113
  rawList.value = [];
117
114
  rawDetails.value = {};
118
115
  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) {
@@ -89,10 +90,6 @@ export function useIntegrationsWorkflows() {
89
90
  function unwrap(res) {
90
91
  return res?.data ?? res;
91
92
  }
92
- function captureError(e, fallback) {
93
- if (e instanceof IntegrationsApiError) return e.message;
94
- return e?.message ?? fallback;
95
- }
96
93
  function remember(api2, into) {
97
94
  if (api2.triggers !== void 0) into[Number(api2.id)] = api2.triggers;
98
95
  }
@@ -123,7 +120,7 @@ export function useIntegrationsWorkflows() {
123
120
  workflows.value = ingestList(list2);
124
121
  loaded.value = true;
125
122
  } catch (e) {
126
- error.value = captureError(e, t("integration.workflows.error.loadFailed"));
123
+ error.value = errorReason(e, t("integration.workflows.error.loadFailed"));
127
124
  loaded.value = false;
128
125
  } finally {
129
126
  gate.release();
@@ -157,7 +154,7 @@ export function useIntegrationsWorkflows() {
157
154
  async function duplicate(id, name) {
158
155
  const source = await get(id);
159
156
  if (!source) throw new Error(t("integration.workflows.error.notFound"));
160
- const blob = JSON.parse(JSON.stringify(source.blob));
157
+ const blob = deepClone(source.blob);
161
158
  const handleMap = /* @__PURE__ */ new Map();
162
159
  for (const trigger of blob.triggers ?? []) {
163
160
  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
+ }
@@ -1,4 +1,5 @@
1
1
  import { useIntegrationsT } from "../composables/useIntegrationsT.js";
2
+ import { deepClone } from "../utils/deepClone.js";
2
3
  const PAYLOAD_REF_RE = /\$\{\{\s*trigger\.payload\.([a-zA-Z0-9_.-]+)/g;
3
4
  const FILE_KEYS = /* @__PURE__ */ new Set(["data", "filename", "contentType", "size"]);
4
5
  function collectRefs(value, out) {
@@ -91,7 +92,7 @@ export function getPath(source, path) {
91
92
  return cursor;
92
93
  }
93
94
  export function buildPayload(fields, textValues, files, extras) {
94
- const payload = JSON.parse(JSON.stringify(extras ?? {}));
95
+ const payload = deepClone(extras ?? {});
95
96
  for (const field of fields) {
96
97
  if (field.kind === "file") {
97
98
  const file = files[field.path];
@@ -111,7 +112,7 @@ export function buildPayload(fields, textValues, files, extras) {
111
112
  return payload;
112
113
  }
113
114
  export function splitPayload(fields, payload) {
114
- const extras = JSON.parse(JSON.stringify(payload ?? {}));
115
+ const extras = deepClone(payload ?? {});
115
116
  const textValues = {};
116
117
  const files = {};
117
118
  const deleteAt = (path) => {