@revenexx/studio-integrations 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +279 -0
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/CredentialDetailDialog.d.vue.ts +20 -0
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +342 -0
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue.d.ts +20 -0
- package/dist/runtime/components/Integrations/CredentialLogo.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/CredentialLogo.vue +37 -0
- package/dist/runtime/components/Integrations/CredentialLogo.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +364 -0
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/CredentialTypePicker.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +71 -0
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/DeadLettersPanel.d.vue.ts +5 -0
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +151 -0
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue.d.ts +5 -0
- package/dist/runtime/components/Integrations/EditorTip.d.vue.ts +17 -0
- package/dist/runtime/components/Integrations/EditorTip.vue +46 -0
- package/dist/runtime/components/Integrations/EditorTip.vue.d.ts +17 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +64 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +19 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +236 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +19 -0
- package/dist/runtime/components/Integrations/GroupFrameView.d.vue.ts +28 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +222 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue.d.ts +28 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +184 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +102 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +23 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +587 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +23 -0
- package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.vue +80 -0
- package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +22 -3
- package/dist/runtime/components/Integrations/NodeInspector.vue +1074 -294
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +22 -3
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +38 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/NodePalette.d.vue.ts +22 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +411 -90
- package/dist/runtime/components/Integrations/NodePalette.vue.d.ts +22 -1
- package/dist/runtime/components/Integrations/QueryBuilder.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/QueryBuilder.vue +239 -0
- package/dist/runtime/components/Integrations/QueryBuilder.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/{RunDetailDialog.d.vue.ts → RunActionsMenu.d.vue.ts} +3 -4
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +86 -0
- package/dist/runtime/components/Integrations/{RunDetailDialog.vue.d.ts → RunActionsMenu.vue.d.ts} +3 -4
- package/dist/runtime/components/Integrations/RunDetailPanel.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +777 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/RunPayloadInput.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +175 -0
- package/dist/runtime/components/Integrations/RunPayloadInput.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +27 -0
- package/dist/runtime/components/Integrations/RunResponseModal.vue +84 -0
- package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +27 -0
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/RunsTable.vue +114 -0
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.d.vue.ts +11 -0
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +91 -0
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue.d.ts +11 -0
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +28 -0
- package/dist/runtime/components/Integrations/StickyNoteView.vue +208 -0
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +28 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +29 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +201 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +29 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +32 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +138 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +32 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +322 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +63 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +12 -1
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +68 -6
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +12 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +8 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +1424 -144
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +8 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +417 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +10 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +89 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +10 -0
- package/dist/runtime/components/Integrations/WorkflowIconView.d.vue.ts +10 -0
- package/dist/runtime/components/Integrations/WorkflowIconView.vue +35 -0
- package/dist/runtime/components/Integrations/WorkflowIconView.vue.d.ts +10 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +5 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +114 -84
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +5 -1
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +654 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +36 -0
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +136 -0
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +36 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +63 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +35 -15
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +265 -0
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowsTable.d.vue.ts +4 -1
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +122 -39
- package/dist/runtime/components/Integrations/WorkflowsTable.vue.d.ts +4 -1
- package/dist/runtime/components/Integrations/workflowLayout.d.ts +5 -0
- package/dist/runtime/components/Integrations/workflowLayout.js +5 -3
- package/dist/runtime/composables/integrationsApi.d.ts +6 -2
- package/dist/runtime/composables/integrationsApi.js +5 -4
- package/dist/runtime/composables/useCanvasShortcuts.d.ts +37 -0
- package/dist/runtime/composables/useCanvasShortcuts.js +62 -0
- package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +22 -0
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +50 -0
- package/dist/runtime/composables/useIntegrationsNodes.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +6 -3
- package/dist/runtime/composables/useIntegrationsRevisions.d.ts +40 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +51 -0
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +21 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +92 -4
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -0
- package/dist/runtime/composables/useIntegrationsTemplates.js +32 -4
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +1 -0
- package/dist/runtime/composables/useIntegrationsWorkflows.js +57 -1
- package/dist/runtime/composables/useMiniMapLayout.d.ts +34 -0
- package/dist/runtime/composables/useMiniMapLayout.js +119 -0
- package/dist/runtime/composables/useNodeConfigResolve.d.ts +43 -0
- package/dist/runtime/composables/useNodeConfigResolve.js +123 -0
- package/dist/runtime/composables/useNodeDrag.d.ts +22 -0
- package/dist/runtime/composables/useNodeDrag.js +10 -0
- package/dist/runtime/composables/usePaletteLayout.d.ts +41 -0
- package/dist/runtime/composables/usePaletteLayout.js +113 -0
- package/dist/runtime/data/expressionEditor.d.ts +22 -0
- package/dist/runtime/data/expressionEditor.js +40 -0
- package/dist/runtime/data/integrationNodeCatalog.js +8 -8
- package/dist/runtime/data/nodeInputData.d.ts +37 -0
- package/dist/runtime/data/nodeInputData.js +62 -0
- package/dist/runtime/data/nodeRunData.d.ts +56 -0
- package/dist/runtime/data/nodeRunData.js +91 -0
- package/dist/runtime/data/payloadForm.d.ts +44 -0
- package/dist/runtime/data/payloadForm.js +126 -0
- package/dist/runtime/data/workflowIconCatalog.d.ts +34 -0
- package/dist/runtime/data/workflowIconCatalog.js +116 -0
- package/dist/runtime/pages/integrations/audit.vue +25 -35
- package/dist/runtime/pages/integrations/credentials.vue +117 -122
- package/dist/runtime/pages/integrations/index.vue +133 -36
- package/dist/runtime/pages/integrations/runs.vue +125 -75
- package/dist/runtime/pages/integrations/schedules.vue +5 -16
- package/dist/runtime/pages/integrations/secrets.vue +1 -3
- package/dist/runtime/pages/integrations/workflows/[id].vue +599 -194
- package/dist/runtime/pages/integrations/workflows/index.vue +10 -8
- package/dist/runtime/types/integrations.d.ts +190 -4
- package/dist/runtime/utils/credentialState.d.ts +28 -0
- package/dist/runtime/utils/credentialState.js +29 -0
- package/dist/runtime/utils/localize.d.ts +3 -1
- package/dist/runtime/utils/localize.js +1 -2
- package/dist/runtime/utils/statusStyle.d.ts +6 -0
- package/dist/runtime/utils/statusStyle.js +15 -0
- package/package.json +11 -1
- package/dist/runtime/components/Integrations/RunDetailDialog.vue +0 -169
- package/dist/runtime/pages/integrations/workflows/new.d.vue.ts +0 -2
- package/dist/runtime/pages/integrations/workflows/new.vue +0 -153
- package/dist/runtime/pages/integrations/workflows/new.vue.d.ts +0 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, onMounted } from "vue";
|
|
3
|
-
import {
|
|
2
|
+
import { computed, nextTick, onMounted, provide, ref, watch } from "vue";
|
|
3
|
+
import { TrashBinMinimalistic as Trash2, Copy } from "#solar-icons";
|
|
4
|
+
import { ChevronLeft, ChevronRight, Clock, Play, Search, Webhook, X, Zap } from "lucide-vue-next";
|
|
5
|
+
import { toast } from "vue-sonner";
|
|
4
6
|
import {
|
|
5
7
|
Button,
|
|
6
8
|
Input,
|
|
@@ -13,112 +15,803 @@ import {
|
|
|
13
15
|
Switch,
|
|
14
16
|
Textarea
|
|
15
17
|
} from "@revenexx/studio";
|
|
16
|
-
import
|
|
18
|
+
import EventTypeCombobox from "./EventTypeCombobox.vue";
|
|
19
|
+
import { NODE_CATEGORIES, findManifest } from "../../data/integrationNodeCatalog";
|
|
20
|
+
import {
|
|
21
|
+
collectUpstreamNodes,
|
|
22
|
+
nodeOutputExpr,
|
|
23
|
+
portsForNode,
|
|
24
|
+
triggerPayloadExpr,
|
|
25
|
+
variableExpr
|
|
26
|
+
} from "../../data/nodeInputData";
|
|
27
|
+
import {
|
|
28
|
+
dataTypeOf,
|
|
29
|
+
portValuesFromStepOutput,
|
|
30
|
+
previewValue,
|
|
31
|
+
valueAtPath
|
|
32
|
+
} from "../../data/nodeRunData";
|
|
17
33
|
const props = defineProps({
|
|
18
34
|
selection: { type: [Object, null], required: true },
|
|
19
|
-
secretKeys: { type: Array, required: true }
|
|
35
|
+
secretKeys: { type: Array, required: true },
|
|
36
|
+
variables: { type: Object, required: false },
|
|
37
|
+
upstreamOutputs: { type: Array, required: false },
|
|
38
|
+
autofocusName: { type: Boolean, required: false },
|
|
39
|
+
nodes: { type: Array, required: false },
|
|
40
|
+
edges: { type: Array, required: false },
|
|
41
|
+
triggers: { type: Array, required: false },
|
|
42
|
+
dynamicOutputs: { type: Object, required: false },
|
|
43
|
+
workflowId: { type: [Number, String], required: false }
|
|
20
44
|
});
|
|
21
|
-
const emit = defineEmits(["update", "close", "remove"]);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
onMounted(() => {
|
|
25
|
-
void loadCredentials();
|
|
26
|
-
});
|
|
27
|
-
function credsOfType(slug) {
|
|
28
|
-
if (!slug) return credentials.value;
|
|
29
|
-
return credentials.value.filter((c) => c.credential_type_slug === slug);
|
|
45
|
+
const emit = defineEmits(["update", "close", "remove", "navigate"]);
|
|
46
|
+
function clone(v) {
|
|
47
|
+
return JSON.parse(JSON.stringify(v));
|
|
30
48
|
}
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
const draft = ref(clone(props.selection));
|
|
50
|
+
watch(() => props.selection?.id, () => {
|
|
51
|
+
if (props.selection && props.selection.id !== draft.value.id) draft.value = clone(props.selection);
|
|
52
|
+
});
|
|
53
|
+
const isNode = computed(() => "nodeSlug" in draft.value);
|
|
54
|
+
const draftNode = computed(() => isNode.value ? draft.value : null);
|
|
55
|
+
const draftTrigger = computed(() => isNode.value ? null : draft.value);
|
|
56
|
+
const dirty = computed(() => JSON.stringify(draft.value) !== JSON.stringify(props.selection));
|
|
57
|
+
function save() {
|
|
58
|
+
if (dirty.value) emit("update", clone(draft.value));
|
|
59
|
+
emit("close");
|
|
33
60
|
}
|
|
34
|
-
const
|
|
61
|
+
const nameInput = ref(null);
|
|
62
|
+
onMounted(() => {
|
|
63
|
+
if (!props.autofocusName) return;
|
|
64
|
+
void nextTick(() => {
|
|
65
|
+
nameInput.value?.focus();
|
|
66
|
+
nameInput.value?.select();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
35
69
|
const manifest = computed(() => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return findManifest(wn.nodeSlug, wn.nodeVersion);
|
|
70
|
+
const wn = draftNode.value;
|
|
71
|
+
return wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
|
|
39
72
|
});
|
|
40
73
|
function labelText(v) {
|
|
41
74
|
if (!v) return "";
|
|
42
|
-
return typeof v === "string" ? v : Object.values(v)[0] ?? "";
|
|
75
|
+
return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
|
|
43
76
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
const TRIGGER_GLYPHS = { manual: Play, schedule: Clock, webhook: Webhook, event: Zap };
|
|
78
|
+
const TRIGGER_LABELS = { manual: "Manual", schedule: "Schedule", webhook: "Webhook", event: "Event" };
|
|
79
|
+
const triggerGlyph = computed(() => TRIGGER_GLYPHS[draftTrigger.value?.type ?? "manual"]);
|
|
80
|
+
const namePlaceholder = computed(() => {
|
|
81
|
+
if (draftNode.value) return labelText(manifest.value?.name) || draftNode.value.nodeSlug;
|
|
82
|
+
return TRIGGER_LABELS[draftTrigger.value?.type ?? "manual"] + " trigger";
|
|
83
|
+
});
|
|
84
|
+
const subtitleText = computed(() => {
|
|
85
|
+
const wn = draftNode.value;
|
|
86
|
+
if (wn) return wn.nodeSlug + "@" + wn.nodeVersion;
|
|
87
|
+
return TRIGGER_LABELS[draftTrigger.value?.type ?? "manual"] + " trigger";
|
|
88
|
+
});
|
|
89
|
+
const groupLabel = computed(() => {
|
|
90
|
+
if (!isNode.value) return "Trigger";
|
|
91
|
+
const groups = manifest.value?.groups;
|
|
92
|
+
if (groups?.length) return labelText(groups[groups.length - 1]);
|
|
93
|
+
return NODE_CATEGORIES.find((c) => c.key === manifest.value?.category)?.label ?? "Node";
|
|
94
|
+
});
|
|
95
|
+
function writeName(name) {
|
|
96
|
+
draft.value = { ...draft.value, name };
|
|
97
|
+
}
|
|
98
|
+
const tab = ref("parameters");
|
|
99
|
+
const showSettingsTab = computed(() => isNode.value);
|
|
100
|
+
const SETTINGS_FIELD_KEY = /(^|_)(timeout|retry|retries|backoff)|(timeout|retry|retries|backoff)(ms|sec|seconds|attempts)?$/i;
|
|
101
|
+
const parameterFields = computed(() => (manifest.value?.config ?? []).filter((f) => !SETTINGS_FIELD_KEY.test(f.key)));
|
|
102
|
+
const settingsFields = computed(() => (manifest.value?.config ?? []).filter((f) => SETTINGS_FIELD_KEY.test(f.key)));
|
|
103
|
+
function dependentKeys(changedKey) {
|
|
104
|
+
const fields = manifest.value?.config ?? [];
|
|
105
|
+
const out = /* @__PURE__ */ new Set();
|
|
106
|
+
const queue = [changedKey];
|
|
107
|
+
while (queue.length > 0) {
|
|
108
|
+
const dep = queue.shift();
|
|
109
|
+
for (const f of fields) {
|
|
110
|
+
if ((f.dependsOn ?? []).includes(dep) && !out.has(f.key)) {
|
|
111
|
+
out.add(f.key);
|
|
112
|
+
queue.push(f.key);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
out.delete(changedKey);
|
|
117
|
+
return out;
|
|
47
118
|
}
|
|
48
119
|
function writeConfig(key, value) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
120
|
+
const wn = draftNode.value;
|
|
121
|
+
if (!wn) return;
|
|
122
|
+
const next = { ...wn.config, [key]: value };
|
|
123
|
+
const cleared = dependentKeys(key);
|
|
124
|
+
for (const k of cleared) delete next[k];
|
|
125
|
+
const clearedDynamicSchema = [...cleared].some(
|
|
126
|
+
(k) => (manifest.value?.config ?? []).some((f) => f.key === k && f.type === "dynamic-schema")
|
|
127
|
+
);
|
|
128
|
+
if (clearedDynamicSchema) {
|
|
129
|
+
const staticKeys = new Set((manifest.value?.config ?? []).map((f) => f.key));
|
|
130
|
+
for (const k of Object.keys(next)) if (!staticKeys.has(k)) delete next[k];
|
|
131
|
+
}
|
|
132
|
+
wn.config = next;
|
|
52
133
|
}
|
|
53
|
-
function
|
|
54
|
-
if (!
|
|
55
|
-
|
|
134
|
+
function patchNode(patch) {
|
|
135
|
+
if (!draftNode.value) return;
|
|
136
|
+
draft.value = { ...draftNode.value, ...patch };
|
|
137
|
+
}
|
|
138
|
+
function patchTrigger(patch) {
|
|
139
|
+
if (!draftTrigger.value) return;
|
|
140
|
+
draft.value = { ...draftTrigger.value, ...patch };
|
|
141
|
+
}
|
|
142
|
+
const requiredMissingCount = computed(() => {
|
|
143
|
+
if (!draftNode.value || !manifest.value?.config) return 0;
|
|
144
|
+
const cfg = draftNode.value.config ?? {};
|
|
145
|
+
const empty = (v) => v === void 0 || v === null || v === "" || Array.isArray(v) && v.length === 0;
|
|
146
|
+
return manifest.value.config.filter((f) => f.required && empty(cfg[f.key])).length;
|
|
147
|
+
});
|
|
148
|
+
async function copyConfig() {
|
|
149
|
+
if (!draftNode.value) return;
|
|
150
|
+
try {
|
|
151
|
+
await navigator.clipboard.writeText(JSON.stringify(draftNode.value.config ?? {}, null, 2));
|
|
152
|
+
toast.success("Config copied");
|
|
153
|
+
} catch {
|
|
154
|
+
toast.error("Could not copy config");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function pasteConfig() {
|
|
158
|
+
if (!draftNode.value) return;
|
|
159
|
+
let text;
|
|
160
|
+
try {
|
|
161
|
+
text = await navigator.clipboard.readText();
|
|
162
|
+
} catch {
|
|
163
|
+
toast.error("Could not read the clipboard");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
let parsed;
|
|
167
|
+
try {
|
|
168
|
+
parsed = JSON.parse(text);
|
|
169
|
+
} catch {
|
|
170
|
+
toast.error("Clipboard is not valid JSON");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
174
|
+
toast.error("Clipboard is not a JSON config object");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
patchNode({ config: parsed });
|
|
178
|
+
toast.success("Config pasted");
|
|
56
179
|
}
|
|
57
180
|
function readTriggerConfig(key) {
|
|
58
|
-
|
|
59
|
-
return props.selection.config[key];
|
|
181
|
+
return draftTrigger.value?.config[key];
|
|
60
182
|
}
|
|
61
183
|
function writeTriggerConfig(key, value) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
184
|
+
const wt = draftTrigger.value;
|
|
185
|
+
if (!wt) return;
|
|
186
|
+
patchTrigger({ config: { ...wt.config, [key]: value } });
|
|
65
187
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
188
|
+
const { loadForWorkflow, getSteps } = useIntegrationsRuns();
|
|
189
|
+
const runData = ref(null);
|
|
190
|
+
const runInfo = ref(null);
|
|
191
|
+
const runLoading = ref(false);
|
|
192
|
+
const runError = ref(null);
|
|
193
|
+
async function loadRunData() {
|
|
194
|
+
if (!props.workflowId || runLoading.value) return;
|
|
195
|
+
runLoading.value = true;
|
|
196
|
+
runError.value = null;
|
|
197
|
+
try {
|
|
198
|
+
const { runs } = await loadForWorkflow(Number(props.workflowId), "", { perPage: 10 });
|
|
199
|
+
const source = runs.find((r) => r.status === "succeeded") ?? runs[0];
|
|
200
|
+
if (!source) {
|
|
201
|
+
runError.value = "No runs yet. Run the workflow once to sample real values.";
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const res = await getSteps(props.workflowId, source.id, true);
|
|
205
|
+
const map = {};
|
|
206
|
+
for (const step of res.steps ?? []) {
|
|
207
|
+
const values = portValuesFromStepOutput(step.output);
|
|
208
|
+
const existing = map[step.node_id];
|
|
209
|
+
if (!existing || Object.keys(values).length > 0 && Object.keys(existing).length === 0) {
|
|
210
|
+
map[step.node_id] = values;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
runData.value = map;
|
|
214
|
+
runInfo.value = { id: source.id, startedAt: source.startedAt };
|
|
215
|
+
} catch (e) {
|
|
216
|
+
runError.value = e?.message ?? "Could not load run data.";
|
|
217
|
+
} finally {
|
|
218
|
+
runLoading.value = false;
|
|
219
|
+
}
|
|
69
220
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
221
|
+
function clearRunData() {
|
|
222
|
+
runData.value = null;
|
|
223
|
+
runInfo.value = null;
|
|
224
|
+
runError.value = null;
|
|
72
225
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
226
|
+
const pinnedSamples = ref({});
|
|
227
|
+
const pinStorageKey = computed(() => `integrations.sample-outputs.${props.workflowId ?? "wf"}`);
|
|
228
|
+
onMounted(() => {
|
|
229
|
+
try {
|
|
230
|
+
const raw = localStorage.getItem(pinStorageKey.value);
|
|
231
|
+
const parsed = raw ? JSON.parse(raw) : null;
|
|
232
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
233
|
+
pinnedSamples.value = parsed;
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
function persistPins() {
|
|
239
|
+
try {
|
|
240
|
+
localStorage.setItem(pinStorageKey.value, JSON.stringify(pinnedSamples.value));
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const testOutputs = ref({});
|
|
245
|
+
function sampleFor(nodeId) {
|
|
246
|
+
return testOutputs.value[nodeId] ?? pinnedSamples.value[nodeId] ?? runData.value?.[nodeId] ?? void 0;
|
|
247
|
+
}
|
|
248
|
+
function withSampledFields(ports, sample) {
|
|
249
|
+
if (!sample) return ports;
|
|
250
|
+
return ports.map((p) => {
|
|
251
|
+
const value = sample[p.name];
|
|
252
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return p;
|
|
253
|
+
const known = new Set(p.fields.map((f) => f.name));
|
|
254
|
+
const extras = Object.keys(value).filter((k) => !known.has(k)).map((name) => ({ name, dataType: dataTypeOf(value[name]) }));
|
|
255
|
+
return extras.length ? { ...p, fields: [...p.fields, ...extras] } : p;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function chipValue(nodeId, port, field) {
|
|
259
|
+
const sample = sampleFor(nodeId);
|
|
260
|
+
if (!sample || !(port in sample)) return null;
|
|
261
|
+
const v = field ? valueAtPath(sample[port], field.split(".")) : sample[port];
|
|
262
|
+
if (v === void 0) return null;
|
|
263
|
+
return previewValue(v);
|
|
264
|
+
}
|
|
265
|
+
const sampleContext = computed(() => {
|
|
266
|
+
const nodeOutputs = {};
|
|
267
|
+
for (const n of props.nodes ?? []) {
|
|
268
|
+
const s = sampleFor(n.id);
|
|
269
|
+
if (s) nodeOutputs[n.id] = s;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
nodeOutputs,
|
|
273
|
+
variables: Object.fromEntries(Object.entries(props.variables ?? {}).map(([k, v]) => [k, v.default]))
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
provide("integrationsSampleContext", sampleContext);
|
|
277
|
+
provide("integrationsNodeNames", computed(
|
|
278
|
+
() => Object.fromEntries((props.nodes ?? []).map((n) => [n.id, labelText(n.name) || n.nodeSlug]))
|
|
279
|
+
));
|
|
280
|
+
const upstreamSections = computed(() => {
|
|
281
|
+
if (!isNode.value) return [];
|
|
282
|
+
return collectUpstreamNodes(draft.value.id, props.nodes ?? [], props.edges ?? []).map(({ node, direct }) => {
|
|
283
|
+
const m = findManifest(node.nodeSlug, node.nodeVersion);
|
|
284
|
+
return {
|
|
285
|
+
id: node.id,
|
|
286
|
+
name: labelText(node.name) || node.nodeSlug,
|
|
287
|
+
manifest: m,
|
|
288
|
+
direct,
|
|
289
|
+
ports: withSampledFields(portsForNode(node, m, props.dynamicOutputs?.[node.id]), sampleFor(node.id))
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
const variableNames = computed(() => Object.keys(props.variables ?? {}));
|
|
294
|
+
function portDotClass(kind, name) {
|
|
295
|
+
if (name === "true") return "bg-emerald-500";
|
|
296
|
+
if (name === "false") return "bg-amber-400";
|
|
297
|
+
if (kind === "error") return "bg-rose-500";
|
|
298
|
+
if (kind === "branch") return "bg-violet-500";
|
|
299
|
+
return "bg-slate-400";
|
|
300
|
+
}
|
|
301
|
+
const lastField = ref(null);
|
|
302
|
+
const lastFocusKind = ref(null);
|
|
303
|
+
const exprInsertHandler = ref(null);
|
|
304
|
+
provide("integrationsExpressionInsert", exprInsertHandler);
|
|
305
|
+
function onCenterFocusIn(e) {
|
|
306
|
+
const t = e.target;
|
|
307
|
+
if (t instanceof HTMLElement && t.closest(".expr-editor")) {
|
|
308
|
+
lastFocusKind.value = "editor";
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (t instanceof HTMLTextAreaElement) {
|
|
312
|
+
lastField.value = t;
|
|
313
|
+
lastFocusKind.value = "native";
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (t instanceof HTMLInputElement && !["number", "checkbox", "radio", "range", "file", "color"].includes(t.type)) {
|
|
317
|
+
lastField.value = t;
|
|
318
|
+
lastFocusKind.value = "native";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function insertExpression(expression) {
|
|
322
|
+
if (lastFocusKind.value === "editor" && exprInsertHandler.value) {
|
|
323
|
+
exprInsertHandler.value(expression);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const el = lastField.value;
|
|
327
|
+
if (el && el.isConnected) {
|
|
328
|
+
const start = el.selectionStart ?? el.value.length;
|
|
329
|
+
const end = el.selectionEnd ?? start;
|
|
330
|
+
el.setRangeText(expression, start, end, "end");
|
|
331
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
332
|
+
el.focus();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
void navigator.clipboard?.writeText(expression);
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
toast("Expression copied. Click into a field first to insert it directly.");
|
|
340
|
+
}
|
|
341
|
+
const chipClass = "max-w-full cursor-grab truncate rounded-md border border-black/[0.06] bg-slate-50 px-1.5 py-0.5 font-mono text-[11px] text-slate-700 transition hover:border-black/[0.12] hover:bg-slate-100 active:cursor-grabbing";
|
|
342
|
+
function onChipDragStart(e, expression) {
|
|
343
|
+
e.dataTransfer?.setData("text/plain", expression);
|
|
344
|
+
if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy";
|
|
345
|
+
dragActive.value = true;
|
|
346
|
+
}
|
|
347
|
+
const dragActive = ref(false);
|
|
348
|
+
const dropHoverEl = ref(null);
|
|
349
|
+
function clearDropHover() {
|
|
350
|
+
dropHoverEl.value?.classList.remove("chip-drop-hover");
|
|
351
|
+
dropHoverEl.value = null;
|
|
352
|
+
}
|
|
353
|
+
function onChipDragEnd() {
|
|
354
|
+
dragActive.value = false;
|
|
355
|
+
clearDropHover();
|
|
356
|
+
}
|
|
357
|
+
function expressionTarget(t) {
|
|
358
|
+
if (t instanceof HTMLTextAreaElement) return t;
|
|
359
|
+
if (t instanceof HTMLInputElement && !["number", "checkbox", "radio", "range", "file", "color"].includes(t.type)) return t;
|
|
360
|
+
if (t instanceof HTMLElement) return t.closest(".expr-editor");
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
function onCenterDragEnter(e) {
|
|
364
|
+
if (!dragActive.value) return;
|
|
365
|
+
const el = expressionTarget(e.target);
|
|
366
|
+
if (!el || el === dropHoverEl.value) return;
|
|
367
|
+
clearDropHover();
|
|
368
|
+
dropHoverEl.value = el;
|
|
369
|
+
el.classList.add("chip-drop-hover");
|
|
370
|
+
}
|
|
371
|
+
function onCenterDragLeave(e) {
|
|
372
|
+
if (e.target === dropHoverEl.value) clearDropHover();
|
|
373
|
+
}
|
|
374
|
+
function onCenterDrop() {
|
|
375
|
+
clearDropHover();
|
|
376
|
+
dragActive.value = false;
|
|
377
|
+
}
|
|
378
|
+
const closedNodes = ref(/* @__PURE__ */ new Set());
|
|
379
|
+
watch(upstreamSections, (sections) => {
|
|
380
|
+
const closed = /* @__PURE__ */ new Set();
|
|
381
|
+
for (const s of sections) {
|
|
382
|
+
if (!s.direct) closed.add(s.id);
|
|
383
|
+
}
|
|
384
|
+
closedNodes.value = closed;
|
|
385
|
+
}, { immediate: true });
|
|
386
|
+
function toggleNodeSection(id) {
|
|
387
|
+
const next = new Set(closedNodes.value);
|
|
388
|
+
if (next.has(id)) next.delete(id);
|
|
389
|
+
else next.add(id);
|
|
390
|
+
closedNodes.value = next;
|
|
77
391
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
392
|
+
const inputSearch = ref("");
|
|
393
|
+
const searchQuery = computed(() => inputSearch.value.trim().toLowerCase());
|
|
394
|
+
function sectionClosed(id) {
|
|
395
|
+
return !searchQuery.value && closedNodes.value.has(id);
|
|
396
|
+
}
|
|
397
|
+
const visibleSections = computed(() => {
|
|
398
|
+
const q = searchQuery.value;
|
|
399
|
+
if (!q) return upstreamSections.value;
|
|
400
|
+
return upstreamSections.value.map((s) => {
|
|
401
|
+
if (s.name.toLowerCase().includes(q)) return s;
|
|
402
|
+
const ports = s.ports.map((p) => p.name.toLowerCase().includes(q) ? p : { ...p, fields: p.fields.filter((f) => f.name.toLowerCase().includes(q)) }).filter((p) => p.name.toLowerCase().includes(q) || p.fields.length > 0);
|
|
403
|
+
return { ...s, ports };
|
|
404
|
+
}).filter((s) => s.ports.length > 0);
|
|
405
|
+
});
|
|
406
|
+
const showTriggerChip = computed(() => !searchQuery.value || "trigger.payload".includes(searchQuery.value));
|
|
407
|
+
const visibleVariableNames = computed(() => {
|
|
408
|
+
const q = searchQuery.value;
|
|
409
|
+
return q ? variableNames.value.filter((n) => n.toLowerCase().includes(q)) : variableNames.value;
|
|
410
|
+
});
|
|
411
|
+
const searchEmpty = computed(
|
|
412
|
+
() => !!searchQuery.value && !visibleSections.value.length && !showTriggerChip.value && !visibleVariableNames.value.length
|
|
413
|
+
);
|
|
414
|
+
function highlightSegments(label) {
|
|
415
|
+
const q = searchQuery.value;
|
|
416
|
+
if (!q) return [{ text: label, hit: false }];
|
|
417
|
+
const lower = label.toLowerCase();
|
|
418
|
+
const out = [];
|
|
419
|
+
let i = 0;
|
|
420
|
+
while (i < label.length) {
|
|
421
|
+
const idx = lower.indexOf(q, i);
|
|
422
|
+
if (idx === -1) {
|
|
423
|
+
out.push({ text: label.slice(i), hit: false });
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
if (idx > i) out.push({ text: label.slice(i, idx), hit: false });
|
|
427
|
+
out.push({ text: label.slice(idx, idx + q.length), hit: true });
|
|
428
|
+
i = idx + q.length;
|
|
429
|
+
}
|
|
430
|
+
return out;
|
|
431
|
+
}
|
|
432
|
+
const TYPE_GLYPHS = {
|
|
433
|
+
string: { char: "A", cls: "text-sky-600" },
|
|
434
|
+
number: { char: "#", cls: "text-amber-600" },
|
|
435
|
+
integer: { char: "#", cls: "text-amber-600" },
|
|
436
|
+
boolean: { char: "\u2713", cls: "text-emerald-600" },
|
|
437
|
+
object: { char: "{}", cls: "text-violet-600" },
|
|
438
|
+
array: { char: "[]", cls: "text-violet-600" }
|
|
439
|
+
};
|
|
440
|
+
function typeGlyph(dataType) {
|
|
441
|
+
return dataType ? TYPE_GLYPHS[dataType] ?? null : null;
|
|
442
|
+
}
|
|
443
|
+
const sequence = computed(() => [
|
|
444
|
+
...(props.triggers ?? []).map((t) => t.id),
|
|
445
|
+
...(props.nodes ?? []).map((n) => n.id)
|
|
446
|
+
]);
|
|
447
|
+
const seqIndex = computed(() => sequence.value.indexOf(draft.value.id));
|
|
448
|
+
const hasPrev = computed(() => seqIndex.value > 0);
|
|
449
|
+
const hasNext = computed(() => seqIndex.value >= 0 && seqIndex.value < sequence.value.length - 1);
|
|
450
|
+
function navigate(offset) {
|
|
451
|
+
const target = sequence.value[seqIndex.value + offset];
|
|
452
|
+
if (!target) return;
|
|
453
|
+
if (dirty.value) emit("update", clone(draft.value));
|
|
454
|
+
emit("navigate", target);
|
|
455
|
+
}
|
|
456
|
+
const outputView = ref("schema");
|
|
457
|
+
const selfSample = computed(() => draftNode.value ? sampleFor(draftNode.value.id) : void 0);
|
|
458
|
+
const selfPorts = computed(() => draftNode.value ? withSampledFields(portsForNode(draftNode.value, manifest.value, props.dynamicOutputs?.[draftNode.value.id]), selfSample.value) : []);
|
|
459
|
+
const selfSampleJson = computed(() => selfSample.value ? JSON.stringify(selfSample.value, null, 2) : "");
|
|
460
|
+
const selfPinned = computed(() => !!draftNode.value && !!pinnedSamples.value[draftNode.value.id]);
|
|
461
|
+
watch(selfSample, (s) => {
|
|
462
|
+
if (!s && outputView.value === "json") outputView.value = "schema";
|
|
463
|
+
});
|
|
464
|
+
const pinEditorOpen = ref(false);
|
|
465
|
+
const pinDraft = ref("");
|
|
466
|
+
const pinError = ref(null);
|
|
467
|
+
function openPinEditor() {
|
|
468
|
+
pinDraft.value = JSON.stringify(selfSample.value ?? { default: {} }, null, 2);
|
|
469
|
+
pinError.value = null;
|
|
470
|
+
pinEditorOpen.value = true;
|
|
471
|
+
}
|
|
472
|
+
function savePin() {
|
|
473
|
+
if (!draftNode.value) return;
|
|
474
|
+
let parsed;
|
|
475
|
+
try {
|
|
476
|
+
parsed = JSON.parse(pinDraft.value);
|
|
477
|
+
} catch {
|
|
478
|
+
pinError.value = "Not valid JSON.";
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
482
|
+
pinError.value = 'Must be an object mapping ports to payloads, e.g. { "default": { \u2026 } }.';
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
pinnedSamples.value = { ...pinnedSamples.value, [draftNode.value.id]: parsed };
|
|
486
|
+
persistPins();
|
|
487
|
+
pinEditorOpen.value = false;
|
|
488
|
+
toast.success("Sample output pinned");
|
|
489
|
+
}
|
|
490
|
+
function removePin() {
|
|
491
|
+
if (!draftNode.value) return;
|
|
492
|
+
const next = { ...pinnedSamples.value };
|
|
493
|
+
delete next[draftNode.value.id];
|
|
494
|
+
pinnedSamples.value = next;
|
|
495
|
+
persistPins();
|
|
496
|
+
pinEditorOpen.value = false;
|
|
497
|
+
}
|
|
498
|
+
const segBase = "rounded px-1.5 py-0.5 text-[11px] font-medium transition";
|
|
499
|
+
const segActive = segBase + " bg-slate-100 text-foreground";
|
|
500
|
+
const segIdle = segBase + " text-muted-foreground hover:text-foreground disabled:opacity-40 disabled:pointer-events-none";
|
|
501
|
+
const api = useIntegrationsApi();
|
|
502
|
+
const testRunning = ref(false);
|
|
503
|
+
const testError = ref(null);
|
|
504
|
+
const testLogs = ref([]);
|
|
505
|
+
const testInputOpen = ref(false);
|
|
506
|
+
const testInputDraft = ref("");
|
|
507
|
+
const primaryUpstreamSample = computed(() => {
|
|
508
|
+
const first = upstreamSections.value.find((s) => s.direct);
|
|
509
|
+
if (!first) return {};
|
|
510
|
+
const def = sampleFor(first.id)?.default;
|
|
511
|
+
return def === void 0 ? {} : def;
|
|
512
|
+
});
|
|
513
|
+
function defaultTestInput() {
|
|
514
|
+
return JSON.stringify({ in: primaryUpstreamSample.value }, null, 2);
|
|
515
|
+
}
|
|
516
|
+
function toggleTestInput() {
|
|
517
|
+
if (!testInputOpen.value && !testInputDraft.value) testInputDraft.value = defaultTestInput();
|
|
518
|
+
testInputOpen.value = !testInputOpen.value;
|
|
519
|
+
}
|
|
520
|
+
async function runTestStep() {
|
|
521
|
+
const wn = draftNode.value;
|
|
522
|
+
if (!wn || testRunning.value) return;
|
|
523
|
+
let inputs;
|
|
524
|
+
try {
|
|
525
|
+
const parsed = JSON.parse(testInputDraft.value.trim() || defaultTestInput());
|
|
526
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
527
|
+
testError.value = "Test input must be a JSON object.";
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
inputs = parsed;
|
|
531
|
+
} catch {
|
|
532
|
+
testError.value = "Test input is not valid JSON.";
|
|
533
|
+
testInputOpen.value = true;
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
testRunning.value = true;
|
|
537
|
+
testError.value = null;
|
|
538
|
+
testLogs.value = [];
|
|
539
|
+
try {
|
|
540
|
+
const res = await api.post(
|
|
541
|
+
`/nodes/${encodeURIComponent(wn.nodeSlug)}/${encodeURIComponent(wn.nodeVersion)}/execute:test`,
|
|
542
|
+
{ config: wn.config ?? {}, inputs }
|
|
543
|
+
);
|
|
544
|
+
testLogs.value = res.logs ?? [];
|
|
545
|
+
testOutputs.value = {
|
|
546
|
+
...testOutputs.value,
|
|
547
|
+
[wn.id]: res.outputs && typeof res.outputs === "object" ? res.outputs : {}
|
|
548
|
+
};
|
|
549
|
+
outputView.value = "json";
|
|
550
|
+
toast.success("Test run finished");
|
|
551
|
+
} catch (e) {
|
|
552
|
+
const body = e.body;
|
|
553
|
+
testError.value = body?.error?.message ?? body?.message ?? e?.message ?? "Test run failed.";
|
|
554
|
+
testLogs.value = body?.logs ?? [];
|
|
555
|
+
} finally {
|
|
556
|
+
testRunning.value = false;
|
|
557
|
+
}
|
|
82
558
|
}
|
|
83
559
|
</script>
|
|
84
560
|
|
|
85
561
|
<template>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
562
|
+
<!-- Fills the editor's near-fullscreen config lightbox; the Dialog owns
|
|
563
|
+
width/height, border and the close control. -->
|
|
564
|
+
<div class="flex min-h-0 flex-1 flex-col bg-white">
|
|
565
|
+
<!-- Header -->
|
|
566
|
+
<div class="flex shrink-0 items-center gap-3 border-b border-black/[0.06] px-4 py-3 pr-14">
|
|
567
|
+
<div class="flex size-9 shrink-0 items-center justify-center rounded-md border border-black/[0.06] bg-slate-50 text-slate-600">
|
|
568
|
+
<IntegrationsNodeLogo v-if="isNode" :manifest="manifest" :size="20" />
|
|
569
|
+
<component :is="triggerGlyph" v-else :size="18" />
|
|
570
|
+
</div>
|
|
571
|
+
<div class="min-w-0 flex-1">
|
|
572
|
+
<input
|
|
573
|
+
ref="nameInput"
|
|
574
|
+
:value="labelText(draft.name)"
|
|
575
|
+
:placeholder="namePlaceholder"
|
|
576
|
+
class="w-full truncate bg-transparent text-sm font-semibold text-foreground outline-none placeholder:text-muted-foreground/60"
|
|
577
|
+
@input="writeName($event.target.value)"
|
|
578
|
+
>
|
|
579
|
+
<div class="truncate font-mono text-[11px] text-muted-foreground">
|
|
580
|
+
{{ subtitleText }}
|
|
96
581
|
</div>
|
|
582
|
+
</div>
|
|
583
|
+
<span class="shrink-0 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider text-slate-600">
|
|
584
|
+
{{ groupLabel }}
|
|
585
|
+
</span>
|
|
586
|
+
<div v-if="sequence.length > 1" class="flex shrink-0 items-center gap-0.5 border-l border-black/[0.06] pl-2">
|
|
587
|
+
<button
|
|
588
|
+
type="button"
|
|
589
|
+
:disabled="!hasPrev"
|
|
590
|
+
title="Previous step (saves changes)"
|
|
591
|
+
class="flex size-7 items-center justify-center rounded-md text-muted-foreground transition hover:bg-black/[0.04] hover:text-foreground disabled:pointer-events-none disabled:opacity-30"
|
|
592
|
+
@click="navigate(-1)"
|
|
593
|
+
>
|
|
594
|
+
<ChevronLeft :size="14" />
|
|
595
|
+
</button>
|
|
97
596
|
<button
|
|
98
597
|
type="button"
|
|
99
|
-
|
|
100
|
-
|
|
598
|
+
:disabled="!hasNext"
|
|
599
|
+
title="Next step (saves changes)"
|
|
600
|
+
class="flex size-7 items-center justify-center rounded-md text-muted-foreground transition hover:bg-black/[0.04] hover:text-foreground disabled:pointer-events-none disabled:opacity-30"
|
|
601
|
+
@click="navigate(1)"
|
|
101
602
|
>
|
|
102
|
-
<
|
|
603
|
+
<ChevronRight :size="14" />
|
|
103
604
|
</button>
|
|
104
605
|
</div>
|
|
606
|
+
</div>
|
|
105
607
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
608
|
+
<!-- Body: Input pane + tabbed center -->
|
|
609
|
+
<div class="flex min-h-0 flex-1">
|
|
610
|
+
<!-- Input pane -->
|
|
611
|
+
<aside class="flex w-[300px] shrink-0 flex-col border-r border-black/[0.06] bg-muted/10" @dragend="onChipDragEnd">
|
|
612
|
+
<div class="shrink-0 border-b border-black/[0.06] px-3 py-2.5">
|
|
613
|
+
<div class="text-sm font-semibold text-foreground">
|
|
614
|
+
Input
|
|
615
|
+
</div>
|
|
616
|
+
<p class="mt-0.5 text-xs leading-snug text-muted-foreground">
|
|
617
|
+
Available data. Click a value to insert it into the focused field, or drag it onto one.
|
|
618
|
+
</p>
|
|
619
|
+
<div class="relative mt-2">
|
|
620
|
+
<Search :size="12" class="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
621
|
+
<input
|
|
622
|
+
v-model="inputSearch"
|
|
623
|
+
type="text"
|
|
624
|
+
placeholder="Search data"
|
|
625
|
+
class="h-7 w-full rounded-md border border-black/[0.08] bg-white pl-6.5 pr-2 text-xs outline-none transition placeholder:text-muted-foreground/60 focus:border-black/[0.16]"
|
|
626
|
+
>
|
|
627
|
+
</div>
|
|
628
|
+
<div v-if="workflowId" class="mt-2 flex items-center gap-1.5">
|
|
629
|
+
<button
|
|
630
|
+
v-if="!runInfo"
|
|
631
|
+
type="button"
|
|
632
|
+
:disabled="runLoading"
|
|
633
|
+
class="rounded-md border border-black/[0.08] bg-white px-2 py-1 text-[11px] font-medium text-muted-foreground transition hover:bg-muted/30 hover:text-foreground disabled:opacity-50"
|
|
634
|
+
@click="loadRunData"
|
|
635
|
+
>
|
|
636
|
+
{{ runLoading ? "Loading run data\u2026" : "Use last run data" }}
|
|
637
|
+
</button>
|
|
638
|
+
<template v-else>
|
|
639
|
+
<span class="min-w-0 flex-1 truncate text-[11px] text-emerald-700">
|
|
640
|
+
Values from run #{{ runInfo.id }}
|
|
641
|
+
</span>
|
|
642
|
+
<button
|
|
643
|
+
type="button"
|
|
644
|
+
title="Clear run data"
|
|
645
|
+
class="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-foreground"
|
|
646
|
+
@click="clearRunData"
|
|
647
|
+
>
|
|
648
|
+
<X :size="11" />
|
|
649
|
+
</button>
|
|
650
|
+
</template>
|
|
651
|
+
</div>
|
|
652
|
+
<p v-if="runError" class="mt-1 text-[11px] leading-snug text-amber-600">
|
|
653
|
+
{{ runError }}
|
|
654
|
+
</p>
|
|
655
|
+
</div>
|
|
656
|
+
<div class="min-h-0 flex-1 space-y-4 overflow-y-auto px-3 py-3">
|
|
657
|
+
<template v-if="isNode">
|
|
658
|
+
<!-- Upstream nodes -->
|
|
659
|
+
<div v-if="!searchQuery || visibleSections.length" class="space-y-3">
|
|
660
|
+
<div class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
661
|
+
Nodes
|
|
662
|
+
</div>
|
|
663
|
+
<p v-if="!upstreamSections.length" class="text-xs leading-snug text-muted-foreground">
|
|
664
|
+
No upstream nodes yet. Connect a node into this one to reference its output here.
|
|
665
|
+
</p>
|
|
666
|
+
<div v-for="s in visibleSections" :key="s.id" class="rounded-md border border-black/[0.06]">
|
|
667
|
+
<button
|
|
668
|
+
type="button"
|
|
669
|
+
class="flex w-full items-center gap-1.5 px-2 py-1.5 text-left"
|
|
670
|
+
@click="toggleNodeSection(s.id)"
|
|
671
|
+
>
|
|
672
|
+
<ChevronRight :size="11" class="shrink-0 text-muted-foreground transition-transform" :class="sectionClosed(s.id) ? '' : 'rotate-90'" />
|
|
673
|
+
<IntegrationsNodeLogo :manifest="s.manifest" :size="14" />
|
|
674
|
+
<span class="min-w-0 truncate text-xs font-medium text-foreground">{{ s.name }}</span>
|
|
675
|
+
<span
|
|
676
|
+
v-if="!s.direct"
|
|
677
|
+
class="shrink-0 rounded bg-slate-100 px-1 py-px text-[9px] font-medium uppercase tracking-wider text-slate-500"
|
|
678
|
+
title="Not directly connected; reachable further upstream"
|
|
679
|
+
>
|
|
680
|
+
upstream
|
|
681
|
+
</span>
|
|
682
|
+
</button>
|
|
683
|
+
<div v-show="!sectionClosed(s.id)" class="space-y-2 border-t border-black/[0.06] px-2 py-2">
|
|
684
|
+
<div v-for="p in s.ports" :key="p.name" class="space-y-1 pl-0.5">
|
|
685
|
+
<div v-if="s.ports.length > 1 || p.kind !== 'default'" class="flex items-center gap-1.5">
|
|
686
|
+
<span class="size-1.5 rounded-full" :class="portDotClass(p.kind, p.name)" />
|
|
687
|
+
<span class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">{{ p.name }}</span>
|
|
688
|
+
</div>
|
|
689
|
+
<div class="flex flex-wrap gap-1">
|
|
690
|
+
<button
|
|
691
|
+
type="button"
|
|
692
|
+
:class="chipClass"
|
|
693
|
+
:title="nodeOutputExpr(s.id, p.name)"
|
|
694
|
+
draggable="true"
|
|
695
|
+
@dragstart="onChipDragStart($event, nodeOutputExpr(s.id, p.name))"
|
|
696
|
+
@click="insertExpression(nodeOutputExpr(s.id, p.name))"
|
|
697
|
+
><span v-for="(seg, i) in highlightSegments(p.name)" :key="i" :class="seg.hit ? 'rounded-[2px] bg-amber-200/80' : ''">{{ seg.text }}</span></button>
|
|
698
|
+
<button
|
|
699
|
+
v-for="f in p.fields"
|
|
700
|
+
:key="f.name"
|
|
701
|
+
type="button"
|
|
702
|
+
:class="chipClass"
|
|
703
|
+
:title="nodeOutputExpr(s.id, p.name, f.name) + (f.dataType ? ' \xB7 ' + f.dataType : '')"
|
|
704
|
+
draggable="true"
|
|
705
|
+
@dragstart="onChipDragStart($event, nodeOutputExpr(s.id, p.name, f.name))"
|
|
706
|
+
@click="insertExpression(nodeOutputExpr(s.id, p.name, f.name))"
|
|
707
|
+
><span v-if="typeGlyph(f.dataType)" class="mr-1 font-semibold" :class="typeGlyph(f.dataType).cls">{{ typeGlyph(f.dataType).char }}</span><span v-for="(seg, i) in highlightSegments(f.name)" :key="i" :class="seg.hit ? 'rounded-[2px] bg-amber-200/80' : ''">{{ seg.text }}</span><span v-if="chipValue(s.id, p.name, f.name) !== null" class="text-slate-400">: {{ chipValue(s.id, p.name, f.name) }}</span></button>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
|
|
714
|
+
<!-- Trigger payload -->
|
|
715
|
+
<div v-if="showTriggerChip" class="space-y-1.5">
|
|
716
|
+
<div class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
717
|
+
Trigger
|
|
718
|
+
</div>
|
|
719
|
+
<div class="flex flex-wrap gap-1">
|
|
720
|
+
<button
|
|
721
|
+
type="button"
|
|
722
|
+
:class="chipClass"
|
|
723
|
+
:title="triggerPayloadExpr()"
|
|
724
|
+
draggable="true"
|
|
725
|
+
@dragstart="onChipDragStart($event, triggerPayloadExpr())"
|
|
726
|
+
@click="insertExpression(triggerPayloadExpr())"
|
|
727
|
+
>
|
|
728
|
+
trigger.payload
|
|
729
|
+
</button>
|
|
730
|
+
</div>
|
|
731
|
+
<p class="text-xs leading-snug text-muted-foreground">
|
|
732
|
+
The payload the run started with. Append a path for a single value, e.g. trigger.payload.orderId.
|
|
733
|
+
</p>
|
|
734
|
+
</div>
|
|
735
|
+
</template>
|
|
736
|
+
|
|
737
|
+
<p v-else class="text-xs leading-snug text-muted-foreground">
|
|
738
|
+
Triggers start the workflow, so there is no upstream data. Workflow variables are available everywhere.
|
|
739
|
+
</p>
|
|
740
|
+
|
|
741
|
+
<!-- Workflow variables -->
|
|
742
|
+
<div v-if="!searchQuery || visibleVariableNames.length" class="space-y-1.5">
|
|
743
|
+
<div class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
744
|
+
Variables
|
|
745
|
+
</div>
|
|
746
|
+
<div v-if="visibleVariableNames.length" class="flex flex-wrap gap-1">
|
|
747
|
+
<button
|
|
748
|
+
v-for="name in visibleVariableNames"
|
|
749
|
+
:key="name"
|
|
750
|
+
type="button"
|
|
751
|
+
:class="chipClass"
|
|
752
|
+
:title="variableExpr(name)"
|
|
753
|
+
draggable="true"
|
|
754
|
+
@dragstart="onChipDragStart($event, variableExpr(name))"
|
|
755
|
+
@click="insertExpression(variableExpr(name))"
|
|
756
|
+
><span v-for="(seg, i) in highlightSegments(name)" :key="i" :class="seg.hit ? 'rounded-[2px] bg-amber-200/80' : ''">{{ seg.text }}</span></button>
|
|
757
|
+
</div>
|
|
758
|
+
<p v-else class="text-xs leading-snug text-muted-foreground">
|
|
759
|
+
No workflow variables defined.
|
|
760
|
+
</p>
|
|
761
|
+
</div>
|
|
762
|
+
|
|
763
|
+
<p v-if="searchEmpty" class="text-xs leading-snug text-muted-foreground">
|
|
764
|
+
Nothing matches "{{ inputSearch.trim() }}". Clear the search to see all available data.
|
|
765
|
+
</p>
|
|
766
|
+
</div>
|
|
767
|
+
</aside>
|
|
768
|
+
|
|
769
|
+
<!-- Center: tabs + content -->
|
|
770
|
+
<section class="flex min-w-0 flex-1 flex-col">
|
|
771
|
+
<div class="flex shrink-0 items-center gap-1 border-b border-black/[0.06] px-4">
|
|
772
|
+
<button
|
|
773
|
+
type="button"
|
|
774
|
+
class="relative -mb-px border-b-2 px-2.5 py-2 text-sm font-medium transition"
|
|
775
|
+
:class="tab === 'parameters' ? 'border-foreground text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'"
|
|
776
|
+
@click="tab = 'parameters'"
|
|
777
|
+
>
|
|
778
|
+
Parameters
|
|
779
|
+
</button>
|
|
780
|
+
<button
|
|
781
|
+
v-if="showSettingsTab"
|
|
782
|
+
type="button"
|
|
783
|
+
class="relative -mb-px border-b-2 px-2.5 py-2 text-sm font-medium transition"
|
|
784
|
+
:class="tab === 'settings' ? 'border-foreground text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'"
|
|
785
|
+
@click="tab = 'settings'"
|
|
786
|
+
>
|
|
787
|
+
Settings
|
|
788
|
+
</button>
|
|
116
789
|
</div>
|
|
117
790
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
791
|
+
<div
|
|
792
|
+
class="min-h-0 flex-1 overflow-y-auto p-4"
|
|
793
|
+
:class="dragActive ? 'chip-drag-live' : ''"
|
|
794
|
+
@focusin="onCenterFocusIn"
|
|
795
|
+
@dragenter="onCenterDragEnter"
|
|
796
|
+
@dragleave="onCenterDragLeave"
|
|
797
|
+
@drop="onCenterDrop"
|
|
798
|
+
>
|
|
799
|
+
<!-- Parameters: trigger config -->
|
|
800
|
+
<div v-if="!isNode && draftTrigger" class="mx-auto w-full max-w-3xl space-y-3">
|
|
801
|
+
<!-- Active toggle. Persists with the workflow save like every other trigger field. -->
|
|
802
|
+
<div class="flex items-center justify-between rounded-lg border border-black/[0.06] bg-muted/20 px-3 py-2">
|
|
803
|
+
<div>
|
|
804
|
+
<div class="text-sm font-medium">Active</div>
|
|
805
|
+
<p class="text-xs text-muted-foreground">
|
|
806
|
+
A run needs at least one active trigger.
|
|
807
|
+
</p>
|
|
808
|
+
</div>
|
|
809
|
+
<Switch
|
|
810
|
+
:model-value="draftTrigger.active !== false"
|
|
811
|
+
@update:model-value="(v) => patchTrigger({ active: v })"
|
|
812
|
+
/>
|
|
813
|
+
</div>
|
|
814
|
+
<template v-if="draftTrigger.type === 'schedule'">
|
|
122
815
|
<div class="space-y-1.5">
|
|
123
816
|
<Label class="text-mono text-muted-foreground">Cron</Label>
|
|
124
817
|
<Input
|
|
@@ -138,7 +831,7 @@ function setArrayItemField(key, index, fieldKey, value) {
|
|
|
138
831
|
/>
|
|
139
832
|
</div>
|
|
140
833
|
</template>
|
|
141
|
-
<template v-else-if="
|
|
834
|
+
<template v-else-if="draftTrigger.type === 'webhook'">
|
|
142
835
|
<div class="space-y-1.5">
|
|
143
836
|
<Label class="text-mono text-muted-foreground">Path</Label>
|
|
144
837
|
<Input
|
|
@@ -181,13 +874,11 @@ function setArrayItemField(key, index, fieldKey, value) {
|
|
|
181
874
|
</Select>
|
|
182
875
|
</div>
|
|
183
876
|
</template>
|
|
184
|
-
<template v-else-if="
|
|
877
|
+
<template v-else-if="draftTrigger.type === 'event'">
|
|
185
878
|
<div class="space-y-1.5">
|
|
186
879
|
<Label class="text-mono text-muted-foreground">Event</Label>
|
|
187
|
-
<
|
|
880
|
+
<EventTypeCombobox
|
|
188
881
|
:model-value="readTriggerConfig('event_type') ?? ''"
|
|
189
|
-
placeholder="customer.created"
|
|
190
|
-
class="h-8 text-sm bg-white font-mono"
|
|
191
882
|
@update:model-value="(v) => writeTriggerConfig('event_type', v)"
|
|
192
883
|
/>
|
|
193
884
|
<p class="text-sm text-muted-foreground">
|
|
@@ -201,250 +892,339 @@ function setArrayItemField(key, index, fieldKey, value) {
|
|
|
201
892
|
</p>
|
|
202
893
|
</template>
|
|
203
894
|
</div>
|
|
204
|
-
</template>
|
|
205
895
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
<div
|
|
210
|
-
v-for="field in manifest.config"
|
|
211
|
-
:key="field.key"
|
|
212
|
-
class="space-y-1.5"
|
|
213
|
-
>
|
|
896
|
+
<!-- Parameters: node config, generated from the manifest -->
|
|
897
|
+
<div v-else-if="tab === 'parameters' && draftNode" class="mx-auto w-full max-w-3xl">
|
|
898
|
+
<div v-if="manifest?.config?.length" class="space-y-3">
|
|
214
899
|
<div class="flex items-center justify-between">
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
900
|
+
<span class="text-mono text-muted-foreground">Configuration</span>
|
|
901
|
+
<div class="flex items-center gap-2">
|
|
902
|
+
<span v-if="requiredMissingCount" class="text-sm text-amber-600">
|
|
903
|
+
{{ requiredMissingCount }} required left
|
|
904
|
+
</span>
|
|
905
|
+
<button
|
|
906
|
+
type="button"
|
|
907
|
+
class="flex items-center gap-1 rounded px-1 py-0.5 text-sm text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
908
|
+
title="Copy this node's config as JSON"
|
|
909
|
+
@click="copyConfig"
|
|
910
|
+
>
|
|
911
|
+
<Copy :size="12" /> Copy
|
|
912
|
+
</button>
|
|
913
|
+
<button
|
|
914
|
+
type="button"
|
|
915
|
+
class="rounded px-1 py-0.5 text-sm text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
916
|
+
title="Replace this node's config from a JSON object in the clipboard"
|
|
917
|
+
@click="pasteConfig"
|
|
918
|
+
>
|
|
919
|
+
Paste
|
|
920
|
+
</button>
|
|
921
|
+
</div>
|
|
221
922
|
</div>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
:
|
|
227
|
-
:
|
|
228
|
-
|
|
229
|
-
:
|
|
230
|
-
|
|
923
|
+
<IntegrationsNodeConfigField
|
|
924
|
+
v-for="field in parameterFields"
|
|
925
|
+
:key="field.key"
|
|
926
|
+
:field="field"
|
|
927
|
+
:config="draftNode.config ?? {}"
|
|
928
|
+
:slug="draftNode.nodeSlug"
|
|
929
|
+
:version="draftNode.nodeVersion"
|
|
930
|
+
:secret-keys="secretKeys"
|
|
931
|
+
:static-keys="(manifest.config ?? []).map((f2) => f2.key)"
|
|
932
|
+
:depth="0"
|
|
933
|
+
:variables="variables"
|
|
934
|
+
:upstream-outputs="upstreamOutputs"
|
|
935
|
+
@set="(k, v) => writeConfig(k, v)"
|
|
231
936
|
/>
|
|
937
|
+
<p v-if="!parameterFields.length" class="text-sm text-muted-foreground italic">
|
|
938
|
+
All of this node's options live on the Settings tab.
|
|
939
|
+
</p>
|
|
940
|
+
</div>
|
|
941
|
+
<div v-else class="text-sm text-muted-foreground italic">
|
|
942
|
+
This node has no configuration.
|
|
943
|
+
</div>
|
|
944
|
+
</div>
|
|
232
945
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
946
|
+
<!-- Settings: node-instance execution settings -->
|
|
947
|
+
<div v-else-if="tab === 'settings' && draftNode" class="mx-auto w-full max-w-3xl space-y-6">
|
|
948
|
+
<!-- Node behavior: settings-like fields from the node's own config -->
|
|
949
|
+
<div v-if="settingsFields.length" class="space-y-3">
|
|
950
|
+
<span class="text-mono text-muted-foreground">Node behavior</span>
|
|
951
|
+
<IntegrationsNodeConfigField
|
|
952
|
+
v-for="field in settingsFields"
|
|
953
|
+
:key="field.key"
|
|
954
|
+
:field="field"
|
|
955
|
+
:config="draftNode.config ?? {}"
|
|
956
|
+
:slug="draftNode.nodeSlug"
|
|
957
|
+
:version="draftNode.nodeVersion"
|
|
958
|
+
:secret-keys="secretKeys"
|
|
959
|
+
:static-keys="(manifest?.config ?? []).map((f2) => f2.key)"
|
|
960
|
+
:depth="0"
|
|
961
|
+
:variables="variables"
|
|
962
|
+
:upstream-outputs="upstreamOutputs"
|
|
963
|
+
@set="(k, v) => writeConfig(k, v)"
|
|
240
964
|
/>
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
965
|
+
</div>
|
|
966
|
+
<!-- Retry policy -->
|
|
967
|
+
<section class="space-y-2">
|
|
968
|
+
<div class="flex items-center justify-between rounded-lg border border-black/[0.06] bg-muted/20 px-3 py-2">
|
|
969
|
+
<div>
|
|
970
|
+
<div class="text-sm font-medium">Retry on failure</div>
|
|
971
|
+
<p class="text-xs text-muted-foreground">
|
|
972
|
+
Re-run this node automatically when an attempt fails.
|
|
973
|
+
</p>
|
|
974
|
+
</div>
|
|
244
975
|
<Switch
|
|
245
|
-
:model-value="
|
|
246
|
-
@update:model-value="(v) =>
|
|
976
|
+
:model-value="!!draftNode.retry"
|
|
977
|
+
@update:model-value="(v) => patchNode({ retry: v ? { maxAttempts: 3, backoffSeconds: 1, strategy: 'exponential' } : void 0 })"
|
|
247
978
|
/>
|
|
248
|
-
<span class="text-sm text-muted-foreground">
|
|
249
|
-
{{ labelText(field.description) }}
|
|
250
|
-
</span>
|
|
251
979
|
</div>
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
980
|
+
<div v-if="draftNode.retry" class="grid grid-cols-2 gap-3 rounded-lg border border-black/[0.06] p-3">
|
|
981
|
+
<div class="space-y-1.5">
|
|
982
|
+
<Label class="text-mono text-muted-foreground">Max attempts</Label>
|
|
983
|
+
<Input
|
|
984
|
+
type="number"
|
|
985
|
+
min="1"
|
|
986
|
+
:model-value="draftNode.retry?.maxAttempts ?? 3"
|
|
987
|
+
class="h-8 text-sm bg-white"
|
|
988
|
+
@update:model-value="(v) => patchNode({ retry: { ...draftNode.retry, maxAttempts: Number(v) || 1 } })"
|
|
989
|
+
/>
|
|
990
|
+
</div>
|
|
991
|
+
<div class="space-y-1.5">
|
|
992
|
+
<Label class="text-mono text-muted-foreground">Backoff seconds</Label>
|
|
993
|
+
<Input
|
|
994
|
+
type="number"
|
|
995
|
+
min="0"
|
|
996
|
+
:model-value="draftNode.retry?.backoffSeconds ?? 0"
|
|
997
|
+
class="h-8 text-sm bg-white"
|
|
998
|
+
@update:model-value="(v) => patchNode({ retry: { ...draftNode.retry, backoffSeconds: Number(v) || 0 } })"
|
|
999
|
+
/>
|
|
1000
|
+
</div>
|
|
1001
|
+
<div class="col-span-2 space-y-1.5">
|
|
1002
|
+
<Label class="text-mono text-muted-foreground">Strategy</Label>
|
|
1003
|
+
<Select
|
|
1004
|
+
:model-value="draftNode.retry?.strategy ?? 'exponential'"
|
|
1005
|
+
@update:model-value="(v) => patchNode({ retry: { ...draftNode.retry, strategy: v } })"
|
|
1006
|
+
>
|
|
1007
|
+
<SelectTrigger class="h-8 text-sm bg-white">
|
|
1008
|
+
<SelectValue />
|
|
1009
|
+
</SelectTrigger>
|
|
1010
|
+
<SelectContent>
|
|
1011
|
+
<SelectItem value="linear">linear backoff</SelectItem>
|
|
1012
|
+
<SelectItem value="exponential">exponential backoff</SelectItem>
|
|
1013
|
+
</SelectContent>
|
|
1014
|
+
</Select>
|
|
1015
|
+
</div>
|
|
285
1016
|
</div>
|
|
1017
|
+
</section>
|
|
1018
|
+
|
|
1019
|
+
<!-- Timeout: engine start-to-close ceiling for one attempt -->
|
|
1020
|
+
<section class="space-y-1.5">
|
|
1021
|
+
<Label class="text-mono text-muted-foreground">Timeout</Label>
|
|
1022
|
+
<Input
|
|
1023
|
+
type="number"
|
|
1024
|
+
min="1"
|
|
1025
|
+
max="7200"
|
|
1026
|
+
placeholder="600"
|
|
1027
|
+
:model-value="draftNode.timeoutSeconds ?? ''"
|
|
1028
|
+
class="h-8 text-sm bg-white"
|
|
1029
|
+
@update:model-value="(v) => patchNode({ timeoutSeconds: Number(v) > 0 ? Math.min(Math.trunc(Number(v)), 7200) : void 0 })"
|
|
1030
|
+
/>
|
|
1031
|
+
<p class="text-xs text-muted-foreground">
|
|
1032
|
+
Seconds per attempt before the engine cancels the node. Empty uses the default (10 minutes); maximum 2 hours.
|
|
1033
|
+
</p>
|
|
1034
|
+
</section>
|
|
286
1035
|
|
|
287
|
-
|
|
1036
|
+
<!-- On error: what happens once the retries are exhausted -->
|
|
1037
|
+
<section class="space-y-1.5">
|
|
1038
|
+
<Label class="text-mono text-muted-foreground">On error</Label>
|
|
288
1039
|
<Select
|
|
289
|
-
|
|
290
|
-
:model-value="
|
|
291
|
-
@update:model-value="(v) => writeConfig(field.key, v)"
|
|
1040
|
+
:model-value="draftNode.onError ?? 'fail'"
|
|
1041
|
+
@update:model-value="(v) => patchNode({ onError: v === 'route' ? 'route' : void 0 })"
|
|
292
1042
|
>
|
|
293
1043
|
<SelectTrigger class="h-8 text-sm bg-white">
|
|
294
|
-
<SelectValue
|
|
1044
|
+
<SelectValue />
|
|
295
1045
|
</SelectTrigger>
|
|
296
1046
|
<SelectContent>
|
|
297
|
-
<SelectItem
|
|
1047
|
+
<SelectItem value="fail">Fail the run</SelectItem>
|
|
1048
|
+
<SelectItem value="route">Route to error output</SelectItem>
|
|
298
1049
|
</SelectContent>
|
|
299
1050
|
</Select>
|
|
1051
|
+
<p class="text-xs text-muted-foreground">
|
|
1052
|
+
Applies after the retry policy is exhausted. A Stop and Error node always fails the run.
|
|
1053
|
+
</p>
|
|
1054
|
+
</section>
|
|
300
1055
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
:model-value="readConfig(field.key) ?? ''"
|
|
305
|
-
@update:model-value="(v) => writeConfig(field.key, v)"
|
|
306
|
-
>
|
|
307
|
-
<SelectTrigger class="h-8 text-sm bg-white">
|
|
308
|
-
<SelectValue placeholder="Pick a credential" />
|
|
309
|
-
</SelectTrigger>
|
|
310
|
-
<SelectContent>
|
|
311
|
-
<SelectItem v-for="c in credsOfType(field.credentialType)" :key="c.id" :value="c.id">
|
|
312
|
-
{{ c.name }}
|
|
313
|
-
</SelectItem>
|
|
314
|
-
</SelectContent>
|
|
315
|
-
</Select>
|
|
316
|
-
<p v-if="credNeedsReauth(readConfig(field.key))" class="text-sm text-amber-600">
|
|
317
|
-
This credential needs reconnect.
|
|
318
|
-
</p>
|
|
319
|
-
<NuxtLink to="/integrations/credentials" class="text-sm text-violet-600 hover:underline">
|
|
320
|
-
Manage credentials →
|
|
321
|
-
</NuxtLink>
|
|
322
|
-
</template>
|
|
323
|
-
|
|
324
|
-
<!-- array of objects (e.g. headers / mappings) -->
|
|
325
|
-
<div v-else-if="field.type === 'array' && field.items?.type === 'object'" class="space-y-2">
|
|
326
|
-
<div
|
|
327
|
-
v-for="(item, idx) in readArray(field.key)"
|
|
328
|
-
:key="idx"
|
|
329
|
-
class="rounded-md border border-black/[0.06] bg-muted/20 p-2 space-y-1.5"
|
|
330
|
-
>
|
|
331
|
-
<div class="flex items-center justify-between">
|
|
332
|
-
<span class="text-mono text-muted-foreground">
|
|
333
|
-
#{{ idx + 1 }}
|
|
334
|
-
</span>
|
|
335
|
-
<button
|
|
336
|
-
type="button"
|
|
337
|
-
class="flex size-5 items-center justify-center rounded text-muted-foreground hover:text-rose-600"
|
|
338
|
-
@click="removeArrayItem(field.key, idx)"
|
|
339
|
-
>
|
|
340
|
-
<Trash2 :size="11" />
|
|
341
|
-
</button>
|
|
342
|
-
</div>
|
|
343
|
-
<div
|
|
344
|
-
v-for="sub in field.items.properties ?? []"
|
|
345
|
-
:key="sub.key"
|
|
346
|
-
class="space-y-1"
|
|
347
|
-
>
|
|
348
|
-
<Label class="text-mono text-muted-foreground">
|
|
349
|
-
{{ labelText(sub.label) }}
|
|
350
|
-
</Label>
|
|
351
|
-
<Input
|
|
352
|
-
:model-value="item[sub.key] ?? ''"
|
|
353
|
-
:placeholder="labelText(sub.placeholder)"
|
|
354
|
-
class="h-7 text-sm bg-white"
|
|
355
|
-
:class="sub.type === 'expression' ? 'font-mono' : ''"
|
|
356
|
-
@update:model-value="(v) => setArrayItemField(field.key, idx, sub.key, v)"
|
|
357
|
-
/>
|
|
358
|
-
</div>
|
|
359
|
-
</div>
|
|
360
|
-
<Button
|
|
361
|
-
type="button"
|
|
362
|
-
variant="outline"
|
|
363
|
-
size="sm"
|
|
364
|
-
class="h-7 text-sm w-full"
|
|
365
|
-
@click="pushArrayItem(field.key, Object.fromEntries((field.items?.properties ?? []).map((p) => [p.key, ''])))"
|
|
366
|
-
>
|
|
367
|
-
<Plus :size="11" />
|
|
368
|
-
Add row
|
|
369
|
-
</Button>
|
|
370
|
-
</div>
|
|
371
|
-
|
|
372
|
-
<!-- fallback: JSON textarea for complex types -->
|
|
1056
|
+
<!-- Notes: editor-only blob field -->
|
|
1057
|
+
<section class="space-y-1.5">
|
|
1058
|
+
<Label class="text-mono text-muted-foreground">Notes</Label>
|
|
373
1059
|
<Textarea
|
|
374
|
-
|
|
375
|
-
:
|
|
376
|
-
|
|
377
|
-
class="text-sm
|
|
378
|
-
@update:model-value="(v) => {
|
|
379
|
-
try {
|
|
380
|
-
writeConfig(field.key, v ? JSON.parse(String(v)) : void 0);
|
|
381
|
-
} catch {
|
|
382
|
-
}
|
|
383
|
-
}"
|
|
1060
|
+
:model-value="draftNode.notes ?? ''"
|
|
1061
|
+
:rows="3"
|
|
1062
|
+
placeholder="Optional note about this node"
|
|
1063
|
+
class="text-sm bg-white"
|
|
1064
|
+
@update:model-value="(v) => patchNode({ notes: String(v) || void 0 })"
|
|
384
1065
|
/>
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
{{ labelText(field.description) }}
|
|
1066
|
+
<p class="text-xs text-muted-foreground">
|
|
1067
|
+
Only shown in the editor; never sent to the engine.
|
|
388
1068
|
</p>
|
|
1069
|
+
</section>
|
|
1070
|
+
</div>
|
|
1071
|
+
</div>
|
|
1072
|
+
</section>
|
|
1073
|
+
|
|
1074
|
+
<!-- Output pane: what this node hands downstream -->
|
|
1075
|
+
<aside v-if="isNode" class="flex w-[280px] shrink-0 flex-col border-l border-black/[0.06] bg-muted/10">
|
|
1076
|
+
<div class="shrink-0 border-b border-black/[0.06] px-3 py-2.5">
|
|
1077
|
+
<div class="flex items-center justify-between gap-2">
|
|
1078
|
+
<div class="text-sm font-semibold text-foreground">
|
|
1079
|
+
Output
|
|
1080
|
+
</div>
|
|
1081
|
+
<div class="flex items-center rounded-md border border-black/[0.06] bg-white p-0.5">
|
|
1082
|
+
<button type="button" :class="outputView === 'schema' ? segActive : segIdle" @click="outputView = 'schema'">
|
|
1083
|
+
Schema
|
|
1084
|
+
</button>
|
|
1085
|
+
<button type="button" :disabled="!selfSample" :class="outputView === 'json' ? segActive : segIdle" @click="outputView = 'json'">
|
|
1086
|
+
JSON
|
|
1087
|
+
</button>
|
|
389
1088
|
</div>
|
|
390
1089
|
</div>
|
|
391
|
-
<
|
|
392
|
-
|
|
1090
|
+
<p class="mt-0.5 text-xs leading-snug text-muted-foreground">
|
|
1091
|
+
What downstream nodes receive from this one.
|
|
1092
|
+
</p>
|
|
1093
|
+
</div>
|
|
1094
|
+
<div class="shrink-0 space-y-1.5 border-b border-black/[0.06] px-3 py-2">
|
|
1095
|
+
<div class="flex items-center gap-2">
|
|
1096
|
+
<Button
|
|
1097
|
+
size="sm"
|
|
1098
|
+
class="h-7 flex-1 text-xs"
|
|
1099
|
+
:disabled="testRunning"
|
|
1100
|
+
title="Runs this node once with the current draft config"
|
|
1101
|
+
@click="runTestStep"
|
|
1102
|
+
>
|
|
1103
|
+
{{ testRunning ? "Testing\u2026" : "Test step" }}
|
|
1104
|
+
</Button>
|
|
1105
|
+
<button
|
|
1106
|
+
type="button"
|
|
1107
|
+
class="shrink-0 text-[11px] font-medium text-muted-foreground transition hover:text-foreground"
|
|
1108
|
+
@click="toggleTestInput"
|
|
1109
|
+
>
|
|
1110
|
+
{{ testInputOpen ? "Hide input" : "Edit input" }}
|
|
1111
|
+
</button>
|
|
393
1112
|
</div>
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
1113
|
+
<Textarea
|
|
1114
|
+
v-if="testInputOpen"
|
|
1115
|
+
v-model="testInputDraft"
|
|
1116
|
+
:rows="5"
|
|
1117
|
+
spellcheck="false"
|
|
1118
|
+
class="bg-white font-mono text-[11px]"
|
|
1119
|
+
/>
|
|
1120
|
+
<p v-if="testError" class="text-[11px] leading-snug text-rose-600">
|
|
1121
|
+
{{ testError }}
|
|
1122
|
+
</p>
|
|
1123
|
+
<div v-if="testLogs.length" class="max-h-24 space-y-0.5 overflow-y-auto rounded-md border border-black/[0.06] bg-white p-1.5">
|
|
1124
|
+
<div
|
|
1125
|
+
v-for="(l, i) in testLogs"
|
|
1126
|
+
:key="i"
|
|
1127
|
+
class="font-mono text-[10px] leading-snug"
|
|
1128
|
+
:class="l.level === 'error' ? 'text-rose-600' : l.level === 'warn' ? 'text-amber-600' : 'text-slate-600'"
|
|
1129
|
+
>
|
|
1130
|
+
{{ l.message }}
|
|
404
1131
|
</div>
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
<Input
|
|
414
|
-
type="number"
|
|
415
|
-
:model-value="selection.retry?.backoffSeconds ?? 0"
|
|
416
|
-
class="h-7 text-sm bg-white"
|
|
417
|
-
@update:model-value="(v) => emit('update', { ...selection, retry: { ...selection.retry, backoffSeconds: Number(v) || 0 } })"
|
|
418
|
-
/>
|
|
1132
|
+
</div>
|
|
1133
|
+
</div>
|
|
1134
|
+
<div class="min-h-0 flex-1 overflow-y-auto px-3 py-3">
|
|
1135
|
+
<template v-if="outputView === 'schema'">
|
|
1136
|
+
<div v-for="p in selfPorts" :key="p.name" class="mb-3 space-y-1">
|
|
1137
|
+
<div class="flex items-center gap-1.5">
|
|
1138
|
+
<span class="size-1.5 rounded-full" :class="portDotClass(p.kind, p.name)" />
|
|
1139
|
+
<span class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">{{ p.name }}</span>
|
|
419
1140
|
</div>
|
|
420
|
-
<
|
|
421
|
-
|
|
422
|
-
|
|
1141
|
+
<div v-if="p.fields.length" class="space-y-0.5">
|
|
1142
|
+
<div v-for="f in p.fields" :key="f.name" class="flex min-w-0 items-baseline gap-1 font-mono text-[11px]">
|
|
1143
|
+
<span v-if="typeGlyph(f.dataType)" class="shrink-0 font-semibold" :class="typeGlyph(f.dataType).cls">{{ typeGlyph(f.dataType).char }}</span>
|
|
1144
|
+
<span class="shrink-0 text-slate-700">{{ f.name }}</span>
|
|
1145
|
+
<span v-if="chipValue(draft.id, p.name, f.name) !== null" class="min-w-0 truncate text-slate-400">: {{ chipValue(draft.id, p.name, f.name) }}</span>
|
|
1146
|
+
</div>
|
|
1147
|
+
</div>
|
|
1148
|
+
<p v-else class="text-[11px] leading-snug text-muted-foreground">
|
|
1149
|
+
No declared fields<template v-if="chipValue(draft.id, p.name) !== null">; sampled: <span class="font-mono">{{ chipValue(draft.id, p.name) }}</span></template>.
|
|
1150
|
+
</p>
|
|
1151
|
+
</div>
|
|
1152
|
+
<p v-if="!selfPorts.length" class="text-xs leading-snug text-muted-foreground">
|
|
1153
|
+
This node declares no outputs.
|
|
1154
|
+
</p>
|
|
1155
|
+
</template>
|
|
1156
|
+
<pre v-else class="overflow-auto rounded-md border border-black/[0.06] bg-white p-2 font-mono text-[11px] leading-snug">{{ selfSampleJson }}</pre>
|
|
1157
|
+
</div>
|
|
1158
|
+
<div class="shrink-0 border-t border-black/[0.06] px-3 py-2">
|
|
1159
|
+
<template v-if="!pinEditorOpen">
|
|
1160
|
+
<div class="flex items-center gap-2">
|
|
1161
|
+
<button
|
|
1162
|
+
type="button"
|
|
1163
|
+
class="rounded-md border border-black/[0.08] bg-white px-2 py-1 text-[11px] font-medium text-muted-foreground transition hover:bg-muted/30 hover:text-foreground"
|
|
1164
|
+
@click="openPinEditor"
|
|
423
1165
|
>
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
1166
|
+
{{ selfPinned ? "Edit pinned sample" : "Pin sample output" }}
|
|
1167
|
+
</button>
|
|
1168
|
+
<button
|
|
1169
|
+
v-if="selfPinned"
|
|
1170
|
+
type="button"
|
|
1171
|
+
class="text-[11px] font-medium text-rose-600 transition hover:text-rose-700"
|
|
1172
|
+
@click="removePin"
|
|
1173
|
+
>
|
|
1174
|
+
Unpin
|
|
1175
|
+
</button>
|
|
1176
|
+
</div>
|
|
1177
|
+
<p class="mt-1 text-[11px] leading-snug text-muted-foreground">
|
|
1178
|
+
A pinned sample stands in for run data while you map downstream fields.
|
|
1179
|
+
</p>
|
|
1180
|
+
</template>
|
|
1181
|
+
<template v-else>
|
|
1182
|
+
<Textarea
|
|
1183
|
+
v-model="pinDraft"
|
|
1184
|
+
:rows="8"
|
|
1185
|
+
spellcheck="false"
|
|
1186
|
+
class="bg-white font-mono text-[11px]"
|
|
1187
|
+
/>
|
|
1188
|
+
<p v-if="pinError" class="mt-1 text-[11px] text-amber-600">
|
|
1189
|
+
{{ pinError }}
|
|
1190
|
+
</p>
|
|
1191
|
+
<div class="mt-1.5 flex items-center gap-2">
|
|
1192
|
+
<Button size="sm" class="h-7 text-xs" @click="savePin">
|
|
1193
|
+
Pin
|
|
1194
|
+
</Button>
|
|
1195
|
+
<Button size="sm" variant="outline" class="h-7 text-xs" @click="pinEditorOpen = false">
|
|
1196
|
+
Cancel
|
|
1197
|
+
</Button>
|
|
1198
|
+
</div>
|
|
1199
|
+
</template>
|
|
1200
|
+
</div>
|
|
1201
|
+
</aside>
|
|
1202
|
+
</div>
|
|
436
1203
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
1204
|
+
<!-- Footer: draft semantics — nothing reaches the blob until Save -->
|
|
1205
|
+
<div class="flex shrink-0 items-center justify-between gap-3 border-t border-black/[0.06] px-4 py-3">
|
|
1206
|
+
<Button
|
|
1207
|
+
variant="ghost"
|
|
1208
|
+
size="sm"
|
|
1209
|
+
class="h-8 text-sm text-rose-600 hover:bg-rose-500/10 hover:text-rose-700"
|
|
1210
|
+
@click="emit('remove')"
|
|
1211
|
+
>
|
|
1212
|
+
<Trash2 :size="13" />
|
|
1213
|
+
Remove from workflow
|
|
1214
|
+
</Button>
|
|
1215
|
+
<div class="flex items-center gap-2">
|
|
1216
|
+
<span v-if="dirty" class="text-xs text-muted-foreground">Unsaved changes</span>
|
|
1217
|
+
<Button variant="outline" size="sm" class="h-8" @click="emit('close')">
|
|
1218
|
+
Cancel
|
|
1219
|
+
</Button>
|
|
1220
|
+
<Button size="sm" class="h-8" @click="save">
|
|
1221
|
+
Save
|
|
446
1222
|
</Button>
|
|
447
1223
|
</div>
|
|
448
|
-
</
|
|
449
|
-
</
|
|
1224
|
+
</div>
|
|
1225
|
+
</div>
|
|
450
1226
|
</template>
|
|
1227
|
+
|
|
1228
|
+
<style>
|
|
1229
|
+
.chip-drag-live .expr-editor,.chip-drag-live input:not([type=number]):not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]):not([type=color]),.chip-drag-live textarea{border-color:rgba(16,185,129,.55);border-style:dashed}.chip-drop-hover{background-color:rgba(16,185,129,.06)!important;border-color:#10b981!important;border-style:solid!important;box-shadow:0 0 0 3px rgba(16,185,129,.15)}
|
|
1230
|
+
</style>
|