@revenexx/studio-integrations 0.5.0 → 0.6.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 +201 -0
- package/README.md +17 -4
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +15 -14
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +91 -19
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -21
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +4 -8
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -2
- 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 +54 -25
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +12 -12
- package/dist/runtime/components/Integrations/Layout.vue +21 -2
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +10 -11
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +2 -5
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +160 -67
- 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 +1075 -303
- 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 +42 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +100 -34
- package/dist/runtime/components/Integrations/QueryBuilder.vue +8 -1
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -1
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +17 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +2 -1
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +109 -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 +7 -10
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +21 -37
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +23 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +22 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +170 -48
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +22 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +11 -2
- 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 +189 -46
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +7 -2
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -2
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +30 -17
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +47 -7
- package/dist/runtime/composables/useCredentialTypes.js +4 -4
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +4 -4
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- package/dist/runtime/composables/useIntegrationsNodes.js +4 -4
- package/dist/runtime/composables/useIntegrationsRuns.js +11 -6
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +10 -7
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsTemplates.js +12 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +6 -6
- package/dist/runtime/composables/useIntegrationsWorkflows.js +8 -4
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- 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/integrationTemplates.d.ts +39 -0
- package/dist/runtime/data/integrationTemplates.js +15 -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 +27 -3
- 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 +2 -5
- package/dist/runtime/pages/integrations/audit.vue +91 -6
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +6 -4
- package/dist/runtime/pages/integrations/credentials.vue +119 -21
- package/dist/runtime/pages/integrations/index.vue +49 -44
- package/dist/runtime/pages/integrations/runs.vue +1 -1
- package/dist/runtime/pages/integrations/schedules.vue +51 -7
- package/dist/runtime/pages/integrations/secrets.vue +29 -7
- package/dist/runtime/pages/integrations/templates/[slug].vue +52 -30
- package/dist/runtime/pages/integrations/templates/index.vue +57 -86
- package/dist/runtime/pages/integrations/workflows/[id].vue +243 -25
- package/dist/runtime/pages/integrations/workflows/index.vue +15 -2
- 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/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +29 -0
- package/dist/runtime/utils/localize.js +4 -0
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -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 +141 -0
- package/dist/runtime/utils/schedule.js +207 -0
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +53 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/package.json +8 -2
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import type { WorkflowIcon } from '../../types/integrations.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The caller names the picker by pointing at its own `<Label>`'s id — not with
|
|
4
|
+
* `<Label for>`, and not with an `id` of ours on the trigger.
|
|
5
|
+
*
|
|
6
|
+
* An `id` on the trigger would be a quiet regression: `PopoverTrigger` puts
|
|
7
|
+
* reka-ui's generated `triggerId` there, `PopoverContentImpl` points the opened
|
|
8
|
+
* dialog's `aria-labelledby` at exactly that id, and reka's `Slot` merges with
|
|
9
|
+
* the *child's* props winning — so ours would replace it and leave the popover
|
|
10
|
+
* naming an element that no longer exists.
|
|
11
|
+
*
|
|
12
|
+
* `aria-labelledby` also lists the id of the span holding the current choice, so
|
|
13
|
+
* the name stays "Icon Business Central" rather than dropping the value the way
|
|
14
|
+
* a plain label would. Attribute fallthrough is no help here either: the root
|
|
15
|
+
* this component renders is the popover, not the button.
|
|
16
|
+
*/
|
|
17
|
+
type __VLS_Props = {
|
|
18
|
+
triggerLabelledby?: string;
|
|
19
|
+
};
|
|
20
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
3
21
|
modelValue?: WorkflowIcon | null;
|
|
4
22
|
};
|
|
5
23
|
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
@@ -5,6 +5,10 @@ interface NodeData extends WorkflowNode {
|
|
|
5
5
|
* editor without persisting to the blob. When present and non-empty, these
|
|
6
6
|
* drive the rendered handles instead of the static manifest computation. */
|
|
7
7
|
__resolvedOutputs?: NodeManifestOutput[];
|
|
8
|
+
/** Config keys the backend reported as incomplete at the last save (PO-209),
|
|
9
|
+
* injected by the editor the same non-persisting way. Read-only canvases
|
|
10
|
+
* (run replay, history preview) never pass them, so nothing is marked there. */
|
|
11
|
+
__invalidFields?: string[];
|
|
8
12
|
}
|
|
9
13
|
type __VLS_Props = {
|
|
10
14
|
id: string;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, inject, ref, watch } from "vue";
|
|
2
|
+
import { computed, inject, nextTick, ref, watch } from "vue";
|
|
3
3
|
import { Handle, Position, useVueFlow } from "@vue-flow/core";
|
|
4
4
|
import { NodeToolbar } from "@vue-flow/node-toolbar";
|
|
5
5
|
import * as Lucide from "lucide-vue-next";
|
|
6
6
|
import { NODE_CATEGORIES, findManifest } from "../../data/integrationNodeCatalog";
|
|
7
|
+
import { isTerminalManifest } from "../../data/nodeInputData";
|
|
8
|
+
import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
|
|
7
9
|
const props = defineProps({
|
|
8
10
|
id: { type: String, required: true },
|
|
9
11
|
data: { type: Object, required: true },
|
|
@@ -13,42 +15,28 @@ defineOptions({ inheritAttrs: false });
|
|
|
13
15
|
defineEmits(["updateNodeInternals"]);
|
|
14
16
|
const manifest = computed(() => findManifest(props.data.nodeSlug, props.data.nodeVersion));
|
|
15
17
|
const category = computed(() => NODE_CATEGORIES.find((c) => c.key === manifest.value?.category));
|
|
16
|
-
const hasLogo = computed(() => (manifest.value?.images ?? []).some((i) => i.category === "logo"));
|
|
17
|
-
function iconFor(name) {
|
|
18
|
-
if (!name) return Lucide.Box;
|
|
19
|
-
const cleaned = (name.includes(":") ? name.split(":").pop() : name).split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
20
|
-
return Lucide[cleaned] || Lucide.Box;
|
|
21
|
-
}
|
|
22
|
-
function labelText(value) {
|
|
23
|
-
if (!value) return "";
|
|
24
|
-
return typeof value === "string" ? value : value.en ?? Object.values(value)[0] ?? "";
|
|
25
|
-
}
|
|
26
18
|
const title = computed(
|
|
27
|
-
() =>
|
|
19
|
+
() => resolveDisplayName(props.data.name) || resolveLocalized(manifest.value?.name) || props.data.nodeSlug
|
|
28
20
|
);
|
|
29
21
|
const inputRequired = computed(() => manifest.value?.input?.required !== false);
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const drivingSelects = fields.filter((f) => f.type === "select" && driving.has(f.key)).map((f) => f.key);
|
|
35
|
-
if (drivingSelects.length) return drivingSelects;
|
|
36
|
-
return fields.filter((f) => f.type === "select" || f.type === "multiselect").slice(0, 2).map((f) => f.key);
|
|
22
|
+
const invalidFields = computed(() => props.data.__invalidFields ?? []);
|
|
23
|
+
const invalidLabel = computed(() => {
|
|
24
|
+
const n = invalidFields.value.length;
|
|
25
|
+
return `Config incomplete \u2014 ${n} setting${n === 1 ? "" : "s"} still need${n === 1 ? "s" : ""} fixing: ${invalidFields.value.join(", ")}`;
|
|
37
26
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
27
|
+
const notes = computed(() => (props.data.notes ?? "").trim());
|
|
28
|
+
const noteOpen = ref(false);
|
|
29
|
+
const noteReady = ref(false);
|
|
30
|
+
const notePanel = ref(null);
|
|
31
|
+
const notePlacement = ref(Position.Bottom);
|
|
32
|
+
const noteMaxHeight = ref(0);
|
|
33
|
+
let noteTimer = null;
|
|
34
|
+
const notePanelId = computed(() => `node-note-${props.id}`);
|
|
47
35
|
function portsFromOutputs(list) {
|
|
48
36
|
const ports = [];
|
|
49
37
|
for (const o of list) {
|
|
50
38
|
if (o.name) {
|
|
51
|
-
ports.push({ name: o.name, kind: o.kind, label:
|
|
39
|
+
ports.push({ name: o.name, kind: o.kind, label: resolveLocalized(o.label) || o.name });
|
|
52
40
|
} else if (o.sourceFromConfig) {
|
|
53
41
|
const raw = props.data.config?.[o.sourceFromConfig];
|
|
54
42
|
const values = Array.isArray(raw) ? raw : [];
|
|
@@ -60,7 +48,7 @@ function portsFromOutputs(list) {
|
|
|
60
48
|
ports.push({
|
|
61
49
|
name: o.fallback.name,
|
|
62
50
|
kind: o.kind,
|
|
63
|
-
label:
|
|
51
|
+
label: resolveLocalized(o.fallback.label) || o.fallback.name
|
|
64
52
|
});
|
|
65
53
|
}
|
|
66
54
|
}
|
|
@@ -69,24 +57,48 @@ function portsFromOutputs(list) {
|
|
|
69
57
|
}
|
|
70
58
|
const resolvePending = computed(() => (manifest.value?.outputs ?? []).some((o) => o.resolveOutputs === true) && !Array.isArray(props.data.__resolvedOutputs));
|
|
71
59
|
const outputs = computed(() => {
|
|
60
|
+
const m = manifest.value;
|
|
61
|
+
if (isTerminalManifest(m)) return [];
|
|
72
62
|
const resolved = props.data.__resolvedOutputs;
|
|
73
63
|
if (Array.isArray(resolved) && resolved.length) {
|
|
74
64
|
const ports2 = portsFromOutputs(resolved);
|
|
75
65
|
if (!ports2.length) ports2.push({ name: "default", kind: "default", label: "out" });
|
|
76
66
|
return ports2;
|
|
77
67
|
}
|
|
78
|
-
const m = manifest.value;
|
|
79
68
|
if (!m) return [{ name: "default", kind: "default", label: "out" }];
|
|
80
69
|
const ports = portsFromOutputs(m.outputs ?? []);
|
|
81
70
|
if (!ports.length) ports.push({ name: "default", kind: "default", label: "out" });
|
|
82
71
|
return ports;
|
|
83
72
|
});
|
|
73
|
+
function portSemantic(p) {
|
|
74
|
+
if (p.name === "true") return "branch-true";
|
|
75
|
+
if (p.name === "false") return "branch-false";
|
|
76
|
+
if (p.name === "error" || p.kind === "error") return "error";
|
|
77
|
+
if (p.name === "done") return "success";
|
|
78
|
+
if (p.kind === "branch") return "branch";
|
|
79
|
+
const portDef = (manifest.value?.outputs ?? []).find((o) => o.name === p.name);
|
|
80
|
+
return portDef ? "success" : "default";
|
|
81
|
+
}
|
|
84
82
|
function portTone(p) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
switch (portSemantic(p)) {
|
|
84
|
+
case "success":
|
|
85
|
+
case "branch-true":
|
|
86
|
+
// Switch cases / iterator `item`: a taken branch is a normal continuation,
|
|
87
|
+
// so it reads green like any other output rather than a special colour.
|
|
88
|
+
case "branch":
|
|
89
|
+
return { handle: "!bg-emerald-500", label: "text-emerald-600" };
|
|
90
|
+
// `false` is the neutral "didn't match" route, not a warning — muted slate
|
|
91
|
+
// so it recedes behind the green primary path (matches `default`).
|
|
92
|
+
case "branch-false":
|
|
93
|
+
return { handle: "!bg-slate-500", label: "text-slate-500" };
|
|
94
|
+
// Soft rose dot marks the error port; the LABEL stays neutral slate (not
|
|
95
|
+
// rose) so it's legible without a red-alert pull that would out-shout the
|
|
96
|
+
// green success label. Colour lives on the dot, the label just names it.
|
|
97
|
+
case "error":
|
|
98
|
+
return { handle: "!bg-rose-300", label: "text-slate-500" };
|
|
99
|
+
default:
|
|
100
|
+
return { handle: "!bg-slate-500", label: "text-slate-500" };
|
|
101
|
+
}
|
|
90
102
|
}
|
|
91
103
|
const showPortLabels = computed(() => resolvePending.value || outputs.value.length > 1 || outputs.value.some((p) => p.kind !== "default"));
|
|
92
104
|
const IN_SIDES = ["top"];
|
|
@@ -112,23 +124,67 @@ function onHoverLeave() {
|
|
|
112
124
|
hovered.value = false;
|
|
113
125
|
}, 200);
|
|
114
126
|
}
|
|
115
|
-
watch(
|
|
127
|
+
watch(() => Boolean(props.data.nodeVersion), () => flow.updateNodeInternals([props.id]));
|
|
128
|
+
const NOTE_GAP = 6;
|
|
129
|
+
const NOTE_TOOLBAR_CLEARANCE = 42;
|
|
130
|
+
const canvasBottomInset = inject("workflow-canvas-bottom-inset", null);
|
|
131
|
+
const noteTopOffset = ref(NOTE_GAP);
|
|
132
|
+
async function openNote(ev) {
|
|
133
|
+
holdNote();
|
|
134
|
+
if (noteOpen.value) return;
|
|
135
|
+
const card = ev.currentTarget?.closest(".vue-flow__node");
|
|
136
|
+
noteTopOffset.value = nodeActions && (hovered.value || props.selected) ? NOTE_TOOLBAR_CLEARANCE : NOTE_GAP;
|
|
137
|
+
noteOpen.value = true;
|
|
138
|
+
noteReady.value = false;
|
|
139
|
+
await nextTick();
|
|
140
|
+
const bounds = flow.vueFlowRef.value?.getBoundingClientRect();
|
|
141
|
+
if (notePanel.value && card && bounds) {
|
|
142
|
+
const rect = card.getBoundingClientRect();
|
|
143
|
+
const floor = Math.min(bounds.bottom, window.innerHeight) - (canvasBottomInset?.() ?? 0);
|
|
144
|
+
const ceiling = Math.max(bounds.top, 0);
|
|
145
|
+
const below = floor - rect.bottom - NOTE_GAP;
|
|
146
|
+
const above = rect.top - ceiling - noteTopOffset.value;
|
|
147
|
+
const flip = notePanel.value.offsetHeight > below && above > below;
|
|
148
|
+
notePlacement.value = flip ? Position.Top : Position.Bottom;
|
|
149
|
+
noteMaxHeight.value = Math.max(80, Math.floor(flip ? above : below));
|
|
150
|
+
}
|
|
151
|
+
noteReady.value = true;
|
|
152
|
+
}
|
|
153
|
+
function holdNote() {
|
|
154
|
+
if (noteTimer) clearTimeout(noteTimer);
|
|
155
|
+
noteTimer = null;
|
|
156
|
+
}
|
|
157
|
+
function releaseNote() {
|
|
158
|
+
if (noteTimer) clearTimeout(noteTimer);
|
|
159
|
+
noteTimer = setTimeout(() => {
|
|
160
|
+
noteOpen.value = false;
|
|
161
|
+
}, 250);
|
|
162
|
+
}
|
|
116
163
|
const nodeActions = inject("workflow-node-actions", null);
|
|
164
|
+
const canConfigure = computed(() => typeof nodeActions?.configure === "function");
|
|
165
|
+
function configure() {
|
|
166
|
+
nodeActions?.configure?.(props.id);
|
|
167
|
+
}
|
|
117
168
|
function duplicate() {
|
|
118
169
|
nodeActions?.duplicate(props.id);
|
|
119
170
|
}
|
|
120
171
|
function remove() {
|
|
121
|
-
|
|
122
|
-
else flow.removeNodes([props.id]);
|
|
172
|
+
nodeActions?.remove(props.id);
|
|
123
173
|
}
|
|
124
174
|
</script>
|
|
125
175
|
|
|
126
176
|
<template>
|
|
127
|
-
|
|
177
|
+
<!-- No node-actions injected → read-only canvas (e.g. run replay): the
|
|
178
|
+
duplicate/delete affordance would be dead, so don't offer it. -->
|
|
179
|
+
<!-- Icon-only, so each action carries a name — and it names the node, not
|
|
180
|
+
just the verb: the editor header has its own Duplicate (the workflow),
|
|
181
|
+
and "Duplicate" alone would be the same announcement for both. -->
|
|
182
|
+
<NodeToolbar v-if="nodeActions" :is-visible="hovered || selected" :position="Position.Top" :offset="8" class="flex items-center gap-1 rounded-md border border-black/[0.08] bg-white px-1 py-1 shadow-md" @mouseenter="onHoverEnter" @mouseleave="onHoverLeave">
|
|
128
183
|
<button
|
|
129
184
|
type="button"
|
|
130
185
|
class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-foreground/80 hover:bg-muted/50"
|
|
131
186
|
title="Duplicate"
|
|
187
|
+
aria-label="Duplicate node"
|
|
132
188
|
@click="duplicate"
|
|
133
189
|
>
|
|
134
190
|
<Lucide.Copy :size="11" />
|
|
@@ -138,12 +194,37 @@ function remove() {
|
|
|
138
194
|
type="button"
|
|
139
195
|
class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-rose-600 hover:bg-rose-50"
|
|
140
196
|
title="Delete"
|
|
197
|
+
aria-label="Delete node"
|
|
141
198
|
@click="remove"
|
|
142
199
|
>
|
|
143
200
|
<Lucide.Trash2 :size="11" />
|
|
144
201
|
</button>
|
|
145
202
|
</NodeToolbar>
|
|
146
203
|
|
|
204
|
+
<!-- The note itself, hanging off the corner the badge sits in — below the card
|
|
205
|
+
by default, above it when a long note would otherwise run past the bottom
|
|
206
|
+
of the canvas. A NodeToolbar rather than an in-card panel: it escapes the
|
|
207
|
+
node's stacking context, so the text can't end up behind a neighbour. -->
|
|
208
|
+
<NodeToolbar
|
|
209
|
+
v-if="notes"
|
|
210
|
+
:is-visible="noteOpen"
|
|
211
|
+
:position="notePlacement"
|
|
212
|
+
align="end"
|
|
213
|
+
:offset="notePlacement === Position.Top ? noteTopOffset : NOTE_GAP"
|
|
214
|
+
class="nowheel max-w-[240px] overflow-y-auto overscroll-contain rounded-lg border border-black/[0.08] bg-white px-2.5 py-2 text-left shadow-lg"
|
|
215
|
+
:class="noteReady ? '' : 'invisible'"
|
|
216
|
+
:style="noteMaxHeight ? { maxHeight: `${noteMaxHeight}px` } : void 0"
|
|
217
|
+
@mouseenter="holdNote"
|
|
218
|
+
@mouseleave="releaseNote"
|
|
219
|
+
>
|
|
220
|
+
<div ref="notePanel" role="tooltip">
|
|
221
|
+
<div class="mb-1 text-[8px] font-semibold uppercase tracking-[0.08em] text-muted-foreground/60">
|
|
222
|
+
Note
|
|
223
|
+
</div>
|
|
224
|
+
<p class="whitespace-pre-wrap break-words text-[11px] leading-snug text-foreground/80">{{ notes }}</p>
|
|
225
|
+
</div>
|
|
226
|
+
</NodeToolbar>
|
|
227
|
+
|
|
147
228
|
<!-- Input: ONE logical input, attachable from the left or the top — both
|
|
148
229
|
dots feed the same `__in` port. Clearly visible, ring + shadow. -->
|
|
149
230
|
<template v-if="inputRequired">
|
|
@@ -181,10 +262,66 @@ function remove() {
|
|
|
181
262
|
@mouseenter="onHoverEnter"
|
|
182
263
|
@mouseleave="onHoverLeave"
|
|
183
264
|
:class="[
|
|
184
|
-
selected ? 'border-emerald-500/50 shadow-[0_0_0_2px_rgba(16,185,129,0.18)]' : 'border-black/[0.08] shadow-sm hover:shadow-md hover:border-black/[0.16]',
|
|
265
|
+
selected ? 'border-emerald-500/50 shadow-[0_0_0_2px_rgba(16,185,129,0.18)]' : invalidFields.length ? 'border-amber-500/70 shadow-[0_0_0_2px_rgba(245,158,11,0.22)] hover:shadow-md' : 'border-black/[0.08] shadow-sm hover:shadow-md hover:border-black/[0.16]',
|
|
185
266
|
showPortLabels ? 'pb-3.5' : ''
|
|
186
267
|
]"
|
|
187
268
|
>
|
|
269
|
+
<!-- Incomplete settings at the last save: the count, amber, at the corner
|
|
270
|
+
the input handles leave free. Hoverable on purpose — the count alone
|
|
271
|
+
doesn't say what is wrong, so the title spells it out and lists the
|
|
272
|
+
fields; `nodrag` keeps that hover from starting a card drag. -->
|
|
273
|
+
<span
|
|
274
|
+
v-if="invalidFields.length"
|
|
275
|
+
role="img"
|
|
276
|
+
:aria-label="invalidLabel"
|
|
277
|
+
:title="invalidLabel"
|
|
278
|
+
class="nodrag absolute -left-2 -top-2 z-10 flex cursor-help items-center gap-0.5 rounded-full bg-amber-500 px-1.5 py-0.5 text-[9px] font-bold leading-none text-white shadow-sm"
|
|
279
|
+
>
|
|
280
|
+
<Lucide.TriangleAlert :size="9" :stroke-width="2.5" />
|
|
281
|
+
{{ invalidFields.length }}
|
|
282
|
+
</span>
|
|
283
|
+
<!-- Always-visible edit affordance: single click opens the config lightbox.
|
|
284
|
+
(Double-click on the whole card works too.) Hidden in read-only hosts
|
|
285
|
+
that don't provide `configure`. `nodrag` so pressing it never starts a
|
|
286
|
+
node drag; `.stop` so it doesn't bubble to pane/canvas. -->
|
|
287
|
+
<button
|
|
288
|
+
v-if="canConfigure"
|
|
289
|
+
type="button"
|
|
290
|
+
class="nodrag absolute right-1.5 top-1.5 z-10 flex size-5 items-center justify-center rounded text-muted-foreground/50 transition-colors hover:bg-muted hover:text-foreground"
|
|
291
|
+
title="Configure"
|
|
292
|
+
aria-label="Configure"
|
|
293
|
+
@click.stop="configure"
|
|
294
|
+
@mousedown.stop
|
|
295
|
+
@dblclick.stop
|
|
296
|
+
>
|
|
297
|
+
<Lucide.Pencil :size="11" />
|
|
298
|
+
</button>
|
|
299
|
+
<!-- Note badge, mirroring the edit affordance into the opposite corner. Its
|
|
300
|
+
hit area is the whole 20px square, not the 12px glyph. Sits clear of the
|
|
301
|
+
port-label strip when there is one; `nodrag` + `.stop` so grabbing it
|
|
302
|
+
neither drags the node nor opens the config. -->
|
|
303
|
+
<button
|
|
304
|
+
v-if="notes"
|
|
305
|
+
type="button"
|
|
306
|
+
class="nodrag absolute right-1.5 z-10 flex size-5 items-center justify-center rounded text-muted-foreground/45 transition-colors hover:bg-muted hover:text-foreground"
|
|
307
|
+
:class="showPortLabels ? 'bottom-4' : 'bottom-1.5'"
|
|
308
|
+
aria-label="Note"
|
|
309
|
+
:aria-describedby="notePanelId"
|
|
310
|
+
@mouseenter="openNote"
|
|
311
|
+
@mouseleave="releaseNote"
|
|
312
|
+
@focus="openNote"
|
|
313
|
+
@blur="releaseNote"
|
|
314
|
+
@click.stop
|
|
315
|
+
@mousedown.stop
|
|
316
|
+
@dblclick.stop
|
|
317
|
+
>
|
|
318
|
+
<Lucide.MessageSquareText :size="12" :stroke-width="2.25" />
|
|
319
|
+
</button>
|
|
320
|
+
<!-- The badge's description. `sr-only` keeps it out of the picture and out of
|
|
321
|
+
the layout (it is absolutely positioned, so the card's height — which
|
|
322
|
+
feeds the handle positions — is untouched), while keeping it in the
|
|
323
|
+
accessibility tree, where the visible panel cannot be relied on. -->
|
|
324
|
+
<span v-if="notes" :id="notePanelId" class="sr-only">{{ notes }}</span>
|
|
188
325
|
<!-- Port labels sit ABOVE their bottom dots, inside the card's label
|
|
189
326
|
strip, whenever ports are distinguishable (true/false, error, cases). -->
|
|
190
327
|
<template v-if="showPortLabels">
|
|
@@ -212,17 +349,23 @@ function remove() {
|
|
|
212
349
|
<div class="flex items-center gap-2.5 px-3 py-2.5">
|
|
213
350
|
<span
|
|
214
351
|
class="flex size-8 shrink-0 items-center justify-center rounded-md"
|
|
215
|
-
:class="
|
|
352
|
+
:class="category?.color ?? 'bg-muted text-foreground'"
|
|
216
353
|
>
|
|
217
|
-
<
|
|
218
|
-
<component :is="iconFor(manifest?.icon)" v-else :size="15" :stroke-width="2.25" />
|
|
354
|
+
<IntegrationsNodeMark :manifest="manifest" :size="18" />
|
|
219
355
|
</span>
|
|
220
356
|
<div class="min-w-0 flex-1">
|
|
221
357
|
<div class="text-sm font-semibold leading-tight text-foreground truncate">
|
|
222
358
|
{{ title }}
|
|
223
359
|
</div>
|
|
224
|
-
|
|
225
|
-
|
|
360
|
+
<!-- Meta line: the pinned version, small + muted but always visible so
|
|
361
|
+
you can tell which version an instance runs without opening the
|
|
362
|
+
inspector. Left-aligned and on the second line, clear of the
|
|
363
|
+
top-right edit affordance (PO-192). -->
|
|
364
|
+
<div v-if="data.nodeVersion" class="mt-0.5 flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
365
|
+
<span
|
|
366
|
+
class="shrink-0 text-[10px] font-medium leading-none tabular-nums text-muted-foreground/55"
|
|
367
|
+
:title="`Version ${data.nodeVersion}`"
|
|
368
|
+
>v{{ data.nodeVersion }}</span>
|
|
226
369
|
</div>
|
|
227
370
|
</div>
|
|
228
371
|
</div>
|
|
@@ -5,6 +5,10 @@ interface NodeData extends WorkflowNode {
|
|
|
5
5
|
* editor without persisting to the blob. When present and non-empty, these
|
|
6
6
|
* drive the rendered handles instead of the static manifest computation. */
|
|
7
7
|
__resolvedOutputs?: NodeManifestOutput[];
|
|
8
|
+
/** Config keys the backend reported as incomplete at the last save (PO-209),
|
|
9
|
+
* injected by the editor the same non-persisting way. Read-only canvases
|
|
10
|
+
* (run replay, history preview) never pass them, so nothing is marked there. */
|
|
11
|
+
__invalidFields?: string[];
|
|
8
12
|
}
|
|
9
13
|
type __VLS_Props = {
|
|
10
14
|
id: string;
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { computed, markRaw, nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from "vue";
|
|
3
3
|
import {
|
|
4
4
|
MarkerType,
|
|
5
|
-
PanOnScrollMode,
|
|
6
5
|
VueFlow,
|
|
7
6
|
useVueFlow
|
|
8
7
|
} from "@vue-flow/core";
|
|
@@ -150,9 +149,7 @@ preview.onPaneReady(() => {
|
|
|
150
149
|
preview.updateNodeInternals(vfNodes.value.map((n) => n.id));
|
|
151
150
|
void preview.fitView({ padding: 0.2, maxZoom: 1 });
|
|
152
151
|
});
|
|
153
|
-
provide("workflow-node-actions",
|
|
154
|
-
}, remove: () => {
|
|
155
|
-
} });
|
|
152
|
+
provide("workflow-node-actions", null);
|
|
156
153
|
const dynamicOutputs = ref({});
|
|
157
154
|
const dynamicOutputNodes = computed(() => props.workflow.blob.nodes.filter((n) => {
|
|
158
155
|
const m = findManifest(n.nodeSlug, n.nodeVersion);
|
|
@@ -178,20 +175,7 @@ const finalStatus = computed(() => {
|
|
|
178
175
|
const runNodeIds = computed(() => new Set(steps.value.map((s) => s.node_id)));
|
|
179
176
|
const triggerIds = computed(() => new Set(props.workflow.blob.triggers.map((t) => t.id)));
|
|
180
177
|
function edgeColorByKind(kind) {
|
|
181
|
-
|
|
182
|
-
case "success":
|
|
183
|
-
return "#10b981";
|
|
184
|
-
case "error":
|
|
185
|
-
return "#f43f5e";
|
|
186
|
-
case "branch-true":
|
|
187
|
-
return "#10b981";
|
|
188
|
-
case "branch-false":
|
|
189
|
-
return "#f59e0b";
|
|
190
|
-
case "branch":
|
|
191
|
-
return "#8b5cf6";
|
|
192
|
-
default:
|
|
193
|
-
return "#94a3b8";
|
|
194
|
-
}
|
|
178
|
+
return kind === "error" ? "#cbd5e1" : "#94a3b8";
|
|
195
179
|
}
|
|
196
180
|
const GROUP_PAD = 24;
|
|
197
181
|
const GROUP_LABEL_ROOM = 26;
|
|
@@ -304,6 +288,7 @@ const vfEdges = computed(() => {
|
|
|
304
288
|
if (output === "true") return "branch-true";
|
|
305
289
|
if (output === "false") return "branch-false";
|
|
306
290
|
if (output === "error") return "error";
|
|
291
|
+
if (output === "done") return "success";
|
|
307
292
|
const outs = [...manifest.outputs ?? [], ...dynamicOutputs.value[sourceNodeId] ?? []];
|
|
308
293
|
const portDef = outs.find((o) => o.name === output);
|
|
309
294
|
if (portDef) {
|
|
@@ -323,7 +308,7 @@ const vfEdges = computed(() => {
|
|
|
323
308
|
const kind = edgeKind(e.from.nodeId, e.from.output);
|
|
324
309
|
const wn = blob.nodes.find((n) => n.id === e.from.nodeId);
|
|
325
310
|
const manifest = wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
|
|
326
|
-
const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"
|
|
311
|
+
const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"].includes(e.from.output);
|
|
327
312
|
const srcSide = pickSourceSide(e.from.nodeId, e.to.nodeId);
|
|
328
313
|
const sourceRan = triggerIds.value.has(e.from.nodeId) ? steps.value.length > 0 : runNodeIds.value.has(e.from.nodeId);
|
|
329
314
|
const traversed = sourceRan && runNodeIds.value.has(e.to.nodeId);
|
|
@@ -451,7 +436,6 @@ watch(() => props.initialRunId, (id) => {
|
|
|
451
436
|
:nodes-draggable="false"
|
|
452
437
|
:nodes-connectable="false"
|
|
453
438
|
:edges-updatable="false"
|
|
454
|
-
:pan-on-scroll-mode="PanOnScrollMode.Free"
|
|
455
439
|
:delete-key-code="null"
|
|
456
440
|
:min-zoom="0.25"
|
|
457
441
|
:max-zoom="2"
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
DialogHeader,
|
|
10
10
|
DialogTitle,
|
|
11
11
|
Input,
|
|
12
|
+
Label,
|
|
12
13
|
Select,
|
|
13
14
|
SelectContent,
|
|
14
15
|
SelectItem,
|
|
@@ -68,7 +69,9 @@ function formatDateTime(iso) {
|
|
|
68
69
|
:disabled="savingMode"
|
|
69
70
|
@update:model-value="(v) => emit('change-mode', v)"
|
|
70
71
|
>
|
|
71
|
-
|
|
72
|
+
<!-- Named explicitly: the section caption above says "Execution",
|
|
73
|
+
which is not a name the control itself carries. -->
|
|
74
|
+
<SelectTrigger aria-label="Execution mode" class="h-8 w-full bg-white text-sm">
|
|
72
75
|
<SelectValue placeholder="Pick a mode" />
|
|
73
76
|
</SelectTrigger>
|
|
74
77
|
<SelectContent>
|
|
@@ -86,21 +89,24 @@ function formatDateTime(iso) {
|
|
|
86
89
|
|
|
87
90
|
<div class="rounded-lg border border-black/[0.06] bg-white p-4 space-y-3">
|
|
88
91
|
<div class="text-mono text-muted-foreground">Appearance</div>
|
|
92
|
+
<!-- Real labels, not styled captions: each one names its own control
|
|
93
|
+
(`for`/`id`), otherwise a screen reader reads four unlabelled
|
|
94
|
+
fields in a row. -->
|
|
89
95
|
<div class="space-y-1.5">
|
|
90
|
-
<
|
|
91
|
-
<Input v-model="nameDraft" class="bg-white" placeholder="Workflow name" />
|
|
96
|
+
<Label for="wf-set-name" class="text-sm font-normal text-muted-foreground">Name</Label>
|
|
97
|
+
<Input id="wf-set-name" v-model="nameDraft" class="bg-white" placeholder="Workflow name" />
|
|
92
98
|
</div>
|
|
93
99
|
<div class="space-y-1.5">
|
|
94
|
-
<
|
|
95
|
-
<Textarea v-model="descriptionDraft" rows="2" class="bg-white" placeholder="What this workflow does and why it exists." />
|
|
100
|
+
<Label for="wf-set-desc" class="text-sm font-normal text-muted-foreground">Description</Label>
|
|
101
|
+
<Textarea id="wf-set-desc" v-model="descriptionDraft" rows="2" class="bg-white" placeholder="What this workflow does and why it exists." />
|
|
96
102
|
</div>
|
|
97
103
|
<div class="space-y-1.5">
|
|
98
|
-
<
|
|
99
|
-
<WorkflowTagsInput v-model="tagsDraft" :suggestions="tagSuggestions ?? []" />
|
|
104
|
+
<Label for="wf-set-tags" class="text-sm font-normal text-muted-foreground">Tags</Label>
|
|
105
|
+
<WorkflowTagsInput v-model="tagsDraft" input-id="wf-set-tags" :suggestions="tagSuggestions ?? []" />
|
|
100
106
|
</div>
|
|
101
107
|
<div class="space-y-1.5">
|
|
102
|
-
<
|
|
103
|
-
<WorkflowIconPicker v-model="iconDraft" />
|
|
108
|
+
<Label id="wf-set-icon-label" class="text-sm font-normal text-muted-foreground">Icon</Label>
|
|
109
|
+
<WorkflowIconPicker v-model="iconDraft" trigger-labelledby="wf-set-icon-label" />
|
|
104
110
|
</div>
|
|
105
111
|
<div class="flex justify-end">
|
|
106
112
|
<Button
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { ref, useId } from "vue";
|
|
3
3
|
const model = defineModel({ type: Array, ...{ default: () => [] } });
|
|
4
4
|
const props = defineProps({
|
|
5
|
-
suggestions: { type: Array, required: false, default: () => [] }
|
|
5
|
+
suggestions: { type: Array, required: false, default: () => [] },
|
|
6
|
+
inputId: { type: String, required: false }
|
|
6
7
|
});
|
|
7
8
|
const draft = ref("");
|
|
8
9
|
const inputRef = ref(null);
|
|
@@ -28,7 +29,7 @@ function onKeydown(e) {
|
|
|
28
29
|
|
|
29
30
|
<template>
|
|
30
31
|
<div
|
|
31
|
-
class="flex min-h-9 cursor-text flex-wrap items-center gap-1 rounded-md border border-black/[0.08] bg-white px-2 py-1.5"
|
|
32
|
+
class="flex min-h-9 cursor-text flex-wrap items-center gap-1 rounded-md border border-black/[0.08] bg-white px-2 py-1.5 focus-within:ring-2 focus-within:ring-primary/40"
|
|
32
33
|
@click="inputRef?.focus()"
|
|
33
34
|
>
|
|
34
35
|
<span
|
|
@@ -41,12 +42,16 @@ function onKeydown(e) {
|
|
|
41
42
|
type="button"
|
|
42
43
|
class="text-muted-foreground transition hover:text-foreground"
|
|
43
44
|
:title="`Remove ${t}`"
|
|
45
|
+
:aria-label="`Remove ${t}`"
|
|
44
46
|
@click.stop="remove(t)"
|
|
45
47
|
>
|
|
46
48
|
×
|
|
47
49
|
</button>
|
|
48
50
|
</span>
|
|
51
|
+
<!-- No ring of its own: the wrapper above is the field the user sees, and
|
|
52
|
+
it carries the focus ring via focus-within. -->
|
|
49
53
|
<input
|
|
54
|
+
:id="props.inputId"
|
|
50
55
|
ref="inputRef"
|
|
51
56
|
v-model="draft"
|
|
52
57
|
type="text"
|
|
@@ -3,6 +3,8 @@ import { computed, ref } from "vue";
|
|
|
3
3
|
import { Handle, Position, useVueFlow } from "@vue-flow/core";
|
|
4
4
|
import { NodeToolbar } from "@vue-flow/node-toolbar";
|
|
5
5
|
import { ClockCircle as Clock, Play, TrashBinMinimalistic as Trash2, LinkRound as Webhook, Bolt } from "#solar-icons";
|
|
6
|
+
import { resolveDisplayName } from "../../utils/localize";
|
|
7
|
+
import { describeCron } from "../../utils/schedule";
|
|
6
8
|
const props = defineProps({
|
|
7
9
|
id: { type: String, required: true },
|
|
8
10
|
data: { type: Object, required: true },
|
|
@@ -15,7 +17,7 @@ const icon = computed(() => {
|
|
|
15
17
|
});
|
|
16
18
|
const subtitle = computed(() => {
|
|
17
19
|
const c = props.data.config;
|
|
18
|
-
if (props.data.type === "schedule") return c.cron ?? "no
|
|
20
|
+
if (props.data.type === "schedule") return describeCron(c.cron) ?? c.cron ?? "no schedule set";
|
|
19
21
|
if (props.data.type === "webhook") return `${c.method ?? "POST"} ${c.path ?? "/"}`;
|
|
20
22
|
if (props.data.type === "event") return c.event_type ? `on ${c.event_type}` : "no event set";
|
|
21
23
|
return "On demand";
|
|
@@ -23,7 +25,7 @@ const subtitle = computed(() => {
|
|
|
23
25
|
const title = computed(() => {
|
|
24
26
|
const n = props.data.name;
|
|
25
27
|
if (!n) return props.data.type.charAt(0).toUpperCase() + props.data.type.slice(1);
|
|
26
|
-
return
|
|
28
|
+
return resolveDisplayName(n);
|
|
27
29
|
});
|
|
28
30
|
const hovered = ref(false);
|
|
29
31
|
let hoverTimer = null;
|