@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
@@ -0,0 +1,219 @@
1
+ import { useIntegrationsT } from "../composables/useIntegrationsT.js";
2
+ import { studioLocale } from "./localize.js";
3
+ const WEEKDAY_KEYS = [
4
+ "integration.schedules.cron.weekday.sunday",
5
+ "integration.schedules.cron.weekday.monday",
6
+ "integration.schedules.cron.weekday.tuesday",
7
+ "integration.schedules.cron.weekday.wednesday",
8
+ "integration.schedules.cron.weekday.thursday",
9
+ "integration.schedules.cron.weekday.friday",
10
+ "integration.schedules.cron.weekday.saturday"
11
+ ];
12
+ export const PLATFORM_TIMEZONE = "UTC";
13
+ export function scheduleTimezone(timezone) {
14
+ return typeof timezone === "string" && timezone.trim() !== "" ? timezone.trim() : PLATFORM_TIMEZONE;
15
+ }
16
+ const WEEKDAYS = [1, 2, 3, 4, 5];
17
+ const WEEKEND = [0, 6];
18
+ function isEvery(field) {
19
+ return field === "*";
20
+ }
21
+ function fixed(field, min, max) {
22
+ if (!/^\d{1,2}$/.test(field)) return null;
23
+ const n = Number(field);
24
+ return n >= min && n <= max ? n : null;
25
+ }
26
+ function step(field, max, range) {
27
+ const m = /^\*\/(\d{1,2})$/.exec(field);
28
+ if (!m) return null;
29
+ const n = Number(m[1]);
30
+ return n >= 2 && n <= max && range % n === 0 ? n : null;
31
+ }
32
+ function numberSet(field, min, max) {
33
+ if (isEvery(field)) return null;
34
+ const out = /* @__PURE__ */ new Set();
35
+ for (const part of field.split(",")) {
36
+ const run = /^(\d{1,2})-(\d{1,2})$/.exec(part);
37
+ if (run) {
38
+ const from = Number(run[1]);
39
+ const to = Number(run[2]);
40
+ if (from < min || to > max || from >= to) return null;
41
+ for (let n = from; n <= to; n++) out.add(n);
42
+ continue;
43
+ }
44
+ const one = fixed(part, min, max);
45
+ if (one == null) return null;
46
+ out.add(one);
47
+ }
48
+ return out.size ? [...out].sort((a, b) => a - b) : null;
49
+ }
50
+ function daySet(field) {
51
+ const raw = numberSet(field, 0, 7);
52
+ if (!raw) return null;
53
+ return [...new Set(raw.map((d) => d === 7 ? 0 : d))].sort((a, b) => a - b);
54
+ }
55
+ function isRun(values) {
56
+ return values.length > 0 && values[values.length - 1] - values[0] === values.length - 1;
57
+ }
58
+ function readsAsSpan(hours) {
59
+ return hours.length >= 3 && isRun(hours);
60
+ }
61
+ function saysAsSpan(hours) {
62
+ return hours.length >= 4 && isRun(hours);
63
+ }
64
+ function writeSet(values) {
65
+ return readsAsSpan(values) ? `${values[0]}-${values[values.length - 1]}` : values.join(",");
66
+ }
67
+ export function parseCron(cron) {
68
+ if (!cron) return null;
69
+ const fields = cron.trim().split(/\s+/);
70
+ if (fields.length !== 5) return null;
71
+ const [minute, hour, dom, month, dow] = fields;
72
+ if (!isEvery(month)) return null;
73
+ const everyDay = isEvery(dom) && isEvery(dow);
74
+ if (isEvery(hour) && everyDay) {
75
+ if (isEvery(minute)) return { kind: "minutes", interval: 1 };
76
+ const perMinute = step(minute, 59, 60);
77
+ if (perMinute) return { kind: "minutes", interval: perMinute };
78
+ }
79
+ const min = fixed(minute, 0, 59);
80
+ if (min == null) return null;
81
+ if (everyDay) {
82
+ if (isEvery(hour)) return { kind: "hourly", minute: min };
83
+ const perHour = step(hour, 23, 24);
84
+ if (perHour) return { kind: "hours", interval: perHour, minute: min };
85
+ }
86
+ const hr = fixed(hour, 0, 23);
87
+ if (hr == null && isEvery(dom)) {
88
+ const hours = numberSet(hour, 0, 23);
89
+ if (hours && hours.length >= 2) {
90
+ const days = isEvery(dow) ? null : daySet(dow);
91
+ if (!isEvery(dow) && !days) return null;
92
+ return { kind: "times", minute: min, hours, days };
93
+ }
94
+ }
95
+ if (hr == null) return null;
96
+ if (everyDay) return { kind: "daily", hour: hr, minute: min };
97
+ if (isEvery(dom)) {
98
+ const days = daySet(dow);
99
+ return days ? { kind: "weekly", days, hour: hr, minute: min } : null;
100
+ }
101
+ if (isEvery(dow)) {
102
+ const day = fixed(dom, 1, 31);
103
+ if (day) return { kind: "monthly", day, hour: hr, minute: min };
104
+ }
105
+ return null;
106
+ }
107
+ export function toCron(r) {
108
+ switch (r.kind) {
109
+ case "minutes":
110
+ return r.interval <= 1 ? "* * * * *" : `*/${r.interval} * * * *`;
111
+ case "hourly":
112
+ return `${r.minute} * * * *`;
113
+ case "hours":
114
+ return r.interval <= 1 ? `${r.minute} * * * *` : `${r.minute} */${r.interval} * * *`;
115
+ case "daily":
116
+ return `${r.minute} ${r.hour} * * *`;
117
+ case "times": {
118
+ const hours = [...new Set(r.hours)].sort((a, b) => a - b);
119
+ if (hours.length < 2) {
120
+ const hour = hours[0] ?? 0;
121
+ return r.days ? toCron({ kind: "weekly", days: r.days, hour, minute: r.minute }) : toCron({ kind: "daily", hour, minute: r.minute });
122
+ }
123
+ const days = r.days ? [...new Set(r.days.map((d) => d === 7 ? 0 : d))].sort((a, b) => a - b) : [];
124
+ const dow = !days.length || days.length === 7 ? "*" : writeSet(days);
125
+ return `${r.minute} ${writeSet(hours)} * * ${dow}`;
126
+ }
127
+ case "weekly": {
128
+ const days = [...new Set(r.days.map((d) => d === 7 ? 0 : d))].sort((a, b) => a - b);
129
+ if (!days.length || days.length === 7) return `${r.minute} ${r.hour} * * *`;
130
+ return `${r.minute} ${r.hour} * * ${days.join(",")}`;
131
+ }
132
+ case "monthly":
133
+ return `${r.minute} ${r.hour} ${r.day} * *`;
134
+ }
135
+ }
136
+ function at(hour, minute) {
137
+ return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
138
+ }
139
+ function ordinal(n) {
140
+ if (studioLocale().split("-")[0] === "de") return `${n}.`;
141
+ const suffix = n % 100 >= 11 && n % 100 <= 13 ? "th" : ["th", "st", "nd", "rd"][n % 10] ?? "th";
142
+ return `${n}${suffix}`;
143
+ }
144
+ function sameDays(days, other) {
145
+ return days.length === other.length && days.every((d) => other.includes(d));
146
+ }
147
+ function listWords(words, t) {
148
+ if (words.length <= 1) return words[0] ?? "";
149
+ return `${words.slice(0, -1).join(", ")} ${t("integration.common.list.and")} ${words[words.length - 1]}`;
150
+ }
151
+ function isEveryDay(days) {
152
+ if (!days) return true;
153
+ const set = new Set(days.map((d) => d === 7 ? 0 : d));
154
+ return set.size === 0 || set.size === 7;
155
+ }
156
+ function dayPhrase(days, t) {
157
+ if (isEveryDay(days)) return t("integration.schedules.cron.day.daily");
158
+ const set = [...new Set((days ?? []).map((d) => d === 7 ? 0 : d))].sort((a, b) => a - b);
159
+ if (sameDays(set, WEEKDAYS)) return t("integration.schedules.cron.day.weekdays");
160
+ if (sameDays(set, WEEKEND)) return t("integration.schedules.cron.day.weekends");
161
+ return listWords(set.map((d) => t(WEEKDAY_KEYS[d])), t);
162
+ }
163
+ export function describeRecurrence(r) {
164
+ const t = useIntegrationsT();
165
+ switch (r.kind) {
166
+ case "minutes":
167
+ return r.interval <= 1 ? t("integration.schedules.cron.everyMinute") : t("integration.schedules.cron.everyNMinutes", { n: r.interval });
168
+ case "hourly":
169
+ return r.minute === 0 ? t("integration.schedules.cron.hourlyOnTheHour") : t("integration.schedules.cron.hourlyAt", { minute: String(r.minute).padStart(2, "0") });
170
+ case "hours":
171
+ if (r.interval <= 1) return describeRecurrence({ kind: "hourly", minute: r.minute });
172
+ return r.minute === 0 ? t("integration.schedules.cron.everyNHoursOnTheHour", { n: r.interval }) : t("integration.schedules.cron.everyNHoursAt", { n: r.interval, minute: String(r.minute).padStart(2, "0") });
173
+ case "daily":
174
+ return t("integration.schedules.cron.dailyAt", { time: at(r.hour, r.minute) });
175
+ case "times": {
176
+ const hours = [...new Set(r.hours)].sort((a, b) => a - b);
177
+ if (hours.length < 2) return describeRecurrence({ kind: "weekly", days: r.days ?? [0, 1, 2, 3, 4, 5, 6], hour: hours[0] ?? 0, minute: r.minute });
178
+ if (saysAsSpan(hours)) {
179
+ const from = at(hours[0], r.minute);
180
+ const to = at(hours[hours.length - 1], r.minute);
181
+ return isEveryDay(r.days) ? t("integration.schedules.cron.spanAllDay", { from, to }) : t("integration.schedules.cron.spanWithDays", { days: dayPhrase(r.days, t), from, to });
182
+ }
183
+ return t("integration.schedules.cron.namedTimes", { days: dayPhrase(r.days, t), times: listWords(hours.map((h) => at(h, r.minute)), t) });
184
+ }
185
+ case "weekly":
186
+ return t("integration.schedules.cron.weeklyAt", { days: dayPhrase(r.days, t), time: at(r.hour, r.minute) });
187
+ case "monthly":
188
+ return t("integration.schedules.cron.monthlyAt", { ordinal: ordinal(r.day), time: at(r.hour, r.minute) });
189
+ }
190
+ }
191
+ export function describeCron(cron) {
192
+ const r = parseCron(cron);
193
+ return r ? describeRecurrence(r) : null;
194
+ }
195
+ export function cadenceRank(cron) {
196
+ const r = parseCron(cron);
197
+ if (!r) return Number.MAX_SAFE_INTEGER;
198
+ switch (r.kind) {
199
+ case "minutes":
200
+ return Math.max(1, r.interval);
201
+ case "hourly":
202
+ return 60;
203
+ case "hours":
204
+ return Math.max(1, r.interval) * 60;
205
+ case "daily":
206
+ return 1440;
207
+ // Several times a day, thinned out by the days it skips.
208
+ case "times":
209
+ return Math.round(
210
+ 1440 / Math.max(1, r.hours.length) * (7 / Math.min(7, Math.max(1, r.days?.length ?? 7)))
211
+ );
212
+ // A schedule on five weekdays fires roughly every 1.4 days, which belongs
213
+ // between daily and weekly rather than lumped in with a Monday-only one.
214
+ case "weekly":
215
+ return Math.round(10080 / Math.min(7, Math.max(1, r.days.length)));
216
+ case "monthly":
217
+ return 43200;
218
+ }
219
+ }
@@ -13,9 +13,33 @@ export declare function statusTone(status: string | null | undefined): StatusTon
13
13
  export declare function statusClass(status: string | null | undefined): string;
14
14
  /** Tone classes directly (when you already have a tone). */
15
15
  export declare function toneClass(tone: StatusTone): string;
16
+ /** The run-status words the studio itself displays (as opposed to whatever
17
+ * vocabulary Temporal or the raw API used to get there) — the only ones
18
+ * `runStatusLabel` translates. Anything else passes through unlabelled: a
19
+ * status this catalogue doesn't know is a raw field, not the studio's chrome.
20
+ *
21
+ * An array rather than a set, and exported, because the runs list offers these
22
+ * five as the choices it can be narrowed to and has to offer them in a fixed
23
+ * order. It used to keep its own copy of the list, so a sixth word added here
24
+ * would have been labelled everywhere and silently missing from that menu. */
25
+ export declare const RUN_STATUS_WORDS: readonly ["running", "succeeded", "failed", "cancelled", "queued"];
16
26
  /**
17
27
  * Raw API/Temporal run vocabulary → the cockpit's run-status labels, so a run
18
- * never reads "completed" in one surface and "succeeded" in another. Unknown
19
- * statuses pass through lowercased.
28
+ * never reads "completed" in one surface and "succeeded" in another. This IS
29
+ * the studio's own chrome (Temporal's "completed" becomes our "succeeded"), so
30
+ * unlike a raw `run.status` read elsewhere, it is translated. Unknown statuses
31
+ * still pass through lowercased, untranslated — this function only relabels
32
+ * the fixed run-status vocabulary, not arbitrary strings.
33
+ */
34
+ /**
35
+ * The platform's run vocabulary reduced to the studio's own five words — Temporal's
36
+ * `completed` is our `succeeded`, `pending` is `queued`, and two ways of dying are
37
+ * both `failed`. A status this catalogue does not know comes back as itself.
38
+ *
39
+ * Exported because a run's status is used as an *identity* as well as a word: the runs
40
+ * list narrows by it, and what a filter stores has to be something that does not move
41
+ * when the reader's language does. Both sides read this, so a filter and a row can
42
+ * never disagree about which status they mean.
20
43
  */
44
+ export declare function normalisedRunStatus(status: string | null | undefined): string;
21
45
  export declare function runStatusLabel(status: string | null | undefined): string;
@@ -76,7 +76,9 @@ export function statusClass(status) {
76
76
  export function toneClass(tone) {
77
77
  return TONE_CLASS[tone];
78
78
  }
79
- export function runStatusLabel(status) {
79
+ export const RUN_STATUS_WORDS = ["running", "succeeded", "failed", "cancelled", "queued"];
80
+ const RUN_STATUS_WORD_SET = new Set(RUN_STATUS_WORDS);
81
+ export function normalisedRunStatus(status) {
80
82
  if (!status) return "";
81
83
  const s = String(status).toLowerCase().replace(/[\s-]+/g, "_");
82
84
  switch (s) {
@@ -91,3 +93,9 @@ export function runStatusLabel(status) {
91
93
  return s;
92
94
  }
93
95
  }
96
+ export function runStatusLabel(status) {
97
+ const normalized = normalisedRunStatus(status);
98
+ if (!normalized) return "";
99
+ if (!RUN_STATUS_WORD_SET.has(normalized)) return normalized;
100
+ return useIntegrationsT()(`integration.runs.status.${normalized}`);
101
+ }
@@ -0,0 +1,101 @@
1
+ import type { WorkflowNode } from '../types/integrations.js';
2
+ /**
3
+ * Reading the backend's validation findings — for the editor's validation
4
+ * banner (behavior specs/workflow-validation.md, how docs/workflow-validation.md)
5
+ * and for the node configuration dialog (specs/node-configuration.md AC-8).
6
+ *
7
+ * The backend reports one finding per incomplete setting, each keyed by its
8
+ * blob path (`blob.nodes.4.config.companyId`). The two readers want different
9
+ * cuts of that list:
10
+ *
11
+ * - `groupFindingsByNode` — for the banner. Rendered one-per-line that repeats
12
+ * the same node name three times over for a node missing three fields, so the
13
+ * banner groups them by node and names the fields instead of the messages.
14
+ * The full messages ride along for the hover title.
15
+ * - `findingsByNodeField` — for the dialog. Above the canvas the message alone
16
+ * is enough; inside a node's configuration it is not, because the dialog has
17
+ * to know WHICH FIELD the sentence belongs to or the user is left comparing a
18
+ * message against a form.
19
+ *
20
+ * Both live here rather than in their components because the parsing is the
21
+ * part worth pinning in a unit test: a finding whose path names no node must
22
+ * still reach the user (nothing may be silently dropped on the way to the
23
+ * banner), and a path shape that changes must not quietly mark the wrong field.
24
+ */
25
+ export interface ValidationFinding {
26
+ path: string;
27
+ message: string;
28
+ }
29
+ /**
30
+ * Either a node with its incomplete settings, or a single finding that belongs
31
+ * to no node. `nodeId` discriminates the two, and `label` / `fields` are only
32
+ * populated in the first case — an unattributed finding has no node to name and
33
+ * no config key to point at, so it is rendered from `messages` alone.
34
+ */
35
+ export interface FindingNodeGroup {
36
+ /** Blob node id, or `null` when the path names no node in the blob. */
37
+ nodeId: string | null;
38
+ /** The node's human label; `null` when `nodeId` is. */
39
+ label: string | null;
40
+ /**
41
+ * One compact entry per finding, in order and de-duplicated: the config key
42
+ * the path ends in, falling back to the message when the path carries none.
43
+ * **Empty when `nodeId` is `null`** — read `messages` instead.
44
+ */
45
+ fields: string[];
46
+ /** The full messages behind this group, in order. Never empty. */
47
+ messages: string[];
48
+ }
49
+ /**
50
+ * How a node is named wherever it is talked ABOUT rather than rendered: the
51
+ * instance name the user typed, else the manifest's name, else the raw slug.
52
+ * The same three steps the node card itself uses — a banner that stops after
53
+ * two of them prints `revenexx:business-central-create-customer` at the user.
54
+ */
55
+ export declare function nodeDisplayLabel(node: WorkflowNode): string;
56
+ /**
57
+ * The setting a finding is about: the config key its path ends in.
58
+ *
59
+ * Only the FIRST segment after `config.` — `config.mapping[0].source` reads as
60
+ * `mapping`, so two findings on different sub-paths of one field collapse to a
61
+ * single `fields` entry while both still count towards the banner's total. That
62
+ * is deliberate: the line names the setting to open, not every leaf inside it,
63
+ * and the full messages survive in `messages` for the hover. It does mean a node
64
+ * can show two field names under a header that counted three findings.
65
+ */
66
+ export declare function findingFieldKey(path: string): string | null;
67
+ /**
68
+ * Group findings by the node they belong to, preserving first-appearance order.
69
+ *
70
+ * A finding whose path resolves to no node becomes a group of its own (with a
71
+ * `null` nodeId), rendered after the node groups — it has no node to hide
72
+ * behind, and dropping it would lose an issue the user has to fix.
73
+ */
74
+ export declare function groupFindingsByNode(findings: readonly ValidationFinding[], nodes: readonly WorkflowNode[], labelOf?: (node: WorkflowNode) => string): FindingNodeGroup[];
75
+ /**
76
+ * Node id → config key → the message to show AT that setting
77
+ * (specs/node-configuration.md AC-8).
78
+ *
79
+ * First finding wins: the backend reports the most specific reason first, and
80
+ * two sentences stacked under one input would be noise.
81
+ *
82
+ * Findings whose path names no node, or no config key, are absent here on
83
+ * purpose — they have no field to sit next to. They still reach the user in the
84
+ * warnings above the canvas via `groupFindingsByNode`, so nothing is dropped.
85
+ */
86
+ export declare function findingsByNodeField(findings: readonly ValidationFinding[], nodes: readonly WorkflowNode[]): Record<string, Record<string, string>>;
87
+ /**
88
+ * "6 settings in 4 nodes still need fixing" — the count of findings and the count
89
+ * of nodes, but only naming nodes while there IS a node to attribute them to.
90
+ *
91
+ * Extracted because two surfaces have to give the same reason: the banner above
92
+ * the canvas, and the refusal a run comes back with (specs/workflow-validation.md
93
+ * AC-1 and AC-11). AC-11 exists precisely because those two disagreed — the gate
94
+ * named the settings and the run reported a generic failure — so the phrase they
95
+ * share is the wrong thing to write twice.
96
+ *
97
+ * NOT the wording of the activation toast, which is deliberately shorter ("N
98
+ * settings still need fixing", no node count) and pinned by AC-4. Three surfaces,
99
+ * two phrases; this is the one used where the node names are shown alongside.
100
+ */
101
+ export declare function findingsSummary(findingCount: number, nodeCount: number): string;
@@ -0,0 +1,54 @@
1
+ import { useIntegrationsT } from "../composables/useIntegrationsT.js";
2
+ import { findManifest } from "../data/integrationNodeCatalog.js";
3
+ import { resolveLocalized } from "./localize.js";
4
+ export function nodeDisplayLabel(node) {
5
+ const manifest = findManifest(node.nodeSlug, node.nodeVersion);
6
+ return resolveLocalized(node.name) || resolveLocalized(manifest?.name) || node.nodeSlug;
7
+ }
8
+ export function findingFieldKey(path) {
9
+ const m = /^blob\.nodes\.\d+\.config\.([^.[]+)/.exec(path);
10
+ return m ? m[1] : null;
11
+ }
12
+ function findingNode(path, nodes) {
13
+ const m = /^blob\.nodes\.(\d+)\b/.exec(path);
14
+ return m ? nodes[Number(m[1])] : void 0;
15
+ }
16
+ export function groupFindingsByNode(findings, nodes, labelOf = nodeDisplayLabel) {
17
+ const nodeGroups = [];
18
+ const orphans = [];
19
+ const byNodeId = /* @__PURE__ */ new Map();
20
+ for (const finding of findings) {
21
+ const node = findingNode(finding.path, nodes);
22
+ if (!node) {
23
+ orphans.push({ nodeId: null, label: null, fields: [], messages: [finding.message] });
24
+ continue;
25
+ }
26
+ let group = byNodeId.get(node.id);
27
+ if (!group) {
28
+ group = { nodeId: node.id, label: labelOf(node), fields: [], messages: [] };
29
+ byNodeId.set(node.id, group);
30
+ nodeGroups.push(group);
31
+ }
32
+ const field = findingFieldKey(finding.path) ?? finding.message;
33
+ if (!group.fields.includes(field)) group.fields.push(field);
34
+ group.messages.push(finding.message);
35
+ }
36
+ return [...nodeGroups, ...orphans];
37
+ }
38
+ export function findingsByNodeField(findings, nodes) {
39
+ const out = {};
40
+ for (const finding of findings) {
41
+ const node = findingNode(finding.path, nodes);
42
+ if (!node) continue;
43
+ const field = findingFieldKey(finding.path);
44
+ if (!field) continue;
45
+ const forNode = out[node.id] ??= {};
46
+ forNode[field] ??= finding.message;
47
+ }
48
+ return out;
49
+ }
50
+ export function findingsSummary(findingCount, nodeCount) {
51
+ const t = useIntegrationsT();
52
+ const nodesClause = nodeCount ? ` ${t("integration.workflows.validation.summary.nodesClause", nodeCount)}` : "";
53
+ return t("integration.workflows.validation.summary", findingCount, { nodesClause });
54
+ }
@@ -0,0 +1,42 @@
1
+ import type { Ref } from 'vue';
2
+ /**
3
+ * Resolves once the load a `loading` flag stands for has finished.
4
+ *
5
+ * Every composable in the studio guards its `load()` against a second caller
6
+ * arriving while one is in flight. Nine of them did it by handing that caller the
7
+ * state **as it stands** — empty, on a cold visit — so whatever it fed then fanned
8
+ * out over nothing and cached the nothing as an answer. On the Runs page the
9
+ * missed-runs tab showed neither a count nor an entry on a first visit, and the
10
+ * dead-letter endpoint was never called at all; the dashboard was empty for the
11
+ * same reason. Waiting is the honest answer, and it is what this does.
12
+ *
13
+ * The watcher lives in a **detached** scope on purpose. The caller waiting here is
14
+ * usually a page, and the operator can leave it while the request is still out; a
15
+ * watcher owned by that page would be disposed with it, this promise would never
16
+ * settle, and the caller's own loading flag would stay up for the rest of the
17
+ * session — the same tab empty again, and this time permanently. That was found by
18
+ * PO-219 and is the single most important line in this file.
19
+ *
20
+ * Three things it cannot do, so nobody has to find them out:
21
+ *
22
+ * - **It must not be reached during server-side setup.** `watch(source, cb)`
23
+ * without `immediate` is a no-op there, so the promise would never settle and the
24
+ * render would hang. Every call site is `onMounted` or an event handler today; a
25
+ * `useAsyncData` / `callOnce` / `onServerPrefetch` caller has to await the load
26
+ * outside setup instead.
27
+ * - **A hung request parks every waiting caller.** `$fetch` carries no timeout
28
+ * here, so a socket that never answers keeps them waiting rather than letting
29
+ * them proceed with an empty list. Deliberate — the empty list was the bug — but
30
+ * it is the one input that turns this into a permanent stall.
31
+ * - **It does not decide what happens next.** Whether a caller that waited then
32
+ * sends its own request is the difference between a forced re-read and an
33
+ * ordinary one, and every guard that answered it by hand got it wrong the same
34
+ * way — two forced callers each sent a request, so the flag went down with one
35
+ * still out. That rule now lives in `loadGate.ts`, which is what the composables
36
+ * use; call this directly only where there is nothing to decide.
37
+ *
38
+ * An already-idle flag resolves immediately rather than waiting for a load that
39
+ * will never start — so this is safe to call without checking first, which is what
40
+ * stops it becoming a hang the next time somebody reuses it.
41
+ */
42
+ export declare function whenIdle(loading: Ref<boolean>): Promise<void>;
@@ -0,0 +1,12 @@
1
+ import { effectScope, watch } from "vue";
2
+ export function whenIdle(loading) {
3
+ if (!loading.value) return Promise.resolve();
4
+ const scope = effectScope(true);
5
+ return new Promise((resolve) => {
6
+ scope.run(() => watch(loading, (busy) => {
7
+ if (busy) return;
8
+ scope.stop();
9
+ resolve();
10
+ }));
11
+ });
12
+ }
package/dist/types.d.mts CHANGED
@@ -1,9 +1,5 @@
1
- import type { NuxtModule } from '@nuxt/schema'
2
-
3
- import type { default as Module } from './module.mjs'
4
-
5
- export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
-
7
1
  export { default } from './module.mjs'
8
2
 
3
+ export { type ModuleOptions } from './module.mjs'
4
+
9
5
  export * from '../dist/runtime/types/integrations.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Integration Studio (core: workflows, node editor, runs, schedules, credentials, secrets, audit) as a standalone, publishable Nuxt module.",
6
6
  "repository": {
@@ -17,6 +17,7 @@
17
17
  },
18
18
  "files": [
19
19
  "dist",
20
+ "CHANGELOG.md",
20
21
  "LICENSE"
21
22
  ],
22
23
  "publishConfig": {
@@ -25,7 +26,7 @@
25
26
  },
26
27
  "dependencies": {
27
28
  "@nuxt/kit": "^4.0.0",
28
- "@revenexx/studio-shared": "^0.2.0",
29
+ "@revenexx/studio-shared": "^0.3.0",
29
30
  "@solar-icons/nuxt": "^1.2.0",
30
31
  "marked": "^18.0.5"
31
32
  },
@@ -50,9 +51,11 @@
50
51
  "vue-sonner": "^2.0.0"
51
52
  },
52
53
  "devDependencies": {
54
+ "@changesets/cli": "^2.31.0",
53
55
  "@nuxt/module-builder": "^1.0.2",
54
56
  "@playwright/test": "^1.57.0",
55
57
  "@nuxt/kit": "^4.0.0",
58
+ "@revenexx/talkback-js": "^0.3.0",
56
59
  "@nuxt/schema": "^4.0.0",
57
60
  "nuxt": "^4.2.2",
58
61
  "vitest": "^3.2.0",
@@ -63,6 +66,10 @@
63
66
  "scripts": {
64
67
  "build": "nuxt-module-build build",
65
68
  "test": "vitest run",
66
- "test:e2e": "playwright test"
69
+ "test:e2e": "playwright test",
70
+ "typecheck": "node scripts/typecheck.mjs",
71
+ "spec:check": "node scripts/spec-check.mjs",
72
+ "changeset": "changeset",
73
+ "version-packages": "changeset version"
67
74
  }
68
75
  }