@revenexx/studio-integrations 2.6.0 → 2.8.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 +22 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +2 -1
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +2 -1
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +2 -1
- package/dist/runtime/components/Integrations/NodeInspector.vue +11 -7
- package/dist/runtime/components/Integrations/NodeMark.vue +1 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +8 -1
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +2 -1
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +2 -1
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +1 -1
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +2 -1
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +5 -2
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +21 -2
- package/dist/runtime/components/Integrations/WorkflowIconView.vue +1 -1
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +5 -1
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +5 -1
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +5 -1
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +5 -1
- package/dist/runtime/composables/useIntegrationsAudit.d.ts +3 -0
- package/dist/runtime/composables/useIntegrationsNodes.d.ts +12 -1
- package/dist/runtime/composables/useIntegrationsNodes.js +24 -5
- package/dist/runtime/composables/useIntegrationsRevisions.d.ts +6 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsTemplates.js +25 -16
- package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -1
- package/dist/runtime/data/eventTrigger.d.ts +22 -0
- package/dist/runtime/data/eventTrigger.js +18 -0
- package/dist/runtime/data/integrationNodeCatalog.js +1 -1
- package/dist/runtime/i18n/messages/de.json +3 -0
- package/dist/runtime/i18n/messages/en.json +3 -0
- package/dist/runtime/pages/integrations/audit.vue +23 -3
- package/dist/runtime/pages/integrations/credentials.vue +2 -1
- package/dist/runtime/pages/integrations/secrets.vue +2 -1
- package/dist/runtime/pages/integrations/templates/[slug].vue +3 -2
- package/dist/runtime/pages/integrations/templates/index.vue +1 -1
- package/dist/runtime/pages/integrations/workflows/[id].vue +4 -3
- package/dist/runtime/plugins/realtime.client.d.ts +15 -13
- package/dist/runtime/plugins/realtime.client.js +0 -1
- package/dist/runtime/types/integrations.d.ts +16 -0
- package/dist/runtime/utils/detailLoader.d.ts +52 -0
- package/dist/runtime/utils/detailLoader.js +65 -0
- package/dist/runtime/utils/dialog.d.ts +20 -0
- package/dist/runtime/utils/dialog.js +3 -0
- package/dist/runtime/utils/nodeCatalogLevel.d.ts +18 -0
- package/dist/runtime/utils/nodeCatalogLevel.js +7 -0
- package/dist/runtime/utils/personLabel.d.ts +18 -0
- package/dist/runtime/utils/personLabel.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @revenexx/studio-integrations
|
|
2
2
|
|
|
3
|
+
## 2.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f7ae810: The dashboard, the template gallery, a template's own page and the workflow icons read a slim node catalogue (`GET /nodes?fields=summary`) instead of every manifest in full; the editor still reads the full catalogue and upgrades to it when a summary is in hand. Template previews read their definitions in one request (`GET /templates?include=definition&slugs=…`), and two surfaces asking for the same template at once share one request. Against an Integrations API that predates either parameter the studio falls back to what it did before: the full catalogue, and one request per template, four at a time (PE-990).
|
|
8
|
+
|
|
9
|
+
The studio no longer subscribes its eleven realtime channels at app boot: the first Integration Studio surface that needs them (the runs list, a workflow, a run panel, the workflows list) takes them and keeps them for the rest of the tab. A host page outside the studio no longer mints eleven subscription tokens on every load (PE-990).
|
|
10
|
+
|
|
11
|
+
- 25755bc: The Audit Log names whoever made a change by name, with their e-mail beside it, and the search finds them by either; the entry's panel keeps their identifier for tracing. A workflow's History names whoever saved each version, the current one included, and dates each by when it was saved rather than by when a later save replaced it. Entries and versions from before the platform recorded names show the identifier, or nobody, as before. Needs an Integrations API that sends `actor_name`/`actor_email`, `saved_by_*` and `saved_at` (revenexx/integrations#344); against an older one the page looks as it did (PO-609).
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 1772593: A workflow with an event trigger can be saved again. The studio stored the chosen event as `event_type`, which the platform does not read, so every such save was refused with "The event subject is required for event triggers." The event is now stored as the trigger's `subject`; a trigger still carrying `event_type` is read and sent as its subject (PO-641).
|
|
16
|
+
- c3ea68f: A click beside a dialog with an entry in it no longer closes it. Every dialog in the studio where something is typed or chosen took a click on the dimmed page as Cancel — a node's or a trigger's configuration, the workflow's settings, variables and state namespaces, the run dialog, declaring a state namespace, a new endpoint secret, adding, editing or opening a credential, a new revenexx key, importing an SFTP account, a new secret, a new or duplicated workflow, and using a template. A host draws its messages under that dimming, so dismissing the message after "Test step" threw a node's settings away before Done (PO-648). Cancel, Escape and the ✕ still close at once, without asking. Dialogs that only show something — the canvas shortcuts, a run's response, a timeline span — still close on a click beside them.
|
|
17
|
+
- 406f9db: A search at the root of the node palette lists its hits in the order the folders show them — folder by folder, and inside each folder in the order the node's package gives — instead of by node identifier (PO-661).
|
|
18
|
+
|
|
19
|
+
## 2.7.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- 02e7a5a: A package can declare the icon that marks its folder in the node palette (`package.icon`, from `revenexx.icon` in its `package.json`). A folder whose package declares one carries that icon; one that declares nothing keeps the first logo among its nodes, as before. A folder several packages share carries the icon only when every one of them declares the same. A service's folder inside a package keeps its own logo (PO-640).
|
|
24
|
+
|
|
3
25
|
## 2.6.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/dist/module.json
CHANGED
|
@@ -22,6 +22,7 @@ import { toast } from "vue-sonner";
|
|
|
22
22
|
import { resolveLocalized } from "../../utils/localize";
|
|
23
23
|
import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
|
|
24
24
|
import { errorReason } from "../../utils/apiError";
|
|
25
|
+
import { keepOpenBeside } from "../../utils/dialog";
|
|
25
26
|
const props = defineProps({
|
|
26
27
|
open: { type: Boolean, required: true },
|
|
27
28
|
credentialType: { type: [String, Array], required: false }
|
|
@@ -136,7 +137,7 @@ async function submit() {
|
|
|
136
137
|
|
|
137
138
|
<template>
|
|
138
139
|
<Dialog :open="open" @update:open="(v) => emit('update:open', v)">
|
|
139
|
-
<DialogContent class="sm:max-w-lg">
|
|
140
|
+
<DialogContent class="sm:max-w-lg" @interact-outside="keepOpenBeside">
|
|
140
141
|
<DialogHeader>
|
|
141
142
|
<DialogTitle>{{ t("integration.credentials.shared.dialog.title.create") }}</DialogTitle>
|
|
142
143
|
<DialogDescription class="text-sm">
|
|
@@ -38,6 +38,7 @@ import { useNow } from "../../composables/useNow";
|
|
|
38
38
|
import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
|
|
39
39
|
import { resolveLocalized } from "../../utils/localize";
|
|
40
40
|
import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
|
|
41
|
+
import { keepOpenBeside } from "../../utils/dialog";
|
|
41
42
|
const props = defineProps({
|
|
42
43
|
open: { type: Boolean, required: true },
|
|
43
44
|
credential: { type: [Object, null], required: true },
|
|
@@ -207,7 +208,7 @@ async function connect() {
|
|
|
207
208
|
|
|
208
209
|
<template>
|
|
209
210
|
<Dialog :open="open" @update:open="(v) => emit('update:open', v)">
|
|
210
|
-
<DialogContent class="sm:max-w-3xl">
|
|
211
|
+
<DialogContent class="sm:max-w-3xl" @interact-outside="keepOpenBeside">
|
|
211
212
|
<DialogHeader>
|
|
212
213
|
<div class="flex items-start gap-3">
|
|
213
214
|
<IntegrationsCredentialLogo :type="type" :size="22" />
|
|
@@ -26,6 +26,7 @@ import { toast } from "vue-sonner";
|
|
|
26
26
|
import { REVENEXX_SOURCE_KIND_BY_SLUG } from "../../utils/revenexxCredentialSource";
|
|
27
27
|
import { cockpitSources } from "../../utils/cockpitSources";
|
|
28
28
|
import { errorReason } from "../../utils/apiError";
|
|
29
|
+
import { keepOpenBeside } from "../../utils/dialog";
|
|
29
30
|
const props = defineProps({
|
|
30
31
|
type: { type: [Object, null], required: true }
|
|
31
32
|
});
|
|
@@ -371,7 +372,7 @@ function submitCreate() {
|
|
|
371
372
|
|
|
372
373
|
<!-- Stacked "New …" lightbox -->
|
|
373
374
|
<Dialog v-model:open="createOpen">
|
|
374
|
-
<DialogContent class="sm:max-w-md">
|
|
375
|
+
<DialogContent class="sm:max-w-md" @interact-outside="keepOpenBeside">
|
|
375
376
|
<DialogHeader>
|
|
376
377
|
<DialogTitle>{{ kind ? NEW_LABEL[kind] : t("integration.credentials.revenexxSource.new.fallback") }}</DialogTitle>
|
|
377
378
|
<DialogDescription class="text-sm">
|
|
@@ -45,6 +45,7 @@ import { deepClone } from "../../utils/deepClone";
|
|
|
45
45
|
import { copyToClipboard } from "../../utils/clipboard";
|
|
46
46
|
import { nodeDisplayLabel } from "../../utils/validationFindings";
|
|
47
47
|
import { authChoiceFromConfig, configPatchForAuthChoice } from "../../data/webhookAuth";
|
|
48
|
+
import { eventSubject, eventTopicOf, withEventSubject } from "../../data/eventTrigger";
|
|
48
49
|
import {
|
|
49
50
|
dataTypeOf,
|
|
50
51
|
portValuesFromStepOutput,
|
|
@@ -55,6 +56,7 @@ import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
|
|
|
55
56
|
import { nodeDisplayName } from "../../utils/nodeDisplayName";
|
|
56
57
|
import { describeRecurrence, parseCron, scheduleTimezone, toCron } from "../../utils/schedule";
|
|
57
58
|
import { errorReason } from "../../utils/apiError";
|
|
59
|
+
import { keepOpenBeside } from "../../utils/dialog";
|
|
58
60
|
const t = useIntegrationsT();
|
|
59
61
|
const props = defineProps({
|
|
60
62
|
selection: { type: [Object, null], required: true },
|
|
@@ -446,6 +448,11 @@ function writeTriggerConfigPatch(patch) {
|
|
|
446
448
|
if (!wt) return;
|
|
447
449
|
patchTrigger({ config: { ...wt.config, ...patch } });
|
|
448
450
|
}
|
|
451
|
+
function writeEventSubject(subject) {
|
|
452
|
+
const wt = draftTrigger.value;
|
|
453
|
+
if (!wt) return;
|
|
454
|
+
patchTrigger({ config: withEventSubject(wt.config, subject) });
|
|
455
|
+
}
|
|
449
456
|
function applyAuthChoice(v) {
|
|
450
457
|
writeTriggerConfigPatch(configPatchForAuthChoice(v));
|
|
451
458
|
}
|
|
@@ -494,10 +501,7 @@ async function copyWebhookUrl() {
|
|
|
494
501
|
}
|
|
495
502
|
const definedEvents = typeof useDefinedEvents === "function" ? useDefinedEvents() : { emits: ref([]), load: async () => {
|
|
496
503
|
} };
|
|
497
|
-
const eventTopic = computed(() =>
|
|
498
|
-
const raw = readTriggerConfig("event_type");
|
|
499
|
-
return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
|
|
500
|
-
});
|
|
504
|
+
const eventTopic = computed(() => eventTopicOf(draftTrigger.value?.config));
|
|
501
505
|
watch(eventTopic, (topic) => {
|
|
502
506
|
if (topic) void definedEvents.load();
|
|
503
507
|
}, { immediate: true });
|
|
@@ -1687,7 +1691,7 @@ async function runTestStep() {
|
|
|
1687
1691
|
</div>
|
|
1688
1692
|
|
|
1689
1693
|
<Dialog v-model:open="rotateOpen">
|
|
1690
|
-
<DialogContent class="sm:max-w-md">
|
|
1694
|
+
<DialogContent class="sm:max-w-md" @interact-outside="keepOpenBeside">
|
|
1691
1695
|
<DialogHeader>
|
|
1692
1696
|
<DialogTitle>{{ t("integration.nodeInspector.trigger.webhook.rotate.dialogTitle") }}</DialogTitle>
|
|
1693
1697
|
<DialogDescription>
|
|
@@ -1720,8 +1724,8 @@ async function runTestStep() {
|
|
|
1720
1724
|
<Label for="ni-event-type" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.event.label") }}</Label>
|
|
1721
1725
|
<EventTypeCombobox
|
|
1722
1726
|
input-id="ni-event-type"
|
|
1723
|
-
:model-value="
|
|
1724
|
-
@update:model-value="(v) =>
|
|
1727
|
+
:model-value="eventSubject(draftTrigger?.config) ?? ''"
|
|
1728
|
+
@update:model-value="(v) => writeEventSubject(v)"
|
|
1725
1729
|
/>
|
|
1726
1730
|
<p class="text-xs text-muted-foreground">
|
|
1727
1731
|
{{ t("integration.nodeInspector.trigger.event.hint.intro") }} <code class="font-mono">customer.created</code>, <code class="font-mono">order.placed</code>{{ t("integration.nodeInspector.trigger.event.hint.mid") }} <code class="font-mono">trigger.payload</code>.
|
|
@@ -164,6 +164,13 @@ function subtreeManifests(g) {
|
|
|
164
164
|
for (const child of g.children.values()) out.push(...subtreeManifests(child));
|
|
165
165
|
return out;
|
|
166
166
|
}
|
|
167
|
+
function inBrowseOrder(g) {
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const child of g.children.values()) out.push(...inBrowseOrder(child));
|
|
170
|
+
out.push(...g.leaves);
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
const rootBrowseOrder = computed(() => [...inBrowseOrder(tree.value.core), ...inBrowseOrder(tree.value.apps)]);
|
|
167
174
|
function groupLogoManifest(g) {
|
|
168
175
|
const all = subtreeManifests(g);
|
|
169
176
|
return all.find(hasLogo) ?? all.find((m) => (m.images ?? []).length > 0) ?? null;
|
|
@@ -207,7 +214,7 @@ const levelKey = computed(() => stack.value.length);
|
|
|
207
214
|
const searchHits = computed(() => {
|
|
208
215
|
const q = search.value.trim().toLowerCase();
|
|
209
216
|
if (!q || triggersLevel.value) return [];
|
|
210
|
-
const scope = currentGroup.value ?
|
|
217
|
+
const scope = currentGroup.value ? inBrowseOrder(currentGroup.value) : rootBrowseOrder.value;
|
|
211
218
|
const hits = [];
|
|
212
219
|
for (const m of scope) {
|
|
213
220
|
const hay = `${resolveLocalized(m.name)} ${m.slug} ${resolveLocalized(m.description)}`.toLowerCase();
|
|
@@ -4,6 +4,7 @@ import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
|
4
4
|
import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@revenexx/studio";
|
|
5
5
|
import LoadingAnnouncement from "./LoadingAnnouncement.vue";
|
|
6
6
|
import { useIntegrationsCredentials } from "../../composables/useIntegrationsCredentials";
|
|
7
|
+
import { keepOpenBeside } from "../../utils/dialog";
|
|
7
8
|
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
8
9
|
const emit = defineEmits(["created"]);
|
|
9
10
|
const t = useIntegrationsT();
|
|
@@ -68,7 +69,7 @@ async function importAccount(account) {
|
|
|
68
69
|
|
|
69
70
|
<template>
|
|
70
71
|
<Dialog v-model:open="open">
|
|
71
|
-
<DialogContent class="sm:max-w-md">
|
|
72
|
+
<DialogContent class="sm:max-w-md" @interact-outside="keepOpenBeside">
|
|
72
73
|
<DialogHeader>
|
|
73
74
|
<DialogTitle>{{ t("integration.credentials.sftpImport.title") }}</DialogTitle>
|
|
74
75
|
<DialogDescription class="text-sm">
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
SelectValue
|
|
18
18
|
} from "@revenexx/studio";
|
|
19
19
|
import { stateNamespaceErrorKey } from "../../utils/stateNamespaceName";
|
|
20
|
+
import { keepOpenBeside } from "../../utils/dialog";
|
|
20
21
|
const props = defineProps({
|
|
21
22
|
open: { type: Boolean, required: true },
|
|
22
23
|
role: { type: String, required: true },
|
|
@@ -45,7 +46,7 @@ function declare() {
|
|
|
45
46
|
|
|
46
47
|
<template>
|
|
47
48
|
<Dialog :open="open" @update:open="(v) => emit('update:open', v)">
|
|
48
|
-
<DialogContent class="max-w-[94vw] sm:max-w-lg">
|
|
49
|
+
<DialogContent class="max-w-[94vw] sm:max-w-lg" @interact-outside="keepOpenBeside">
|
|
49
50
|
<DialogHeader>
|
|
50
51
|
<DialogTitle>{{ t("integration.nodeConfig.state.create.title") }}</DialogTitle>
|
|
51
52
|
<DialogDescription class="text-sm">
|
|
@@ -15,7 +15,7 @@ const props = defineProps({
|
|
|
15
15
|
const t = useIntegrationsT();
|
|
16
16
|
const { load: loadNodeCatalog } = useIntegrationsNodes();
|
|
17
17
|
onMounted(() => {
|
|
18
|
-
void loadNodeCatalog();
|
|
18
|
+
void loadNodeCatalog(false, { summary: true });
|
|
19
19
|
});
|
|
20
20
|
const STOP_SLUG = "revenexx:stop-and-error";
|
|
21
21
|
const NODE_W = 220;
|
|
@@ -27,6 +27,7 @@ import WorkflowIconPicker from "./WorkflowIconPicker.vue";
|
|
|
27
27
|
import WorkflowTagsInput from "./WorkflowTagsInput.vue";
|
|
28
28
|
import { resolveLocalized } from "../../utils/localize";
|
|
29
29
|
import { errorReason } from "../../utils/apiError";
|
|
30
|
+
import { keepOpenBeside } from "../../utils/dialog";
|
|
30
31
|
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
31
32
|
const t = useIntegrationsT();
|
|
32
33
|
const router = useRouter();
|
|
@@ -136,7 +137,7 @@ async function installed(id) {
|
|
|
136
137
|
|
|
137
138
|
<template>
|
|
138
139
|
<Dialog v-model:open="open">
|
|
139
|
-
<DialogContent class="sm:max-w-4xl">
|
|
140
|
+
<DialogContent class="sm:max-w-4xl" @interact-outside="keepOpenBeside">
|
|
140
141
|
<DialogHeader>
|
|
141
142
|
<DialogTitle>{{ t("integration.workflows.create.dialog.title") }}</DialogTitle>
|
|
142
143
|
</DialogHeader>
|
|
@@ -4,6 +4,7 @@ import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
|
4
4
|
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input } from "@revenexx/studio";
|
|
5
5
|
import { useIntegrationsWorkflows } from "../../composables/useIntegrationsWorkflows";
|
|
6
6
|
import { errorReason } from "../../utils/apiError";
|
|
7
|
+
import { keepOpenBeside } from "../../utils/dialog";
|
|
7
8
|
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
8
9
|
const props = defineProps({
|
|
9
10
|
workflow: { type: [Object, null], required: true }
|
|
@@ -39,7 +40,7 @@ async function submit() {
|
|
|
39
40
|
|
|
40
41
|
<template>
|
|
41
42
|
<Dialog v-model:open="open">
|
|
42
|
-
<DialogContent class="sm:max-w-md">
|
|
43
|
+
<DialogContent class="sm:max-w-md" @interact-outside="keepOpenBeside">
|
|
43
44
|
<DialogHeader>
|
|
44
45
|
<DialogTitle>{{ t("integration.workflows.duplicate.dialog.title") }}</DialogTitle>
|
|
45
46
|
<DialogDescription class="text-sm">
|
|
@@ -60,6 +60,7 @@ import { groupFindingsByNode, findingsByNodeField } from "../../utils/validation
|
|
|
60
60
|
import { reorderBranchSubtrees } from "../../data/branchReorder";
|
|
61
61
|
import { resolveLocalized } from "../../utils/localize";
|
|
62
62
|
import { nodeDisplayName } from "../../utils/nodeDisplayName";
|
|
63
|
+
import { keepOpenBeside } from "../../utils/dialog";
|
|
63
64
|
const props = defineProps({
|
|
64
65
|
workflow: { type: Object, required: true },
|
|
65
66
|
draft: { type: [Object, null], required: false },
|
|
@@ -1936,8 +1937,9 @@ const edgeTypes = {
|
|
|
1936
1937
|
<CanvasShortcutsDialog v-model:open="shortcutsOpen" />
|
|
1937
1938
|
|
|
1938
1939
|
<!-- Node/trigger configuration — a near-fullscreen workbench lightbox
|
|
1939
|
-
(Input pane / Parameters+Settings tabs /
|
|
1940
|
-
inspector edits a draft; closing without
|
|
1940
|
+
(Input pane / Parameters+Settings tabs / Done+Cancel footer). The
|
|
1941
|
+
inspector edits a draft; closing without Done discards it, and a click
|
|
1942
|
+
beside it does not close it (dialog-dismissal AC-1). -->
|
|
1941
1943
|
<Dialog :open="!!selection" @update:open="(v) => {
|
|
1942
1944
|
if (!v) {
|
|
1943
1945
|
selectedId = null;
|
|
@@ -1960,6 +1962,7 @@ const edgeTypes = {
|
|
|
1960
1962
|
class="flex max-w-none flex-col gap-0 overflow-hidden rounded-lg p-0 sm:max-w-none"
|
|
1961
1963
|
:class="selectionIsTrigger ? 'h-[min(520px,86vh)] w-[min(600px,94vw)]' : 'h-[min(680px,86vh)] w-[min(1100px,94vw)]'"
|
|
1962
1964
|
@open-auto-focus="onConfigOpenAutoFocus"
|
|
1965
|
+
@interact-outside="keepOpenBeside"
|
|
1963
1966
|
>
|
|
1964
1967
|
<NodeInspector
|
|
1965
1968
|
v-if="selection"
|
|
@@ -24,6 +24,7 @@ import { inputHandleId, outputHandleId } from "../../data/handleId";
|
|
|
24
24
|
import { iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
|
|
25
25
|
import { useIntegrationsRevisions } from "../../composables/useIntegrationsRevisions";
|
|
26
26
|
import { errorReason } from "../../utils/apiError";
|
|
27
|
+
import { personLabel } from "../../utils/personLabel";
|
|
27
28
|
const props = defineProps({
|
|
28
29
|
workflow: { type: Object, required: true },
|
|
29
30
|
draftPending: { type: Boolean, required: false }
|
|
@@ -53,6 +54,9 @@ async function loadList(reset = false) {
|
|
|
53
54
|
listLoading.value = false;
|
|
54
55
|
}
|
|
55
56
|
}
|
|
57
|
+
function revisionAuthor(rev) {
|
|
58
|
+
return { sub: rev.saved_by_sub, name: rev.saved_by_name, email: rev.saved_by_email };
|
|
59
|
+
}
|
|
56
60
|
function loadMore() {
|
|
57
61
|
if (page.value >= lastPage.value) return;
|
|
58
62
|
page.value += 1;
|
|
@@ -348,7 +352,11 @@ onMounted(() => {
|
|
|
348
352
|
@click="select(null)"
|
|
349
353
|
>
|
|
350
354
|
<div class="flex items-center justify-between gap-2">
|
|
351
|
-
|
|
355
|
+
<!-- Dated by when this version was saved, beside whoever saved it
|
|
356
|
+
(PO-609): `updated_at` also moves on a rename, which writes no
|
|
357
|
+
version. It stays as the fallback for a version from before the
|
|
358
|
+
moment was recorded. -->
|
|
359
|
+
<span class="text-sm font-medium tabular-nums">{{ formatDateTime(workflow.savedAt ?? workflow.updated_at) }}</span>
|
|
352
360
|
<Badge variant="outline" class="text-xs font-medium uppercase border-emerald-500/30 text-emerald-700">{{ t("integration.history.badge.current") }}</Badge>
|
|
353
361
|
<Badge
|
|
354
362
|
variant="outline"
|
|
@@ -360,6 +368,11 @@ onMounted(() => {
|
|
|
360
368
|
</Badge>
|
|
361
369
|
</div>
|
|
362
370
|
<div class="mt-0.5 text-sm text-muted-foreground">{{ t("integration.history.list.revisionLabel", { revision: workflow.revision }) }}</div>
|
|
371
|
+
<!-- Nobody recorded — a platform write, or a version from before
|
|
372
|
+
authors were — says nothing rather than guessing. -->
|
|
373
|
+
<div v-if="personLabel(workflow.savedBy)" class="mt-0.5 truncate text-sm text-muted-foreground" :title="workflow.savedBy?.name ? workflow.savedBy.email ?? void 0 : void 0">
|
|
374
|
+
{{ t("integration.history.list.savedBy", { name: personLabel(workflow.savedBy) }) }}
|
|
375
|
+
</div>
|
|
363
376
|
</button>
|
|
364
377
|
|
|
365
378
|
<!-- Archived revisions -->
|
|
@@ -371,9 +384,15 @@ onMounted(() => {
|
|
|
371
384
|
>
|
|
372
385
|
<button type="button" class="w-full p-2.5 text-left" @click="select(rev.revision)">
|
|
373
386
|
<div class="flex items-center justify-between gap-2">
|
|
374
|
-
|
|
387
|
+
<!-- Not `archived_at` first: that is when the NEXT save replaced
|
|
388
|
+
this version, and beside its author's name it would name the
|
|
389
|
+
wrong person for that moment. -->
|
|
390
|
+
<span class="text-sm font-medium tabular-nums">{{ formatDateTime(rev.saved_at ?? rev.archived_at) }}</span>
|
|
375
391
|
</div>
|
|
376
392
|
<div class="mt-0.5 text-sm text-muted-foreground">{{ t("integration.history.list.revisionLabel", { revision: rev.revision }) }}</div>
|
|
393
|
+
<div v-if="personLabel(revisionAuthor(rev))" class="mt-0.5 truncate text-sm text-muted-foreground" :title="rev.saved_by_name ? rev.saved_by_email ?? void 0 : void 0">
|
|
394
|
+
{{ t("integration.history.list.savedBy", { name: personLabel(revisionAuthor(rev)) }) }}
|
|
395
|
+
</div>
|
|
377
396
|
</button>
|
|
378
397
|
<div class="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 transition group-hover:opacity-100">
|
|
379
398
|
<Button
|
|
@@ -8,7 +8,7 @@ const props = defineProps({
|
|
|
8
8
|
});
|
|
9
9
|
const { load } = useIntegrationsNodes();
|
|
10
10
|
onMounted(() => {
|
|
11
|
-
if (props.icon?.kind === "image") void load();
|
|
11
|
+
if (props.icon?.kind === "image") void load(false, { summary: true });
|
|
12
12
|
});
|
|
13
13
|
const resolvedSrc = computed(() => {
|
|
14
14
|
if (props.icon?.kind !== "image" || !props.icon.src) return null;
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "@revenexx/studio";
|
|
20
20
|
import WorkflowIconPicker from "./WorkflowIconPicker.vue";
|
|
21
21
|
import WorkflowTagsInput from "./WorkflowTagsInput.vue";
|
|
22
|
+
import { keepOpenBeside } from "../../utils/dialog";
|
|
22
23
|
const props = defineProps({
|
|
23
24
|
open: { type: Boolean, required: true },
|
|
24
25
|
workflow: { type: Object, required: true },
|
|
@@ -64,7 +65,10 @@ function formatDateTime(iso) {
|
|
|
64
65
|
|
|
65
66
|
<template>
|
|
66
67
|
<Dialog :open="open" @update:open="(v) => emit('update:open', v)">
|
|
67
|
-
<DialogContent
|
|
68
|
+
<DialogContent
|
|
69
|
+
class="sm:max-w-lg"
|
|
70
|
+
@interact-outside="keepOpenBeside"
|
|
71
|
+
>
|
|
68
72
|
<DialogHeader>
|
|
69
73
|
<DialogTitle>{{ t("integration.workflows.settings.dialog.title") }}</DialogTitle>
|
|
70
74
|
<DialogDescription class="text-sm">
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "@revenexx/studio";
|
|
20
20
|
import { deepClone } from "../../utils/deepClone";
|
|
21
21
|
import { stateNamespaceErrorKey } from "../../utils/stateNamespaceName";
|
|
22
|
+
import { keepOpenBeside } from "../../utils/dialog";
|
|
22
23
|
const props = defineProps({
|
|
23
24
|
open: { type: Boolean, required: true },
|
|
24
25
|
state: { type: Array, required: true }
|
|
@@ -66,7 +67,10 @@ function addNamespace() {
|
|
|
66
67
|
|
|
67
68
|
<template>
|
|
68
69
|
<Dialog :open="open" @update:open="(v) => emit('update:open', v)">
|
|
69
|
-
<DialogContent
|
|
70
|
+
<DialogContent
|
|
71
|
+
class="max-w-[94vw] sm:max-w-3xl"
|
|
72
|
+
@interact-outside="keepOpenBeside"
|
|
73
|
+
>
|
|
70
74
|
<DialogHeader>
|
|
71
75
|
<DialogTitle>{{ t("integration.workflows.state.dialog.title") }}</DialogTitle>
|
|
72
76
|
<DialogDescription class="text-sm">
|
|
@@ -9,6 +9,7 @@ import { triggerGlyph as glyphForTrigger } from "../../data/triggerGlyphs";
|
|
|
9
9
|
import { resolveDisplayName } from "../../utils/localize";
|
|
10
10
|
import { NODE_CATEGORIES, triggerTypeLabel } from "../../data/integrationNodeCatalog";
|
|
11
11
|
import { describeCron } from "../../utils/schedule";
|
|
12
|
+
import { eventSubject } from "../../data/eventTrigger";
|
|
12
13
|
const props = defineProps({
|
|
13
14
|
id: { type: String, required: true },
|
|
14
15
|
data: { type: Object, required: true },
|
|
@@ -23,7 +24,10 @@ const subtitle = computed(() => {
|
|
|
23
24
|
const c = props.data.config;
|
|
24
25
|
if (props.data.type === "schedule") return describeCron(c.cron) ?? c.cron ?? t("integration.canvas.trigger.subtitle.noSchedule");
|
|
25
26
|
if (props.data.type === "webhook") return `${c.method ?? "POST"} ${c.path ?? "/"}`;
|
|
26
|
-
if (props.data.type === "event")
|
|
27
|
+
if (props.data.type === "event") {
|
|
28
|
+
const event = eventSubject(c);
|
|
29
|
+
return event ? t("integration.canvas.trigger.subtitle.onEvent", { event }) : t("integration.canvas.trigger.subtitle.noEvent");
|
|
30
|
+
}
|
|
27
31
|
return t("integration.canvas.trigger.subtitle.onDemand");
|
|
28
32
|
});
|
|
29
33
|
const title = computed(() => {
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { deepClone } from "../../utils/deepClone";
|
|
22
22
|
import { copyToClipboard } from "../../utils/clipboard";
|
|
23
23
|
import { languageKeyRead, languageKeyToWrite, resolveDisplayName } from "../../utils/localize";
|
|
24
|
+
import { keepOpenBeside } from "../../utils/dialog";
|
|
24
25
|
const props = defineProps({
|
|
25
26
|
open: { type: Boolean, required: true },
|
|
26
27
|
variables: { type: Object, required: true }
|
|
@@ -156,7 +157,10 @@ async function copyUsage(key) {
|
|
|
156
157
|
|
|
157
158
|
<template>
|
|
158
159
|
<Dialog :open="open" @update:open="(v2) => emit('update:open', v2)">
|
|
159
|
-
<DialogContent
|
|
160
|
+
<DialogContent
|
|
161
|
+
class="max-w-[94vw] sm:max-w-3xl"
|
|
162
|
+
@interact-outside="keepOpenBeside"
|
|
163
|
+
>
|
|
160
164
|
<DialogHeader>
|
|
161
165
|
<DialogTitle>{{ t("integration.workflows.variables.dialog.title") }}</DialogTitle>
|
|
162
166
|
<DialogDescription class="text-sm">
|
|
@@ -5,6 +5,9 @@ export interface AuditEntry {
|
|
|
5
5
|
org_id?: string;
|
|
6
6
|
tenant_id?: string;
|
|
7
7
|
actor_sub?: string | null;
|
|
8
|
+
/** As the actor's sign-in stated it when the entry was written; null for the platform, and for entries from before names were recorded (PO-609). */
|
|
9
|
+
actor_name?: string | null;
|
|
10
|
+
actor_email?: string | null;
|
|
8
11
|
action: string;
|
|
9
12
|
resource_type: string;
|
|
10
13
|
resource_id?: string;
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { findManifest } from '../data/integrationNodeCatalog.js';
|
|
2
2
|
import { resolveLocalized } from '../utils/localize.js';
|
|
3
|
+
export interface NodeCatalogLoadOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Enough to name, draw and group a node — no settings schemas (PE-990). For the
|
|
6
|
+
* surfaces that only show nodes: the dashboard, the template gallery and pages,
|
|
7
|
+
* a workflow's icon. The editor asks without it and gets every manifest whole; a
|
|
8
|
+
* summary is never asked for once the full catalogue is in, so it cannot replace
|
|
9
|
+
* what the editor reads.
|
|
10
|
+
*/
|
|
11
|
+
summary?: boolean;
|
|
12
|
+
}
|
|
3
13
|
export declare function useIntegrationsNodes(): {
|
|
4
14
|
loading: any;
|
|
5
15
|
error: any;
|
|
6
16
|
loaded: any;
|
|
7
|
-
|
|
17
|
+
level: any;
|
|
18
|
+
load: (force?: boolean, opts?: NodeCatalogLoadOptions) => Promise<void>;
|
|
8
19
|
findManifest: typeof findManifest;
|
|
9
20
|
resolveLocalized: typeof resolveLocalized;
|
|
10
21
|
};
|
|
@@ -5,21 +5,38 @@ import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
|
5
5
|
import { errorReason } from "../utils/apiError.js";
|
|
6
6
|
import { unwrapList } from "../utils/apiEnvelope.js";
|
|
7
7
|
import { useLoadGate } from "./useLoadGate.js";
|
|
8
|
+
import { whenIdle } from "../utils/whenIdle.js";
|
|
9
|
+
import { catalogNeedsLoad, catalogRequest } from "../utils/nodeCatalogLevel.js";
|
|
8
10
|
const RETRY_COOLDOWN_MS = 3e4;
|
|
9
11
|
export function useIntegrationsNodes() {
|
|
10
12
|
const loading = useState("integrations.nodes.loading", () => false);
|
|
11
13
|
const error = useState("integrations.nodes.error", () => null);
|
|
12
14
|
const loaded = useState("integrations.nodes.loaded", () => false);
|
|
15
|
+
const level = useState("integrations.nodes.level", () => null);
|
|
13
16
|
const failedAt = useState("integrations.nodes.failedAt", () => 0);
|
|
14
17
|
const gate = useLoadGate("integrations.nodes", { loading, loaded });
|
|
15
18
|
const api = useIntegrationsApi();
|
|
16
19
|
const t = useIntegrationsT();
|
|
17
|
-
async function load(force = false) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
async function load(force = false, opts = {}) {
|
|
21
|
+
const wanted = opts.summary ? "summary" : "full";
|
|
22
|
+
const coolingDown = () => !!failedAt.value && Date.now() - failedAt.value < RETRY_COOLDOWN_MS;
|
|
23
|
+
if (!force && coolingDown()) return;
|
|
24
|
+
if (!force) {
|
|
25
|
+
while (loading.value) await whenIdle(loading);
|
|
26
|
+
if (coolingDown()) return;
|
|
27
|
+
if (!catalogNeedsLoad(loaded.value ? level.value : null, wanted)) return;
|
|
28
|
+
}
|
|
29
|
+
if (!await gate.claim(force || loaded.value)) return;
|
|
20
30
|
error.value = null;
|
|
31
|
+
const request = catalogRequest(loaded.value ? level.value : null, wanted);
|
|
32
|
+
if (request === "full" && level.value === "summary") {
|
|
33
|
+
applyNodeCatalog([]);
|
|
34
|
+
loaded.value = false;
|
|
35
|
+
level.value = null;
|
|
36
|
+
}
|
|
21
37
|
try {
|
|
22
|
-
const
|
|
38
|
+
const res = await api.get(request === "summary" ? "/nodes?fields=summary" : "/nodes");
|
|
39
|
+
const list = unwrapList(res);
|
|
23
40
|
const manifests = list.filter((n) => n.manifest).map((n) => ({
|
|
24
41
|
...n.manifest,
|
|
25
42
|
slug: n.manifest.slug ?? n.slug,
|
|
@@ -40,10 +57,12 @@ export function useIntegrationsNodes() {
|
|
|
40
57
|
__packagePosition: typeof n.package_position === "number" ? n.package_position : void 0
|
|
41
58
|
}));
|
|
42
59
|
applyNodeCatalog(manifests);
|
|
60
|
+
level.value = request === "summary" && res?.fields === "summary" ? "summary" : "full";
|
|
43
61
|
loaded.value = true;
|
|
44
62
|
failedAt.value = 0;
|
|
45
63
|
} catch (e) {
|
|
46
64
|
applyNodeCatalog([]);
|
|
65
|
+
level.value = null;
|
|
47
66
|
loaded.value = false;
|
|
48
67
|
failedAt.value = Date.now();
|
|
49
68
|
error.value = errorReason(e, t("integration.nodes.error.loadFailed"));
|
|
@@ -51,5 +70,5 @@ export function useIntegrationsNodes() {
|
|
|
51
70
|
gate.release();
|
|
52
71
|
}
|
|
53
72
|
}
|
|
54
|
-
return { loading, error, loaded, load, findManifest, resolveLocalized };
|
|
73
|
+
return { loading, error, loaded, level, load, findManifest, resolveLocalized };
|
|
55
74
|
}
|
|
@@ -15,6 +15,12 @@ export interface WorkflowRevisionMeta {
|
|
|
15
15
|
description: string | null;
|
|
16
16
|
active: boolean;
|
|
17
17
|
blob_definition_version: string;
|
|
18
|
+
/** Who saved this revision (PO-609); all null for a platform write and for a revision from before authors were recorded. */
|
|
19
|
+
saved_by_sub?: string | null;
|
|
20
|
+
saved_by_name?: string | null;
|
|
21
|
+
saved_by_email?: string | null;
|
|
22
|
+
/** When that person saved it — not `archived_at`, which is when the next save replaced it. */
|
|
23
|
+
saved_at?: string | null;
|
|
18
24
|
created_at: string | null;
|
|
19
25
|
updated_at: string | null;
|
|
20
26
|
archived_at: string | null;
|
|
@@ -10,7 +10,7 @@ export declare function useIntegrationsTemplates(): {
|
|
|
10
10
|
list: () => WorkflowTemplate[];
|
|
11
11
|
get: (slug: string) => WorkflowTemplate | null;
|
|
12
12
|
fetchDetail: (slug: string) => Promise<WorkflowTemplate | null>;
|
|
13
|
-
ensureDetails: (slugs: string[],
|
|
13
|
+
ensureDetails: (slugs: string[], _concurrency?: number) => Promise<void>;
|
|
14
14
|
requirements: (slug: string) => Promise<TemplateRequirements | null>;
|
|
15
15
|
instantiate: (slug: string, opts?: {
|
|
16
16
|
name?: string;
|