@revenexx/studio-integrations 0.6.0 → 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 +227 -0
- package/README.md +31 -0
- 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 +29 -28
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +38 -37
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
- package/dist/runtime/components/Integrations/ExpressionInput.vue +16 -15
- package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
- package/dist/runtime/components/Integrations/Layout.vue +28 -3
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +15 -14
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +9 -8
- package/dist/runtime/components/Integrations/NodeConfigField.vue +43 -42
- package/dist/runtime/components/Integrations/NodeInspector.vue +225 -216
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +1 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +3 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +1 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +30 -29
- package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -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 +37 -23
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/TemplateCard.vue +16 -6
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +10 -9
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +36 -35
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +82 -79
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +24 -23
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +19 -20
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +52 -39
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +20 -19
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +12 -9
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +27 -26
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +4 -2
- package/dist/runtime/composables/useIntegrationsCredentials.js +6 -4
- 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 +3 -1
- 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 +3 -1
- package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
- 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 +16 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +4 -2
- package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -3
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +14 -0
- package/dist/runtime/data/integrationTemplates.d.ts +9 -0
- package/dist/runtime/data/integrationTemplates.js +7 -1
- package/dist/runtime/data/payloadForm.js +2 -1
- package/dist/runtime/data/workflowIconCatalog.js +2 -1
- 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 +52 -34
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
- package/dist/runtime/pages/integrations/credentials.vue +70 -52
- package/dist/runtime/pages/integrations/index.vue +50 -56
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +49 -28
- package/dist/runtime/pages/integrations/secrets.vue +34 -34
- package/dist/runtime/pages/integrations/templates/[slug].vue +77 -68
- package/dist/runtime/pages/integrations/templates/index.vue +35 -27
- package/dist/runtime/pages/integrations/workflows/[id].vue +164 -99
- package/dist/runtime/pages/integrations/workflows/index.vue +10 -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/utils/credentialState.d.ts +24 -1
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/localize.d.ts +18 -5
- package/dist/runtime/utils/localize.js +32 -9
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/schedule.d.ts +0 -25
- package/dist/runtime/utils/schedule.js +37 -25
- package/dist/runtime/utils/statusStyle.d.ts +26 -2
- package/dist/runtime/utils/statusStyle.js +9 -1
- package/dist/runtime/utils/validationFindings.d.ts +2 -2
- package/dist/runtime/utils/validationFindings.js +4 -3
- package/dist/types.d.mts +2 -6
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, nextTick, onMounted, provide, ref, watch } from "vue";
|
|
3
|
-
import { AddCircle as Plus, TrashBinMinimalistic as
|
|
3
|
+
import { AddCircle as Plus, TrashBinMinimalistic as TrashBin, Copy } from "#solar-icons";
|
|
4
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 {
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
ToggleGroupItem
|
|
28
28
|
} from "@revenexx/studio";
|
|
29
29
|
import EventTypeCombobox from "./EventTypeCombobox.vue";
|
|
30
|
-
import {
|
|
30
|
+
import { findManifest, nodeCategoryLabel } from "../../data/integrationNodeCatalog";
|
|
31
31
|
import {
|
|
32
32
|
collectUpstreamNodes,
|
|
33
33
|
nodeOutputExpr,
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
} from "../../data/nodeRunData";
|
|
46
46
|
import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
|
|
47
47
|
import { describeRecurrence, parseCron, scheduleTimezone, toCron } from "../../utils/schedule";
|
|
48
|
+
const t = useIntegrationsT();
|
|
48
49
|
const props = defineProps({
|
|
49
50
|
selection: { type: [Object, null], required: true },
|
|
50
51
|
secretKeys: { type: Array, required: true },
|
|
@@ -87,23 +88,28 @@ const manifest = computed(() => {
|
|
|
87
88
|
return wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
|
|
88
89
|
});
|
|
89
90
|
const TRIGGER_GLYPHS = { manual: Play, schedule: Clock, webhook: Webhook, event: Zap };
|
|
90
|
-
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
|
+
}));
|
|
91
97
|
const triggerGlyph = computed(() => TRIGGER_GLYPHS[draftTrigger.value?.type ?? "manual"]);
|
|
92
98
|
const namePlaceholder = computed(() => {
|
|
93
99
|
if (draftNode.value) return resolveLocalized(manifest.value?.name) || draftNode.value.nodeSlug;
|
|
94
|
-
return TRIGGER_LABELS[draftTrigger.value?.type ?? "manual"]
|
|
100
|
+
return t("integration.nodeInspector.trigger.placeholder", { type: TRIGGER_LABELS.value[draftTrigger.value?.type ?? "manual"] });
|
|
95
101
|
});
|
|
96
102
|
const subtitleText = computed(() => {
|
|
97
103
|
const wn = draftNode.value;
|
|
98
104
|
if (wn) return wn.nodeSlug + "@" + wn.nodeVersion;
|
|
99
|
-
return TRIGGER_LABELS[draftTrigger.value?.type ?? "manual"]
|
|
105
|
+
return t("integration.nodeInspector.trigger.placeholder", { type: TRIGGER_LABELS.value[draftTrigger.value?.type ?? "manual"] });
|
|
100
106
|
});
|
|
101
107
|
const manifestDescription = computed(() => resolveLocalized(manifest.value?.description));
|
|
102
108
|
const groupLabel = computed(() => {
|
|
103
|
-
if (!isNode.value) return "
|
|
109
|
+
if (!isNode.value) return t("integration.nodeInspector.header.groupTrigger");
|
|
104
110
|
const groups = manifest.value?.groups;
|
|
105
111
|
if (groups?.length) return resolveLocalized(groups[groups.length - 1]);
|
|
106
|
-
return
|
|
112
|
+
return nodeCategoryLabel(manifest.value?.category) || t("integration.nodeInspector.header.groupNode");
|
|
107
113
|
});
|
|
108
114
|
const nameValue = computed(() => {
|
|
109
115
|
const name = draft.value.name;
|
|
@@ -114,7 +120,7 @@ function writeName(name) {
|
|
|
114
120
|
}
|
|
115
121
|
const noteOpen = ref(false);
|
|
116
122
|
const noteText = computed(() => (draftNode.value?.notes ?? "").trim());
|
|
117
|
-
const noteLabel = computed(() => noteText.value ? "
|
|
123
|
+
const noteLabel = computed(() => noteText.value ? t("integration.nodeInspector.note.label") : t("integration.nodeInspector.note.addLabel"));
|
|
118
124
|
function writeNote(v) {
|
|
119
125
|
const text = String(v ?? "");
|
|
120
126
|
patchNode({ notes: text.trim() ? text : void 0 });
|
|
@@ -122,8 +128,11 @@ function writeNote(v) {
|
|
|
122
128
|
const tab = ref("parameters");
|
|
123
129
|
const showSettingsTab = computed(() => isNode.value);
|
|
124
130
|
const inspectorTabs = computed(() => [
|
|
125
|
-
{ key: "parameters", label: "
|
|
126
|
-
...showSettingsTab.value ? [
|
|
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
|
+
] : []
|
|
127
136
|
]);
|
|
128
137
|
watch(showSettingsTab, (hasThem) => {
|
|
129
138
|
if (!hasThem) tab.value = "parameters";
|
|
@@ -184,9 +193,9 @@ async function copyConfig() {
|
|
|
184
193
|
if (!draftNode.value) return;
|
|
185
194
|
try {
|
|
186
195
|
await navigator.clipboard.writeText(JSON.stringify(draftNode.value.config ?? {}, null, 2));
|
|
187
|
-
toast.success("
|
|
196
|
+
toast.success(t("integration.nodeInspector.toast.configCopied"));
|
|
188
197
|
} catch {
|
|
189
|
-
toast.error("
|
|
198
|
+
toast.error(t("integration.nodeInspector.toast.configCopyFailed"));
|
|
190
199
|
}
|
|
191
200
|
}
|
|
192
201
|
async function pasteConfig() {
|
|
@@ -195,22 +204,22 @@ async function pasteConfig() {
|
|
|
195
204
|
try {
|
|
196
205
|
text = await navigator.clipboard.readText();
|
|
197
206
|
} catch {
|
|
198
|
-
toast.error("
|
|
207
|
+
toast.error(t("integration.nodeInspector.toast.clipboardReadFailed"));
|
|
199
208
|
return;
|
|
200
209
|
}
|
|
201
210
|
let parsed;
|
|
202
211
|
try {
|
|
203
212
|
parsed = JSON.parse(text);
|
|
204
213
|
} catch {
|
|
205
|
-
toast.error("
|
|
214
|
+
toast.error(t("integration.nodeInspector.toast.clipboardInvalidJson"));
|
|
206
215
|
return;
|
|
207
216
|
}
|
|
208
217
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
209
|
-
toast.error("
|
|
218
|
+
toast.error(t("integration.nodeInspector.toast.clipboardNotConfigObject"));
|
|
210
219
|
return;
|
|
211
220
|
}
|
|
212
221
|
patchNode({ config: parsed });
|
|
213
|
-
toast.success("
|
|
222
|
+
toast.success(t("integration.nodeInspector.toast.configPasted"));
|
|
214
223
|
}
|
|
215
224
|
function readTriggerConfig(key) {
|
|
216
225
|
return draftTrigger.value?.config[key];
|
|
@@ -223,24 +232,24 @@ function writeTriggerConfig(key, value) {
|
|
|
223
232
|
const MINUTE_INTERVALS = [1, 2, 5, 10, 15, 20, 30];
|
|
224
233
|
const HOUR_INTERVALS = [2, 3, 4, 6, 8, 12];
|
|
225
234
|
const MINUTES_PAST = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
|
|
226
|
-
const DAY_OPTIONS = [
|
|
227
|
-
{ value: 1, short: "
|
|
228
|
-
{ value: 2, short: "
|
|
229
|
-
{ value: 3, short: "
|
|
230
|
-
{ value: 4, short: "
|
|
231
|
-
{ value: 5, short: "
|
|
232
|
-
{ value: 6, short: "
|
|
233
|
-
{ value: 0, short: "
|
|
234
|
-
];
|
|
235
|
-
const FREQUENCY_OPTIONS = [
|
|
236
|
-
{ value: "minutes", label: "
|
|
237
|
-
{ value: "hourly", label: "
|
|
238
|
-
{ value: "hours", label: "
|
|
239
|
-
{ value: "times", label: "
|
|
240
|
-
{ value: "daily", label: "
|
|
241
|
-
{ value: "weekly", label: "
|
|
242
|
-
{ value: "monthly", label: "
|
|
243
|
-
];
|
|
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
|
+
]);
|
|
244
253
|
const HOURS_OF_DAY = Array.from({ length: 24 }, (_, h) => h);
|
|
245
254
|
const FREQUENCY_ADVANCED = "advanced";
|
|
246
255
|
const cronValue = computed(() => readTriggerConfig("cron") ?? "");
|
|
@@ -416,10 +425,10 @@ async function rotateEndpointSecret() {
|
|
|
416
425
|
rotatedSecret.value = secret;
|
|
417
426
|
rotateOpen.value = true;
|
|
418
427
|
} else {
|
|
419
|
-
toast.error("
|
|
428
|
+
toast.error(t("integration.nodeInspector.toast.rotateNoSecret"));
|
|
420
429
|
}
|
|
421
430
|
} catch (e) {
|
|
422
|
-
toast.error(e.message || "
|
|
431
|
+
toast.error(e.message || t("integration.nodeInspector.toast.rotateFailed"));
|
|
423
432
|
} finally {
|
|
424
433
|
rotateBusy.value = false;
|
|
425
434
|
}
|
|
@@ -428,25 +437,25 @@ async function copySecret() {
|
|
|
428
437
|
if (!rotatedSecret.value) return;
|
|
429
438
|
try {
|
|
430
439
|
await navigator.clipboard.writeText(rotatedSecret.value);
|
|
431
|
-
toast.success("
|
|
440
|
+
toast.success(t("integration.nodeInspector.toast.secretCopied"));
|
|
432
441
|
} catch {
|
|
433
|
-
toast.error("
|
|
442
|
+
toast.error(t("integration.nodeInspector.toast.secretCopyFailed"));
|
|
434
443
|
}
|
|
435
444
|
}
|
|
436
445
|
async function copyWebhookUrl() {
|
|
437
446
|
if (!webhookUrl.value) return;
|
|
438
447
|
try {
|
|
439
448
|
await navigator.clipboard.writeText(webhookUrl.value);
|
|
440
|
-
toast.success("
|
|
449
|
+
toast.success(t("integration.nodeInspector.toast.webhookUrlCopied"));
|
|
441
450
|
} catch {
|
|
442
|
-
toast.error("
|
|
451
|
+
toast.error(t("integration.nodeInspector.toast.webhookUrlCopyFailed"));
|
|
443
452
|
}
|
|
444
453
|
}
|
|
445
454
|
const definedEvents = typeof useDefinedEvents === "function" ? useDefinedEvents() : { emits: ref([]), load: async () => {
|
|
446
455
|
} };
|
|
447
456
|
const eventTopic = computed(() => {
|
|
448
|
-
const
|
|
449
|
-
return typeof
|
|
457
|
+
const raw = readTriggerConfig("event_type");
|
|
458
|
+
return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
|
|
450
459
|
});
|
|
451
460
|
watch(eventTopic, (topic) => {
|
|
452
461
|
if (topic) void definedEvents.load();
|
|
@@ -490,7 +499,7 @@ async function loadRunData() {
|
|
|
490
499
|
const { runs } = await loadForWorkflow(Number(props.workflowId), "", { perPage: 10 });
|
|
491
500
|
const source = runs.find((r) => r.status === "succeeded") ?? runs[0];
|
|
492
501
|
if (!source) {
|
|
493
|
-
runError.value = "
|
|
502
|
+
runError.value = t("integration.nodeInspector.input.runData.empty");
|
|
494
503
|
return;
|
|
495
504
|
}
|
|
496
505
|
const res = await getSteps(props.workflowId, source.id, true);
|
|
@@ -505,7 +514,7 @@ async function loadRunData() {
|
|
|
505
514
|
runData.value = map;
|
|
506
515
|
runInfo.value = { id: source.id, startedAt: source.startedAt };
|
|
507
516
|
} catch (e) {
|
|
508
|
-
runError.value = e?.message ?? "
|
|
517
|
+
runError.value = e?.message ?? t("integration.nodeInspector.input.runData.loadFailed");
|
|
509
518
|
} finally {
|
|
510
519
|
runLoading.value = false;
|
|
511
520
|
}
|
|
@@ -589,19 +598,19 @@ const inputPaneOpen = ref(false);
|
|
|
589
598
|
const exprInsertHandler = ref(null);
|
|
590
599
|
provide("integrationsExpressionInsert", exprInsertHandler);
|
|
591
600
|
function onCenterFocusIn(e) {
|
|
592
|
-
const
|
|
593
|
-
if (
|
|
601
|
+
const target = e.target;
|
|
602
|
+
if (target instanceof HTMLElement && target.closest(".expr-editor")) {
|
|
594
603
|
lastFocusKind.value = "editor";
|
|
595
604
|
inputPaneOpen.value = true;
|
|
596
605
|
return;
|
|
597
606
|
}
|
|
598
|
-
if (
|
|
599
|
-
lastField.value =
|
|
607
|
+
if (target instanceof HTMLTextAreaElement) {
|
|
608
|
+
lastField.value = target;
|
|
600
609
|
lastFocusKind.value = "native";
|
|
601
610
|
return;
|
|
602
611
|
}
|
|
603
|
-
if (
|
|
604
|
-
lastField.value =
|
|
612
|
+
if (target instanceof HTMLInputElement && !["number", "checkbox", "radio", "range", "file", "color"].includes(target.type)) {
|
|
613
|
+
lastField.value = target;
|
|
605
614
|
lastFocusKind.value = "native";
|
|
606
615
|
}
|
|
607
616
|
}
|
|
@@ -623,7 +632,7 @@ function insertExpression(expression) {
|
|
|
623
632
|
void navigator.clipboard?.writeText(expression);
|
|
624
633
|
} catch {
|
|
625
634
|
}
|
|
626
|
-
toast("
|
|
635
|
+
toast(t("integration.nodeInspector.toast.expressionCopied"));
|
|
627
636
|
}
|
|
628
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";
|
|
629
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";
|
|
@@ -642,10 +651,10 @@ function onChipDragEnd() {
|
|
|
642
651
|
dragActive.value = false;
|
|
643
652
|
clearDropHover();
|
|
644
653
|
}
|
|
645
|
-
function expressionTarget(
|
|
646
|
-
if (
|
|
647
|
-
if (
|
|
648
|
-
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");
|
|
649
658
|
return null;
|
|
650
659
|
}
|
|
651
660
|
function onCenterDragEnter(e) {
|
|
@@ -737,7 +746,7 @@ function typeGlyph(dataType) {
|
|
|
737
746
|
return dataType ? TYPE_GLYPHS[dataType] ?? null : null;
|
|
738
747
|
}
|
|
739
748
|
const sequence = computed(() => [
|
|
740
|
-
...(props.triggers ?? []).map((
|
|
749
|
+
...(props.triggers ?? []).map((t2) => t2.id),
|
|
741
750
|
...(props.nodes ?? []).map((n) => n.id)
|
|
742
751
|
]);
|
|
743
752
|
const seqIndex = computed(() => sequence.value.indexOf(draft.value.id));
|
|
@@ -771,17 +780,17 @@ function savePin() {
|
|
|
771
780
|
try {
|
|
772
781
|
parsed = JSON.parse(pinDraft.value);
|
|
773
782
|
} catch {
|
|
774
|
-
pinError.value = "
|
|
783
|
+
pinError.value = t("integration.nodeInspector.output.pin.invalidJson");
|
|
775
784
|
return;
|
|
776
785
|
}
|
|
777
786
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
778
|
-
pinError.value =
|
|
787
|
+
pinError.value = t("integration.nodeInspector.output.pin.invalidShape");
|
|
779
788
|
return;
|
|
780
789
|
}
|
|
781
790
|
pinnedSamples.value = { ...pinnedSamples.value, [draftNode.value.id]: parsed };
|
|
782
791
|
persistPins();
|
|
783
792
|
pinEditorOpen.value = false;
|
|
784
|
-
toast.success("
|
|
793
|
+
toast.success(t("integration.nodeInspector.toast.samplePinned"));
|
|
785
794
|
}
|
|
786
795
|
function removePin() {
|
|
787
796
|
if (!draftNode.value) return;
|
|
@@ -820,12 +829,12 @@ async function runTestStep() {
|
|
|
820
829
|
try {
|
|
821
830
|
const parsed = JSON.parse(testInputDraft.value.trim() || defaultTestInput());
|
|
822
831
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
823
|
-
testError.value = "
|
|
832
|
+
testError.value = t("integration.nodeInspector.output.test.inputNotObject");
|
|
824
833
|
return;
|
|
825
834
|
}
|
|
826
835
|
inputs = parsed;
|
|
827
836
|
} catch {
|
|
828
|
-
testError.value = "
|
|
837
|
+
testError.value = t("integration.nodeInspector.output.test.inputInvalidJson");
|
|
829
838
|
testInputOpen.value = true;
|
|
830
839
|
return;
|
|
831
840
|
}
|
|
@@ -843,10 +852,10 @@ async function runTestStep() {
|
|
|
843
852
|
[wn.id]: res.outputs && typeof res.outputs === "object" ? res.outputs : {}
|
|
844
853
|
};
|
|
845
854
|
outputView.value = "json";
|
|
846
|
-
toast.success("
|
|
855
|
+
toast.success(t("integration.nodeInspector.toast.testRunFinished"));
|
|
847
856
|
} catch (e) {
|
|
848
857
|
const body = e.body;
|
|
849
|
-
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");
|
|
850
859
|
testLogs.value = body?.logs ?? [];
|
|
851
860
|
} finally {
|
|
852
861
|
testRunning.value = false;
|
|
@@ -870,7 +879,7 @@ async function runTestStep() {
|
|
|
870
879
|
type, not for the field. -->
|
|
871
880
|
<input
|
|
872
881
|
ref="nameInput"
|
|
873
|
-
aria-label="
|
|
882
|
+
:aria-label="t('integration.nodeInspector.header.nameAria')"
|
|
874
883
|
:value="nameValue"
|
|
875
884
|
:placeholder="namePlaceholder"
|
|
876
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"
|
|
@@ -895,13 +904,13 @@ async function runTestStep() {
|
|
|
895
904
|
the description at all. -->
|
|
896
905
|
<button
|
|
897
906
|
type="button"
|
|
898
|
-
aria-label="
|
|
907
|
+
:aria-label="t('integration.nodeInspector.header.whatThisDoes')"
|
|
899
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"
|
|
900
909
|
>
|
|
901
910
|
?
|
|
902
911
|
</button>
|
|
903
912
|
</PopoverTrigger>
|
|
904
|
-
<PopoverContent aria-label="
|
|
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">
|
|
905
914
|
<div class="flex items-center gap-2.5">
|
|
906
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">
|
|
907
916
|
<IntegrationsNodeMark :manifest="manifest" :size="18" />
|
|
@@ -943,13 +952,13 @@ async function runTestStep() {
|
|
|
943
952
|
{{ noteLabel }}
|
|
944
953
|
</button>
|
|
945
954
|
</PopoverTrigger>
|
|
946
|
-
<PopoverContent aria-label="
|
|
947
|
-
<Label class="text-mono text-muted-foreground">
|
|
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>
|
|
948
957
|
<Textarea
|
|
949
|
-
aria-label="
|
|
958
|
+
:aria-label="t('integration.nodeInspector.note.label')"
|
|
950
959
|
:model-value="draftNode.notes ?? ''"
|
|
951
960
|
:rows="4"
|
|
952
|
-
placeholder="
|
|
961
|
+
:placeholder="t('integration.nodeInspector.note.placeholder')"
|
|
953
962
|
class="rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
|
|
954
963
|
@update:model-value="writeNote"
|
|
955
964
|
/>
|
|
@@ -972,7 +981,7 @@ async function runTestStep() {
|
|
|
972
981
|
@click="inputPaneOpen = !inputPaneOpen"
|
|
973
982
|
>
|
|
974
983
|
<Database :size="12" />
|
|
975
|
-
|
|
984
|
+
{{ t("integration.nodeInspector.header.inputData") }}
|
|
976
985
|
</button>
|
|
977
986
|
<!-- Icon-only controls: `title` alone leaves them nameless to a screen
|
|
978
987
|
reader (and unaddressable to a test without a CSS selector), so each
|
|
@@ -981,8 +990,8 @@ async function runTestStep() {
|
|
|
981
990
|
<button
|
|
982
991
|
type="button"
|
|
983
992
|
:disabled="!hasPrev"
|
|
984
|
-
aria-label="
|
|
985
|
-
title="
|
|
993
|
+
:aria-label="t('integration.nodeInspector.header.previousStep')"
|
|
994
|
+
:title="t('integration.nodeInspector.header.previousStep')"
|
|
986
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"
|
|
987
996
|
@click="navigate(-1)"
|
|
988
997
|
>
|
|
@@ -991,8 +1000,8 @@ async function runTestStep() {
|
|
|
991
1000
|
<button
|
|
992
1001
|
type="button"
|
|
993
1002
|
:disabled="!hasNext"
|
|
994
|
-
aria-label="
|
|
995
|
-
title="
|
|
1003
|
+
:aria-label="t('integration.nodeInspector.header.nextStep')"
|
|
1004
|
+
:title="t('integration.nodeInspector.header.nextStep')"
|
|
996
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"
|
|
997
1006
|
@click="navigate(1)"
|
|
998
1007
|
>
|
|
@@ -1003,8 +1012,8 @@ async function runTestStep() {
|
|
|
1003
1012
|
(the DialogContent built-in X floats at its own offset). -->
|
|
1004
1013
|
<button
|
|
1005
1014
|
type="button"
|
|
1006
|
-
aria-label="
|
|
1007
|
-
title="
|
|
1015
|
+
:aria-label="t('integration.common.action.close')"
|
|
1016
|
+
:title="t('integration.common.action.close')"
|
|
1008
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"
|
|
1009
1018
|
@click="emit('close')"
|
|
1010
1019
|
>
|
|
@@ -1018,18 +1027,18 @@ async function runTestStep() {
|
|
|
1018
1027
|
<aside v-if="inputPaneOpen" class="flex w-[300px] shrink-0 flex-col border-r border-black/[0.06] bg-muted/10" @dragend="onChipDragEnd">
|
|
1019
1028
|
<div class="shrink-0 border-b border-black/[0.06] px-3 py-2.5">
|
|
1020
1029
|
<div class="text-mono text-muted-foreground">
|
|
1021
|
-
|
|
1030
|
+
{{ t("integration.nodeInspector.input.title") }}
|
|
1022
1031
|
</div>
|
|
1023
1032
|
<p class="mt-0.5 text-xs leading-snug text-muted-foreground">
|
|
1024
|
-
|
|
1033
|
+
{{ t("integration.nodeInspector.input.description") }}
|
|
1025
1034
|
</p>
|
|
1026
1035
|
<div class="relative mt-2">
|
|
1027
1036
|
<Search :size="12" class="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
1028
1037
|
<input
|
|
1029
1038
|
v-model="inputSearch"
|
|
1030
1039
|
type="text"
|
|
1031
|
-
aria-label="
|
|
1032
|
-
placeholder="
|
|
1040
|
+
:aria-label="t('integration.nodeInspector.input.searchAria')"
|
|
1041
|
+
:placeholder="t('integration.nodeInspector.input.searchAria')"
|
|
1033
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"
|
|
1034
1043
|
>
|
|
1035
1044
|
</div>
|
|
@@ -1041,15 +1050,15 @@ async function runTestStep() {
|
|
|
1041
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"
|
|
1042
1051
|
@click="loadRunData"
|
|
1043
1052
|
>
|
|
1044
|
-
{{ runLoading ? "
|
|
1053
|
+
{{ runLoading ? t("integration.nodeInspector.input.runData.loading") : t("integration.nodeInspector.input.runData.use") }}
|
|
1045
1054
|
</button>
|
|
1046
1055
|
<template v-else>
|
|
1047
1056
|
<span class="min-w-0 flex-1 truncate text-[11px] text-emerald-700">
|
|
1048
|
-
|
|
1057
|
+
{{ t("integration.nodeInspector.input.runData.from", { id: runInfo.id }) }}
|
|
1049
1058
|
</span>
|
|
1050
1059
|
<button
|
|
1051
1060
|
type="button"
|
|
1052
|
-
title="
|
|
1061
|
+
:title="t('integration.nodeInspector.input.runData.clear')"
|
|
1053
1062
|
class="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-foreground"
|
|
1054
1063
|
@click="clearRunData"
|
|
1055
1064
|
>
|
|
@@ -1066,10 +1075,10 @@ async function runTestStep() {
|
|
|
1066
1075
|
<!-- Upstream nodes -->
|
|
1067
1076
|
<div v-if="!searchQuery || visibleSections.length" class="space-y-3">
|
|
1068
1077
|
<div class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
1069
|
-
|
|
1078
|
+
{{ t("integration.nodeInspector.input.nodes.heading") }}
|
|
1070
1079
|
</div>
|
|
1071
1080
|
<p v-if="!upstreamSections.length" class="text-xs leading-snug text-muted-foreground">
|
|
1072
|
-
|
|
1081
|
+
{{ t("integration.nodeInspector.input.nodes.empty") }}
|
|
1073
1082
|
</p>
|
|
1074
1083
|
<div v-for="s in visibleSections" :key="s.id" class="rounded-md border border-black/[0.06]">
|
|
1075
1084
|
<button
|
|
@@ -1083,9 +1092,9 @@ async function runTestStep() {
|
|
|
1083
1092
|
<span
|
|
1084
1093
|
v-if="!s.direct"
|
|
1085
1094
|
class="shrink-0 rounded bg-slate-100 px-1 py-px text-[9px] font-medium uppercase tracking-wider text-slate-500"
|
|
1086
|
-
title="
|
|
1095
|
+
:title="t('integration.nodeInspector.input.nodes.indirectTitle')"
|
|
1087
1096
|
>
|
|
1088
|
-
|
|
1097
|
+
{{ t("integration.nodeInspector.input.nodes.indirectBadge") }}
|
|
1089
1098
|
</span>
|
|
1090
1099
|
</button>
|
|
1091
1100
|
<div v-show="!sectionClosed(s.id)" class="space-y-2 border-t border-black/[0.06] px-2 py-2">
|
|
@@ -1134,7 +1143,7 @@ async function runTestStep() {
|
|
|
1134
1143
|
<!-- Trigger payload -->
|
|
1135
1144
|
<div v-if="showTriggerChip" class="space-y-1.5">
|
|
1136
1145
|
<div class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
1137
|
-
|
|
1146
|
+
{{ t("integration.nodeInspector.input.trigger.heading") }}
|
|
1138
1147
|
</div>
|
|
1139
1148
|
<div class="flex flex-wrap gap-1">
|
|
1140
1149
|
<button
|
|
@@ -1150,19 +1159,19 @@ async function runTestStep() {
|
|
|
1150
1159
|
</button>
|
|
1151
1160
|
</div>
|
|
1152
1161
|
<p class="text-xs leading-snug text-muted-foreground">
|
|
1153
|
-
|
|
1162
|
+
{{ t("integration.nodeInspector.input.trigger.hint") }}
|
|
1154
1163
|
</p>
|
|
1155
1164
|
</div>
|
|
1156
1165
|
</template>
|
|
1157
1166
|
|
|
1158
1167
|
<p v-else class="text-xs leading-snug text-muted-foreground">
|
|
1159
|
-
|
|
1168
|
+
{{ t("integration.nodeInspector.input.noUpstreamForTrigger") }}
|
|
1160
1169
|
</p>
|
|
1161
1170
|
|
|
1162
1171
|
<!-- Workflow variables -->
|
|
1163
1172
|
<div v-if="!searchQuery || visibleVariableNames.length" class="space-y-1.5">
|
|
1164
1173
|
<div class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
1165
|
-
|
|
1174
|
+
{{ t("integration.nodeInspector.input.variables.heading") }}
|
|
1166
1175
|
</div>
|
|
1167
1176
|
<!-- Schema rows with the declared default as the value. -->
|
|
1168
1177
|
<div v-if="variableRows.length" class="space-y-px">
|
|
@@ -1183,12 +1192,12 @@ async function runTestStep() {
|
|
|
1183
1192
|
</button>
|
|
1184
1193
|
</div>
|
|
1185
1194
|
<p v-else class="text-xs leading-snug text-muted-foreground">
|
|
1186
|
-
|
|
1195
|
+
{{ t("integration.nodeInspector.input.variables.empty") }}
|
|
1187
1196
|
</p>
|
|
1188
1197
|
</div>
|
|
1189
1198
|
|
|
1190
1199
|
<p v-if="searchEmpty" class="text-xs leading-snug text-muted-foreground">
|
|
1191
|
-
|
|
1200
|
+
{{ t("integration.nodeInspector.input.search.empty", { query: inputSearch.trim() }) }}
|
|
1192
1201
|
</p>
|
|
1193
1202
|
</div>
|
|
1194
1203
|
</aside>
|
|
@@ -1215,15 +1224,15 @@ async function runTestStep() {
|
|
|
1215
1224
|
<!-- The name lives in the text next to the switch, which is not
|
|
1216
1225
|
a <label> — so the switch says it itself. -->
|
|
1217
1226
|
<Switch
|
|
1218
|
-
aria-label="
|
|
1227
|
+
:aria-label="t('integration.nodeInspector.trigger.active.label')"
|
|
1219
1228
|
class="data-[state=unchecked]:bg-slate-300"
|
|
1220
1229
|
:model-value="draftTrigger.active !== false"
|
|
1221
1230
|
@update:model-value="(v) => patchTrigger({ active: v })"
|
|
1222
1231
|
/>
|
|
1223
1232
|
<div>
|
|
1224
|
-
<div class="text-sm font-medium">
|
|
1233
|
+
<div class="text-sm font-medium">{{ t("integration.nodeInspector.trigger.active.label") }}</div>
|
|
1225
1234
|
<p class="text-xs text-muted-foreground">
|
|
1226
|
-
|
|
1235
|
+
{{ t("integration.nodeInspector.trigger.active.hint") }}
|
|
1227
1236
|
</p>
|
|
1228
1237
|
</div>
|
|
1229
1238
|
</div>
|
|
@@ -1237,29 +1246,29 @@ async function runTestStep() {
|
|
|
1237
1246
|
<div class="space-y-1.5">
|
|
1238
1247
|
<!-- "Frequency", not "Runs": a run is an execution everywhere
|
|
1239
1248
|
else in the studio, and there is a whole page of them. -->
|
|
1240
|
-
<Label for="ni-frequency" class="text-mono text-muted-foreground">
|
|
1249
|
+
<Label for="ni-frequency" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.frequency.label") }}</Label>
|
|
1241
1250
|
<Select :model-value="frequency" @update:model-value="applyFrequency">
|
|
1242
|
-
<SelectTrigger id="ni-frequency" aria-label="
|
|
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">
|
|
1243
1252
|
<SelectValue />
|
|
1244
1253
|
</SelectTrigger>
|
|
1245
1254
|
<SelectContent>
|
|
1246
1255
|
<SelectItem v-for="f in FREQUENCY_OPTIONS" :key="f.value" :value="f.value">{{ f.label }}</SelectItem>
|
|
1247
1256
|
<SelectSeparator />
|
|
1248
|
-
<SelectItem :value="FREQUENCY_ADVANCED">
|
|
1257
|
+
<SelectItem :value="FREQUENCY_ADVANCED">{{ t("integration.nodeInspector.schedule.frequency.advanced") }}</SelectItem>
|
|
1249
1258
|
</SelectContent>
|
|
1250
1259
|
</Select>
|
|
1251
1260
|
</div>
|
|
1252
1261
|
|
|
1253
1262
|
<!-- Every few minutes: the gap between firings. -->
|
|
1254
1263
|
<div v-if="frequency === 'minutes'" class="space-y-1.5">
|
|
1255
|
-
<Label for="ni-minute-interval" class="text-mono text-muted-foreground">
|
|
1264
|
+
<Label for="ni-minute-interval" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.interval.label") }}</Label>
|
|
1256
1265
|
<Select :model-value="String(carry.minuteInterval)" @update:model-value="applyMinuteInterval">
|
|
1257
|
-
<SelectTrigger id="ni-minute-interval" aria-label="
|
|
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">
|
|
1258
1267
|
<SelectValue />
|
|
1259
1268
|
</SelectTrigger>
|
|
1260
1269
|
<SelectContent>
|
|
1261
1270
|
<SelectItem v-for="n in minuteIntervalOptions" :key="n" :value="String(n)">
|
|
1262
|
-
{{
|
|
1271
|
+
{{ t("integration.nodeInspector.schedule.minuteInterval.option", n) }}
|
|
1263
1272
|
</SelectItem>
|
|
1264
1273
|
</SelectContent>
|
|
1265
1274
|
</Select>
|
|
@@ -1267,21 +1276,21 @@ async function runTestStep() {
|
|
|
1267
1276
|
|
|
1268
1277
|
<!-- Every few hours: the gap, plus where in the hour it lands. -->
|
|
1269
1278
|
<div v-if="frequency === 'hours'" class="space-y-1.5">
|
|
1270
|
-
<Label for="ni-hour-interval" class="text-mono text-muted-foreground">
|
|
1279
|
+
<Label for="ni-hour-interval" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.interval.label") }}</Label>
|
|
1271
1280
|
<Select :model-value="String(carry.hourInterval)" @update:model-value="applyHourInterval">
|
|
1272
|
-
<SelectTrigger id="ni-hour-interval" aria-label="
|
|
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">
|
|
1273
1282
|
<SelectValue />
|
|
1274
1283
|
</SelectTrigger>
|
|
1275
1284
|
<SelectContent>
|
|
1276
|
-
<SelectItem v-for="n in HOUR_INTERVALS" :key="n" :value="String(n)">
|
|
1285
|
+
<SelectItem v-for="n in HOUR_INTERVALS" :key="n" :value="String(n)">{{ t("integration.nodeInspector.schedule.hourInterval.option", { n }) }}</SelectItem>
|
|
1277
1286
|
</SelectContent>
|
|
1278
1287
|
</Select>
|
|
1279
1288
|
</div>
|
|
1280
1289
|
|
|
1281
1290
|
<div v-if="frequency === 'hourly' || frequency === 'hours' || frequency === 'times'" class="space-y-1.5">
|
|
1282
|
-
<Label for="ni-minute-past" class="text-mono text-muted-foreground">
|
|
1291
|
+
<Label for="ni-minute-past" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.minutePast.label") }}</Label>
|
|
1283
1292
|
<Select :model-value="String(carry.minute)" @update:model-value="applyMinutePast">
|
|
1284
|
-
<SelectTrigger id="ni-minute-past" aria-label="
|
|
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">
|
|
1285
1294
|
<SelectValue />
|
|
1286
1295
|
</SelectTrigger>
|
|
1287
1296
|
<SelectContent>
|
|
@@ -1294,11 +1303,11 @@ async function runTestStep() {
|
|
|
1294
1303
|
rhythms share, which is why it survives a change of
|
|
1295
1304
|
frequency. -->
|
|
1296
1305
|
<div v-if="frequency === 'daily' || frequency === 'weekly' || frequency === 'monthly'" class="space-y-1.5">
|
|
1297
|
-
<Label for="ni-time" class="text-mono text-muted-foreground">
|
|
1306
|
+
<Label for="ni-time" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.time.label") }}</Label>
|
|
1298
1307
|
<Input
|
|
1299
1308
|
id="ni-time"
|
|
1300
1309
|
type="time"
|
|
1301
|
-
aria-label="
|
|
1310
|
+
:aria-label="t('integration.nodeInspector.schedule.time.label')"
|
|
1302
1311
|
:model-value="timeValue"
|
|
1303
1312
|
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white tabular-nums"
|
|
1304
1313
|
@update:model-value="applyTime"
|
|
@@ -1306,9 +1315,9 @@ async function runTestStep() {
|
|
|
1306
1315
|
</div>
|
|
1307
1316
|
|
|
1308
1317
|
<div v-if="frequency === 'monthly'" class="space-y-1.5">
|
|
1309
|
-
<Label for="ni-day-of-month" class="text-mono text-muted-foreground">
|
|
1318
|
+
<Label for="ni-day-of-month" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.dayOfMonth.label") }}</Label>
|
|
1310
1319
|
<Select :model-value="String(carry.day)" @update:model-value="applyDayOfMonth">
|
|
1311
|
-
<SelectTrigger id="ni-day-of-month" aria-label="
|
|
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">
|
|
1312
1321
|
<SelectValue />
|
|
1313
1322
|
</SelectTrigger>
|
|
1314
1323
|
<SelectContent>
|
|
@@ -1319,10 +1328,10 @@ async function runTestStep() {
|
|
|
1319
1328
|
|
|
1320
1329
|
<!-- Advanced: the expression itself, the only place it is shown. -->
|
|
1321
1330
|
<div v-if="frequency === FREQUENCY_ADVANCED" class="space-y-1.5">
|
|
1322
|
-
<Label for="ni-cron" class="text-mono text-muted-foreground">
|
|
1331
|
+
<Label for="ni-cron" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.cron.label") }}</Label>
|
|
1323
1332
|
<Input
|
|
1324
1333
|
id="ni-cron"
|
|
1325
|
-
aria-label="
|
|
1334
|
+
:aria-label="t('integration.nodeInspector.schedule.cron.label')"
|
|
1326
1335
|
:model-value="cronValue"
|
|
1327
1336
|
placeholder="0 9-17 * * 1-5"
|
|
1328
1337
|
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white font-mono"
|
|
@@ -1338,10 +1347,10 @@ async function runTestStep() {
|
|
|
1338
1347
|
schedule triggers rather than one field per row.
|
|
1339
1348
|
-->
|
|
1340
1349
|
<div v-if="frequency === 'times'" class="space-y-1.5">
|
|
1341
|
-
<Label class="text-mono text-muted-foreground">
|
|
1350
|
+
<Label class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.hours.label") }}</Label>
|
|
1342
1351
|
<ToggleGroup
|
|
1343
1352
|
type="multiple"
|
|
1344
|
-
aria-label="
|
|
1353
|
+
:aria-label="t('integration.nodeInspector.schedule.hours.label')"
|
|
1345
1354
|
:model-value="selectedHours"
|
|
1346
1355
|
class="grid w-fit grid-cols-12 gap-1"
|
|
1347
1356
|
@update:model-value="applyHours"
|
|
@@ -1361,10 +1370,10 @@ async function runTestStep() {
|
|
|
1361
1370
|
<!-- Weekdays: full width, because seven toggles in half of the
|
|
1362
1371
|
grid would wrap. Serves both rhythms that fall on days. -->
|
|
1363
1372
|
<div v-if="frequency === 'weekly' || frequency === 'times'" class="space-y-1.5">
|
|
1364
|
-
<Label class="text-mono text-muted-foreground">
|
|
1373
|
+
<Label class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.schedule.days.label") }}</Label>
|
|
1365
1374
|
<ToggleGroup
|
|
1366
1375
|
type="multiple"
|
|
1367
|
-
aria-label="
|
|
1376
|
+
:aria-label="t('integration.nodeInspector.schedule.days.label')"
|
|
1368
1377
|
:model-value="selectedDays"
|
|
1369
1378
|
class="justify-start gap-1"
|
|
1370
1379
|
@update:model-value="applyDays"
|
|
@@ -1386,25 +1395,25 @@ async function runTestStep() {
|
|
|
1386
1395
|
the expression's numbers as wall-clock time in that zone, so a time
|
|
1387
1396
|
without its zone would be half an answer.
|
|
1388
1397
|
-->
|
|
1389
|
-
<p v-if="scheduleSentence" aria-label="
|
|
1398
|
+
<p v-if="scheduleSentence" :aria-label="t('integration.nodeInspector.schedule.whenItFiresAria')" class="text-xs text-emerald-700">{{ scheduleSentence }}</p>
|
|
1390
1399
|
<!-- No expression at all is its own answer, and the same one the
|
|
1391
1400
|
Schedules page gives for this trigger: it is not written as
|
|
1392
1401
|
anything, and it does not run. A trigger placed from the palette
|
|
1393
1402
|
carries an expression, so this is for one that arrived without
|
|
1394
1403
|
one. -->
|
|
1395
|
-
<p v-else-if="!hasCron" aria-label="
|
|
1396
|
-
|
|
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") }}
|
|
1397
1406
|
</p>
|
|
1398
1407
|
<p v-else class="text-xs text-muted-foreground">
|
|
1399
|
-
|
|
1408
|
+
{{ t("integration.nodeInspector.schedule.writtenAsExpression") }}
|
|
1400
1409
|
</p>
|
|
1401
1410
|
<p v-if="frequency === 'monthly' && carry.day >= 29" class="text-xs text-muted-foreground">
|
|
1402
|
-
|
|
1411
|
+
{{ t("integration.nodeInspector.schedule.monthlySkipHint") }}
|
|
1403
1412
|
</p>
|
|
1404
1413
|
</template>
|
|
1405
1414
|
<template v-else-if="draftTrigger.type === 'webhook'">
|
|
1406
1415
|
<div class="space-y-1.5">
|
|
1407
|
-
<Label for="ni-webhook-url" 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>
|
|
1408
1417
|
<div v-if="webhookUrl" class="flex items-center gap-1.5">
|
|
1409
1418
|
<Input
|
|
1410
1419
|
id="ni-webhook-url"
|
|
@@ -1415,7 +1424,7 @@ async function runTestStep() {
|
|
|
1415
1424
|
/>
|
|
1416
1425
|
<button
|
|
1417
1426
|
type="button"
|
|
1418
|
-
title="
|
|
1427
|
+
:title="t('integration.nodeInspector.trigger.webhook.url.copyTitle')"
|
|
1419
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"
|
|
1420
1429
|
@click="copyWebhookUrl"
|
|
1421
1430
|
>
|
|
@@ -1423,27 +1432,27 @@ async function runTestStep() {
|
|
|
1423
1432
|
</button>
|
|
1424
1433
|
</div>
|
|
1425
1434
|
<p class="text-xs text-muted-foreground">
|
|
1426
|
-
<template v-if="webhookIsPublic">
|
|
1427
|
-
<template v-else>
|
|
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>
|
|
1428
1437
|
</p>
|
|
1429
1438
|
</div>
|
|
1430
1439
|
<div class="flex items-center gap-3 rounded-lg border border-black/[0.06] bg-muted/20 px-3 py-2">
|
|
1431
1440
|
<Switch
|
|
1432
|
-
aria-label="
|
|
1441
|
+
:aria-label="t('integration.nodeInspector.trigger.webhook.public.label')"
|
|
1433
1442
|
class="data-[state=unchecked]:bg-slate-300"
|
|
1434
1443
|
:model-value="webhookIsPublic"
|
|
1435
1444
|
@update:model-value="(v) => writeTriggerConfigPatch(v ? { public: true, ...draftTrigger.config.authProfile ? {} : configPatchForAuthChoice('token') } : { public: void 0 })"
|
|
1436
1445
|
/>
|
|
1437
1446
|
<div>
|
|
1438
|
-
<div class="text-sm font-medium">
|
|
1447
|
+
<div class="text-sm font-medium">{{ t("integration.nodeInspector.trigger.webhook.public.label") }}</div>
|
|
1439
1448
|
<p class="text-xs text-muted-foreground">
|
|
1440
|
-
|
|
1449
|
+
{{ t("integration.nodeInspector.trigger.webhook.public.hint") }}
|
|
1441
1450
|
</p>
|
|
1442
1451
|
</div>
|
|
1443
1452
|
</div>
|
|
1444
1453
|
<div class="grid grid-cols-2 gap-3">
|
|
1445
1454
|
<div class="space-y-1.5">
|
|
1446
|
-
<Label for="ni-webhook-method" class="text-mono text-muted-foreground">
|
|
1455
|
+
<Label for="ni-webhook-method" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.method.label") }}</Label>
|
|
1447
1456
|
<Select
|
|
1448
1457
|
:model-value="readTriggerConfig('method') ?? 'POST'"
|
|
1449
1458
|
@update:model-value="(v) => writeTriggerConfig('method', v)"
|
|
@@ -1461,23 +1470,23 @@ async function runTestStep() {
|
|
|
1461
1470
|
</Select>
|
|
1462
1471
|
</div>
|
|
1463
1472
|
<div v-if="webhookIsPublic" class="space-y-1.5">
|
|
1464
|
-
<Label for="ni-webhook-protection" class="text-mono text-muted-foreground">
|
|
1473
|
+
<Label for="ni-webhook-protection" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.protection.label") }}</Label>
|
|
1465
1474
|
<Select :model-value="authChoice" @update:model-value="applyAuthChoice">
|
|
1466
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">
|
|
1467
1476
|
<SelectValue />
|
|
1468
1477
|
</SelectTrigger>
|
|
1469
1478
|
<SelectContent>
|
|
1470
|
-
<SelectItem value="token">
|
|
1471
|
-
<SelectItem value="bearer">
|
|
1472
|
-
<SelectItem value="basic">
|
|
1473
|
-
<SelectItem value="none">
|
|
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>
|
|
1474
1483
|
</SelectContent>
|
|
1475
1484
|
</Select>
|
|
1476
|
-
<p class="text-xs text-muted-foreground">
|
|
1485
|
+
<p class="text-xs text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.protection.hint") }}</p>
|
|
1477
1486
|
</div>
|
|
1478
1487
|
</div>
|
|
1479
1488
|
<div v-if="webhookIsPublic && authChoice === 'token'" class="space-y-1.5">
|
|
1480
|
-
<Label for="ni-auth-header" class="text-mono text-muted-foreground">
|
|
1489
|
+
<Label for="ni-auth-header" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.header.label") }}</Label>
|
|
1481
1490
|
<Input
|
|
1482
1491
|
id="ni-auth-header"
|
|
1483
1492
|
:model-value="draftTrigger.config.authConfig?.header ?? ''"
|
|
@@ -1485,33 +1494,34 @@ async function runTestStep() {
|
|
|
1485
1494
|
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
|
|
1486
1495
|
@update:model-value="(v) => writeAuthConfigKey('header', String(v))"
|
|
1487
1496
|
/>
|
|
1488
|
-
<p class="text-xs text-muted-foreground">
|
|
1497
|
+
<p class="text-xs text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.header.hint") }}</p>
|
|
1489
1498
|
</div>
|
|
1490
1499
|
<div v-if="webhookIsPublic && authChoice === 'basic'" class="space-y-1.5">
|
|
1491
|
-
<Label for="ni-auth-username" class="text-mono text-muted-foreground">
|
|
1500
|
+
<Label for="ni-auth-username" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.username.label") }}</Label>
|
|
1492
1501
|
<Input
|
|
1493
1502
|
id="ni-auth-username"
|
|
1494
1503
|
:model-value="draftTrigger.config.authConfig?.username ?? ''"
|
|
1495
|
-
placeholder="
|
|
1504
|
+
:placeholder="t('integration.nodeInspector.trigger.webhook.username.placeholder')"
|
|
1496
1505
|
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
|
|
1497
1506
|
@update:model-value="(v) => writeAuthConfigKey('username', String(v))"
|
|
1498
1507
|
/>
|
|
1499
|
-
<p class="text-xs text-muted-foreground">
|
|
1508
|
+
<p class="text-xs text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.username.hint") }}</p>
|
|
1500
1509
|
</div>
|
|
1501
1510
|
<div v-if="webhookExposed && authChoice !== 'none'" class="flex items-center gap-2">
|
|
1502
1511
|
<Button type="button" variant="outline" size="sm" class="h-7 text-sm" :disabled="rotateBusy" @click="rotateEndpointSecret">
|
|
1503
|
-
{{ rotateBusy ? "
|
|
1512
|
+
{{ rotateBusy ? t("integration.nodeInspector.trigger.webhook.rotate.busy") : t("integration.nodeInspector.trigger.webhook.rotate.action") }}
|
|
1504
1513
|
</Button>
|
|
1505
|
-
<p class="text-xs text-muted-foreground">
|
|
1514
|
+
<p class="text-xs text-muted-foreground">{{ t("integration.nodeInspector.trigger.webhook.rotate.hint") }}</p>
|
|
1506
1515
|
</div>
|
|
1507
1516
|
|
|
1508
1517
|
<Dialog v-model:open="rotateOpen">
|
|
1509
1518
|
<DialogContent class="sm:max-w-md">
|
|
1510
1519
|
<DialogHeader>
|
|
1511
|
-
<DialogTitle>
|
|
1520
|
+
<DialogTitle>{{ t("integration.nodeInspector.trigger.webhook.rotate.dialogTitle") }}</DialogTitle>
|
|
1512
1521
|
<DialogDescription>
|
|
1513
|
-
|
|
1514
|
-
|
|
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
|
+
}) }}
|
|
1515
1525
|
</DialogDescription>
|
|
1516
1526
|
</DialogHeader>
|
|
1517
1527
|
<div class="flex items-center gap-1.5">
|
|
@@ -1523,7 +1533,7 @@ async function runTestStep() {
|
|
|
1523
1533
|
/>
|
|
1524
1534
|
<button
|
|
1525
1535
|
type="button"
|
|
1526
|
-
title="
|
|
1536
|
+
:title="t('integration.nodeInspector.trigger.webhook.rotate.copyTitle')"
|
|
1527
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"
|
|
1528
1538
|
@click="copySecret"
|
|
1529
1539
|
>
|
|
@@ -1535,36 +1545,36 @@ async function runTestStep() {
|
|
|
1535
1545
|
</template>
|
|
1536
1546
|
<template v-else-if="draftTrigger.type === 'event'">
|
|
1537
1547
|
<div class="space-y-1.5">
|
|
1538
|
-
<Label for="ni-event-type" 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>
|
|
1539
1549
|
<EventTypeCombobox
|
|
1540
1550
|
input-id="ni-event-type"
|
|
1541
1551
|
:model-value="readTriggerConfig('event_type') ?? ''"
|
|
1542
1552
|
@update:model-value="(v) => writeTriggerConfig('event_type', v)"
|
|
1543
1553
|
/>
|
|
1544
1554
|
<p class="text-xs text-muted-foreground">
|
|
1545
|
-
|
|
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>.
|
|
1546
1556
|
</p>
|
|
1547
1557
|
</div>
|
|
1548
1558
|
<div v-if="eventSample" class="space-y-1.5">
|
|
1549
1559
|
<!-- A caption, not a field label: it heads a read-only block,
|
|
1550
1560
|
so it stays a plain line rather than an orphan <label>. -->
|
|
1551
|
-
<div class="text-mono text-muted-foreground">
|
|
1561
|
+
<div class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.event.sample.heading") }}</div>
|
|
1552
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>
|
|
1553
|
-
<p class="text-xs text-muted-foreground">
|
|
1563
|
+
<p class="text-xs text-muted-foreground">{{ t("integration.nodeInspector.trigger.event.sample.hint") }} <code class="font-mono">trigger.payload</code>.</p>
|
|
1554
1564
|
</div>
|
|
1555
1565
|
<p v-else-if="eventTopic" class="text-xs text-muted-foreground">
|
|
1556
|
-
|
|
1566
|
+
{{ t("integration.nodeInspector.trigger.event.sample.empty") }}
|
|
1557
1567
|
</p>
|
|
1558
1568
|
</template>
|
|
1559
1569
|
<template v-else>
|
|
1560
1570
|
<div class="space-y-1.5">
|
|
1561
1571
|
<!-- Caption for the field list below, not a label for one
|
|
1562
1572
|
control — the row's own controls are named individually. -->
|
|
1563
|
-
<div class="text-mono text-muted-foreground">
|
|
1573
|
+
<div class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.trigger.manual.heading") }}</div>
|
|
1564
1574
|
<p class="text-xs text-muted-foreground">
|
|
1565
|
-
|
|
1566
|
-
<code class="font-mono">trigger.payload.<key></code
|
|
1567
|
-
|
|
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") }}
|
|
1568
1578
|
</p>
|
|
1569
1579
|
</div>
|
|
1570
1580
|
<div v-if="manualFields.length" class="space-y-1.5">
|
|
@@ -1572,41 +1582,41 @@ async function runTestStep() {
|
|
|
1572
1582
|
<!-- A repeating row has no room for visible labels, so each
|
|
1573
1583
|
control carries its own name and says which row it is. -->
|
|
1574
1584
|
<Select :model-value="mf.kind ?? 'text'" @update:model-value="(v) => setManualField(idx, { kind: v })">
|
|
1575
|
-
<SelectTrigger :aria-label="
|
|
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">
|
|
1576
1586
|
<SelectValue />
|
|
1577
1587
|
</SelectTrigger>
|
|
1578
1588
|
<SelectContent>
|
|
1579
|
-
<SelectItem value="text">
|
|
1580
|
-
<SelectItem value="file">
|
|
1589
|
+
<SelectItem value="text">{{ t("integration.nodeInspector.trigger.manual.kind.text") }}</SelectItem>
|
|
1590
|
+
<SelectItem value="file">{{ t("integration.nodeInspector.trigger.manual.kind.file") }}</SelectItem>
|
|
1581
1591
|
</SelectContent>
|
|
1582
1592
|
</Select>
|
|
1583
1593
|
<Input
|
|
1584
|
-
:aria-label="
|
|
1594
|
+
:aria-label="t('integration.nodeInspector.trigger.manual.field.keyAria', { n: idx + 1 })"
|
|
1585
1595
|
:model-value="mf.path"
|
|
1586
|
-
placeholder="
|
|
1596
|
+
:placeholder="t('integration.nodeInspector.trigger.manual.field.keyPlaceholder')"
|
|
1587
1597
|
class="h-8 w-[38%] rounded-md border border-black/[0.08] ring-0 shadow-none bg-white font-mono text-sm"
|
|
1588
1598
|
@update:model-value="(v) => setManualField(idx, { path: String(v) })"
|
|
1589
1599
|
/>
|
|
1590
1600
|
<Input
|
|
1591
|
-
:aria-label="
|
|
1601
|
+
:aria-label="t('integration.nodeInspector.trigger.manual.field.labelAria', { n: idx + 1 })"
|
|
1592
1602
|
:model-value="mf.label ?? ''"
|
|
1593
|
-
placeholder="
|
|
1603
|
+
:placeholder="t('integration.nodeInspector.trigger.manual.field.labelPlaceholder')"
|
|
1594
1604
|
class="h-8 min-w-0 flex-1 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
|
|
1595
1605
|
@update:model-value="(v) => setManualField(idx, { label: String(v) })"
|
|
1596
1606
|
/>
|
|
1597
1607
|
<button
|
|
1598
1608
|
type="button"
|
|
1599
|
-
:aria-label="
|
|
1609
|
+
:aria-label="t('integration.nodeInspector.trigger.manual.field.removeAria', { n: idx + 1 })"
|
|
1600
1610
|
class="flex size-7 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
|
|
1601
1611
|
@click="removeManualField(idx)"
|
|
1602
1612
|
>
|
|
1603
|
-
<
|
|
1613
|
+
<TrashBin :size="13" />
|
|
1604
1614
|
</button>
|
|
1605
1615
|
</div>
|
|
1606
1616
|
</div>
|
|
1607
1617
|
<Button type="button" variant="outline" size="sm" class="h-7 text-sm" @click="addManualField">
|
|
1608
1618
|
<Plus :size="12" />
|
|
1609
|
-
|
|
1619
|
+
{{ t("integration.nodeInspector.trigger.manual.action.addField") }}
|
|
1610
1620
|
</Button>
|
|
1611
1621
|
</template>
|
|
1612
1622
|
</div>
|
|
@@ -1615,23 +1625,23 @@ async function runTestStep() {
|
|
|
1615
1625
|
<div v-else-if="tab === 'parameters' && draftNode" class="mx-auto w-full max-w-5xl">
|
|
1616
1626
|
<div v-if="manifest?.config?.length" class="space-y-3">
|
|
1617
1627
|
<div class="flex items-center justify-between">
|
|
1618
|
-
<span class="text-mono text-muted-foreground">
|
|
1628
|
+
<span class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.parameters.heading") }}</span>
|
|
1619
1629
|
<div class="flex items-center gap-2">
|
|
1620
1630
|
<button
|
|
1621
1631
|
type="button"
|
|
1622
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"
|
|
1623
|
-
title="
|
|
1633
|
+
:title="t('integration.nodeInspector.parameters.copyTitle')"
|
|
1624
1634
|
@click="copyConfig"
|
|
1625
1635
|
>
|
|
1626
|
-
<Copy :size="12" />
|
|
1636
|
+
<Copy :size="12" /> {{ t("integration.common.action.copy") }}
|
|
1627
1637
|
</button>
|
|
1628
1638
|
<button
|
|
1629
1639
|
type="button"
|
|
1630
1640
|
class="rounded px-1 py-0.5 text-sm text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
1631
|
-
title="
|
|
1641
|
+
:title="t('integration.nodeInspector.parameters.pasteTitle')"
|
|
1632
1642
|
@click="pasteConfig"
|
|
1633
1643
|
>
|
|
1634
|
-
|
|
1644
|
+
{{ t("integration.common.action.paste") }}
|
|
1635
1645
|
</button>
|
|
1636
1646
|
</div>
|
|
1637
1647
|
</div>
|
|
@@ -1662,7 +1672,7 @@ async function runTestStep() {
|
|
|
1662
1672
|
@click="optionalOpen = true"
|
|
1663
1673
|
>
|
|
1664
1674
|
<ChevronRight :size="13" />
|
|
1665
|
-
|
|
1675
|
+
{{ t("integration.nodeInspector.parameters.optional.show", optionalFields.length) }}
|
|
1666
1676
|
</button>
|
|
1667
1677
|
<template v-else>
|
|
1668
1678
|
<button
|
|
@@ -1672,7 +1682,7 @@ async function runTestStep() {
|
|
|
1672
1682
|
@click="optionalOpen = false"
|
|
1673
1683
|
>
|
|
1674
1684
|
<ChevronDown :size="13" />
|
|
1675
|
-
|
|
1685
|
+
{{ t("integration.nodeInspector.parameters.optional.hide", optionalFields.length) }}
|
|
1676
1686
|
</button>
|
|
1677
1687
|
<IntegrationsNodeConfigField
|
|
1678
1688
|
v-for="field in optionalFields"
|
|
@@ -1693,11 +1703,11 @@ async function runTestStep() {
|
|
|
1693
1703
|
</template>
|
|
1694
1704
|
|
|
1695
1705
|
<p v-if="!parameterFields.length" class="text-sm text-muted-foreground italic">
|
|
1696
|
-
|
|
1706
|
+
{{ t("integration.nodeInspector.parameters.allOnSettingsTab") }}
|
|
1697
1707
|
</p>
|
|
1698
1708
|
</div>
|
|
1699
1709
|
<div v-else class="text-sm text-muted-foreground italic">
|
|
1700
|
-
|
|
1710
|
+
{{ t("integration.nodeInspector.parameters.noConfiguration") }}
|
|
1701
1711
|
</div>
|
|
1702
1712
|
</div>
|
|
1703
1713
|
|
|
@@ -1710,9 +1720,9 @@ async function runTestStep() {
|
|
|
1710
1720
|
identical-looking numbers with nothing to tell them apart. -->
|
|
1711
1721
|
<div v-if="settingsFields.length" class="space-y-3">
|
|
1712
1722
|
<div>
|
|
1713
|
-
<span class="text-mono text-muted-foreground">
|
|
1723
|
+
<span class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.settings.ownOptions.heading") }}</span>
|
|
1714
1724
|
<p class="text-xs text-muted-foreground">
|
|
1715
|
-
|
|
1725
|
+
{{ t("integration.nodeInspector.settings.ownOptions.hint") }}
|
|
1716
1726
|
</p>
|
|
1717
1727
|
</div>
|
|
1718
1728
|
<IntegrationsNodeConfigField
|
|
@@ -1734,9 +1744,9 @@ async function runTestStep() {
|
|
|
1734
1744
|
<!-- The rule only earns its place when there is something above to
|
|
1735
1745
|
divide from. -->
|
|
1736
1746
|
<div :class="settingsFields.length ? 'border-t border-black/[0.06] pt-5' : ''">
|
|
1737
|
-
<span class="text-mono text-muted-foreground">
|
|
1747
|
+
<span class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.settings.engine.heading") }}</span>
|
|
1738
1748
|
<p class="text-xs text-muted-foreground">
|
|
1739
|
-
|
|
1749
|
+
{{ t("integration.nodeInspector.settings.engine.hint") }}
|
|
1740
1750
|
</p>
|
|
1741
1751
|
</div>
|
|
1742
1752
|
|
|
@@ -1746,21 +1756,21 @@ async function runTestStep() {
|
|
|
1746
1756
|
<section class="rounded-lg border border-black/[0.06] bg-muted/20">
|
|
1747
1757
|
<div class="flex items-center gap-3 px-3 py-2">
|
|
1748
1758
|
<Switch
|
|
1749
|
-
aria-label="
|
|
1759
|
+
:aria-label="t('integration.nodeInspector.settings.retry.label')"
|
|
1750
1760
|
class="data-[state=unchecked]:bg-slate-300"
|
|
1751
1761
|
:model-value="!!draftNode.retry"
|
|
1752
1762
|
@update:model-value="(v) => patchNode({ retry: v ? { maxAttempts: 3, backoffSeconds: 1, strategy: 'exponential' } : void 0 })"
|
|
1753
1763
|
/>
|
|
1754
1764
|
<div>
|
|
1755
|
-
<div class="text-sm font-medium">
|
|
1765
|
+
<div class="text-sm font-medium">{{ t("integration.nodeInspector.settings.retry.label") }}</div>
|
|
1756
1766
|
<p class="text-xs text-muted-foreground">
|
|
1757
|
-
|
|
1767
|
+
{{ t("integration.nodeInspector.settings.retry.hint") }}
|
|
1758
1768
|
</p>
|
|
1759
1769
|
</div>
|
|
1760
1770
|
</div>
|
|
1761
1771
|
<div v-if="draftNode.retry" class="grid grid-cols-4 gap-3 border-t border-black/[0.06] p-3">
|
|
1762
1772
|
<div class="space-y-1.5">
|
|
1763
|
-
<Label for="ni-retry-attempts" 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>
|
|
1764
1774
|
<Input
|
|
1765
1775
|
id="ni-retry-attempts"
|
|
1766
1776
|
type="number"
|
|
@@ -1771,7 +1781,7 @@ async function runTestStep() {
|
|
|
1771
1781
|
/>
|
|
1772
1782
|
</div>
|
|
1773
1783
|
<div class="space-y-1.5">
|
|
1774
|
-
<Label for="ni-retry-backoff" 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>
|
|
1775
1785
|
<Input
|
|
1776
1786
|
id="ni-retry-backoff"
|
|
1777
1787
|
type="number"
|
|
@@ -1782,7 +1792,7 @@ async function runTestStep() {
|
|
|
1782
1792
|
/>
|
|
1783
1793
|
</div>
|
|
1784
1794
|
<div class="col-span-2 space-y-1.5">
|
|
1785
|
-
<Label for="ni-retry-strategy" 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>
|
|
1786
1796
|
<Select
|
|
1787
1797
|
:model-value="draftNode.retry?.strategy ?? 'exponential'"
|
|
1788
1798
|
@update:model-value="(v) => patchNode({ retry: { ...draftNode.retry, strategy: v } })"
|
|
@@ -1791,8 +1801,8 @@ async function runTestStep() {
|
|
|
1791
1801
|
<SelectValue />
|
|
1792
1802
|
</SelectTrigger>
|
|
1793
1803
|
<SelectContent>
|
|
1794
|
-
<SelectItem value="linear">
|
|
1795
|
-
<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>
|
|
1796
1806
|
</SelectContent>
|
|
1797
1807
|
</Select>
|
|
1798
1808
|
</div>
|
|
@@ -1801,7 +1811,7 @@ async function runTestStep() {
|
|
|
1801
1811
|
|
|
1802
1812
|
<!-- Timeout: engine start-to-close ceiling for one attempt -->
|
|
1803
1813
|
<section class="space-y-1.5">
|
|
1804
|
-
<Label for="ni-timeout" class="text-mono text-muted-foreground">
|
|
1814
|
+
<Label for="ni-timeout" class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.settings.timeout.label") }}</Label>
|
|
1805
1815
|
<Input
|
|
1806
1816
|
id="ni-timeout"
|
|
1807
1817
|
type="number"
|
|
@@ -1813,13 +1823,13 @@ async function runTestStep() {
|
|
|
1813
1823
|
@update:model-value="(v) => patchNode({ timeoutSeconds: Number(v) > 0 ? Math.min(Math.trunc(Number(v)), 7200) : void 0 })"
|
|
1814
1824
|
/>
|
|
1815
1825
|
<p class="text-xs text-muted-foreground">
|
|
1816
|
-
|
|
1826
|
+
{{ t("integration.nodeInspector.settings.timeout.hint") }}
|
|
1817
1827
|
</p>
|
|
1818
1828
|
</section>
|
|
1819
1829
|
|
|
1820
1830
|
<!-- On error: what happens once the retries are exhausted -->
|
|
1821
1831
|
<section class="space-y-1.5">
|
|
1822
|
-
<Label for="ni-on-error" 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>
|
|
1823
1833
|
<Select
|
|
1824
1834
|
:model-value="draftNode.onError ?? 'fail'"
|
|
1825
1835
|
@update:model-value="(v) => patchNode({ onError: v === 'route' ? 'route' : void 0 })"
|
|
@@ -1828,12 +1838,12 @@ async function runTestStep() {
|
|
|
1828
1838
|
<SelectValue />
|
|
1829
1839
|
</SelectTrigger>
|
|
1830
1840
|
<SelectContent>
|
|
1831
|
-
<SelectItem value="fail">
|
|
1832
|
-
<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>
|
|
1833
1843
|
</SelectContent>
|
|
1834
1844
|
</Select>
|
|
1835
1845
|
<p class="text-xs text-muted-foreground">
|
|
1836
|
-
|
|
1846
|
+
{{ t("integration.nodeInspector.settings.onError.hint") }}
|
|
1837
1847
|
</p>
|
|
1838
1848
|
</section>
|
|
1839
1849
|
</div>
|
|
@@ -1845,15 +1855,14 @@ async function runTestStep() {
|
|
|
1845
1855
|
<section class="space-y-2">
|
|
1846
1856
|
<div class="flex items-center justify-between gap-2">
|
|
1847
1857
|
<p class="text-xs text-muted-foreground">
|
|
1848
|
-
|
|
1849
|
-
filled with real values once the step has run.
|
|
1858
|
+
{{ t("integration.nodeInspector.output.description") }}
|
|
1850
1859
|
</p>
|
|
1851
1860
|
<div class="flex shrink-0 items-center rounded-md border border-black/[0.06] bg-white p-0.5">
|
|
1852
1861
|
<button type="button" :class="outputView === 'schema' ? segActive : segIdle" @click="outputView = 'schema'">
|
|
1853
|
-
|
|
1862
|
+
{{ t("integration.nodeInspector.output.view.schema") }}
|
|
1854
1863
|
</button>
|
|
1855
1864
|
<button type="button" :disabled="!selfSample" :class="outputView === 'json' ? segActive : segIdle" @click="outputView = 'json'">
|
|
1856
|
-
|
|
1865
|
+
{{ t("integration.nodeInspector.output.view.json") }}
|
|
1857
1866
|
</button>
|
|
1858
1867
|
</div>
|
|
1859
1868
|
</div>
|
|
@@ -1864,32 +1873,32 @@ async function runTestStep() {
|
|
|
1864
1873
|
</section>
|
|
1865
1874
|
|
|
1866
1875
|
<section class="space-y-2 border-t border-black/[0.06] pt-4">
|
|
1867
|
-
<span class="text-mono text-muted-foreground">
|
|
1876
|
+
<span class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.output.realValues.heading") }}</span>
|
|
1868
1877
|
<div class="flex items-center gap-2">
|
|
1869
1878
|
<Button
|
|
1870
1879
|
size="sm"
|
|
1871
1880
|
class="h-8"
|
|
1872
1881
|
:disabled="testRunning"
|
|
1873
|
-
title="
|
|
1882
|
+
:title="t('integration.nodeInspector.output.test.runTitle')"
|
|
1874
1883
|
@click="runTestStep"
|
|
1875
1884
|
>
|
|
1876
|
-
{{ testRunning ? "
|
|
1885
|
+
{{ testRunning ? t("integration.nodeInspector.output.test.running") : t("integration.nodeInspector.output.test.action") }}
|
|
1877
1886
|
</Button>
|
|
1878
1887
|
<button
|
|
1879
1888
|
type="button"
|
|
1880
1889
|
class="rounded px-1 py-1 text-sm font-medium text-muted-foreground transition hover:text-foreground"
|
|
1881
1890
|
@click="toggleTestInput"
|
|
1882
1891
|
>
|
|
1883
|
-
{{ testInputOpen ? "
|
|
1892
|
+
{{ testInputOpen ? t("integration.nodeInspector.output.test.hideInput") : t("integration.nodeInspector.output.test.editInput") }}
|
|
1884
1893
|
</button>
|
|
1885
1894
|
<span class="text-xs text-muted-foreground">
|
|
1886
|
-
|
|
1895
|
+
{{ t("integration.nodeInspector.output.test.runHint") }}
|
|
1887
1896
|
</span>
|
|
1888
1897
|
</div>
|
|
1889
1898
|
<Textarea
|
|
1890
1899
|
v-if="testInputOpen"
|
|
1891
1900
|
v-model="testInputDraft"
|
|
1892
|
-
aria-label="
|
|
1901
|
+
:aria-label="t('integration.nodeInspector.output.test.inputAria')"
|
|
1893
1902
|
:rows="8"
|
|
1894
1903
|
spellcheck="false"
|
|
1895
1904
|
class="rounded-md border border-black/[0.08] ring-0 shadow-none bg-white font-mono text-xs"
|
|
@@ -1913,7 +1922,7 @@ async function runTestStep() {
|
|
|
1913
1922
|
<template v-if="!pinEditorOpen">
|
|
1914
1923
|
<div class="flex items-center gap-2">
|
|
1915
1924
|
<Button variant="outline" size="sm" class="h-8" @click="openPinEditor">
|
|
1916
|
-
{{ selfPinned ? "
|
|
1925
|
+
{{ selfPinned ? t("integration.nodeInspector.output.pin.editAction") : t("integration.nodeInspector.output.pin.pinAction") }}
|
|
1917
1926
|
</Button>
|
|
1918
1927
|
<button
|
|
1919
1928
|
v-if="selfPinned"
|
|
@@ -1921,17 +1930,17 @@ async function runTestStep() {
|
|
|
1921
1930
|
class="text-sm font-medium text-rose-600 transition hover:text-rose-700"
|
|
1922
1931
|
@click="removePin"
|
|
1923
1932
|
>
|
|
1924
|
-
|
|
1933
|
+
{{ t("integration.nodeInspector.output.pin.unpinAction") }}
|
|
1925
1934
|
</button>
|
|
1926
1935
|
<span class="text-xs text-muted-foreground">
|
|
1927
|
-
|
|
1936
|
+
{{ t("integration.nodeInspector.output.pin.hint") }}
|
|
1928
1937
|
</span>
|
|
1929
1938
|
</div>
|
|
1930
1939
|
</template>
|
|
1931
1940
|
<template v-else>
|
|
1932
1941
|
<Textarea
|
|
1933
1942
|
v-model="pinDraft"
|
|
1934
|
-
aria-label="
|
|
1943
|
+
:aria-label="t('integration.nodeInspector.output.pin.editorAria')"
|
|
1935
1944
|
:rows="10"
|
|
1936
1945
|
spellcheck="false"
|
|
1937
1946
|
class="rounded-md border border-black/[0.08] ring-0 shadow-none bg-white font-mono text-xs"
|
|
@@ -1941,10 +1950,10 @@ async function runTestStep() {
|
|
|
1941
1950
|
</p>
|
|
1942
1951
|
<div class="flex items-center gap-2">
|
|
1943
1952
|
<Button size="sm" class="h-8" @click="savePin">
|
|
1944
|
-
|
|
1953
|
+
{{ t("integration.nodeInspector.output.pin.saveAction") }}
|
|
1945
1954
|
</Button>
|
|
1946
1955
|
<Button size="sm" variant="outline" class="h-8" @click="pinEditorOpen = false">
|
|
1947
|
-
|
|
1956
|
+
{{ t("integration.common.action.cancel") }}
|
|
1948
1957
|
</Button>
|
|
1949
1958
|
</div>
|
|
1950
1959
|
</template>
|
|
@@ -1973,24 +1982,24 @@ async function runTestStep() {
|
|
|
1973
1982
|
<Button
|
|
1974
1983
|
variant="ghost"
|
|
1975
1984
|
size="sm"
|
|
1976
|
-
aria-label="
|
|
1985
|
+
:aria-label="t('integration.nodeInspector.footer.removeFromWorkflow')"
|
|
1977
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"
|
|
1978
1987
|
@click="emit('remove')"
|
|
1979
1988
|
>
|
|
1980
|
-
<
|
|
1989
|
+
<TrashBin :size="13" />
|
|
1981
1990
|
<span
|
|
1982
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"
|
|
1983
1992
|
>
|
|
1984
|
-
|
|
1993
|
+
{{ t("integration.nodeInspector.footer.removeFromWorkflow") }}
|
|
1985
1994
|
</span>
|
|
1986
1995
|
</Button>
|
|
1987
1996
|
<div class="flex items-center gap-2">
|
|
1988
|
-
<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>
|
|
1989
1998
|
<Button variant="outline" size="sm" class="h-8" @click="emit('close')">
|
|
1990
|
-
|
|
1999
|
+
{{ t("integration.common.action.cancel") }}
|
|
1991
2000
|
</Button>
|
|
1992
2001
|
<Button size="sm" class="h-8" @click="save">
|
|
1993
|
-
|
|
2002
|
+
{{ t("integration.common.action.save") }}
|
|
1994
2003
|
</Button>
|
|
1995
2004
|
</div>
|
|
1996
2005
|
</div>
|