@revenexx/studio-integrations 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +428 -0
- package/README.md +48 -4
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +10 -2
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
- package/dist/runtime/components/Integrations/Layout.vue +48 -4
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
- package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
- package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
- package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
- package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
- package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +8 -6
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
- package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
- package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
- package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
- package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
- package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
- package/dist/runtime/composables/useIntegrationsT.js +9 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
- package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
- package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
- package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/branchReorder.d.ts +37 -0
- package/dist/runtime/data/branchReorder.js +71 -0
- package/dist/runtime/data/handleId.d.ts +23 -0
- package/dist/runtime/data/handleId.js +6 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +14 -0
- package/dist/runtime/data/integrationTemplates.d.ts +48 -0
- package/dist/runtime/data/integrationTemplates.js +21 -0
- package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
- package/dist/runtime/data/nodeIcons.d.ts +2 -0
- package/dist/runtime/data/nodeIcons.js +73 -0
- package/dist/runtime/data/nodeInputData.d.ts +16 -4
- package/dist/runtime/data/nodeInputData.js +4 -0
- package/dist/runtime/data/payloadForm.d.ts +9 -2
- package/dist/runtime/data/payloadForm.js +29 -4
- package/dist/runtime/data/portTone.d.ts +17 -0
- package/dist/runtime/data/portTone.js +7 -0
- package/dist/runtime/data/webhookAuth.d.ts +18 -0
- package/dist/runtime/data/webhookAuth.js +19 -0
- package/dist/runtime/data/workflowIconCatalog.js +4 -6
- package/dist/runtime/i18n/messages/de.json +1207 -0
- package/dist/runtime/i18n/messages/en.json +1207 -0
- package/dist/runtime/i18n/translate.d.ts +83 -0
- package/dist/runtime/i18n/translate.js +41 -0
- package/dist/runtime/pages/integrations/audit.vue +140 -37
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
- package/dist/runtime/pages/integrations/credentials.vue +180 -64
- package/dist/runtime/pages/integrations/index.vue +91 -92
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +88 -23
- package/dist/runtime/pages/integrations/secrets.vue +57 -35
- package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
- package/dist/runtime/pages/integrations/templates/index.vue +82 -103
- package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
- package/dist/runtime/plugins/locale.d.ts +10 -0
- package/dist/runtime/plugins/locale.js +6 -0
- package/dist/runtime/plugins/realtime.client.d.ts +32 -0
- package/dist/runtime/plugins/realtime.client.js +4 -0
- package/dist/runtime/types/integrations.d.ts +65 -6
- package/dist/runtime/utils/apiError.d.ts +47 -0
- package/dist/runtime/utils/apiError.js +32 -0
- package/dist/runtime/utils/cockpitSources.d.ts +153 -0
- package/dist/runtime/utils/cockpitSources.js +14 -0
- package/dist/runtime/utils/credentialState.d.ts +24 -1
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +46 -4
- package/dist/runtime/utils/localize.js +34 -7
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -0
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
- package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
- package/dist/runtime/utils/schedule.d.ts +116 -0
- package/dist/runtime/utils/schedule.js +219 -0
- package/dist/runtime/utils/statusStyle.d.ts +26 -2
- package/dist/runtime/utils/statusStyle.js +9 -1
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +54 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/dist/types.d.mts +2 -6
- package/package.json +10 -3
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, nextTick, onMounted, provide, ref, watch } from "vue";
|
|
3
|
-
import { TrashBinMinimalistic as
|
|
4
|
-
import { ChevronLeft, ChevronRight, Clock, Play, Search, Webhook, X, Zap } from "lucide-vue-next";
|
|
3
|
+
import { AddCircle as Plus, TrashBinMinimalistic as TrashBin, Copy } from "#solar-icons";
|
|
4
|
+
import { ChevronDown, ChevronLeft, ChevronRight, Clock, Database, MessageSquareText, Play, Search, Webhook, X, Zap } from "lucide-vue-next";
|
|
5
5
|
import { toast } from "vue-sonner";
|
|
6
6
|
import {
|
|
7
7
|
Button,
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
8
13
|
Input,
|
|
9
14
|
Label,
|
|
15
|
+
Popover,
|
|
16
|
+
PopoverContent,
|
|
17
|
+
PopoverTrigger,
|
|
10
18
|
Select,
|
|
11
19
|
SelectContent,
|
|
12
20
|
SelectItem,
|
|
21
|
+
SelectSeparator,
|
|
13
22
|
SelectTrigger,
|
|
14
23
|
SelectValue,
|
|
15
24
|
Switch,
|
|
16
|
-
Textarea
|
|
25
|
+
Textarea,
|
|
26
|
+
ToggleGroup,
|
|
27
|
+
ToggleGroupItem
|
|
17
28
|
} from "@revenexx/studio";
|
|
18
29
|
import EventTypeCombobox from "./EventTypeCombobox.vue";
|
|
19
|
-
import {
|
|
30
|
+
import { findManifest, nodeCategoryLabel } from "../../data/integrationNodeCatalog";
|
|
20
31
|
import {
|
|
21
32
|
collectUpstreamNodes,
|
|
22
33
|
nodeOutputExpr,
|
|
@@ -24,12 +35,17 @@ import {
|
|
|
24
35
|
triggerPayloadExpr,
|
|
25
36
|
variableExpr
|
|
26
37
|
} from "../../data/nodeInputData";
|
|
38
|
+
import { portDotClass } from "../../data/portTone";
|
|
39
|
+
import { authChoiceFromConfig, configPatchForAuthChoice } from "../../data/webhookAuth";
|
|
27
40
|
import {
|
|
28
41
|
dataTypeOf,
|
|
29
42
|
portValuesFromStepOutput,
|
|
30
43
|
previewValue,
|
|
31
44
|
valueAtPath
|
|
32
45
|
} from "../../data/nodeRunData";
|
|
46
|
+
import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
|
|
47
|
+
import { describeRecurrence, parseCron, scheduleTimezone, toCron } from "../../utils/schedule";
|
|
48
|
+
const t = useIntegrationsT();
|
|
33
49
|
const props = defineProps({
|
|
34
50
|
selection: { type: [Object, null], required: true },
|
|
35
51
|
secretKeys: { type: Array, required: true },
|
|
@@ -40,7 +56,8 @@ const props = defineProps({
|
|
|
40
56
|
edges: { type: Array, required: false },
|
|
41
57
|
triggers: { type: Array, required: false },
|
|
42
58
|
dynamicOutputs: { type: Object, required: false },
|
|
43
|
-
workflowId: { type: [Number, String], required: false }
|
|
59
|
+
workflowId: { type: [Number, String], required: false },
|
|
60
|
+
refusedFields: { type: Object, required: false }
|
|
44
61
|
});
|
|
45
62
|
const emit = defineEmits(["update", "close", "remove", "navigate"]);
|
|
46
63
|
function clone(v) {
|
|
@@ -70,36 +87,69 @@ const manifest = computed(() => {
|
|
|
70
87
|
const wn = draftNode.value;
|
|
71
88
|
return wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
|
|
72
89
|
});
|
|
73
|
-
function labelText(v) {
|
|
74
|
-
if (!v) return "";
|
|
75
|
-
return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
|
|
76
|
-
}
|
|
77
90
|
const TRIGGER_GLYPHS = { manual: Play, schedule: Clock, webhook: Webhook, event: Zap };
|
|
78
|
-
const TRIGGER_LABELS =
|
|
91
|
+
const TRIGGER_LABELS = computed(() => ({
|
|
92
|
+
manual: t("integration.nodeInspector.trigger.type.manual"),
|
|
93
|
+
schedule: t("integration.nodeInspector.trigger.type.schedule"),
|
|
94
|
+
webhook: t("integration.nodeInspector.trigger.type.webhook"),
|
|
95
|
+
event: t("integration.nodeInspector.trigger.type.event")
|
|
96
|
+
}));
|
|
79
97
|
const triggerGlyph = computed(() => TRIGGER_GLYPHS[draftTrigger.value?.type ?? "manual"]);
|
|
80
98
|
const namePlaceholder = computed(() => {
|
|
81
|
-
if (draftNode.value) return
|
|
82
|
-
return TRIGGER_LABELS[draftTrigger.value?.type ?? "manual"]
|
|
99
|
+
if (draftNode.value) return resolveLocalized(manifest.value?.name) || draftNode.value.nodeSlug;
|
|
100
|
+
return t("integration.nodeInspector.trigger.placeholder", { type: TRIGGER_LABELS.value[draftTrigger.value?.type ?? "manual"] });
|
|
83
101
|
});
|
|
84
102
|
const subtitleText = computed(() => {
|
|
85
103
|
const wn = draftNode.value;
|
|
86
104
|
if (wn) return wn.nodeSlug + "@" + wn.nodeVersion;
|
|
87
|
-
return TRIGGER_LABELS[draftTrigger.value?.type ?? "manual"]
|
|
105
|
+
return t("integration.nodeInspector.trigger.placeholder", { type: TRIGGER_LABELS.value[draftTrigger.value?.type ?? "manual"] });
|
|
88
106
|
});
|
|
107
|
+
const manifestDescription = computed(() => resolveLocalized(manifest.value?.description));
|
|
89
108
|
const groupLabel = computed(() => {
|
|
90
|
-
if (!isNode.value) return "
|
|
109
|
+
if (!isNode.value) return t("integration.nodeInspector.header.groupTrigger");
|
|
91
110
|
const groups = manifest.value?.groups;
|
|
92
|
-
if (groups?.length) return
|
|
93
|
-
return
|
|
111
|
+
if (groups?.length) return resolveLocalized(groups[groups.length - 1]);
|
|
112
|
+
return nodeCategoryLabel(manifest.value?.category) || t("integration.nodeInspector.header.groupNode");
|
|
113
|
+
});
|
|
114
|
+
const nameValue = computed(() => {
|
|
115
|
+
const name = draft.value.name;
|
|
116
|
+
return resolveDisplayName(name);
|
|
94
117
|
});
|
|
95
118
|
function writeName(name) {
|
|
96
119
|
draft.value = { ...draft.value, name };
|
|
97
120
|
}
|
|
121
|
+
const noteOpen = ref(false);
|
|
122
|
+
const noteText = computed(() => (draftNode.value?.notes ?? "").trim());
|
|
123
|
+
const noteLabel = computed(() => noteText.value ? t("integration.nodeInspector.note.label") : t("integration.nodeInspector.note.addLabel"));
|
|
124
|
+
function writeNote(v) {
|
|
125
|
+
const text = String(v ?? "");
|
|
126
|
+
patchNode({ notes: text.trim() ? text : void 0 });
|
|
127
|
+
}
|
|
98
128
|
const tab = ref("parameters");
|
|
99
129
|
const showSettingsTab = computed(() => isNode.value);
|
|
130
|
+
const inspectorTabs = computed(() => [
|
|
131
|
+
{ key: "parameters", label: t("integration.nodeInspector.tab.parameters") },
|
|
132
|
+
...showSettingsTab.value ? [
|
|
133
|
+
{ key: "settings", label: t("integration.nodeInspector.tab.settings") },
|
|
134
|
+
{ key: "output", label: t("integration.nodeInspector.tab.output") }
|
|
135
|
+
] : []
|
|
136
|
+
]);
|
|
137
|
+
watch(showSettingsTab, (hasThem) => {
|
|
138
|
+
if (!hasThem) tab.value = "parameters";
|
|
139
|
+
});
|
|
100
140
|
const SETTINGS_FIELD_KEY = /(^|_)(timeout|retry|retries|backoff)|(timeout|retry|retries|backoff)(ms|sec|seconds|attempts)?$/i;
|
|
101
141
|
const parameterFields = computed(() => (manifest.value?.config ?? []).filter((f) => !SETTINGS_FIELD_KEY.test(f.key)));
|
|
102
142
|
const settingsFields = computed(() => (manifest.value?.config ?? []).filter((f) => SETTINGS_FIELD_KEY.test(f.key)));
|
|
143
|
+
const requiredFields = computed(() => parameterFields.value.filter((f) => f.required));
|
|
144
|
+
const optionalFields = computed(() => parameterFields.value.filter((f) => !f.required));
|
|
145
|
+
function optionalShouldOpen() {
|
|
146
|
+
if (!requiredFields.value.length) return true;
|
|
147
|
+
return optionalFields.value.some((f) => props.refusedFields?.[f.key] !== void 0);
|
|
148
|
+
}
|
|
149
|
+
const optionalOpen = ref(optionalShouldOpen());
|
|
150
|
+
watch([() => props.selection?.id, manifest], () => {
|
|
151
|
+
optionalOpen.value = optionalShouldOpen();
|
|
152
|
+
});
|
|
103
153
|
function dependentKeys(changedKey) {
|
|
104
154
|
const fields = manifest.value?.config ?? [];
|
|
105
155
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -139,19 +189,13 @@ function patchTrigger(patch) {
|
|
|
139
189
|
if (!draftTrigger.value) return;
|
|
140
190
|
draft.value = { ...draftTrigger.value, ...patch };
|
|
141
191
|
}
|
|
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
192
|
async function copyConfig() {
|
|
149
193
|
if (!draftNode.value) return;
|
|
150
194
|
try {
|
|
151
195
|
await navigator.clipboard.writeText(JSON.stringify(draftNode.value.config ?? {}, null, 2));
|
|
152
|
-
toast.success("
|
|
196
|
+
toast.success(t("integration.nodeInspector.toast.configCopied"));
|
|
153
197
|
} catch {
|
|
154
|
-
toast.error("
|
|
198
|
+
toast.error(t("integration.nodeInspector.toast.configCopyFailed"));
|
|
155
199
|
}
|
|
156
200
|
}
|
|
157
201
|
async function pasteConfig() {
|
|
@@ -160,22 +204,22 @@ async function pasteConfig() {
|
|
|
160
204
|
try {
|
|
161
205
|
text = await navigator.clipboard.readText();
|
|
162
206
|
} catch {
|
|
163
|
-
toast.error("
|
|
207
|
+
toast.error(t("integration.nodeInspector.toast.clipboardReadFailed"));
|
|
164
208
|
return;
|
|
165
209
|
}
|
|
166
210
|
let parsed;
|
|
167
211
|
try {
|
|
168
212
|
parsed = JSON.parse(text);
|
|
169
213
|
} catch {
|
|
170
|
-
toast.error("
|
|
214
|
+
toast.error(t("integration.nodeInspector.toast.clipboardInvalidJson"));
|
|
171
215
|
return;
|
|
172
216
|
}
|
|
173
217
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
174
|
-
toast.error("
|
|
218
|
+
toast.error(t("integration.nodeInspector.toast.clipboardNotConfigObject"));
|
|
175
219
|
return;
|
|
176
220
|
}
|
|
177
221
|
patchNode({ config: parsed });
|
|
178
|
-
toast.success("
|
|
222
|
+
toast.success(t("integration.nodeInspector.toast.configPasted"));
|
|
179
223
|
}
|
|
180
224
|
function readTriggerConfig(key) {
|
|
181
225
|
return draftTrigger.value?.config[key];
|
|
@@ -185,6 +229,263 @@ function writeTriggerConfig(key, value) {
|
|
|
185
229
|
if (!wt) return;
|
|
186
230
|
patchTrigger({ config: { ...wt.config, [key]: value } });
|
|
187
231
|
}
|
|
232
|
+
const MINUTE_INTERVALS = [1, 2, 5, 10, 15, 20, 30];
|
|
233
|
+
const HOUR_INTERVALS = [2, 3, 4, 6, 8, 12];
|
|
234
|
+
const MINUTES_PAST = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
|
|
235
|
+
const DAY_OPTIONS = computed(() => [
|
|
236
|
+
{ value: 1, short: t("integration.nodeInspector.schedule.day.monday.short"), name: t("integration.nodeInspector.schedule.day.monday") },
|
|
237
|
+
{ value: 2, short: t("integration.nodeInspector.schedule.day.tuesday.short"), name: t("integration.nodeInspector.schedule.day.tuesday") },
|
|
238
|
+
{ value: 3, short: t("integration.nodeInspector.schedule.day.wednesday.short"), name: t("integration.nodeInspector.schedule.day.wednesday") },
|
|
239
|
+
{ value: 4, short: t("integration.nodeInspector.schedule.day.thursday.short"), name: t("integration.nodeInspector.schedule.day.thursday") },
|
|
240
|
+
{ value: 5, short: t("integration.nodeInspector.schedule.day.friday.short"), name: t("integration.nodeInspector.schedule.day.friday") },
|
|
241
|
+
{ value: 6, short: t("integration.nodeInspector.schedule.day.saturday.short"), name: t("integration.nodeInspector.schedule.day.saturday") },
|
|
242
|
+
{ value: 0, short: t("integration.nodeInspector.schedule.day.sunday.short"), name: t("integration.nodeInspector.schedule.day.sunday") }
|
|
243
|
+
]);
|
|
244
|
+
const FREQUENCY_OPTIONS = computed(() => [
|
|
245
|
+
{ value: "minutes", label: t("integration.nodeInspector.schedule.frequency.minutes") },
|
|
246
|
+
{ value: "hourly", label: t("integration.nodeInspector.schedule.frequency.hourly") },
|
|
247
|
+
{ value: "hours", label: t("integration.nodeInspector.schedule.frequency.hours") },
|
|
248
|
+
{ value: "times", label: t("integration.nodeInspector.schedule.frequency.times") },
|
|
249
|
+
{ value: "daily", label: t("integration.nodeInspector.schedule.frequency.daily") },
|
|
250
|
+
{ value: "weekly", label: t("integration.nodeInspector.schedule.frequency.weekly") },
|
|
251
|
+
{ value: "monthly", label: t("integration.nodeInspector.schedule.frequency.monthly") }
|
|
252
|
+
]);
|
|
253
|
+
const HOURS_OF_DAY = Array.from({ length: 24 }, (_, h) => h);
|
|
254
|
+
const FREQUENCY_ADVANCED = "advanced";
|
|
255
|
+
const cronValue = computed(() => readTriggerConfig("cron") ?? "");
|
|
256
|
+
const hasCron = computed(() => cronValue.value.trim() !== "");
|
|
257
|
+
const recurrence = computed(() => parseCron(cronValue.value));
|
|
258
|
+
const advancedOpen = ref(false);
|
|
259
|
+
watch(() => draftTrigger.value?.id, () => {
|
|
260
|
+
advancedOpen.value = false;
|
|
261
|
+
});
|
|
262
|
+
const frequency = computed(() => advancedOpen.value ? FREQUENCY_ADVANCED : recurrence.value?.kind ?? FREQUENCY_ADVANCED);
|
|
263
|
+
const ALL_DAYS = [0, 1, 2, 3, 4, 5, 6];
|
|
264
|
+
const carry = ref({
|
|
265
|
+
hour: 8,
|
|
266
|
+
minute: 0,
|
|
267
|
+
days: [1],
|
|
268
|
+
day: 1,
|
|
269
|
+
minuteInterval: 15,
|
|
270
|
+
hourInterval: 4,
|
|
271
|
+
// "Several times a day" carries its own two sets: its days start at every day,
|
|
272
|
+
// where a weekly schedule's start at one, and its hours start empty so the
|
|
273
|
+
// first pair is built from the time already on screen rather than from a
|
|
274
|
+
// working hour nobody chose.
|
|
275
|
+
hours: [],
|
|
276
|
+
timeDays: ALL_DAYS
|
|
277
|
+
});
|
|
278
|
+
watch(recurrence, (r) => {
|
|
279
|
+
if (!r) return;
|
|
280
|
+
if ("hour" in r) carry.value.hour = r.hour;
|
|
281
|
+
if ("minute" in r) carry.value.minute = r.minute;
|
|
282
|
+
if (r.kind === "weekly") carry.value.days = r.days;
|
|
283
|
+
if (r.kind === "monthly") carry.value.day = r.day;
|
|
284
|
+
if (r.kind === "minutes") carry.value.minuteInterval = r.interval;
|
|
285
|
+
if (r.kind === "hours") carry.value.hourInterval = r.interval;
|
|
286
|
+
if (r.kind === "times") {
|
|
287
|
+
carry.value.hours = r.hours;
|
|
288
|
+
carry.value.timeDays = r.days ?? ALL_DAYS;
|
|
289
|
+
}
|
|
290
|
+
}, { immediate: true });
|
|
291
|
+
function recurrenceFor(kind) {
|
|
292
|
+
const c = carry.value;
|
|
293
|
+
switch (kind) {
|
|
294
|
+
case "minutes":
|
|
295
|
+
return { kind: "minutes", interval: c.minuteInterval };
|
|
296
|
+
case "hourly":
|
|
297
|
+
return { kind: "hourly", minute: c.minute };
|
|
298
|
+
case "hours":
|
|
299
|
+
return { kind: "hours", interval: c.hourInterval, minute: c.minute };
|
|
300
|
+
case "daily":
|
|
301
|
+
return { kind: "daily", hour: c.hour, minute: c.minute };
|
|
302
|
+
case "times":
|
|
303
|
+
return {
|
|
304
|
+
kind: "times",
|
|
305
|
+
minute: c.minute,
|
|
306
|
+
// Two is the smallest set that is "several"; one hour is the daily rhythm.
|
|
307
|
+
// With none carried yet, the rhythm starts from the hour that was on screen
|
|
308
|
+
// and the one after it — a schedule set for 02:00 becomes 02:00 and 03:00
|
|
309
|
+
// rather than losing the time the author had already chosen.
|
|
310
|
+
hours: c.hours.length >= 2 ? c.hours : [c.hour, (c.hour + 1) % 24].sort((a, b) => a - b),
|
|
311
|
+
days: c.timeDays.length ? c.timeDays : null
|
|
312
|
+
};
|
|
313
|
+
case "weekly":
|
|
314
|
+
return { kind: "weekly", days: c.days.length ? c.days : [1], hour: c.hour, minute: c.minute };
|
|
315
|
+
case "monthly":
|
|
316
|
+
return { kind: "monthly", day: c.day, hour: c.hour, minute: c.minute };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function writeRecurrence(kind) {
|
|
320
|
+
writeTriggerConfig("cron", toCron(recurrenceFor(kind)));
|
|
321
|
+
}
|
|
322
|
+
function applyFrequency(v) {
|
|
323
|
+
const next = v;
|
|
324
|
+
advancedOpen.value = next === FREQUENCY_ADVANCED;
|
|
325
|
+
if (next !== FREQUENCY_ADVANCED) writeRecurrence(next);
|
|
326
|
+
}
|
|
327
|
+
function applyToCurrent() {
|
|
328
|
+
const f = frequency.value;
|
|
329
|
+
if (f !== FREQUENCY_ADVANCED) writeRecurrence(f);
|
|
330
|
+
}
|
|
331
|
+
function pad2(n) {
|
|
332
|
+
return String(n).padStart(2, "0");
|
|
333
|
+
}
|
|
334
|
+
function withCurrent(options, current) {
|
|
335
|
+
return options.includes(current) ? options : [...options, current].sort((a, b) => a - b);
|
|
336
|
+
}
|
|
337
|
+
const minuteIntervalOptions = computed(() => withCurrent(MINUTE_INTERVALS, carry.value.minuteInterval));
|
|
338
|
+
const minutePastOptions = computed(() => withCurrent(MINUTES_PAST, carry.value.minute));
|
|
339
|
+
const timeValue = computed(() => `${pad2(carry.value.hour)}:${pad2(carry.value.minute)}`);
|
|
340
|
+
function applyTime(v) {
|
|
341
|
+
const m = /^(\d{1,2}):(\d{2})/.exec(String(v ?? ""));
|
|
342
|
+
if (!m) return;
|
|
343
|
+
carry.value.hour = Math.min(23, Number(m[1]));
|
|
344
|
+
carry.value.minute = Math.min(59, Number(m[2]));
|
|
345
|
+
applyToCurrent();
|
|
346
|
+
}
|
|
347
|
+
const selectedDays = computed(() => (frequency.value === "times" ? carry.value.timeDays : carry.value.days).map(String));
|
|
348
|
+
function applyDays(v) {
|
|
349
|
+
const picked = (Array.isArray(v) ? v : []).map(Number).filter((n) => Number.isInteger(n) && n >= 0 && n <= 6);
|
|
350
|
+
const forTimes = frequency.value === "times";
|
|
351
|
+
const previous = forTimes ? carry.value.timeDays : carry.value.days;
|
|
352
|
+
const days = [...new Set(picked.length ? picked : previous)].sort((a, b) => a - b);
|
|
353
|
+
if (forTimes) {
|
|
354
|
+
carry.value.timeDays = days;
|
|
355
|
+
writeRecurrence("times");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
carry.value.days = days;
|
|
359
|
+
writeRecurrence("weekly");
|
|
360
|
+
}
|
|
361
|
+
const selectedHours = computed(() => carry.value.hours.map(String));
|
|
362
|
+
function applyHours(v) {
|
|
363
|
+
const picked = (Array.isArray(v) ? v : []).map(Number).filter((n) => Number.isInteger(n) && n >= 0 && n <= 23);
|
|
364
|
+
const hours = [...new Set(picked)].sort((a, b) => a - b);
|
|
365
|
+
carry.value.hours = hours.length >= 2 ? hours : carry.value.hours;
|
|
366
|
+
writeRecurrence("times");
|
|
367
|
+
}
|
|
368
|
+
function applyMinuteInterval(v) {
|
|
369
|
+
carry.value.minuteInterval = Number(v);
|
|
370
|
+
writeRecurrence("minutes");
|
|
371
|
+
}
|
|
372
|
+
function applyHourInterval(v) {
|
|
373
|
+
carry.value.hourInterval = Number(v);
|
|
374
|
+
writeRecurrence("hours");
|
|
375
|
+
}
|
|
376
|
+
function applyMinutePast(v) {
|
|
377
|
+
carry.value.minute = Number(v);
|
|
378
|
+
applyToCurrent();
|
|
379
|
+
}
|
|
380
|
+
function applyDayOfMonth(v) {
|
|
381
|
+
carry.value.day = Number(v);
|
|
382
|
+
writeRecurrence("monthly");
|
|
383
|
+
}
|
|
384
|
+
const scheduleSentence = computed(() => {
|
|
385
|
+
const r = recurrence.value;
|
|
386
|
+
if (!r) return null;
|
|
387
|
+
return `${describeRecurrence(r)} \xB7 ${scheduleTimezone(readTriggerConfig("timezone"))}`;
|
|
388
|
+
});
|
|
389
|
+
const webhookUrl = computed(() => {
|
|
390
|
+
const wt = draftTrigger.value;
|
|
391
|
+
if (!wt || wt.type !== "webhook") return null;
|
|
392
|
+
const inbound = wt.config.inbound;
|
|
393
|
+
if (inbound?.url) return inbound.url;
|
|
394
|
+
const base = String(useRuntimeConfig().public.integrationsApi ?? "").replace(/\/+$/, "");
|
|
395
|
+
return base ? `${base}/webhooks/${wt.id}` : null;
|
|
396
|
+
});
|
|
397
|
+
const webhookIsPublic = computed(() => !!draftTrigger.value?.config?.public);
|
|
398
|
+
const authChoice = computed(() => authChoiceFromConfig(draftTrigger.value?.config));
|
|
399
|
+
function writeTriggerConfigPatch(patch) {
|
|
400
|
+
const wt = draftTrigger.value;
|
|
401
|
+
if (!wt) return;
|
|
402
|
+
patchTrigger({ config: { ...wt.config, ...patch } });
|
|
403
|
+
}
|
|
404
|
+
function applyAuthChoice(v) {
|
|
405
|
+
writeTriggerConfigPatch(configPatchForAuthChoice(v));
|
|
406
|
+
}
|
|
407
|
+
function writeAuthConfigKey(key, value) {
|
|
408
|
+
const cfg = draftTrigger.value?.config;
|
|
409
|
+
writeTriggerConfigPatch({ authConfig: { ...cfg?.authConfig, [key]: value || void 0 } });
|
|
410
|
+
}
|
|
411
|
+
const webhookExposed = computed(() => !!draftTrigger.value?.config?.inbound?.url);
|
|
412
|
+
const rotateBusy = ref(false);
|
|
413
|
+
const rotatedSecret = ref(null);
|
|
414
|
+
const rotateOpen = ref(false);
|
|
415
|
+
async function rotateEndpointSecret() {
|
|
416
|
+
const wt = draftTrigger.value;
|
|
417
|
+
if (!wt || !props.workflowId) return;
|
|
418
|
+
rotateBusy.value = true;
|
|
419
|
+
try {
|
|
420
|
+
const api2 = useIntegrationsApi();
|
|
421
|
+
const res = await api2.post(`/workflows/${props.workflowId}/triggers/${wt.id}/rotate-secret`, {});
|
|
422
|
+
const body = res ?? {};
|
|
423
|
+
const secret = body.inbound_secret ?? body.data?.inbound_secret ?? null;
|
|
424
|
+
if (secret) {
|
|
425
|
+
rotatedSecret.value = secret;
|
|
426
|
+
rotateOpen.value = true;
|
|
427
|
+
} else {
|
|
428
|
+
toast.error(t("integration.nodeInspector.toast.rotateNoSecret"));
|
|
429
|
+
}
|
|
430
|
+
} catch (e) {
|
|
431
|
+
toast.error(e.message || t("integration.nodeInspector.toast.rotateFailed"));
|
|
432
|
+
} finally {
|
|
433
|
+
rotateBusy.value = false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function copySecret() {
|
|
437
|
+
if (!rotatedSecret.value) return;
|
|
438
|
+
try {
|
|
439
|
+
await navigator.clipboard.writeText(rotatedSecret.value);
|
|
440
|
+
toast.success(t("integration.nodeInspector.toast.secretCopied"));
|
|
441
|
+
} catch {
|
|
442
|
+
toast.error(t("integration.nodeInspector.toast.secretCopyFailed"));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async function copyWebhookUrl() {
|
|
446
|
+
if (!webhookUrl.value) return;
|
|
447
|
+
try {
|
|
448
|
+
await navigator.clipboard.writeText(webhookUrl.value);
|
|
449
|
+
toast.success(t("integration.nodeInspector.toast.webhookUrlCopied"));
|
|
450
|
+
} catch {
|
|
451
|
+
toast.error(t("integration.nodeInspector.toast.webhookUrlCopyFailed"));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const definedEvents = typeof useDefinedEvents === "function" ? useDefinedEvents() : { emits: ref([]), load: async () => {
|
|
455
|
+
} };
|
|
456
|
+
const eventTopic = computed(() => {
|
|
457
|
+
const raw = readTriggerConfig("event_type");
|
|
458
|
+
return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
|
|
459
|
+
});
|
|
460
|
+
watch(eventTopic, (topic) => {
|
|
461
|
+
if (topic) void definedEvents.load();
|
|
462
|
+
}, { immediate: true });
|
|
463
|
+
const eventSample = computed(() => {
|
|
464
|
+
if (!eventTopic.value) return null;
|
|
465
|
+
const sample = definedEvents.emits.value.find((e) => e.topic === eventTopic.value)?.sample;
|
|
466
|
+
return sample ? JSON.stringify(sample, null, 2) : null;
|
|
467
|
+
});
|
|
468
|
+
const manualFields = computed(() => readTriggerConfig("payloadFields") ?? []);
|
|
469
|
+
function writeManualFields(next) {
|
|
470
|
+
writeTriggerConfig("payloadFields", next.length ? next : void 0);
|
|
471
|
+
}
|
|
472
|
+
function setManualField(idx, patch) {
|
|
473
|
+
writeManualFields(manualFields.value.map((f, i) => i === idx ? { ...f, ...patch } : f));
|
|
474
|
+
}
|
|
475
|
+
function addManualField() {
|
|
476
|
+
writeManualFields([...manualFields.value, { path: "", label: "", kind: "text" }]);
|
|
477
|
+
}
|
|
478
|
+
function removeManualField(idx) {
|
|
479
|
+
manualFieldUids.value.splice(idx, 1);
|
|
480
|
+
writeManualFields(manualFields.value.filter((_, i) => i !== idx));
|
|
481
|
+
}
|
|
482
|
+
let nextManualFieldUid = 0;
|
|
483
|
+
const manualFieldUids = ref([]);
|
|
484
|
+
watch(() => manualFields.value.length, (n) => {
|
|
485
|
+
const uids = manualFieldUids.value;
|
|
486
|
+
while (uids.length < n) uids.push(nextManualFieldUid++);
|
|
487
|
+
if (uids.length > n) uids.splice(n);
|
|
488
|
+
}, { immediate: true });
|
|
188
489
|
const { loadForWorkflow, getSteps } = useIntegrationsRuns();
|
|
189
490
|
const runData = ref(null);
|
|
190
491
|
const runInfo = ref(null);
|
|
@@ -198,7 +499,7 @@ async function loadRunData() {
|
|
|
198
499
|
const { runs } = await loadForWorkflow(Number(props.workflowId), "", { perPage: 10 });
|
|
199
500
|
const source = runs.find((r) => r.status === "succeeded") ?? runs[0];
|
|
200
501
|
if (!source) {
|
|
201
|
-
runError.value = "
|
|
502
|
+
runError.value = t("integration.nodeInspector.input.runData.empty");
|
|
202
503
|
return;
|
|
203
504
|
}
|
|
204
505
|
const res = await getSteps(props.workflowId, source.id, true);
|
|
@@ -213,7 +514,7 @@ async function loadRunData() {
|
|
|
213
514
|
runData.value = map;
|
|
214
515
|
runInfo.value = { id: source.id, startedAt: source.startedAt };
|
|
215
516
|
} catch (e) {
|
|
216
|
-
runError.value = e?.message ?? "
|
|
517
|
+
runError.value = e?.message ?? t("integration.nodeInspector.input.runData.loadFailed");
|
|
217
518
|
} finally {
|
|
218
519
|
runLoading.value = false;
|
|
219
520
|
}
|
|
@@ -275,7 +576,7 @@ const sampleContext = computed(() => {
|
|
|
275
576
|
});
|
|
276
577
|
provide("integrationsSampleContext", sampleContext);
|
|
277
578
|
provide("integrationsNodeNames", computed(
|
|
278
|
-
() => Object.fromEntries((props.nodes ?? []).map((n) => [n.id,
|
|
579
|
+
() => Object.fromEntries((props.nodes ?? []).map((n) => [n.id, resolveDisplayName(n.name) || n.nodeSlug]))
|
|
279
580
|
));
|
|
280
581
|
const upstreamSections = computed(() => {
|
|
281
582
|
if (!isNode.value) return [];
|
|
@@ -283,7 +584,7 @@ const upstreamSections = computed(() => {
|
|
|
283
584
|
const m = findManifest(node.nodeSlug, node.nodeVersion);
|
|
284
585
|
return {
|
|
285
586
|
id: node.id,
|
|
286
|
-
name:
|
|
587
|
+
name: resolveDisplayName(node.name) || node.nodeSlug,
|
|
287
588
|
manifest: m,
|
|
288
589
|
direct,
|
|
289
590
|
ports: withSampledFields(portsForNode(node, m, props.dynamicOutputs?.[node.id]), sampleFor(node.id))
|
|
@@ -291,30 +592,25 @@ const upstreamSections = computed(() => {
|
|
|
291
592
|
});
|
|
292
593
|
});
|
|
293
594
|
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
595
|
const lastField = ref(null);
|
|
302
596
|
const lastFocusKind = ref(null);
|
|
597
|
+
const inputPaneOpen = ref(false);
|
|
303
598
|
const exprInsertHandler = ref(null);
|
|
304
599
|
provide("integrationsExpressionInsert", exprInsertHandler);
|
|
305
600
|
function onCenterFocusIn(e) {
|
|
306
|
-
const
|
|
307
|
-
if (
|
|
601
|
+
const target = e.target;
|
|
602
|
+
if (target instanceof HTMLElement && target.closest(".expr-editor")) {
|
|
308
603
|
lastFocusKind.value = "editor";
|
|
604
|
+
inputPaneOpen.value = true;
|
|
309
605
|
return;
|
|
310
606
|
}
|
|
311
|
-
if (
|
|
312
|
-
lastField.value =
|
|
607
|
+
if (target instanceof HTMLTextAreaElement) {
|
|
608
|
+
lastField.value = target;
|
|
313
609
|
lastFocusKind.value = "native";
|
|
314
610
|
return;
|
|
315
611
|
}
|
|
316
|
-
if (
|
|
317
|
-
lastField.value =
|
|
612
|
+
if (target instanceof HTMLInputElement && !["number", "checkbox", "radio", "range", "file", "color"].includes(target.type)) {
|
|
613
|
+
lastField.value = target;
|
|
318
614
|
lastFocusKind.value = "native";
|
|
319
615
|
}
|
|
320
616
|
}
|
|
@@ -336,9 +632,10 @@ function insertExpression(expression) {
|
|
|
336
632
|
void navigator.clipboard?.writeText(expression);
|
|
337
633
|
} catch {
|
|
338
634
|
}
|
|
339
|
-
toast("
|
|
635
|
+
toast(t("integration.nodeInspector.toast.expressionCopied"));
|
|
340
636
|
}
|
|
341
637
|
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";
|
|
638
|
+
const rowClass = "flex w-full min-w-0 cursor-grab items-center gap-1.5 rounded-md px-1.5 py-[3px] text-left font-mono text-[11px] text-slate-700 transition hover:bg-slate-100 active:cursor-grabbing";
|
|
342
639
|
function onChipDragStart(e, expression) {
|
|
343
640
|
e.dataTransfer?.setData("text/plain", expression);
|
|
344
641
|
if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy";
|
|
@@ -354,10 +651,10 @@ function onChipDragEnd() {
|
|
|
354
651
|
dragActive.value = false;
|
|
355
652
|
clearDropHover();
|
|
356
653
|
}
|
|
357
|
-
function expressionTarget(
|
|
358
|
-
if (
|
|
359
|
-
if (
|
|
360
|
-
if (
|
|
654
|
+
function expressionTarget(t2) {
|
|
655
|
+
if (t2 instanceof HTMLTextAreaElement) return t2;
|
|
656
|
+
if (t2 instanceof HTMLInputElement && !["number", "checkbox", "radio", "range", "file", "color"].includes(t2.type)) return t2;
|
|
657
|
+
if (t2 instanceof HTMLElement) return t2.closest(".expr-editor");
|
|
361
658
|
return null;
|
|
362
659
|
}
|
|
363
660
|
function onCenterDragEnter(e) {
|
|
@@ -408,6 +705,14 @@ const visibleVariableNames = computed(() => {
|
|
|
408
705
|
const q = searchQuery.value;
|
|
409
706
|
return q ? variableNames.value.filter((n) => n.toLowerCase().includes(q)) : variableNames.value;
|
|
410
707
|
});
|
|
708
|
+
const variableRows = computed(() => visibleVariableNames.value.map((name) => {
|
|
709
|
+
const def = props.variables?.[name]?.default;
|
|
710
|
+
return {
|
|
711
|
+
name,
|
|
712
|
+
glyph: typeGlyph(dataTypeOf(def)),
|
|
713
|
+
value: def === void 0 || def === "" ? null : previewValue(def)
|
|
714
|
+
};
|
|
715
|
+
}));
|
|
411
716
|
const searchEmpty = computed(
|
|
412
717
|
() => !!searchQuery.value && !visibleSections.value.length && !showTriggerChip.value && !visibleVariableNames.value.length
|
|
413
718
|
);
|
|
@@ -441,7 +746,7 @@ function typeGlyph(dataType) {
|
|
|
441
746
|
return dataType ? TYPE_GLYPHS[dataType] ?? null : null;
|
|
442
747
|
}
|
|
443
748
|
const sequence = computed(() => [
|
|
444
|
-
...(props.triggers ?? []).map((
|
|
749
|
+
...(props.triggers ?? []).map((t2) => t2.id),
|
|
445
750
|
...(props.nodes ?? []).map((n) => n.id)
|
|
446
751
|
]);
|
|
447
752
|
const seqIndex = computed(() => sequence.value.indexOf(draft.value.id));
|
|
@@ -475,17 +780,17 @@ function savePin() {
|
|
|
475
780
|
try {
|
|
476
781
|
parsed = JSON.parse(pinDraft.value);
|
|
477
782
|
} catch {
|
|
478
|
-
pinError.value = "
|
|
783
|
+
pinError.value = t("integration.nodeInspector.output.pin.invalidJson");
|
|
479
784
|
return;
|
|
480
785
|
}
|
|
481
786
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
482
|
-
pinError.value =
|
|
787
|
+
pinError.value = t("integration.nodeInspector.output.pin.invalidShape");
|
|
483
788
|
return;
|
|
484
789
|
}
|
|
485
790
|
pinnedSamples.value = { ...pinnedSamples.value, [draftNode.value.id]: parsed };
|
|
486
791
|
persistPins();
|
|
487
792
|
pinEditorOpen.value = false;
|
|
488
|
-
toast.success("
|
|
793
|
+
toast.success(t("integration.nodeInspector.toast.samplePinned"));
|
|
489
794
|
}
|
|
490
795
|
function removePin() {
|
|
491
796
|
if (!draftNode.value) return;
|
|
@@ -524,12 +829,12 @@ async function runTestStep() {
|
|
|
524
829
|
try {
|
|
525
830
|
const parsed = JSON.parse(testInputDraft.value.trim() || defaultTestInput());
|
|
526
831
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
527
|
-
testError.value = "
|
|
832
|
+
testError.value = t("integration.nodeInspector.output.test.inputNotObject");
|
|
528
833
|
return;
|
|
529
834
|
}
|
|
530
835
|
inputs = parsed;
|
|
531
836
|
} catch {
|
|
532
|
-
testError.value = "
|
|
837
|
+
testError.value = t("integration.nodeInspector.output.test.inputInvalidJson");
|
|
533
838
|
testInputOpen.value = true;
|
|
534
839
|
return;
|
|
535
840
|
}
|
|
@@ -547,10 +852,10 @@ async function runTestStep() {
|
|
|
547
852
|
[wn.id]: res.outputs && typeof res.outputs === "object" ? res.outputs : {}
|
|
548
853
|
};
|
|
549
854
|
outputView.value = "json";
|
|
550
|
-
toast.success("
|
|
855
|
+
toast.success(t("integration.nodeInspector.toast.testRunFinished"));
|
|
551
856
|
} catch (e) {
|
|
552
857
|
const body = e.body;
|
|
553
|
-
testError.value = body?.error?.message ?? body?.message ?? e?.message ?? "
|
|
858
|
+
testError.value = body?.error?.message ?? body?.message ?? e?.message ?? t("integration.nodeInspector.output.test.runFailed");
|
|
554
859
|
testLogs.value = body?.logs ?? [];
|
|
555
860
|
} finally {
|
|
556
861
|
testRunning.value = false;
|
|
@@ -563,31 +868,130 @@ async function runTestStep() {
|
|
|
563
868
|
width/height, border and the close control. -->
|
|
564
869
|
<div class="flex min-h-0 flex-1 flex-col bg-white">
|
|
565
870
|
<!-- Header -->
|
|
566
|
-
<div class="flex shrink-0 items-center gap-3 border-b border-black/[0.06] px-4 py-3
|
|
871
|
+
<div class="flex shrink-0 items-center gap-3 border-b border-black/[0.06] px-4 py-3">
|
|
567
872
|
<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
|
-
<
|
|
873
|
+
<IntegrationsNodeMark v-if="isNode" :manifest="manifest" :size="20" />
|
|
569
874
|
<component :is="triggerGlyph" v-else :size="18" />
|
|
570
875
|
</div>
|
|
571
876
|
<div class="min-w-0 flex-1">
|
|
877
|
+
<!-- Named explicitly: the placeholder is the node's slug, which left
|
|
878
|
+
this field accessibly called `revenexx:sftp-list` — a name for the
|
|
879
|
+
type, not for the field. -->
|
|
572
880
|
<input
|
|
573
881
|
ref="nameInput"
|
|
574
|
-
:
|
|
882
|
+
:aria-label="t('integration.nodeInspector.header.nameAria')"
|
|
883
|
+
:value="nameValue"
|
|
575
884
|
:placeholder="namePlaceholder"
|
|
576
|
-
class="w-full truncate bg-transparent text-sm font-semibold text-foreground outline-none placeholder:text-muted-foreground/60"
|
|
885
|
+
class="w-full truncate rounded-sm bg-transparent text-sm font-semibold text-foreground outline-none placeholder:text-muted-foreground/60 focus:ring-2 focus:ring-primary/40"
|
|
577
886
|
@input="writeName($event.target.value)"
|
|
578
887
|
>
|
|
579
|
-
<div class="
|
|
580
|
-
{{ subtitleText }}
|
|
888
|
+
<div class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
889
|
+
<span class="truncate">{{ subtitleText }}</span>
|
|
890
|
+
<!-- What the node TYPE is for, one click behind the line that names
|
|
891
|
+
the type. Not printed into the header: it is read once, when the
|
|
892
|
+
node is new to someone, and a manifest that writes three
|
|
893
|
+
paragraphs would otherwise push the form off the screen every
|
|
894
|
+
time it is opened by someone who already knows. No description in
|
|
895
|
+
the manifest, no question mark — nothing to ask. -->
|
|
896
|
+
<Popover v-if="manifestDescription">
|
|
897
|
+
<PopoverTrigger as-child>
|
|
898
|
+
<!-- The ring stays 16px because that is what a `?` in a subtitle
|
|
899
|
+
line should look like; the TARGET is grown to 24px by the
|
|
900
|
+
`after` overlay. A 16px target inline under the full-width
|
|
901
|
+
name input misses WCAG 2.5.8 and cannot claim the spacing
|
|
902
|
+
exception — a 24px circle centred here reaches into the input,
|
|
903
|
+
which is a target of its own. And this is the only route to
|
|
904
|
+
the description at all. -->
|
|
905
|
+
<button
|
|
906
|
+
type="button"
|
|
907
|
+
:aria-label="t('integration.nodeInspector.header.whatThisDoes')"
|
|
908
|
+
class="relative flex size-4 shrink-0 items-center justify-center rounded-full border border-black/[0.12] text-[10px] font-semibold leading-none transition after:absolute after:-inset-1 after:content-[''] hover:border-black/25 hover:bg-black/[0.04] hover:text-foreground"
|
|
909
|
+
>
|
|
910
|
+
?
|
|
911
|
+
</button>
|
|
912
|
+
</PopoverTrigger>
|
|
913
|
+
<PopoverContent :aria-label="t('integration.nodeInspector.header.whatThisDoes')" align="start" class="w-96 rounded-lg border border-black/[0.06] bg-white p-4 shadow-lg">
|
|
914
|
+
<div class="flex items-center gap-2.5">
|
|
915
|
+
<span class="flex size-8 shrink-0 items-center justify-center rounded-md border border-black/[0.06] bg-slate-50 text-slate-600">
|
|
916
|
+
<IntegrationsNodeMark :manifest="manifest" :size="18" />
|
|
917
|
+
</span>
|
|
918
|
+
<div class="min-w-0">
|
|
919
|
+
<div class="truncate text-sm font-semibold text-foreground">
|
|
920
|
+
{{ resolveLocalized(manifest?.name) }}
|
|
921
|
+
</div>
|
|
922
|
+
<div class="truncate text-xs text-muted-foreground">
|
|
923
|
+
{{ subtitleText }}
|
|
924
|
+
</div>
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
<!-- Reading matter, so it gets reading measure and scrolls when
|
|
928
|
+
the node package is generous. -->
|
|
929
|
+
<p class="mt-3 max-h-64 overflow-y-auto text-sm leading-relaxed text-foreground/80">
|
|
930
|
+
{{ manifestDescription }}
|
|
931
|
+
</p>
|
|
932
|
+
</PopoverContent>
|
|
933
|
+
</Popover>
|
|
581
934
|
</div>
|
|
582
935
|
</div>
|
|
583
936
|
<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
937
|
{{ groupLabel }}
|
|
585
938
|
</span>
|
|
939
|
+
<!-- The user's own note. A control of its own, deliberately not a second
|
|
940
|
+
paragraph under the name: the line above is what the node TYPE does
|
|
941
|
+
and cannot be edited, this is what one person wrote about this one
|
|
942
|
+
node. Named like the badge on the card, so the canvas and the dialog
|
|
943
|
+
agree on what the thing is called. -->
|
|
944
|
+
<Popover v-if="draftNode" v-model:open="noteOpen">
|
|
945
|
+
<PopoverTrigger as-child>
|
|
946
|
+
<button
|
|
947
|
+
type="button"
|
|
948
|
+
class="flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium transition"
|
|
949
|
+
:class="noteText ? 'border-amber-500/40 bg-amber-500/10 text-amber-700' : 'border-black/[0.08] text-muted-foreground hover:bg-black/[0.04] hover:text-foreground'"
|
|
950
|
+
>
|
|
951
|
+
<MessageSquareText :size="12" />
|
|
952
|
+
{{ noteLabel }}
|
|
953
|
+
</button>
|
|
954
|
+
</PopoverTrigger>
|
|
955
|
+
<PopoverContent :aria-label="t('integration.nodeInspector.note.label')" align="end" class="w-80 space-y-1.5 rounded-lg border border-black/[0.06] bg-white p-3 shadow-lg">
|
|
956
|
+
<Label class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.note.label") }}</Label>
|
|
957
|
+
<Textarea
|
|
958
|
+
:aria-label="t('integration.nodeInspector.note.label')"
|
|
959
|
+
:model-value="draftNode.notes ?? ''"
|
|
960
|
+
:rows="4"
|
|
961
|
+
:placeholder="t('integration.nodeInspector.note.placeholder')"
|
|
962
|
+
class="rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
|
|
963
|
+
@update:model-value="writeNote"
|
|
964
|
+
/>
|
|
965
|
+
</PopoverContent>
|
|
966
|
+
</Popover>
|
|
967
|
+
<!-- The Input pane on request. Also opens by itself when the cursor lands
|
|
968
|
+
in an expression field — this is for reading it, or for dragging out
|
|
969
|
+
of it, before a field is chosen.
|
|
970
|
+
|
|
971
|
+
Not offered on a trigger: a trigger starts the workflow, so there is no
|
|
972
|
+
upstream data, and none of its settings takes an expression — the pane
|
|
973
|
+
would open onto its own explanation of why it is empty, in a dialog
|
|
974
|
+
sized for the few controls a trigger has. -->
|
|
975
|
+
<button
|
|
976
|
+
v-if="isNode"
|
|
977
|
+
type="button"
|
|
978
|
+
class="flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium transition"
|
|
979
|
+
:class="inputPaneOpen ? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-700' : 'border-black/[0.08] text-muted-foreground hover:bg-black/[0.04] hover:text-foreground'"
|
|
980
|
+
:aria-pressed="inputPaneOpen"
|
|
981
|
+
@click="inputPaneOpen = !inputPaneOpen"
|
|
982
|
+
>
|
|
983
|
+
<Database :size="12" />
|
|
984
|
+
{{ t("integration.nodeInspector.header.inputData") }}
|
|
985
|
+
</button>
|
|
986
|
+
<!-- Icon-only controls: `title` alone leaves them nameless to a screen
|
|
987
|
+
reader (and unaddressable to a test without a CSS selector), so each
|
|
988
|
+
carries the same sentence as an aria-label. -->
|
|
586
989
|
<div v-if="sequence.length > 1" class="flex shrink-0 items-center gap-0.5 border-l border-black/[0.06] pl-2">
|
|
587
990
|
<button
|
|
588
991
|
type="button"
|
|
589
992
|
:disabled="!hasPrev"
|
|
590
|
-
|
|
993
|
+
:aria-label="t('integration.nodeInspector.header.previousStep')"
|
|
994
|
+
:title="t('integration.nodeInspector.header.previousStep')"
|
|
591
995
|
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
996
|
@click="navigate(-1)"
|
|
593
997
|
>
|
|
@@ -596,33 +1000,46 @@ async function runTestStep() {
|
|
|
596
1000
|
<button
|
|
597
1001
|
type="button"
|
|
598
1002
|
:disabled="!hasNext"
|
|
599
|
-
|
|
1003
|
+
:aria-label="t('integration.nodeInspector.header.nextStep')"
|
|
1004
|
+
:title="t('integration.nodeInspector.header.nextStep')"
|
|
600
1005
|
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
1006
|
@click="navigate(1)"
|
|
602
1007
|
>
|
|
603
1008
|
<ChevronRight :size="14" />
|
|
604
1009
|
</button>
|
|
605
1010
|
</div>
|
|
1011
|
+
<!-- Close lives in the header row so it lines up with the chevrons
|
|
1012
|
+
(the DialogContent built-in X floats at its own offset). -->
|
|
1013
|
+
<button
|
|
1014
|
+
type="button"
|
|
1015
|
+
:aria-label="t('integration.common.action.close')"
|
|
1016
|
+
:title="t('integration.common.action.close')"
|
|
1017
|
+
class="-mr-1.5 flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-black/[0.04] hover:text-foreground"
|
|
1018
|
+
@click="emit('close')"
|
|
1019
|
+
>
|
|
1020
|
+
<X :size="15" />
|
|
1021
|
+
</button>
|
|
606
1022
|
</div>
|
|
607
1023
|
|
|
608
1024
|
<!-- Body: Input pane + tabbed center -->
|
|
609
1025
|
<div class="flex min-h-0 flex-1">
|
|
610
1026
|
<!-- Input pane -->
|
|
611
|
-
<aside class="flex w-[300px] shrink-0 flex-col border-r border-black/[0.06] bg-muted/10" @dragend="onChipDragEnd">
|
|
1027
|
+
<aside v-if="inputPaneOpen" class="flex w-[300px] shrink-0 flex-col border-r border-black/[0.06] bg-muted/10" @dragend="onChipDragEnd">
|
|
612
1028
|
<div class="shrink-0 border-b border-black/[0.06] px-3 py-2.5">
|
|
613
|
-
<div class="text-
|
|
614
|
-
|
|
1029
|
+
<div class="text-mono text-muted-foreground">
|
|
1030
|
+
{{ t("integration.nodeInspector.input.title") }}
|
|
615
1031
|
</div>
|
|
616
1032
|
<p class="mt-0.5 text-xs leading-snug text-muted-foreground">
|
|
617
|
-
|
|
1033
|
+
{{ t("integration.nodeInspector.input.description") }}
|
|
618
1034
|
</p>
|
|
619
1035
|
<div class="relative mt-2">
|
|
620
1036
|
<Search :size="12" class="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
621
1037
|
<input
|
|
622
1038
|
v-model="inputSearch"
|
|
623
1039
|
type="text"
|
|
624
|
-
|
|
625
|
-
|
|
1040
|
+
:aria-label="t('integration.nodeInspector.input.searchAria')"
|
|
1041
|
+
:placeholder="t('integration.nodeInspector.input.searchAria')"
|
|
1042
|
+
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] focus:ring-2 focus:ring-primary/40"
|
|
626
1043
|
>
|
|
627
1044
|
</div>
|
|
628
1045
|
<div v-if="workflowId" class="mt-2 flex items-center gap-1.5">
|
|
@@ -633,15 +1050,15 @@ async function runTestStep() {
|
|
|
633
1050
|
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
1051
|
@click="loadRunData"
|
|
635
1052
|
>
|
|
636
|
-
{{ runLoading ? "
|
|
1053
|
+
{{ runLoading ? t("integration.nodeInspector.input.runData.loading") : t("integration.nodeInspector.input.runData.use") }}
|
|
637
1054
|
</button>
|
|
638
1055
|
<template v-else>
|
|
639
1056
|
<span class="min-w-0 flex-1 truncate text-[11px] text-emerald-700">
|
|
640
|
-
|
|
1057
|
+
{{ t("integration.nodeInspector.input.runData.from", { id: runInfo.id }) }}
|
|
641
1058
|
</span>
|
|
642
1059
|
<button
|
|
643
1060
|
type="button"
|
|
644
|
-
title="
|
|
1061
|
+
:title="t('integration.nodeInspector.input.runData.clear')"
|
|
645
1062
|
class="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-foreground"
|
|
646
1063
|
@click="clearRunData"
|
|
647
1064
|
>
|
|
@@ -658,10 +1075,10 @@ async function runTestStep() {
|
|
|
658
1075
|
<!-- Upstream nodes -->
|
|
659
1076
|
<div v-if="!searchQuery || visibleSections.length" class="space-y-3">
|
|
660
1077
|
<div class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
661
|
-
|
|
1078
|
+
{{ t("integration.nodeInspector.input.nodes.heading") }}
|
|
662
1079
|
</div>
|
|
663
1080
|
<p v-if="!upstreamSections.length" class="text-xs leading-snug text-muted-foreground">
|
|
664
|
-
|
|
1081
|
+
{{ t("integration.nodeInspector.input.nodes.empty") }}
|
|
665
1082
|
</p>
|
|
666
1083
|
<div v-for="s in visibleSections" :key="s.id" class="rounded-md border border-black/[0.06]">
|
|
667
1084
|
<button
|
|
@@ -670,14 +1087,14 @@ async function runTestStep() {
|
|
|
670
1087
|
@click="toggleNodeSection(s.id)"
|
|
671
1088
|
>
|
|
672
1089
|
<ChevronRight :size="11" class="shrink-0 text-muted-foreground transition-transform" :class="sectionClosed(s.id) ? '' : 'rotate-90'" />
|
|
673
|
-
<
|
|
1090
|
+
<IntegrationsNodeMark :manifest="s.manifest" :size="14" :badge="false" />
|
|
674
1091
|
<span class="min-w-0 truncate text-xs font-medium text-foreground">{{ s.name }}</span>
|
|
675
1092
|
<span
|
|
676
1093
|
v-if="!s.direct"
|
|
677
1094
|
class="shrink-0 rounded bg-slate-100 px-1 py-px text-[9px] font-medium uppercase tracking-wider text-slate-500"
|
|
678
|
-
title="
|
|
1095
|
+
:title="t('integration.nodeInspector.input.nodes.indirectTitle')"
|
|
679
1096
|
>
|
|
680
|
-
|
|
1097
|
+
{{ t("integration.nodeInspector.input.nodes.indirectBadge") }}
|
|
681
1098
|
</span>
|
|
682
1099
|
</button>
|
|
683
1100
|
<div v-show="!sectionClosed(s.id)" class="space-y-2 border-t border-black/[0.06] px-2 py-2">
|
|
@@ -686,25 +1103,37 @@ async function runTestStep() {
|
|
|
686
1103
|
<span class="size-1.5 rounded-full" :class="portDotClass(p.kind, p.name)" />
|
|
687
1104
|
<span class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">{{ p.name }}</span>
|
|
688
1105
|
</div>
|
|
689
|
-
|
|
1106
|
+
<!-- Schema rows, n8n-style: glyph · key · sampled value. -->
|
|
1107
|
+
<div class="space-y-px">
|
|
690
1108
|
<button
|
|
691
1109
|
type="button"
|
|
692
|
-
:class="
|
|
1110
|
+
:class="rowClass"
|
|
1111
|
+
:aria-label="nodeOutputExpr(s.id, p.name)"
|
|
693
1112
|
:title="nodeOutputExpr(s.id, p.name)"
|
|
694
1113
|
draggable="true"
|
|
695
1114
|
@dragstart="onChipDragStart($event, nodeOutputExpr(s.id, p.name))"
|
|
696
1115
|
@click="insertExpression(nodeOutputExpr(s.id, p.name))"
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
v-for="
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1116
|
+
>
|
|
1117
|
+
<span class="w-3 shrink-0 text-center font-semibold text-slate-400">{}</span>
|
|
1118
|
+
<span class="shrink-0 font-medium"><span v-for="(seg, i) in highlightSegments(p.name)" :key="i" :class="seg.hit ? 'rounded-[2px] bg-amber-200/80' : ''">{{ seg.text }}</span></span>
|
|
1119
|
+
</button>
|
|
1120
|
+
<div class="space-y-px pl-3">
|
|
1121
|
+
<button
|
|
1122
|
+
v-for="f in p.fields"
|
|
1123
|
+
:key="f.name"
|
|
1124
|
+
type="button"
|
|
1125
|
+
:class="rowClass"
|
|
1126
|
+
:aria-label="nodeOutputExpr(s.id, p.name, f.name)"
|
|
1127
|
+
:title="nodeOutputExpr(s.id, p.name, f.name) + (f.dataType ? ' \xB7 ' + f.dataType : '')"
|
|
1128
|
+
draggable="true"
|
|
1129
|
+
@dragstart="onChipDragStart($event, nodeOutputExpr(s.id, p.name, f.name))"
|
|
1130
|
+
@click="insertExpression(nodeOutputExpr(s.id, p.name, f.name))"
|
|
1131
|
+
>
|
|
1132
|
+
<span class="w-3 shrink-0 text-center font-semibold" :class="typeGlyph(f.dataType)?.cls ?? 'text-slate-300'">{{ typeGlyph(f.dataType)?.char ?? "\xB7" }}</span>
|
|
1133
|
+
<span class="shrink-0"><span v-for="(seg, i) in highlightSegments(f.name)" :key="i" :class="seg.hit ? 'rounded-[2px] bg-amber-200/80' : ''">{{ seg.text }}</span></span>
|
|
1134
|
+
<span v-if="chipValue(s.id, p.name, f.name) !== null" class="min-w-0 flex-1 truncate text-slate-400">{{ chipValue(s.id, p.name, f.name) }}</span>
|
|
1135
|
+
</button>
|
|
1136
|
+
</div>
|
|
708
1137
|
</div>
|
|
709
1138
|
</div>
|
|
710
1139
|
</div>
|
|
@@ -714,12 +1143,13 @@ async function runTestStep() {
|
|
|
714
1143
|
<!-- Trigger payload -->
|
|
715
1144
|
<div v-if="showTriggerChip" class="space-y-1.5">
|
|
716
1145
|
<div class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
717
|
-
|
|
1146
|
+
{{ t("integration.nodeInspector.input.trigger.heading") }}
|
|
718
1147
|
</div>
|
|
719
1148
|
<div class="flex flex-wrap gap-1">
|
|
720
1149
|
<button
|
|
721
1150
|
type="button"
|
|
722
1151
|
:class="chipClass"
|
|
1152
|
+
:aria-label="triggerPayloadExpr()"
|
|
723
1153
|
:title="triggerPayloadExpr()"
|
|
724
1154
|
draggable="true"
|
|
725
1155
|
@dragstart="onChipDragStart($event, triggerPayloadExpr())"
|
|
@@ -729,64 +1159,53 @@ async function runTestStep() {
|
|
|
729
1159
|
</button>
|
|
730
1160
|
</div>
|
|
731
1161
|
<p class="text-xs leading-snug text-muted-foreground">
|
|
732
|
-
|
|
1162
|
+
{{ t("integration.nodeInspector.input.trigger.hint") }}
|
|
733
1163
|
</p>
|
|
734
1164
|
</div>
|
|
735
1165
|
</template>
|
|
736
1166
|
|
|
737
1167
|
<p v-else class="text-xs leading-snug text-muted-foreground">
|
|
738
|
-
|
|
1168
|
+
{{ t("integration.nodeInspector.input.noUpstreamForTrigger") }}
|
|
739
1169
|
</p>
|
|
740
1170
|
|
|
741
1171
|
<!-- Workflow variables -->
|
|
742
1172
|
<div v-if="!searchQuery || visibleVariableNames.length" class="space-y-1.5">
|
|
743
1173
|
<div class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
744
|
-
|
|
1174
|
+
{{ t("integration.nodeInspector.input.variables.heading") }}
|
|
745
1175
|
</div>
|
|
746
|
-
|
|
1176
|
+
<!-- Schema rows with the declared default as the value. -->
|
|
1177
|
+
<div v-if="variableRows.length" class="space-y-px">
|
|
747
1178
|
<button
|
|
748
|
-
v-for="
|
|
749
|
-
:key="name"
|
|
1179
|
+
v-for="row in variableRows"
|
|
1180
|
+
:key="row.name"
|
|
750
1181
|
type="button"
|
|
751
|
-
:class="
|
|
752
|
-
:
|
|
1182
|
+
:class="rowClass"
|
|
1183
|
+
:aria-label="variableExpr(row.name)"
|
|
1184
|
+
:title="variableExpr(row.name)"
|
|
753
1185
|
draggable="true"
|
|
754
|
-
@dragstart="onChipDragStart($event, variableExpr(name))"
|
|
755
|
-
@click="insertExpression(variableExpr(name))"
|
|
756
|
-
|
|
1186
|
+
@dragstart="onChipDragStart($event, variableExpr(row.name))"
|
|
1187
|
+
@click="insertExpression(variableExpr(row.name))"
|
|
1188
|
+
>
|
|
1189
|
+
<span class="w-3 shrink-0 text-center font-semibold" :class="row.glyph?.cls ?? 'text-slate-300'">{{ row.glyph?.char ?? "\xB7" }}</span>
|
|
1190
|
+
<span class="shrink-0"><span v-for="(seg, i) in highlightSegments(row.name)" :key="i" :class="seg.hit ? 'rounded-[2px] bg-amber-200/80' : ''">{{ seg.text }}</span></span>
|
|
1191
|
+
<span v-if="row.value !== null" class="min-w-0 flex-1 truncate text-slate-400">{{ row.value }}</span>
|
|
1192
|
+
</button>
|
|
757
1193
|
</div>
|
|
758
1194
|
<p v-else class="text-xs leading-snug text-muted-foreground">
|
|
759
|
-
|
|
1195
|
+
{{ t("integration.nodeInspector.input.variables.empty") }}
|
|
760
1196
|
</p>
|
|
761
1197
|
</div>
|
|
762
1198
|
|
|
763
1199
|
<p v-if="searchEmpty" class="text-xs leading-snug text-muted-foreground">
|
|
764
|
-
|
|
1200
|
+
{{ t("integration.nodeInspector.input.search.empty", { query: inputSearch.trim() }) }}
|
|
765
1201
|
</p>
|
|
766
1202
|
</div>
|
|
767
1203
|
</aside>
|
|
768
1204
|
|
|
769
1205
|
<!-- Center: tabs + content -->
|
|
770
1206
|
<section class="flex min-w-0 flex-1 flex-col">
|
|
771
|
-
|
|
772
|
-
|
|
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>
|
|
789
|
-
</div>
|
|
1207
|
+
<!-- Same TabBar as the workflow page — one tab look everywhere. -->
|
|
1208
|
+
<TabBar v-model="tab" :tabs="inspectorTabs" class="shrink-0 px-4" />
|
|
790
1209
|
|
|
791
1210
|
<div
|
|
792
1211
|
class="min-h-0 flex-1 overflow-y-auto p-4"
|
|
@@ -797,131 +1216,437 @@ async function runTestStep() {
|
|
|
797
1216
|
@drop="onCenterDrop"
|
|
798
1217
|
>
|
|
799
1218
|
<!-- Parameters: trigger config -->
|
|
800
|
-
|
|
1219
|
+
<!-- Full width: triggers have no output pane, a capped column
|
|
1220
|
+
would float lost in the wide center. -->
|
|
1221
|
+
<div v-if="!isNode && draftTrigger" class="w-full space-y-3">
|
|
801
1222
|
<!-- Active toggle. Persists with the workflow save like every other trigger field. -->
|
|
802
|
-
<div class="flex items-center
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
<p class="text-xs text-muted-foreground">
|
|
806
|
-
A run needs at least one active trigger.
|
|
807
|
-
</p>
|
|
808
|
-
</div>
|
|
1223
|
+
<div class="flex items-center gap-3 rounded-lg border border-black/[0.06] bg-muted/20 px-3 py-2">
|
|
1224
|
+
<!-- The name lives in the text next to the switch, which is not
|
|
1225
|
+
a <label> — so the switch says it itself. -->
|
|
809
1226
|
<Switch
|
|
1227
|
+
:aria-label="t('integration.nodeInspector.trigger.active.label')"
|
|
1228
|
+
class="data-[state=unchecked]:bg-slate-300"
|
|
810
1229
|
:model-value="draftTrigger.active !== false"
|
|
811
1230
|
@update:model-value="(v) => patchTrigger({ active: v })"
|
|
812
1231
|
/>
|
|
1232
|
+
<div>
|
|
1233
|
+
<div class="text-sm font-medium">{{ t("integration.nodeInspector.trigger.active.label") }}</div>
|
|
1234
|
+
<p class="text-xs text-muted-foreground">
|
|
1235
|
+
{{ t("integration.nodeInspector.trigger.active.hint") }}
|
|
1236
|
+
</p>
|
|
1237
|
+
</div>
|
|
813
1238
|
</div>
|
|
1239
|
+
<!--
|
|
1240
|
+
The schedule is picked, not written. Each frequency shows only its
|
|
1241
|
+
own fields, and the expression appears in one place only: the
|
|
1242
|
+
Advanced field, for a schedule no picker can represent.
|
|
1243
|
+
-->
|
|
814
1244
|
<template v-if="draftTrigger.type === 'schedule'">
|
|
815
|
-
<div class="
|
|
816
|
-
<
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1245
|
+
<div class="grid grid-cols-2 gap-3">
|
|
1246
|
+
<div class="space-y-1.5">
|
|
1247
|
+
<!-- "Frequency", not "Runs": a run is an execution everywhere
|
|
1248
|
+
else in the studio, and there is a whole page of them. -->
|
|
1249
|
+
<Label for="ni-frequency" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.frequency.label") }}</Label>
|
|
1250
|
+
<Select :model-value="frequency" @update:model-value="applyFrequency">
|
|
1251
|
+
<SelectTrigger id="ni-frequency" :aria-label="t('integration.nodeInspector.schedule.frequency.label')" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
1252
|
+
<SelectValue />
|
|
1253
|
+
</SelectTrigger>
|
|
1254
|
+
<SelectContent>
|
|
1255
|
+
<SelectItem v-for="f in FREQUENCY_OPTIONS" :key="f.value" :value="f.value">{{ f.label }}</SelectItem>
|
|
1256
|
+
<SelectSeparator />
|
|
1257
|
+
<SelectItem :value="FREQUENCY_ADVANCED">{{ t("integration.nodeInspector.schedule.frequency.advanced") }}</SelectItem>
|
|
1258
|
+
</SelectContent>
|
|
1259
|
+
</Select>
|
|
1260
|
+
</div>
|
|
1261
|
+
|
|
1262
|
+
<!-- Every few minutes: the gap between firings. -->
|
|
1263
|
+
<div v-if="frequency === 'minutes'" class="space-y-1.5">
|
|
1264
|
+
<Label for="ni-minute-interval" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.interval.label") }}</Label>
|
|
1265
|
+
<Select :model-value="String(carry.minuteInterval)" @update:model-value="applyMinuteInterval">
|
|
1266
|
+
<SelectTrigger id="ni-minute-interval" :aria-label="t('integration.nodeInspector.schedule.interval.label')" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
1267
|
+
<SelectValue />
|
|
1268
|
+
</SelectTrigger>
|
|
1269
|
+
<SelectContent>
|
|
1270
|
+
<SelectItem v-for="n in minuteIntervalOptions" :key="n" :value="String(n)">
|
|
1271
|
+
{{ t("integration.nodeInspector.schedule.minuteInterval.option", n) }}
|
|
1272
|
+
</SelectItem>
|
|
1273
|
+
</SelectContent>
|
|
1274
|
+
</Select>
|
|
1275
|
+
</div>
|
|
1276
|
+
|
|
1277
|
+
<!-- Every few hours: the gap, plus where in the hour it lands. -->
|
|
1278
|
+
<div v-if="frequency === 'hours'" class="space-y-1.5">
|
|
1279
|
+
<Label for="ni-hour-interval" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.interval.label") }}</Label>
|
|
1280
|
+
<Select :model-value="String(carry.hourInterval)" @update:model-value="applyHourInterval">
|
|
1281
|
+
<SelectTrigger id="ni-hour-interval" :aria-label="t('integration.nodeInspector.schedule.interval.label')" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
1282
|
+
<SelectValue />
|
|
1283
|
+
</SelectTrigger>
|
|
1284
|
+
<SelectContent>
|
|
1285
|
+
<SelectItem v-for="n in HOUR_INTERVALS" :key="n" :value="String(n)">{{ t("integration.nodeInspector.schedule.hourInterval.option", { n }) }}</SelectItem>
|
|
1286
|
+
</SelectContent>
|
|
1287
|
+
</Select>
|
|
1288
|
+
</div>
|
|
1289
|
+
|
|
1290
|
+
<div v-if="frequency === 'hourly' || frequency === 'hours' || frequency === 'times'" class="space-y-1.5">
|
|
1291
|
+
<Label for="ni-minute-past" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.minutePast.label") }}</Label>
|
|
1292
|
+
<Select :model-value="String(carry.minute)" @update:model-value="applyMinutePast">
|
|
1293
|
+
<SelectTrigger id="ni-minute-past" :aria-label="t('integration.nodeInspector.schedule.minutePast.label')" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
1294
|
+
<SelectValue />
|
|
1295
|
+
</SelectTrigger>
|
|
1296
|
+
<SelectContent>
|
|
1297
|
+
<SelectItem v-for="n in minutePastOptions" :key="n" :value="String(n)">:{{ pad2(n) }}</SelectItem>
|
|
1298
|
+
</SelectContent>
|
|
1299
|
+
</Select>
|
|
1300
|
+
</div>
|
|
1301
|
+
|
|
1302
|
+
<!-- A fixed time of day: the one field all three calendar
|
|
1303
|
+
rhythms share, which is why it survives a change of
|
|
1304
|
+
frequency. -->
|
|
1305
|
+
<div v-if="frequency === 'daily' || frequency === 'weekly' || frequency === 'monthly'" class="space-y-1.5">
|
|
1306
|
+
<Label for="ni-time" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.time.label") }}</Label>
|
|
1307
|
+
<Input
|
|
1308
|
+
id="ni-time"
|
|
1309
|
+
type="time"
|
|
1310
|
+
:aria-label="t('integration.nodeInspector.schedule.time.label')"
|
|
1311
|
+
:model-value="timeValue"
|
|
1312
|
+
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white tabular-nums"
|
|
1313
|
+
@update:model-value="applyTime"
|
|
1314
|
+
/>
|
|
1315
|
+
</div>
|
|
1316
|
+
|
|
1317
|
+
<div v-if="frequency === 'monthly'" class="space-y-1.5">
|
|
1318
|
+
<Label for="ni-day-of-month" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.dayOfMonth.label") }}</Label>
|
|
1319
|
+
<Select :model-value="String(carry.day)" @update:model-value="applyDayOfMonth">
|
|
1320
|
+
<SelectTrigger id="ni-day-of-month" :aria-label="t('integration.nodeInspector.schedule.dayOfMonth.label')" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
1321
|
+
<SelectValue />
|
|
1322
|
+
</SelectTrigger>
|
|
1323
|
+
<SelectContent>
|
|
1324
|
+
<SelectItem v-for="n in 31" :key="n" :value="String(n)">{{ n }}</SelectItem>
|
|
1325
|
+
</SelectContent>
|
|
1326
|
+
</Select>
|
|
1327
|
+
</div>
|
|
1328
|
+
|
|
1329
|
+
<!-- Advanced: the expression itself, the only place it is shown. -->
|
|
1330
|
+
<div v-if="frequency === FREQUENCY_ADVANCED" class="space-y-1.5">
|
|
1331
|
+
<Label for="ni-cron" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.cron.label") }}</Label>
|
|
1332
|
+
<Input
|
|
1333
|
+
id="ni-cron"
|
|
1334
|
+
:aria-label="t('integration.nodeInspector.schedule.cron.label')"
|
|
1335
|
+
:model-value="cronValue"
|
|
1336
|
+
placeholder="0 9-17 * * 1-5"
|
|
1337
|
+
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white font-mono"
|
|
1338
|
+
@update:model-value="(v) => writeTriggerConfig('cron', v)"
|
|
1339
|
+
/>
|
|
1340
|
+
</div>
|
|
823
1341
|
</div>
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1342
|
+
|
|
1343
|
+
<!--
|
|
1344
|
+
The hours of "several times a day". Two rows of twelve, and the
|
|
1345
|
+
minute above is shared by all of them: cron cannot express times
|
|
1346
|
+
with different minutes in one expression, so 07:30 and 09:15 is two
|
|
1347
|
+
schedule triggers rather than one field per row.
|
|
1348
|
+
-->
|
|
1349
|
+
<div v-if="frequency === 'times'" class="space-y-1.5">
|
|
1350
|
+
<Label class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.hours.label") }}</Label>
|
|
1351
|
+
<ToggleGroup
|
|
1352
|
+
type="multiple"
|
|
1353
|
+
:aria-label="t('integration.nodeInspector.schedule.hours.label')"
|
|
1354
|
+
:model-value="selectedHours"
|
|
1355
|
+
class="grid w-fit grid-cols-12 gap-1"
|
|
1356
|
+
@update:model-value="applyHours"
|
|
1357
|
+
>
|
|
1358
|
+
<ToggleGroupItem
|
|
1359
|
+
v-for="h in HOURS_OF_DAY"
|
|
1360
|
+
:key="h"
|
|
1361
|
+
:value="String(h)"
|
|
1362
|
+
:aria-label="`${pad2(h)}:${pad2(carry.minute)}`"
|
|
1363
|
+
class="h-7 w-9 rounded-md border border-black/[0.08] bg-white text-xs tabular-nums data-[state=on]:border-emerald-500/40 data-[state=on]:bg-emerald-500/10 data-[state=on]:text-emerald-700"
|
|
1364
|
+
>
|
|
1365
|
+
{{ pad2(h) }}
|
|
1366
|
+
</ToggleGroupItem>
|
|
1367
|
+
</ToggleGroup>
|
|
832
1368
|
</div>
|
|
1369
|
+
|
|
1370
|
+
<!-- Weekdays: full width, because seven toggles in half of the
|
|
1371
|
+
grid would wrap. Serves both rhythms that fall on days. -->
|
|
1372
|
+
<div v-if="frequency === 'weekly' || frequency === 'times'" class="space-y-1.5">
|
|
1373
|
+
<Label class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.days.label") }}</Label>
|
|
1374
|
+
<ToggleGroup
|
|
1375
|
+
type="multiple"
|
|
1376
|
+
:aria-label="t('integration.nodeInspector.schedule.days.label')"
|
|
1377
|
+
:model-value="selectedDays"
|
|
1378
|
+
class="justify-start gap-1"
|
|
1379
|
+
@update:model-value="applyDays"
|
|
1380
|
+
>
|
|
1381
|
+
<ToggleGroupItem
|
|
1382
|
+
v-for="d in DAY_OPTIONS"
|
|
1383
|
+
:key="d.value"
|
|
1384
|
+
:value="String(d.value)"
|
|
1385
|
+
:aria-label="d.name"
|
|
1386
|
+
class="h-8 w-10 rounded-md border border-black/[0.08] bg-white text-sm data-[state=on]:border-emerald-500/40 data-[state=on]:bg-emerald-500/10 data-[state=on]:text-emerald-700"
|
|
1387
|
+
>
|
|
1388
|
+
{{ d.short }}
|
|
1389
|
+
</ToggleGroupItem>
|
|
1390
|
+
</ToggleGroup>
|
|
1391
|
+
</div>
|
|
1392
|
+
|
|
1393
|
+
<!--
|
|
1394
|
+
The confirmation. The timezone is in it because the platform reads
|
|
1395
|
+
the expression's numbers as wall-clock time in that zone, so a time
|
|
1396
|
+
without its zone would be half an answer.
|
|
1397
|
+
-->
|
|
1398
|
+
<p v-if="scheduleSentence" :aria-label="t('integration.nodeInspector.schedule.whenItFiresAria')" class="text-xs text-emerald-700">{{ scheduleSentence }}</p>
|
|
1399
|
+
<!-- No expression at all is its own answer, and the same one the
|
|
1400
|
+
Schedules page gives for this trigger: it is not written as
|
|
1401
|
+
anything, and it does not run. A trigger placed from the palette
|
|
1402
|
+
carries an expression, so this is for one that arrived without
|
|
1403
|
+
one. -->
|
|
1404
|
+
<p v-else-if="!hasCron" :aria-label="t('integration.nodeInspector.schedule.whenItFiresAria')" class="text-xs text-muted-foreground">
|
|
1405
|
+
{{ t("integration.nodeInspector.schedule.noneSet") }}
|
|
1406
|
+
</p>
|
|
1407
|
+
<p v-else class="text-xs text-muted-foreground">
|
|
1408
|
+
{{ t("integration.nodeInspector.schedule.writtenAsExpression") }}
|
|
1409
|
+
</p>
|
|
1410
|
+
<p v-if="frequency === 'monthly' && carry.day >= 29" class="text-xs text-muted-foreground">
|
|
1411
|
+
{{ t("integration.nodeInspector.schedule.monthlySkipHint") }}
|
|
1412
|
+
</p>
|
|
833
1413
|
</template>
|
|
834
1414
|
<template v-else-if="draftTrigger.type === 'webhook'">
|
|
835
1415
|
<div class="space-y-1.5">
|
|
836
|
-
<Label class="text-mono text-muted-foreground">
|
|
1416
|
+
<Label for="ni-webhook-url" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.url.label") }}</Label>
|
|
1417
|
+
<div v-if="webhookUrl" class="flex items-center gap-1.5">
|
|
1418
|
+
<Input
|
|
1419
|
+
id="ni-webhook-url"
|
|
1420
|
+
readonly
|
|
1421
|
+
:model-value="webhookUrl"
|
|
1422
|
+
class="h-8 flex-1 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white font-mono text-xs"
|
|
1423
|
+
@focus="$event.target.select()"
|
|
1424
|
+
/>
|
|
1425
|
+
<button
|
|
1426
|
+
type="button"
|
|
1427
|
+
:title="t('integration.nodeInspector.trigger.webhook.url.copyTitle')"
|
|
1428
|
+
class="flex size-8 shrink-0 items-center justify-center rounded-md border border-black/[0.08] bg-white text-muted-foreground transition hover:text-foreground"
|
|
1429
|
+
@click="copyWebhookUrl"
|
|
1430
|
+
>
|
|
1431
|
+
<Copy :size="14" />
|
|
1432
|
+
</button>
|
|
1433
|
+
</div>
|
|
1434
|
+
<p class="text-xs text-muted-foreground">
|
|
1435
|
+
<template v-if="webhookIsPublic">{{ t("integration.nodeInspector.trigger.webhook.url.publicHint") }}</template>
|
|
1436
|
+
<template v-else>{{ t("integration.nodeInspector.trigger.webhook.url.privateHint") }}</template>
|
|
1437
|
+
</p>
|
|
1438
|
+
</div>
|
|
1439
|
+
<div class="flex items-center gap-3 rounded-lg border border-black/[0.06] bg-muted/20 px-3 py-2">
|
|
1440
|
+
<Switch
|
|
1441
|
+
:aria-label="t('integration.nodeInspector.trigger.webhook.public.label')"
|
|
1442
|
+
class="data-[state=unchecked]:bg-slate-300"
|
|
1443
|
+
:model-value="webhookIsPublic"
|
|
1444
|
+
@update:model-value="(v) => writeTriggerConfigPatch(v ? { public: true, ...draftTrigger.config.authProfile ? {} : configPatchForAuthChoice('token') } : { public: void 0 })"
|
|
1445
|
+
/>
|
|
1446
|
+
<div>
|
|
1447
|
+
<div class="text-sm font-medium">{{ t("integration.nodeInspector.trigger.webhook.public.label") }}</div>
|
|
1448
|
+
<p class="text-xs text-muted-foreground">
|
|
1449
|
+
{{ t("integration.nodeInspector.trigger.webhook.public.hint") }}
|
|
1450
|
+
</p>
|
|
1451
|
+
</div>
|
|
1452
|
+
</div>
|
|
1453
|
+
<div class="grid grid-cols-2 gap-3">
|
|
1454
|
+
<div class="space-y-1.5">
|
|
1455
|
+
<Label for="ni-webhook-method" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.method.label") }}</Label>
|
|
1456
|
+
<Select
|
|
1457
|
+
:model-value="readTriggerConfig('method') ?? 'POST'"
|
|
1458
|
+
@update:model-value="(v) => writeTriggerConfig('method', v)"
|
|
1459
|
+
>
|
|
1460
|
+
<SelectTrigger id="ni-webhook-method" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
1461
|
+
<SelectValue />
|
|
1462
|
+
</SelectTrigger>
|
|
1463
|
+
<SelectContent>
|
|
1464
|
+
<SelectItem value="GET">GET</SelectItem>
|
|
1465
|
+
<SelectItem value="POST">POST</SelectItem>
|
|
1466
|
+
<SelectItem value="PUT">PUT</SelectItem>
|
|
1467
|
+
<SelectItem value="PATCH">PATCH</SelectItem>
|
|
1468
|
+
<SelectItem value="DELETE">DELETE</SelectItem>
|
|
1469
|
+
</SelectContent>
|
|
1470
|
+
</Select>
|
|
1471
|
+
</div>
|
|
1472
|
+
<div v-if="webhookIsPublic" class="space-y-1.5">
|
|
1473
|
+
<Label for="ni-webhook-protection" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.protection.label") }}</Label>
|
|
1474
|
+
<Select :model-value="authChoice" @update:model-value="applyAuthChoice">
|
|
1475
|
+
<SelectTrigger id="ni-webhook-protection" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
1476
|
+
<SelectValue />
|
|
1477
|
+
</SelectTrigger>
|
|
1478
|
+
<SelectContent>
|
|
1479
|
+
<SelectItem value="token">{{ t("integration.nodeInspector.trigger.webhook.protection.token") }}</SelectItem>
|
|
1480
|
+
<SelectItem value="bearer">{{ t("integration.nodeInspector.trigger.webhook.protection.bearer") }}</SelectItem>
|
|
1481
|
+
<SelectItem value="basic">{{ t("integration.nodeInspector.trigger.webhook.protection.basic") }}</SelectItem>
|
|
1482
|
+
<SelectItem value="none">{{ t("integration.nodeInspector.trigger.webhook.protection.none") }}</SelectItem>
|
|
1483
|
+
</SelectContent>
|
|
1484
|
+
</Select>
|
|
1485
|
+
<p class="text-xs text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.protection.hint") }}</p>
|
|
1486
|
+
</div>
|
|
1487
|
+
</div>
|
|
1488
|
+
<div v-if="webhookIsPublic && authChoice === 'token'" class="space-y-1.5">
|
|
1489
|
+
<Label for="ni-auth-header" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.header.label") }}</Label>
|
|
837
1490
|
<Input
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1491
|
+
id="ni-auth-header"
|
|
1492
|
+
:model-value="draftTrigger.config.authConfig?.header ?? ''"
|
|
1493
|
+
placeholder="X-Webhook-Token"
|
|
1494
|
+
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
|
|
1495
|
+
@update:model-value="(v) => writeAuthConfigKey('header', String(v))"
|
|
842
1496
|
/>
|
|
1497
|
+
<p class="text-xs text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.header.hint") }}</p>
|
|
843
1498
|
</div>
|
|
844
|
-
<div class="space-y-1.5">
|
|
845
|
-
<Label class="text-mono text-muted-foreground">
|
|
846
|
-
<
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
<SelectItem value="GET">GET</SelectItem>
|
|
855
|
-
<SelectItem value="POST">POST</SelectItem>
|
|
856
|
-
<SelectItem value="PUT">PUT</SelectItem>
|
|
857
|
-
<SelectItem value="PATCH">PATCH</SelectItem>
|
|
858
|
-
<SelectItem value="DELETE">DELETE</SelectItem>
|
|
859
|
-
</SelectContent>
|
|
860
|
-
</Select>
|
|
1499
|
+
<div v-if="webhookIsPublic && authChoice === 'basic'" class="space-y-1.5">
|
|
1500
|
+
<Label for="ni-auth-username" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.username.label") }}</Label>
|
|
1501
|
+
<Input
|
|
1502
|
+
id="ni-auth-username"
|
|
1503
|
+
:model-value="draftTrigger.config.authConfig?.username ?? ''"
|
|
1504
|
+
:placeholder="t('integration.nodeInspector.trigger.webhook.username.placeholder')"
|
|
1505
|
+
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
|
|
1506
|
+
@update:model-value="(v) => writeAuthConfigKey('username', String(v))"
|
|
1507
|
+
/>
|
|
1508
|
+
<p class="text-xs text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.username.hint") }}</p>
|
|
861
1509
|
</div>
|
|
862
|
-
<div class="
|
|
863
|
-
<
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
>
|
|
868
|
-
<SelectTrigger class="h-8 text-sm bg-white">
|
|
869
|
-
<SelectValue placeholder="None" />
|
|
870
|
-
</SelectTrigger>
|
|
871
|
-
<SelectContent>
|
|
872
|
-
<SelectItem v-for="k in secretKeys" :key="k" :value="k">{{ k }}</SelectItem>
|
|
873
|
-
</SelectContent>
|
|
874
|
-
</Select>
|
|
1510
|
+
<div v-if="webhookExposed && authChoice !== 'none'" class="flex items-center gap-2">
|
|
1511
|
+
<Button type="button" variant="outline" size="sm" class="h-7 text-sm" :disabled="rotateBusy" @click="rotateEndpointSecret">
|
|
1512
|
+
{{ rotateBusy ? t("integration.nodeInspector.trigger.webhook.rotate.busy") : t("integration.nodeInspector.trigger.webhook.rotate.action") }}
|
|
1513
|
+
</Button>
|
|
1514
|
+
<p class="text-xs text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.rotate.hint") }}</p>
|
|
875
1515
|
</div>
|
|
1516
|
+
|
|
1517
|
+
<Dialog v-model:open="rotateOpen">
|
|
1518
|
+
<DialogContent class="sm:max-w-md">
|
|
1519
|
+
<DialogHeader>
|
|
1520
|
+
<DialogTitle>{{ t("integration.nodeInspector.trigger.webhook.rotate.dialogTitle") }}</DialogTitle>
|
|
1521
|
+
<DialogDescription>
|
|
1522
|
+
{{ t("integration.nodeInspector.trigger.webhook.rotate.dialogHint", {
|
|
1523
|
+
where: authChoice === "basic" ? t("integration.nodeInspector.trigger.webhook.rotate.whereBasic") : authChoice === "bearer" ? t("integration.nodeInspector.trigger.webhook.rotate.whereBearer") : t("integration.nodeInspector.trigger.webhook.rotate.whereHeader")
|
|
1524
|
+
}) }}
|
|
1525
|
+
</DialogDescription>
|
|
1526
|
+
</DialogHeader>
|
|
1527
|
+
<div class="flex items-center gap-1.5">
|
|
1528
|
+
<Input
|
|
1529
|
+
readonly
|
|
1530
|
+
:model-value="rotatedSecret ?? ''"
|
|
1531
|
+
class="h-8 flex-1 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white font-mono text-xs"
|
|
1532
|
+
@focus="$event.target.select()"
|
|
1533
|
+
/>
|
|
1534
|
+
<button
|
|
1535
|
+
type="button"
|
|
1536
|
+
:title="t('integration.nodeInspector.trigger.webhook.rotate.copyTitle')"
|
|
1537
|
+
class="flex size-8 shrink-0 items-center justify-center rounded-md border border-black/[0.08] bg-white text-muted-foreground transition hover:text-foreground"
|
|
1538
|
+
@click="copySecret"
|
|
1539
|
+
>
|
|
1540
|
+
<Copy :size="14" />
|
|
1541
|
+
</button>
|
|
1542
|
+
</div>
|
|
1543
|
+
</DialogContent>
|
|
1544
|
+
</Dialog>
|
|
876
1545
|
</template>
|
|
877
1546
|
<template v-else-if="draftTrigger.type === 'event'">
|
|
878
1547
|
<div class="space-y-1.5">
|
|
879
|
-
<Label class="text-mono text-muted-foreground">
|
|
1548
|
+
<Label for="ni-event-type" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.event.label") }}</Label>
|
|
880
1549
|
<EventTypeCombobox
|
|
1550
|
+
input-id="ni-event-type"
|
|
881
1551
|
:model-value="readTriggerConfig('event_type') ?? ''"
|
|
882
1552
|
@update:model-value="(v) => writeTriggerConfig('event_type', v)"
|
|
883
1553
|
/>
|
|
884
|
-
<p class="text-
|
|
885
|
-
|
|
1554
|
+
<p class="text-xs text-muted-foreground">
|
|
1555
|
+
{{ t("integration.nodeInspector.trigger.event.hint.intro") }} <code class="font-mono">customer.created</code>, <code class="font-mono">order.placed</code>{{ t("integration.nodeInspector.trigger.event.hint.mid") }} <code class="font-mono">trigger.payload</code>.
|
|
886
1556
|
</p>
|
|
887
1557
|
</div>
|
|
1558
|
+
<div v-if="eventSample" class="space-y-1.5">
|
|
1559
|
+
<!-- A caption, not a field label: it heads a read-only block,
|
|
1560
|
+
so it stays a plain line rather than an orphan <label>. -->
|
|
1561
|
+
<div class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.event.sample.heading") }}</div>
|
|
1562
|
+
<pre class="max-h-72 overflow-auto rounded-md border border-black/[0.06] bg-muted/20 p-2.5 font-mono text-[11px] leading-relaxed text-slate-700">{{ eventSample }}</pre>
|
|
1563
|
+
<p class="text-xs text-muted-foreground">{{ t("integration.nodeInspector.trigger.event.sample.hint") }} <code class="font-mono">trigger.payload</code>.</p>
|
|
1564
|
+
</div>
|
|
1565
|
+
<p v-else-if="eventTopic" class="text-xs text-muted-foreground">
|
|
1566
|
+
{{ t("integration.nodeInspector.trigger.event.sample.empty") }}
|
|
1567
|
+
</p>
|
|
888
1568
|
</template>
|
|
889
1569
|
<template v-else>
|
|
890
|
-
<
|
|
891
|
-
|
|
892
|
-
|
|
1570
|
+
<div class="space-y-1.5">
|
|
1571
|
+
<!-- Caption for the field list below, not a label for one
|
|
1572
|
+
control — the row's own controls are named individually. -->
|
|
1573
|
+
<div class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.manual.heading") }}</div>
|
|
1574
|
+
<p class="text-xs text-muted-foreground">
|
|
1575
|
+
{{ t("integration.nodeInspector.trigger.manual.hint.intro") }}
|
|
1576
|
+
<code class="font-mono">trigger.payload.<key></code>{{ t("integration.nodeInspector.trigger.manual.hint.mid") }}
|
|
1577
|
+
<code class="font-mono">trigger.payload.*</code> {{ t("integration.nodeInspector.trigger.manual.hint.outro") }}
|
|
1578
|
+
</p>
|
|
1579
|
+
</div>
|
|
1580
|
+
<div v-if="manualFields.length" class="space-y-1.5">
|
|
1581
|
+
<div v-for="(mf, idx) in manualFields" :key="manualFieldUids[idx] ?? idx" class="flex items-center gap-1.5">
|
|
1582
|
+
<!-- A repeating row has no room for visible labels, so each
|
|
1583
|
+
control carries its own name and says which row it is. -->
|
|
1584
|
+
<Select :model-value="mf.kind ?? 'text'" @update:model-value="(v) => setManualField(idx, { kind: v })">
|
|
1585
|
+
<SelectTrigger :aria-label="t('integration.nodeInspector.trigger.manual.field.kindAria', { n: idx + 1 })" size="sm" class="h-8 w-24 shrink-0 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
1586
|
+
<SelectValue />
|
|
1587
|
+
</SelectTrigger>
|
|
1588
|
+
<SelectContent>
|
|
1589
|
+
<SelectItem value="text">{{ t("integration.nodeInspector.trigger.manual.kind.text") }}</SelectItem>
|
|
1590
|
+
<SelectItem value="file">{{ t("integration.nodeInspector.trigger.manual.kind.file") }}</SelectItem>
|
|
1591
|
+
</SelectContent>
|
|
1592
|
+
</Select>
|
|
1593
|
+
<Input
|
|
1594
|
+
:aria-label="t('integration.nodeInspector.trigger.manual.field.keyAria', { n: idx + 1 })"
|
|
1595
|
+
:model-value="mf.path"
|
|
1596
|
+
:placeholder="t('integration.nodeInspector.trigger.manual.field.keyPlaceholder')"
|
|
1597
|
+
class="h-8 w-[38%] rounded-md border border-black/[0.08] ring-0 shadow-none bg-white font-mono text-sm"
|
|
1598
|
+
@update:model-value="(v) => setManualField(idx, { path: String(v) })"
|
|
1599
|
+
/>
|
|
1600
|
+
<Input
|
|
1601
|
+
:aria-label="t('integration.nodeInspector.trigger.manual.field.labelAria', { n: idx + 1 })"
|
|
1602
|
+
:model-value="mf.label ?? ''"
|
|
1603
|
+
:placeholder="t('integration.nodeInspector.trigger.manual.field.labelPlaceholder')"
|
|
1604
|
+
class="h-8 min-w-0 flex-1 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
|
|
1605
|
+
@update:model-value="(v) => setManualField(idx, { label: String(v) })"
|
|
1606
|
+
/>
|
|
1607
|
+
<button
|
|
1608
|
+
type="button"
|
|
1609
|
+
:aria-label="t('integration.nodeInspector.trigger.manual.field.removeAria', { n: idx + 1 })"
|
|
1610
|
+
class="flex size-7 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
|
|
1611
|
+
@click="removeManualField(idx)"
|
|
1612
|
+
>
|
|
1613
|
+
<TrashBin :size="13" />
|
|
1614
|
+
</button>
|
|
1615
|
+
</div>
|
|
1616
|
+
</div>
|
|
1617
|
+
<Button type="button" variant="outline" size="sm" class="h-7 text-sm" @click="addManualField">
|
|
1618
|
+
<Plus :size="12" />
|
|
1619
|
+
{{ t("integration.nodeInspector.trigger.manual.action.addField") }}
|
|
1620
|
+
</Button>
|
|
893
1621
|
</template>
|
|
894
1622
|
</div>
|
|
895
1623
|
|
|
896
1624
|
<!-- Parameters: node config, generated from the manifest -->
|
|
897
|
-
<div v-else-if="tab === 'parameters' && draftNode" class="mx-auto w-full max-w-
|
|
1625
|
+
<div v-else-if="tab === 'parameters' && draftNode" class="mx-auto w-full max-w-5xl">
|
|
898
1626
|
<div v-if="manifest?.config?.length" class="space-y-3">
|
|
899
1627
|
<div class="flex items-center justify-between">
|
|
900
|
-
<span class="text-mono text-muted-foreground">
|
|
1628
|
+
<span class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.parameters.heading") }}</span>
|
|
901
1629
|
<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
1630
|
<button
|
|
906
1631
|
type="button"
|
|
907
1632
|
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="
|
|
1633
|
+
:title="t('integration.nodeInspector.parameters.copyTitle')"
|
|
909
1634
|
@click="copyConfig"
|
|
910
1635
|
>
|
|
911
|
-
<Copy :size="12" />
|
|
1636
|
+
<Copy :size="12" /> {{ t("integration.common.action.copy") }}
|
|
912
1637
|
</button>
|
|
913
1638
|
<button
|
|
914
1639
|
type="button"
|
|
915
1640
|
class="rounded px-1 py-0.5 text-sm text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
916
|
-
title="
|
|
1641
|
+
:title="t('integration.nodeInspector.parameters.pasteTitle')"
|
|
917
1642
|
@click="pasteConfig"
|
|
918
1643
|
>
|
|
919
|
-
|
|
1644
|
+
{{ t("integration.common.action.paste") }}
|
|
920
1645
|
</button>
|
|
921
1646
|
</div>
|
|
922
1647
|
</div>
|
|
923
1648
|
<IntegrationsNodeConfigField
|
|
924
|
-
v-for="field in
|
|
1649
|
+
v-for="field in requiredFields"
|
|
925
1650
|
:key="field.key"
|
|
926
1651
|
:field="field"
|
|
927
1652
|
:config="draftNode.config ?? {}"
|
|
@@ -932,22 +1657,74 @@ async function runTestStep() {
|
|
|
932
1657
|
:depth="0"
|
|
933
1658
|
:variables="variables"
|
|
934
1659
|
:upstream-outputs="upstreamOutputs"
|
|
1660
|
+
:refused="refusedFields?.[field.key]"
|
|
935
1661
|
@set="(k, v) => writeConfig(k, v)"
|
|
936
1662
|
/>
|
|
1663
|
+
|
|
1664
|
+
<!-- Optional settings, folded. The count is on the control on
|
|
1665
|
+
purpose: "Show 9 optional settings" is a decision the user can
|
|
1666
|
+
take without opening it, "Show more" is not. -->
|
|
1667
|
+
<template v-if="optionalFields.length">
|
|
1668
|
+
<button
|
|
1669
|
+
v-if="!optionalOpen"
|
|
1670
|
+
type="button"
|
|
1671
|
+
class="flex items-center gap-1.5 rounded px-1 py-1 text-sm text-muted-foreground transition hover:text-foreground"
|
|
1672
|
+
@click="optionalOpen = true"
|
|
1673
|
+
>
|
|
1674
|
+
<ChevronRight :size="13" />
|
|
1675
|
+
{{ t("integration.nodeInspector.parameters.optional.show", optionalFields.length) }}
|
|
1676
|
+
</button>
|
|
1677
|
+
<template v-else>
|
|
1678
|
+
<button
|
|
1679
|
+
v-if="requiredFields.length"
|
|
1680
|
+
type="button"
|
|
1681
|
+
class="flex items-center gap-1.5 rounded px-1 py-1 text-sm text-muted-foreground transition hover:text-foreground"
|
|
1682
|
+
@click="optionalOpen = false"
|
|
1683
|
+
>
|
|
1684
|
+
<ChevronDown :size="13" />
|
|
1685
|
+
{{ t("integration.nodeInspector.parameters.optional.hide", optionalFields.length) }}
|
|
1686
|
+
</button>
|
|
1687
|
+
<IntegrationsNodeConfigField
|
|
1688
|
+
v-for="field in optionalFields"
|
|
1689
|
+
:key="field.key"
|
|
1690
|
+
:field="field"
|
|
1691
|
+
:config="draftNode.config ?? {}"
|
|
1692
|
+
:slug="draftNode.nodeSlug"
|
|
1693
|
+
:version="draftNode.nodeVersion"
|
|
1694
|
+
:secret-keys="secretKeys"
|
|
1695
|
+
:static-keys="(manifest.config ?? []).map((f2) => f2.key)"
|
|
1696
|
+
:depth="0"
|
|
1697
|
+
:variables="variables"
|
|
1698
|
+
:upstream-outputs="upstreamOutputs"
|
|
1699
|
+
:refused="refusedFields?.[field.key]"
|
|
1700
|
+
@set="(k, v) => writeConfig(k, v)"
|
|
1701
|
+
/>
|
|
1702
|
+
</template>
|
|
1703
|
+
</template>
|
|
1704
|
+
|
|
937
1705
|
<p v-if="!parameterFields.length" class="text-sm text-muted-foreground italic">
|
|
938
|
-
|
|
1706
|
+
{{ t("integration.nodeInspector.parameters.allOnSettingsTab") }}
|
|
939
1707
|
</p>
|
|
940
1708
|
</div>
|
|
941
1709
|
<div v-else class="text-sm text-muted-foreground italic">
|
|
942
|
-
|
|
1710
|
+
{{ t("integration.nodeInspector.parameters.noConfiguration") }}
|
|
943
1711
|
</div>
|
|
944
1712
|
</div>
|
|
945
1713
|
|
|
946
1714
|
<!-- Settings: node-instance execution settings -->
|
|
947
|
-
<div v-else-if="tab === 'settings' && draftNode" class="mx-auto w-full max-w-
|
|
948
|
-
<!--
|
|
1715
|
+
<div v-else-if="tab === 'settings' && draftNode" class="mx-auto w-full max-w-5xl space-y-6">
|
|
1716
|
+
<!-- The node's OWN options that happen to be about waiting or
|
|
1717
|
+
retrying. They read like engine settings and sit next to them,
|
|
1718
|
+
which is why both blocks say out loud which is which: a node's
|
|
1719
|
+
"Poll timeout" and the engine's "Timeout" are otherwise two
|
|
1720
|
+
identical-looking numbers with nothing to tell them apart. -->
|
|
949
1721
|
<div v-if="settingsFields.length" class="space-y-3">
|
|
950
|
-
<
|
|
1722
|
+
<div>
|
|
1723
|
+
<span class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.settings.ownOptions.heading") }}</span>
|
|
1724
|
+
<p class="text-xs text-muted-foreground">
|
|
1725
|
+
{{ t("integration.nodeInspector.settings.ownOptions.hint") }}
|
|
1726
|
+
</p>
|
|
1727
|
+
</div>
|
|
951
1728
|
<IntegrationsNodeConfigField
|
|
952
1729
|
v-for="field in settingsFields"
|
|
953
1730
|
:key="field.key"
|
|
@@ -960,56 +1737,72 @@ async function runTestStep() {
|
|
|
960
1737
|
:depth="0"
|
|
961
1738
|
:variables="variables"
|
|
962
1739
|
:upstream-outputs="upstreamOutputs"
|
|
1740
|
+
:refused="refusedFields?.[field.key]"
|
|
963
1741
|
@set="(k, v) => writeConfig(k, v)"
|
|
964
1742
|
/>
|
|
965
1743
|
</div>
|
|
1744
|
+
<!-- The rule only earns its place when there is something above to
|
|
1745
|
+
divide from. -->
|
|
1746
|
+
<div :class="settingsFields.length ? 'border-t border-black/[0.06] pt-5' : ''">
|
|
1747
|
+
<span class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.settings.engine.heading") }}</span>
|
|
1748
|
+
<p class="text-xs text-muted-foreground">
|
|
1749
|
+
{{ t("integration.nodeInspector.settings.engine.hint") }}
|
|
1750
|
+
</p>
|
|
1751
|
+
</div>
|
|
1752
|
+
|
|
966
1753
|
<!-- Retry policy -->
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
<p class="text-xs text-muted-foreground">
|
|
972
|
-
Re-run this node automatically when an attempt fails.
|
|
973
|
-
</p>
|
|
974
|
-
</div>
|
|
1754
|
+
<!-- One card: the toggle heads it, the policy fields expand
|
|
1755
|
+
inside — they belong to the same schematic unit. -->
|
|
1756
|
+
<section class="rounded-lg border border-black/[0.06] bg-muted/20">
|
|
1757
|
+
<div class="flex items-center gap-3 px-3 py-2">
|
|
975
1758
|
<Switch
|
|
1759
|
+
:aria-label="t('integration.nodeInspector.settings.retry.label')"
|
|
1760
|
+
class="data-[state=unchecked]:bg-slate-300"
|
|
976
1761
|
:model-value="!!draftNode.retry"
|
|
977
1762
|
@update:model-value="(v) => patchNode({ retry: v ? { maxAttempts: 3, backoffSeconds: 1, strategy: 'exponential' } : void 0 })"
|
|
978
1763
|
/>
|
|
1764
|
+
<div>
|
|
1765
|
+
<div class="text-sm font-medium">{{ t("integration.nodeInspector.settings.retry.label") }}</div>
|
|
1766
|
+
<p class="text-xs text-muted-foreground">
|
|
1767
|
+
{{ t("integration.nodeInspector.settings.retry.hint") }}
|
|
1768
|
+
</p>
|
|
1769
|
+
</div>
|
|
979
1770
|
</div>
|
|
980
|
-
<div v-if="draftNode.retry" class="grid grid-cols-
|
|
1771
|
+
<div v-if="draftNode.retry" class="grid grid-cols-4 gap-3 border-t border-black/[0.06] p-3">
|
|
981
1772
|
<div class="space-y-1.5">
|
|
982
|
-
<Label class="text-mono text-muted-foreground">
|
|
1773
|
+
<Label for="ni-retry-attempts" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.settings.retry.maxAttempts") }}</Label>
|
|
983
1774
|
<Input
|
|
1775
|
+
id="ni-retry-attempts"
|
|
984
1776
|
type="number"
|
|
985
1777
|
min="1"
|
|
986
1778
|
:model-value="draftNode.retry?.maxAttempts ?? 3"
|
|
987
|
-
class="h-8 text-sm bg-white"
|
|
1779
|
+
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
|
|
988
1780
|
@update:model-value="(v) => patchNode({ retry: { ...draftNode.retry, maxAttempts: Number(v) || 1 } })"
|
|
989
1781
|
/>
|
|
990
1782
|
</div>
|
|
991
1783
|
<div class="space-y-1.5">
|
|
992
|
-
<Label class="text-mono text-muted-foreground">
|
|
1784
|
+
<Label for="ni-retry-backoff" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.settings.retry.backoffSeconds") }}</Label>
|
|
993
1785
|
<Input
|
|
1786
|
+
id="ni-retry-backoff"
|
|
994
1787
|
type="number"
|
|
995
1788
|
min="0"
|
|
996
1789
|
:model-value="draftNode.retry?.backoffSeconds ?? 0"
|
|
997
|
-
class="h-8 text-sm bg-white"
|
|
1790
|
+
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
|
|
998
1791
|
@update:model-value="(v) => patchNode({ retry: { ...draftNode.retry, backoffSeconds: Number(v) || 0 } })"
|
|
999
1792
|
/>
|
|
1000
1793
|
</div>
|
|
1001
1794
|
<div class="col-span-2 space-y-1.5">
|
|
1002
|
-
<Label class="text-mono text-muted-foreground">
|
|
1795
|
+
<Label for="ni-retry-strategy" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.settings.retry.strategy") }}</Label>
|
|
1003
1796
|
<Select
|
|
1004
1797
|
:model-value="draftNode.retry?.strategy ?? 'exponential'"
|
|
1005
1798
|
@update:model-value="(v) => patchNode({ retry: { ...draftNode.retry, strategy: v } })"
|
|
1006
1799
|
>
|
|
1007
|
-
<SelectTrigger class="h-8 text-sm bg-white">
|
|
1800
|
+
<SelectTrigger id="ni-retry-strategy" size="sm" class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
1008
1801
|
<SelectValue />
|
|
1009
1802
|
</SelectTrigger>
|
|
1010
1803
|
<SelectContent>
|
|
1011
|
-
<SelectItem value="linear">
|
|
1012
|
-
<SelectItem value="exponential">
|
|
1804
|
+
<SelectItem value="linear">{{ t("integration.nodeInspector.settings.retry.strategyLinear") }}</SelectItem>
|
|
1805
|
+
<SelectItem value="exponential">{{ t("integration.nodeInspector.settings.retry.strategyExponential") }}</SelectItem>
|
|
1013
1806
|
</SelectContent>
|
|
1014
1807
|
</Select>
|
|
1015
1808
|
</div>
|
|
@@ -1018,207 +1811,195 @@ async function runTestStep() {
|
|
|
1018
1811
|
|
|
1019
1812
|
<!-- Timeout: engine start-to-close ceiling for one attempt -->
|
|
1020
1813
|
<section class="space-y-1.5">
|
|
1021
|
-
<Label class="text-mono text-muted-foreground">
|
|
1814
|
+
<Label for="ni-timeout" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.settings.timeout.label") }}</Label>
|
|
1022
1815
|
<Input
|
|
1816
|
+
id="ni-timeout"
|
|
1023
1817
|
type="number"
|
|
1024
1818
|
min="1"
|
|
1025
1819
|
max="7200"
|
|
1026
1820
|
placeholder="600"
|
|
1027
1821
|
:model-value="draftNode.timeoutSeconds ?? ''"
|
|
1028
|
-
class="h-8 text-sm bg-white"
|
|
1822
|
+
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
|
|
1029
1823
|
@update:model-value="(v) => patchNode({ timeoutSeconds: Number(v) > 0 ? Math.min(Math.trunc(Number(v)), 7200) : void 0 })"
|
|
1030
1824
|
/>
|
|
1031
1825
|
<p class="text-xs text-muted-foreground">
|
|
1032
|
-
|
|
1826
|
+
{{ t("integration.nodeInspector.settings.timeout.hint") }}
|
|
1033
1827
|
</p>
|
|
1034
1828
|
</section>
|
|
1035
1829
|
|
|
1036
1830
|
<!-- On error: what happens once the retries are exhausted -->
|
|
1037
1831
|
<section class="space-y-1.5">
|
|
1038
|
-
<Label class="text-mono text-muted-foreground">
|
|
1832
|
+
<Label for="ni-on-error" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.settings.onError.label") }}</Label>
|
|
1039
1833
|
<Select
|
|
1040
1834
|
:model-value="draftNode.onError ?? 'fail'"
|
|
1041
1835
|
@update:model-value="(v) => patchNode({ onError: v === 'route' ? 'route' : void 0 })"
|
|
1042
1836
|
>
|
|
1043
|
-
<SelectTrigger class="h-8 text-sm bg-white">
|
|
1837
|
+
<SelectTrigger id="ni-on-error" size="sm" class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
1044
1838
|
<SelectValue />
|
|
1045
1839
|
</SelectTrigger>
|
|
1046
1840
|
<SelectContent>
|
|
1047
|
-
<SelectItem value="fail">
|
|
1048
|
-
<SelectItem value="route">
|
|
1841
|
+
<SelectItem value="fail">{{ t("integration.nodeInspector.settings.onError.fail") }}</SelectItem>
|
|
1842
|
+
<SelectItem value="route">{{ t("integration.nodeInspector.settings.onError.route") }}</SelectItem>
|
|
1049
1843
|
</SelectContent>
|
|
1050
1844
|
</Select>
|
|
1051
1845
|
<p class="text-xs text-muted-foreground">
|
|
1052
|
-
|
|
1846
|
+
{{ t("integration.nodeInspector.settings.onError.hint") }}
|
|
1053
1847
|
</p>
|
|
1054
1848
|
</section>
|
|
1849
|
+
</div>
|
|
1055
1850
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1851
|
+
<!-- Test: run this one step and look at what came back. Its own tab
|
|
1852
|
+
rather than a permanent column — it is the last thing done, if it
|
|
1853
|
+
is done at all, and it needs the width when it is. -->
|
|
1854
|
+
<div v-else-if="tab === 'output' && draftNode" class="mx-auto w-full max-w-5xl space-y-5">
|
|
1855
|
+
<section class="space-y-2">
|
|
1856
|
+
<div class="flex items-center justify-between gap-2">
|
|
1857
|
+
<p class="text-xs text-muted-foreground">
|
|
1858
|
+
{{ t("integration.nodeInspector.output.description") }}
|
|
1859
|
+
</p>
|
|
1860
|
+
<div class="flex shrink-0 items-center rounded-md border border-black/[0.06] bg-white p-0.5">
|
|
1861
|
+
<button type="button" :class="outputView === 'schema' ? segActive : segIdle" @click="outputView = 'schema'">
|
|
1862
|
+
{{ t("integration.nodeInspector.output.view.schema") }}
|
|
1863
|
+
</button>
|
|
1864
|
+
<button type="button" :disabled="!selfSample" :class="outputView === 'json' ? segActive : segIdle" @click="outputView = 'json'">
|
|
1865
|
+
{{ t("integration.nodeInspector.output.view.json") }}
|
|
1866
|
+
</button>
|
|
1867
|
+
</div>
|
|
1868
|
+
</div>
|
|
1869
|
+
<template v-if="outputView === 'schema'">
|
|
1870
|
+
<IntegrationsNodeOutputSchema :ports="selfPorts" :value-at="(port, field2) => chipValue(draft.id, port, field2)" />
|
|
1871
|
+
</template>
|
|
1872
|
+
<pre v-else class="overflow-auto rounded-md border border-black/[0.06] bg-white p-3 font-mono text-xs leading-snug">{{ selfSampleJson }}</pre>
|
|
1873
|
+
</section>
|
|
1874
|
+
|
|
1875
|
+
<section class="space-y-2 border-t border-black/[0.06] pt-4">
|
|
1876
|
+
<span class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.output.realValues.heading") }}</span>
|
|
1877
|
+
<div class="flex items-center gap-2">
|
|
1878
|
+
<Button
|
|
1879
|
+
size="sm"
|
|
1880
|
+
class="h-8"
|
|
1881
|
+
:disabled="testRunning"
|
|
1882
|
+
:title="t('integration.nodeInspector.output.test.runTitle')"
|
|
1883
|
+
@click="runTestStep"
|
|
1884
|
+
>
|
|
1885
|
+
{{ testRunning ? t("integration.nodeInspector.output.test.running") : t("integration.nodeInspector.output.test.action") }}
|
|
1886
|
+
</Button>
|
|
1887
|
+
<button
|
|
1888
|
+
type="button"
|
|
1889
|
+
class="rounded px-1 py-1 text-sm font-medium text-muted-foreground transition hover:text-foreground"
|
|
1890
|
+
@click="toggleTestInput"
|
|
1891
|
+
>
|
|
1892
|
+
{{ testInputOpen ? t("integration.nodeInspector.output.test.hideInput") : t("integration.nodeInspector.output.test.editInput") }}
|
|
1893
|
+
</button>
|
|
1894
|
+
<span class="text-xs text-muted-foreground">
|
|
1895
|
+
{{ t("integration.nodeInspector.output.test.runHint") }}
|
|
1896
|
+
</span>
|
|
1897
|
+
</div>
|
|
1059
1898
|
<Textarea
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1899
|
+
v-if="testInputOpen"
|
|
1900
|
+
v-model="testInputDraft"
|
|
1901
|
+
:aria-label="t('integration.nodeInspector.output.test.inputAria')"
|
|
1902
|
+
:rows="8"
|
|
1903
|
+
spellcheck="false"
|
|
1904
|
+
class="rounded-md border border-black/[0.08] ring-0 shadow-none bg-white font-mono text-xs"
|
|
1065
1905
|
/>
|
|
1066
|
-
<p class="text-
|
|
1067
|
-
|
|
1906
|
+
<p v-if="testError" class="text-sm text-rose-600">
|
|
1907
|
+
{{ testError }}
|
|
1068
1908
|
</p>
|
|
1909
|
+
<div v-if="testLogs.length" class="max-h-48 space-y-0.5 overflow-y-auto rounded-md border border-black/[0.06] bg-white p-2">
|
|
1910
|
+
<div
|
|
1911
|
+
v-for="(l, i) in testLogs"
|
|
1912
|
+
:key="i"
|
|
1913
|
+
class="font-mono text-[11px] leading-snug"
|
|
1914
|
+
:class="l.level === 'error' ? 'text-rose-600' : l.level === 'warn' ? 'text-amber-600' : 'text-slate-600'"
|
|
1915
|
+
>
|
|
1916
|
+
{{ l.message }}
|
|
1917
|
+
</div>
|
|
1918
|
+
</div>
|
|
1069
1919
|
</section>
|
|
1070
|
-
</div>
|
|
1071
|
-
</div>
|
|
1072
|
-
</section>
|
|
1073
1920
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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>
|
|
1112
|
-
</div>
|
|
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 }}
|
|
1131
|
-
</div>
|
|
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>
|
|
1140
|
-
</div>
|
|
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>
|
|
1921
|
+
<section class="space-y-2 border-t border-black/[0.06] pt-4">
|
|
1922
|
+
<template v-if="!pinEditorOpen">
|
|
1923
|
+
<div class="flex items-center gap-2">
|
|
1924
|
+
<Button variant="outline" size="sm" class="h-8" @click="openPinEditor">
|
|
1925
|
+
{{ selfPinned ? t("integration.nodeInspector.output.pin.editAction") : t("integration.nodeInspector.output.pin.pinAction") }}
|
|
1926
|
+
</Button>
|
|
1927
|
+
<button
|
|
1928
|
+
v-if="selfPinned"
|
|
1929
|
+
type="button"
|
|
1930
|
+
class="text-sm font-medium text-rose-600 transition hover:text-rose-700"
|
|
1931
|
+
@click="removePin"
|
|
1932
|
+
>
|
|
1933
|
+
{{ t("integration.nodeInspector.output.pin.unpinAction") }}
|
|
1934
|
+
</button>
|
|
1935
|
+
<span class="text-xs text-muted-foreground">
|
|
1936
|
+
{{ t("integration.nodeInspector.output.pin.hint") }}
|
|
1937
|
+
</span>
|
|
1146
1938
|
</div>
|
|
1147
|
-
</
|
|
1148
|
-
<
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
</
|
|
1168
|
-
|
|
1169
|
-
|
|
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>
|
|
1939
|
+
</template>
|
|
1940
|
+
<template v-else>
|
|
1941
|
+
<Textarea
|
|
1942
|
+
v-model="pinDraft"
|
|
1943
|
+
:aria-label="t('integration.nodeInspector.output.pin.editorAria')"
|
|
1944
|
+
:rows="10"
|
|
1945
|
+
spellcheck="false"
|
|
1946
|
+
class="rounded-md border border-black/[0.08] ring-0 shadow-none bg-white font-mono text-xs"
|
|
1947
|
+
/>
|
|
1948
|
+
<p v-if="pinError" class="text-sm text-amber-600">
|
|
1949
|
+
{{ pinError }}
|
|
1950
|
+
</p>
|
|
1951
|
+
<div class="flex items-center gap-2">
|
|
1952
|
+
<Button size="sm" class="h-8" @click="savePin">
|
|
1953
|
+
{{ t("integration.nodeInspector.output.pin.saveAction") }}
|
|
1954
|
+
</Button>
|
|
1955
|
+
<Button size="sm" variant="outline" class="h-8" @click="pinEditorOpen = false">
|
|
1956
|
+
{{ t("integration.common.action.cancel") }}
|
|
1957
|
+
</Button>
|
|
1958
|
+
</div>
|
|
1959
|
+
</template>
|
|
1960
|
+
</section>
|
|
1961
|
+
</div>
|
|
1200
1962
|
</div>
|
|
1201
|
-
</
|
|
1963
|
+
</section>
|
|
1202
1964
|
</div>
|
|
1203
1965
|
|
|
1204
1966
|
<!-- Footer: draft semantics — nothing reaches the blob until Save -->
|
|
1205
1967
|
<div class="flex shrink-0 items-center justify-between gap-3 border-t border-black/[0.06] px-4 py-3">
|
|
1968
|
+
<!-- Quiet until reached for: a bare glyph that unfolds into its name and
|
|
1969
|
+
its colour on hover or focus.
|
|
1970
|
+
|
|
1971
|
+
It is the rarest of the four ways to delete a node — the canvas
|
|
1972
|
+
offers the card's toolbar, the context menu and the Delete key — and
|
|
1973
|
+
it sits in the row the user reads to decide what happens to their
|
|
1974
|
+
edits, where a third, louder option reads as one of the answers.
|
|
1975
|
+
Rose carries a meaning now, too: a setting the last save refused.
|
|
1976
|
+
Spending it on a footer button weakens that.
|
|
1977
|
+
|
|
1978
|
+
The name is on the button itself, not only in the revealed span, so
|
|
1979
|
+
it reaches a screen reader without a pointer. Revealing on focus as
|
|
1980
|
+
well as hover is what keeps it usable from the keyboard. The button
|
|
1981
|
+
grows leftmost into empty space, so nothing else moves. -->
|
|
1206
1982
|
<Button
|
|
1207
1983
|
variant="ghost"
|
|
1208
1984
|
size="sm"
|
|
1209
|
-
|
|
1985
|
+
:aria-label="t('integration.nodeInspector.footer.removeFromWorkflow')"
|
|
1986
|
+
class="group h-8 gap-0 text-sm text-muted-foreground transition-colors hover:bg-rose-500/10 hover:text-rose-700 focus-visible:bg-rose-500/10 focus-visible:text-rose-700"
|
|
1210
1987
|
@click="emit('remove')"
|
|
1211
1988
|
>
|
|
1212
|
-
<
|
|
1213
|
-
|
|
1989
|
+
<TrashBin :size="13" />
|
|
1990
|
+
<span
|
|
1991
|
+
class="max-w-0 overflow-hidden whitespace-nowrap opacity-0 transition-all duration-150 group-hover:ml-1.5 group-hover:max-w-48 group-hover:opacity-100 group-focus-visible:ml-1.5 group-focus-visible:max-w-48 group-focus-visible:opacity-100"
|
|
1992
|
+
>
|
|
1993
|
+
{{ t("integration.nodeInspector.footer.removeFromWorkflow") }}
|
|
1994
|
+
</span>
|
|
1214
1995
|
</Button>
|
|
1215
1996
|
<div class="flex items-center gap-2">
|
|
1216
|
-
<span v-if="dirty" class="text-xs text-muted-foreground">
|
|
1997
|
+
<span v-if="dirty" class="text-xs text-muted-foreground">{{ t("integration.nodeInspector.footer.unsavedChanges") }}</span>
|
|
1217
1998
|
<Button variant="outline" size="sm" class="h-8" @click="emit('close')">
|
|
1218
|
-
|
|
1999
|
+
{{ t("integration.common.action.cancel") }}
|
|
1219
2000
|
</Button>
|
|
1220
2001
|
<Button size="sm" class="h-8" @click="save">
|
|
1221
|
-
|
|
2002
|
+
{{ t("integration.common.action.save") }}
|
|
1222
2003
|
</Button>
|
|
1223
2004
|
</div>
|
|
1224
2005
|
</div>
|