@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.
- package/CHANGELOG.md +428 -0
- package/README.md +48 -4
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +10 -2
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
- package/dist/runtime/components/Integrations/Layout.vue +48 -4
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
- package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
- package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
- package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
- package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
- package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +8 -6
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
- package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
- package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
- package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
- package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
- package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
- package/dist/runtime/composables/useIntegrationsT.js +9 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
- package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
- package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
- package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/branchReorder.d.ts +37 -0
- package/dist/runtime/data/branchReorder.js +71 -0
- package/dist/runtime/data/handleId.d.ts +23 -0
- package/dist/runtime/data/handleId.js +6 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +14 -0
- package/dist/runtime/data/integrationTemplates.d.ts +48 -0
- package/dist/runtime/data/integrationTemplates.js +21 -0
- package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
- package/dist/runtime/data/nodeIcons.d.ts +2 -0
- package/dist/runtime/data/nodeIcons.js +73 -0
- package/dist/runtime/data/nodeInputData.d.ts +16 -4
- package/dist/runtime/data/nodeInputData.js +4 -0
- package/dist/runtime/data/payloadForm.d.ts +9 -2
- package/dist/runtime/data/payloadForm.js +29 -4
- package/dist/runtime/data/portTone.d.ts +17 -0
- package/dist/runtime/data/portTone.js +7 -0
- package/dist/runtime/data/webhookAuth.d.ts +18 -0
- package/dist/runtime/data/webhookAuth.js +19 -0
- package/dist/runtime/data/workflowIconCatalog.js +4 -6
- package/dist/runtime/i18n/messages/de.json +1207 -0
- package/dist/runtime/i18n/messages/en.json +1207 -0
- package/dist/runtime/i18n/translate.d.ts +83 -0
- package/dist/runtime/i18n/translate.js +41 -0
- package/dist/runtime/pages/integrations/audit.vue +140 -37
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
- package/dist/runtime/pages/integrations/credentials.vue +180 -64
- package/dist/runtime/pages/integrations/index.vue +91 -92
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +88 -23
- package/dist/runtime/pages/integrations/secrets.vue +57 -35
- package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
- package/dist/runtime/pages/integrations/templates/index.vue +82 -103
- package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
- package/dist/runtime/plugins/locale.d.ts +10 -0
- package/dist/runtime/plugins/locale.js +6 -0
- package/dist/runtime/plugins/realtime.client.d.ts +32 -0
- package/dist/runtime/plugins/realtime.client.js +4 -0
- package/dist/runtime/types/integrations.d.ts +65 -6
- package/dist/runtime/utils/apiError.d.ts +47 -0
- package/dist/runtime/utils/apiError.js +32 -0
- package/dist/runtime/utils/cockpitSources.d.ts +153 -0
- package/dist/runtime/utils/cockpitSources.js +14 -0
- package/dist/runtime/utils/credentialState.d.ts +24 -1
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +46 -4
- package/dist/runtime/utils/localize.js +34 -7
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -0
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
- package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
- package/dist/runtime/utils/schedule.d.ts +116 -0
- package/dist/runtime/utils/schedule.js +219 -0
- package/dist/runtime/utils/statusStyle.d.ts +26 -2
- package/dist/runtime/utils/statusStyle.js +9 -1
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +54 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/dist/types.d.mts +2 -6
- 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.
|
|
19
|
-
*
|
|
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
|
|
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.
|
|
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.
|
|
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
|
}
|