@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,11 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { onMounted, ref, watch } from "vue";
|
|
3
|
-
import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as
|
|
3
|
+
import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
4
4
|
import { Badge, Button } from "@revenexx/studio";
|
|
5
5
|
import { toast } from "vue-sonner";
|
|
6
6
|
const props = defineProps({
|
|
7
7
|
workflowId: { type: Number, required: true }
|
|
8
8
|
});
|
|
9
|
+
const t = useIntegrationsT();
|
|
9
10
|
const { list, replay, discard } = useIntegrationsDeadLetters();
|
|
10
11
|
const PER_PAGE = 25;
|
|
11
12
|
const items = ref([]);
|
|
@@ -25,7 +26,7 @@ async function load(p = page.value) {
|
|
|
25
26
|
lastPage.value = res.lastPage;
|
|
26
27
|
page.value = res.page;
|
|
27
28
|
} catch (e) {
|
|
28
|
-
error.value = e?.message ?? "
|
|
29
|
+
error.value = e?.message ?? t("integration.deadLetters.error.load");
|
|
29
30
|
} finally {
|
|
30
31
|
loading.value = false;
|
|
31
32
|
}
|
|
@@ -41,10 +42,10 @@ async function onReplay(dl) {
|
|
|
41
42
|
try {
|
|
42
43
|
const res = await replay(dl.id);
|
|
43
44
|
const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
|
|
44
|
-
toast.success(newId ?
|
|
45
|
+
toast.success(newId ? t("integration.deadLetters.toast.replay.started", { id: newId }) : t("integration.deadLetters.toast.replay.done"));
|
|
45
46
|
await load();
|
|
46
47
|
} catch (e) {
|
|
47
|
-
toast.error(e?.message ?? "
|
|
48
|
+
toast.error(e?.message ?? t("integration.deadLetters.toast.replay.error"));
|
|
48
49
|
} finally {
|
|
49
50
|
busyId.value = null;
|
|
50
51
|
}
|
|
@@ -53,10 +54,10 @@ async function onDiscard(dl) {
|
|
|
53
54
|
busyId.value = dl.id;
|
|
54
55
|
try {
|
|
55
56
|
await discard(dl.id);
|
|
56
|
-
toast.success("
|
|
57
|
+
toast.success(t("integration.deadLetters.toast.discard.done"));
|
|
57
58
|
await load();
|
|
58
59
|
} catch (e) {
|
|
59
|
-
toast.error(e?.message ?? "
|
|
60
|
+
toast.error(e?.message ?? t("integration.deadLetters.toast.discard.error"));
|
|
60
61
|
} finally {
|
|
61
62
|
busyId.value = null;
|
|
62
63
|
}
|
|
@@ -80,14 +81,14 @@ function fmt(iso) {
|
|
|
80
81
|
<div class="space-y-3">
|
|
81
82
|
<div class="flex items-center justify-between">
|
|
82
83
|
<div>
|
|
83
|
-
<h3 class="text-sm font-semibold">
|
|
84
|
+
<h3 class="text-sm font-semibold">{{ t("integration.deadLetters.panel.title") }}</h3>
|
|
84
85
|
<p class="mt-0.5 text-sm text-muted-foreground">
|
|
85
|
-
|
|
86
|
+
{{ t("integration.deadLetters.panel.description") }}
|
|
86
87
|
</p>
|
|
87
88
|
</div>
|
|
88
89
|
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="loading" @click="() => load()">
|
|
89
90
|
<Loader2 v-if="loading" :size="12" class="animate-spin" />
|
|
90
|
-
|
|
91
|
+
{{ t("integration.common.action.refresh") }}
|
|
91
92
|
</Button>
|
|
92
93
|
</div>
|
|
93
94
|
|
|
@@ -107,7 +108,7 @@ function fmt(iso) {
|
|
|
107
108
|
</div>
|
|
108
109
|
</div>
|
|
109
110
|
<div v-else-if="!items.length" class="rounded-lg border border-black/[0.06] bg-white p-6 text-center text-sm text-muted-foreground">
|
|
110
|
-
|
|
111
|
+
{{ t("integration.deadLetters.state.empty") }}
|
|
111
112
|
</div>
|
|
112
113
|
|
|
113
114
|
<div v-else class="rounded-lg border border-black/[0.06] bg-white divide-y divide-black/[0.04]">
|
|
@@ -118,7 +119,7 @@ function fmt(iso) {
|
|
|
118
119
|
>
|
|
119
120
|
<Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0 mt-0.5', statusClass(dl.status)]">{{ dl.status }}</Badge>
|
|
120
121
|
<div class="min-w-0 flex-1">
|
|
121
|
-
<div class="text-sm break-words">{{ dl.reason || "
|
|
122
|
+
<div class="text-sm break-words">{{ dl.reason || t("integration.deadLetters.reason.fallback") }}</div>
|
|
122
123
|
<div class="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground font-mono">
|
|
123
124
|
<span>#{{ dl.id }}</span>
|
|
124
125
|
<span v-if="dl.source">{{ dl.source }}</span>
|
|
@@ -130,21 +131,21 @@ function fmt(iso) {
|
|
|
130
131
|
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="busyId === dl.id" @click="onReplay(dl)">
|
|
131
132
|
<Loader2 v-if="busyId === dl.id" :size="12" class="animate-spin" />
|
|
132
133
|
<ReplayIcon v-else :size="12" />
|
|
133
|
-
|
|
134
|
+
{{ t("integration.deadLetters.action.replay") }}
|
|
134
135
|
</Button>
|
|
135
136
|
<Button variant="ghost" size="sm" class="h-7 text-sm text-rose-600 hover:bg-rose-500/10" :disabled="busyId === dl.id" @click="onDiscard(dl)">
|
|
136
|
-
<
|
|
137
|
-
|
|
137
|
+
<TrashBin :size="12" />
|
|
138
|
+
{{ t("integration.deadLetters.action.discard") }}
|
|
138
139
|
</Button>
|
|
139
140
|
</div>
|
|
140
141
|
</div>
|
|
141
142
|
</div>
|
|
142
143
|
|
|
143
144
|
<div v-if="lastPage > 1" class="flex items-center justify-between text-sm text-muted-foreground">
|
|
144
|
-
<span class="tabular-nums">
|
|
145
|
+
<span class="tabular-nums">{{ t("integration.deadLetters.pagination.summary", { page, lastPage, total }) }}</span>
|
|
145
146
|
<div class="flex gap-2">
|
|
146
|
-
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page <= 1 || loading" @click="goPage(page - 1)"
|
|
147
|
-
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page >= lastPage || loading" @click="goPage(page + 1)">
|
|
147
|
+
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page <= 1 || loading" @click="goPage(page - 1)">{{ t("integration.deadLetters.pagination.newer") }}</Button>
|
|
148
|
+
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page >= lastPage || loading" @click="goPage(page + 1)">{{ t("integration.deadLetters.pagination.older") }}</Button>
|
|
148
149
|
</div>
|
|
149
150
|
</div>
|
|
150
151
|
</div>
|
|
@@ -6,6 +6,7 @@ const props = defineProps({
|
|
|
6
6
|
inputId: { type: String, required: false }
|
|
7
7
|
});
|
|
8
8
|
const emit = defineEmits(["update:modelValue"]);
|
|
9
|
+
const t = useIntegrationsT();
|
|
9
10
|
const catalog = typeof useDefinedEvents === "function" ? useDefinedEvents() : { emits: ref([]), listens: ref([]), loading: ref(false), load: async () => {
|
|
10
11
|
} };
|
|
11
12
|
onMounted(() => {
|
|
@@ -15,8 +16,8 @@ const focused = ref(false);
|
|
|
15
16
|
const options = computed(() => {
|
|
16
17
|
const out = /* @__PURE__ */ new Map();
|
|
17
18
|
for (const e of catalog.emits.value) out.set(e.topic, { topic: e.topic, source: e.app });
|
|
18
|
-
for (const
|
|
19
|
-
if (!out.has(
|
|
19
|
+
for (const t2 of catalog.listens.value) {
|
|
20
|
+
if (!out.has(t2)) out.set(t2, { topic: t2, source: null });
|
|
20
21
|
}
|
|
21
22
|
return [...out.values()].sort((a, b) => a.topic.localeCompare(b.topic));
|
|
22
23
|
});
|
|
@@ -48,7 +49,7 @@ function pick(topic) {
|
|
|
48
49
|
class="absolute left-0 right-0 top-full z-30 mt-1 max-h-56 overflow-y-auto rounded-md border border-black/[0.08] bg-white py-1 shadow-md"
|
|
49
50
|
>
|
|
50
51
|
<div v-if="catalog.loading.value && !filtered.length" class="px-2.5 py-1.5 text-sm text-muted-foreground">
|
|
51
|
-
|
|
52
|
+
{{ t("integration.canvas.eventCombobox.loading") }}
|
|
52
53
|
</div>
|
|
53
54
|
<button
|
|
54
55
|
v-for="o in filtered"
|
|
@@ -17,6 +17,7 @@ const props = defineProps({
|
|
|
17
17
|
label: { type: String, required: false }
|
|
18
18
|
});
|
|
19
19
|
const emit = defineEmits(["update:modelValue"]);
|
|
20
|
+
const t = useIntegrationsT();
|
|
20
21
|
const text = computed(() => String(props.modelValue ?? ""));
|
|
21
22
|
const OPEN = "${{ ";
|
|
22
23
|
const CLOSE = " }}";
|
|
@@ -34,12 +35,12 @@ function pillLabel(expr) {
|
|
|
34
35
|
}
|
|
35
36
|
if (path[0] === "trigger" && path[1] === "payload") {
|
|
36
37
|
const rest = path.slice(2).join(".");
|
|
37
|
-
return rest ? "
|
|
38
|
+
return rest ? t("integration.nodeConfig.expression.pill.triggerPath", { rest }) : t("integration.nodeConfig.expression.pill.triggerPayload");
|
|
38
39
|
}
|
|
39
|
-
if (path[0] === "vars") return "
|
|
40
|
+
if (path[0] === "vars") return t("integration.nodeConfig.expression.pill.variable", { path: path.slice(1).join(".") });
|
|
40
41
|
if (path[0] === "in") {
|
|
41
42
|
const rest = path.slice(1).join(".");
|
|
42
|
-
return rest ? "
|
|
43
|
+
return rest ? t("integration.nodeConfig.expression.pill.inputPath", { rest }) : t("integration.nodeConfig.expression.pill.inputPayload");
|
|
43
44
|
}
|
|
44
45
|
return expr;
|
|
45
46
|
}
|
|
@@ -104,9 +105,9 @@ onBeforeUnmount(() => {
|
|
|
104
105
|
if (insertHandler?.value === insertToken) insertHandler.value = null;
|
|
105
106
|
editor.destroy();
|
|
106
107
|
});
|
|
107
|
-
watch(text, (
|
|
108
|
-
if (docToText(editor.getJSON()) !==
|
|
109
|
-
editor.commands.setContent(textToDoc(
|
|
108
|
+
watch(text, (t2) => {
|
|
109
|
+
if (docToText(editor.getJSON()) !== t2) {
|
|
110
|
+
editor.commands.setContent(textToDoc(t2));
|
|
110
111
|
}
|
|
111
112
|
});
|
|
112
113
|
function insertToken(expr) {
|
|
@@ -126,7 +127,7 @@ const sections = computed(() => {
|
|
|
126
127
|
label: key,
|
|
127
128
|
expr: tok("vars." + key)
|
|
128
129
|
}));
|
|
129
|
-
if (varItems.length) out.push({ heading: "
|
|
130
|
+
if (varItems.length) out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.variables"), items: varItems });
|
|
130
131
|
const upstream = props.upstreamOutputs ?? [];
|
|
131
132
|
const nodeItems = upstream.flatMap((u) => {
|
|
132
133
|
const base = "nodes." + u.nodeId + ".outputs." + u.output;
|
|
@@ -139,13 +140,13 @@ const sections = computed(() => {
|
|
|
139
140
|
}
|
|
140
141
|
return rows;
|
|
141
142
|
});
|
|
142
|
-
if (nodeItems.length) out.push({ heading: "
|
|
143
|
+
if (nodeItems.length) out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.nodeOutputs"), items: nodeItems });
|
|
143
144
|
const primary = upstream[0];
|
|
144
|
-
const inItems = [{ label: "
|
|
145
|
+
const inItems = [{ label: t("integration.nodeConfig.expression.suggestion.inputPayload"), expr: tok("in") }];
|
|
145
146
|
for (const f of primary?.fields ?? []) {
|
|
146
147
|
inItems.push({ label: "input." + f, expr: tok("in." + f) });
|
|
147
148
|
}
|
|
148
|
-
out.push({ heading: "
|
|
149
|
+
out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.input"), items: inItems });
|
|
149
150
|
return out;
|
|
150
151
|
});
|
|
151
152
|
const hasSuggestions = computed(() => sections.value.some((s) => s.items.length > 0));
|
|
@@ -201,7 +202,7 @@ function insert(expr) {
|
|
|
201
202
|
</div>
|
|
202
203
|
<button
|
|
203
204
|
type="button"
|
|
204
|
-
title="
|
|
205
|
+
:title="t('integration.nodeConfig.expression.insertTitle')"
|
|
205
206
|
class="flex h-8 shrink-0 items-center rounded-md border border-black/[0.08] bg-white px-1.5 font-mono text-sm text-muted-foreground transition hover:text-foreground hover:bg-muted/30"
|
|
206
207
|
:class="open ? 'ring-2 ring-emerald-500/40' : ''"
|
|
207
208
|
@mousedown.prevent="toggle"
|
|
@@ -213,7 +214,7 @@ function insert(expr) {
|
|
|
213
214
|
<p
|
|
214
215
|
v-if="preview"
|
|
215
216
|
class="mt-1 flex min-w-0 items-baseline gap-1 text-[11px]"
|
|
216
|
-
:title="preview.misses ? '
|
|
217
|
+
:title="preview.misses ? t('integration.nodeConfig.expression.preview.missTitle') : preview.resolved"
|
|
217
218
|
>
|
|
218
219
|
<span class="shrink-0 text-muted-foreground">→</span>
|
|
219
220
|
<span class="min-w-0 truncate font-mono" :class="preview.misses ? 'text-amber-600' : 'text-emerald-700'">{{ preview.resolved }}</span>
|
|
@@ -226,15 +227,15 @@ function insert(expr) {
|
|
|
226
227
|
<div class="shrink-0 border-b border-black/[0.06] p-1">
|
|
227
228
|
<input
|
|
228
229
|
v-model="query"
|
|
229
|
-
aria-label="
|
|
230
|
-
placeholder="
|
|
230
|
+
:aria-label="t('integration.nodeConfig.expression.picker.filterAria')"
|
|
231
|
+
:placeholder="t('integration.nodeConfig.expression.picker.filterPlaceholder')"
|
|
231
232
|
class="w-full rounded px-1.5 py-1 text-sm outline-none placeholder:text-muted-foreground/60 focus:ring-2 focus:ring-primary/40"
|
|
232
233
|
@mousedown.stop
|
|
233
234
|
>
|
|
234
235
|
</div>
|
|
235
236
|
<div class="min-h-0 flex-1 overflow-y-auto py-1">
|
|
236
237
|
<p v-if="!filteredSections.length" class="px-2 py-1.5 text-muted-foreground">
|
|
237
|
-
|
|
238
|
+
{{ t("integration.nodeConfig.expression.picker.empty", { query }) }}
|
|
238
239
|
</p>
|
|
239
240
|
<template v-for="section in filteredSections" :key="section.heading">
|
|
240
241
|
<div class="px-2 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { computed, nextTick, ref } from "vue";
|
|
3
3
|
import { Handle, Position, useVueFlow } from "@vue-flow/core";
|
|
4
4
|
import { NodeToolbar } from "@vue-flow/node-toolbar";
|
|
5
|
-
import { ChevronsDownUp, ChevronsUpDown,
|
|
5
|
+
import { ChevronsDownUp, ChevronsUpDown, Ungroup } from "lucide-vue-next";
|
|
6
|
+
import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
6
7
|
import { resolveDisplayName } from "../../utils/localize";
|
|
7
8
|
const props = defineProps({
|
|
8
9
|
id: { type: String, required: true },
|
|
@@ -10,22 +11,28 @@ const props = defineProps({
|
|
|
10
11
|
selected: { type: Boolean, required: false }
|
|
11
12
|
});
|
|
12
13
|
defineEmits(["updateNodeInternals"]);
|
|
14
|
+
const t = useIntegrationsT();
|
|
13
15
|
const GROUP_COLORS = [
|
|
14
|
-
{
|
|
15
|
-
{
|
|
16
|
-
{
|
|
17
|
-
{
|
|
18
|
-
{
|
|
19
|
-
{
|
|
20
|
-
{
|
|
16
|
+
{ nameKey: "integration.canvas.color.amber", bg: "rgba(254,243,199,0.45)", border: "#fcd34d", chip: "#f59e0b" },
|
|
17
|
+
{ nameKey: "integration.canvas.color.orange", bg: "rgba(255,237,213,0.45)", border: "#fdba74", chip: "#f97316" },
|
|
18
|
+
{ nameKey: "integration.canvas.color.rose", bg: "rgba(255,228,230,0.45)", border: "#fda4af", chip: "#f43f5e" },
|
|
19
|
+
{ nameKey: "integration.canvas.color.green", bg: "rgba(220,252,231,0.45)", border: "#86efac", chip: "#10b981" },
|
|
20
|
+
{ nameKey: "integration.canvas.color.blue", bg: "rgba(219,234,254,0.45)", border: "#93c5fd", chip: "#3b82f6" },
|
|
21
|
+
{ nameKey: "integration.canvas.color.violet", bg: "rgba(237,233,254,0.45)", border: "#c4b5fd", chip: "#8b5cf6" },
|
|
22
|
+
{ nameKey: "integration.canvas.color.gray", bg: "rgba(245,245,244,0.6)", border: "#d6d3d1", chip: "#78716c" }
|
|
21
23
|
];
|
|
22
24
|
const color = computed(() => GROUP_COLORS[props.data.group.color ?? 4] ?? GROUP_COLORS[4]);
|
|
23
|
-
const label = computed(() => resolveDisplayName(props.data.group.label) || "
|
|
25
|
+
const label = computed(() => resolveDisplayName(props.data.group.label) || t("integration.canvas.group.defaultLabel"));
|
|
24
26
|
const collapsed = computed(() => props.data.group.collapsed === true);
|
|
25
27
|
const editable = computed(() => !!props.data.actions);
|
|
26
28
|
const editing = ref(false);
|
|
27
29
|
const draft = ref("");
|
|
28
30
|
const inputEl = ref(null);
|
|
31
|
+
function onLabelDoubleClick(event) {
|
|
32
|
+
if (!editable.value) return;
|
|
33
|
+
event.stopPropagation();
|
|
34
|
+
startRename();
|
|
35
|
+
}
|
|
29
36
|
function startRename() {
|
|
30
37
|
if (!editable.value) return;
|
|
31
38
|
draft.value = label.value;
|
|
@@ -51,7 +58,7 @@ function onHoverLeave() {
|
|
|
51
58
|
}, 250);
|
|
52
59
|
}
|
|
53
60
|
const toolbarVisible = computed(() => editable.value && !editing.value && (hovered.value || !!props.selected));
|
|
54
|
-
const memberCountText = computed(() =>
|
|
61
|
+
const memberCountText = computed(() => t("integration.canvas.group.memberCount", props.data.memberCount));
|
|
55
62
|
const flow = useVueFlow();
|
|
56
63
|
const resizing = ref(false);
|
|
57
64
|
const liveW = ref(0);
|
|
@@ -96,15 +103,15 @@ function onResizeEnd() {
|
|
|
96
103
|
class="size-4.5 rounded-full border transition hover:scale-110"
|
|
97
104
|
:class="(data.group.color ?? 4) === i ? 'ring-2 ring-black/25 ring-offset-1' : ''"
|
|
98
105
|
:style="{ backgroundColor: c.chip, borderColor: c.border, width: '16px', height: '16px' }"
|
|
99
|
-
:title="c.
|
|
100
|
-
:aria-label="c.
|
|
106
|
+
:title="t(c.nameKey)"
|
|
107
|
+
:aria-label="t(c.nameKey)"
|
|
101
108
|
@click="data.actions?.setColor(i)"
|
|
102
109
|
/>
|
|
103
110
|
<div class="mx-0.5 h-4 w-px bg-black/10" />
|
|
104
111
|
<button
|
|
105
112
|
type="button"
|
|
106
113
|
class="flex size-6 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
107
|
-
:title="collapsed ? '
|
|
114
|
+
:title="collapsed ? t('integration.canvas.group.action.expand') : t('integration.canvas.group.action.collapse')"
|
|
108
115
|
@click="data.actions?.toggleCollapse()"
|
|
109
116
|
>
|
|
110
117
|
<component :is="collapsed ? ChevronsUpDown : ChevronsDownUp" :size="13" />
|
|
@@ -112,7 +119,7 @@ function onResizeEnd() {
|
|
|
112
119
|
<button
|
|
113
120
|
type="button"
|
|
114
121
|
class="flex size-6 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
115
|
-
title="
|
|
122
|
+
:title="t('integration.canvas.group.action.ungroup')"
|
|
116
123
|
@click="data.actions?.ungroup()"
|
|
117
124
|
>
|
|
118
125
|
<Ungroup :size="13" />
|
|
@@ -120,10 +127,10 @@ function onResizeEnd() {
|
|
|
120
127
|
<button
|
|
121
128
|
type="button"
|
|
122
129
|
class="flex size-6 items-center justify-center rounded-md text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
|
|
123
|
-
title="
|
|
130
|
+
:title="t('integration.canvas.group.action.removeFrame')"
|
|
124
131
|
@click="data.actions?.ungroup()"
|
|
125
132
|
>
|
|
126
|
-
<
|
|
133
|
+
<TrashBin :size="13" />
|
|
127
134
|
</button>
|
|
128
135
|
</div>
|
|
129
136
|
</NodeToolbar>
|
|
@@ -136,6 +143,9 @@ function onResizeEnd() {
|
|
|
136
143
|
:position="Position.Top"
|
|
137
144
|
class="!size-3 !rounded-full !border-2 !border-white !bg-slate-500 !shadow-sm"
|
|
138
145
|
/>
|
|
146
|
+
<!-- The collapsed card stops its own double-click unconditionally, which is
|
|
147
|
+
safe: both read-only panels build every frame with `collapsed: false`, so
|
|
148
|
+
this surface is the editor's alone. -->
|
|
139
149
|
<div
|
|
140
150
|
class="w-[220px] rounded-lg border-2 bg-white transition-all duration-150"
|
|
141
151
|
:style="{ borderColor: color.border }"
|
|
@@ -183,7 +193,7 @@ function onResizeEnd() {
|
|
|
183
193
|
v-if="editing"
|
|
184
194
|
ref="inputEl"
|
|
185
195
|
v-model="draft"
|
|
186
|
-
aria-label="
|
|
196
|
+
:aria-label="t('integration.canvas.group.input.nameAria')"
|
|
187
197
|
class="nodrag w-40 rounded border border-black/[0.1] bg-white px-1 py-0.5 text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-primary/40"
|
|
188
198
|
@blur="commitRename"
|
|
189
199
|
@keydown.enter.prevent="commitRename"
|
|
@@ -194,7 +204,7 @@ function onResizeEnd() {
|
|
|
194
204
|
v-else
|
|
195
205
|
class="select-none text-sm font-semibold"
|
|
196
206
|
:style="{ color: color.chip }"
|
|
197
|
-
@dblclick
|
|
207
|
+
@dblclick="onLabelDoubleClick"
|
|
198
208
|
>
|
|
199
209
|
{{ label }}
|
|
200
210
|
</span>
|
|
@@ -206,7 +216,7 @@ function onResizeEnd() {
|
|
|
206
216
|
v-if="editable"
|
|
207
217
|
class="nodrag pointer-events-auto absolute bottom-0 right-0 flex size-5 cursor-se-resize items-end justify-end p-1"
|
|
208
218
|
:style="{ color: color.chip }"
|
|
209
|
-
title="
|
|
219
|
+
:title="t('integration.canvas.group.action.resize')"
|
|
210
220
|
@pointerdown="onResizeStart"
|
|
211
221
|
@pointermove="onResizeMove"
|
|
212
222
|
@pointerup="onResizeEnd"
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { nextTick, onMounted, ref } from "vue";
|
|
2
3
|
import Sidebar from "./Sidebar.vue";
|
|
4
|
+
const t = useIntegrationsT();
|
|
3
5
|
const { focused } = useIntegrationsFocus();
|
|
6
|
+
const animate = ref(false);
|
|
7
|
+
onMounted(() => nextTick(() => {
|
|
8
|
+
animate.value = true;
|
|
9
|
+
}));
|
|
4
10
|
</script>
|
|
5
11
|
|
|
6
12
|
<template>
|
|
@@ -10,11 +16,30 @@ const { focused } = useIntegrationsFocus();
|
|
|
10
16
|
href="#integrations-main"
|
|
11
17
|
class="sr-only focus:not-sr-only focus:absolute focus:left-3 focus:top-3 focus:z-50 focus:rounded-md focus:bg-white focus:px-3 focus:py-1.5 focus:text-sm focus:font-medium focus:ring-2 focus:ring-primary/40"
|
|
12
18
|
>
|
|
13
|
-
|
|
19
|
+
{{ t("integration.shell.action.skipToContent") }}
|
|
14
20
|
</a>
|
|
21
|
+
<!-- `inert` + `aria-hidden` are what make the fold real. The inner wrapper
|
|
22
|
+
below keeps its own `w-64` so the slide-out animates, so a folded rail
|
|
23
|
+
is merely clipped by `overflow-hidden`: its links keep their size and
|
|
24
|
+
stay focusable at a negative x, and a keyboard walks into a rail nobody
|
|
25
|
+
can see (PO-273). `display: none` would fix that too and take the
|
|
26
|
+
animation with it.
|
|
27
|
+
|
|
28
|
+
Both attributes are written to be *absent* rather than false when the
|
|
29
|
+
rail stands: `inert` is presence-based, so a renderer that emitted
|
|
30
|
+
`inert="false"` as an attribute would hold the standing rail back
|
|
31
|
+
instead. That the standing rail really does take focus is asserted in
|
|
32
|
+
its own right — specs/studio-navigation.md AC-1 is the control for AC-5.
|
|
33
|
+
|
|
34
|
+
The id is how a test reaches the rail while it is folded: out of the
|
|
35
|
+
accessibility tree, it can no longer be addressed by its role, and
|
|
36
|
+
`nav` alone would catch any navigation the page grows later. -->
|
|
15
37
|
<div
|
|
16
|
-
|
|
17
|
-
|
|
38
|
+
id="integrations-nav"
|
|
39
|
+
class="shrink-0 overflow-hidden border-r border-black/[0.06] bg-white"
|
|
40
|
+
:class="[focused ? 'w-0 !border-r-0' : 'w-64', animate ? 'transition-[width] duration-200 ease-out' : '']"
|
|
41
|
+
:inert="focused || void 0"
|
|
42
|
+
:aria-hidden="focused ? 'true' : void 0"
|
|
18
43
|
>
|
|
19
44
|
<div class="h-full w-64 overflow-y-auto">
|
|
20
45
|
<Sidebar />
|
|
@@ -11,6 +11,7 @@ const props = defineProps({
|
|
|
11
11
|
modelValue: { type: null, required: true }
|
|
12
12
|
});
|
|
13
13
|
const emit = defineEmits(["update:modelValue"]);
|
|
14
|
+
const t = useIntegrationsT();
|
|
14
15
|
const search = ref("");
|
|
15
16
|
const focused = ref(false);
|
|
16
17
|
const q = useNodeConfigResolve({
|
|
@@ -78,25 +79,25 @@ function toggleMulti(optValue) {
|
|
|
78
79
|
<!-- gated: required dependencies still unset -->
|
|
79
80
|
<template v-if="q.gated.value">
|
|
80
81
|
<div :class="disabledControlClass">—</div>
|
|
81
|
-
<p class="mt-1 text-sm text-amber-600">
|
|
82
|
+
<p class="mt-1 text-sm text-amber-600">{{ t("integration.nodeConfig.dynamicField.gated", { keys: q.unsatisfied.value.join(", ") }) }}</p>
|
|
82
83
|
</template>
|
|
83
84
|
|
|
84
85
|
<!-- first load: no data yet -->
|
|
85
86
|
<template v-else-if="q.isFetching.value && options.length === 0">
|
|
86
|
-
<div :class="disabledControlClass">resolving
|
|
87
|
+
<div :class="disabledControlClass">{{ t("integration.nodeConfig.dynamicField.resolving") }}</div>
|
|
87
88
|
</template>
|
|
88
89
|
|
|
89
90
|
<!-- error: build/runtime/etc -->
|
|
90
91
|
<template v-else-if="q.error.value">
|
|
91
92
|
<div :class="disabledControlClass">—</div>
|
|
92
93
|
<div class="mt-1 flex items-center gap-2 text-sm text-amber-600">
|
|
93
|
-
<span>{{ q.error.value.kind === "building" ? "
|
|
94
|
+
<span>{{ q.error.value.kind === "building" ? t("integration.nodeConfig.resolve.error.building") : q.error.value.message }}</span>
|
|
94
95
|
<button
|
|
95
96
|
type="button"
|
|
96
97
|
class="rounded border border-black/[0.08] px-1.5 py-0.5 text-mono text-muted-foreground transition hover:text-foreground"
|
|
97
98
|
@click="q.refetch()"
|
|
98
99
|
>
|
|
99
|
-
|
|
100
|
+
{{ t("integration.nodeConfig.resolve.action.retry") }}
|
|
100
101
|
</button>
|
|
101
102
|
</div>
|
|
102
103
|
</template>
|
|
@@ -106,13 +107,13 @@ function toggleMulti(optValue) {
|
|
|
106
107
|
<Input
|
|
107
108
|
v-if="options.length > 10"
|
|
108
109
|
v-model="multiFilter"
|
|
109
|
-
:aria-label="
|
|
110
|
-
placeholder="
|
|
110
|
+
:aria-label="t('integration.nodeConfig.dynamicField.multiselect.filterAria', { label: resolveLocalized(field.label) })"
|
|
111
|
+
:placeholder="t('integration.nodeConfig.dynamicField.multiselect.filterPlaceholder')"
|
|
111
112
|
class="mb-1.5 h-7 bg-white text-sm"
|
|
112
113
|
/>
|
|
113
114
|
<div role="group" :aria-label="resolveLocalized(field.label)" class="flex flex-wrap gap-1">
|
|
114
115
|
<span v-if="visibleMultiOptions.length === 0" class="text-sm text-muted-foreground">
|
|
115
|
-
{{ options.length === 0 ? "
|
|
116
|
+
{{ options.length === 0 ? t("integration.nodeConfig.dynamicField.empty") : t("integration.nodeConfig.dynamicField.emptyFiltered") }}
|
|
116
117
|
</span>
|
|
117
118
|
<button
|
|
118
119
|
v-for="opt in visibleMultiOptions"
|
|
@@ -131,10 +132,10 @@ function toggleMulti(optValue) {
|
|
|
131
132
|
:key="m"
|
|
132
133
|
class="rounded-md border border-black/[0.08] bg-white px-1.5 py-0.5 text-sm text-muted-foreground opacity-60"
|
|
133
134
|
>
|
|
134
|
-
{{
|
|
135
|
+
{{ t("integration.nodeConfig.dynamicField.notInList", { value: m }) }}
|
|
135
136
|
</span>
|
|
136
137
|
</div>
|
|
137
|
-
<p v-if="q.isFetching.value && q.hasData.value" class="mt-1 text-sm text-muted-foreground">reloading
|
|
138
|
+
<p v-if="q.isFetching.value && q.hasData.value" class="mt-1 text-sm text-muted-foreground">{{ t("integration.nodeConfig.dynamicField.reloading") }}</p>
|
|
138
139
|
</template>
|
|
139
140
|
|
|
140
141
|
<!-- single select: searchable combobox; typing re-resolves with the term -->
|
|
@@ -143,7 +144,7 @@ function toggleMulti(optValue) {
|
|
|
143
144
|
<Input
|
|
144
145
|
:model-value="focused ? search : currentLabel"
|
|
145
146
|
:aria-label="resolveLocalized(field.label)"
|
|
146
|
-
:placeholder="resolveLocalized(field.placeholder) || '
|
|
147
|
+
:placeholder="resolveLocalized(field.placeholder) || t('integration.nodeConfig.select.placeholder')"
|
|
147
148
|
class="h-8 bg-white pr-7 text-sm"
|
|
148
149
|
@update:model-value="(v) => {
|
|
149
150
|
search = String(v);
|
|
@@ -157,7 +158,7 @@ function toggleMulti(optValue) {
|
|
|
157
158
|
class="absolute left-0 right-0 top-full z-30 mt-1 max-h-56 overflow-y-auto rounded-md border border-black/[0.08] bg-white py-1 shadow-md"
|
|
158
159
|
>
|
|
159
160
|
<div v-if="q.isFetching.value" class="px-2.5 py-1.5 text-sm text-muted-foreground">
|
|
160
|
-
{{ search ? "searching
|
|
161
|
+
{{ search ? t("integration.nodeConfig.dynamicField.searching") : t("integration.nodeConfig.dynamicField.resolving") }}
|
|
161
162
|
</div>
|
|
162
163
|
<button
|
|
163
164
|
v-for="opt in visibleOptions"
|
|
@@ -170,14 +171,14 @@ function toggleMulti(optValue) {
|
|
|
170
171
|
<span class="min-w-0 truncate">{{ resolveLocalized(opt.label) || String(opt.value) }}</span>
|
|
171
172
|
</button>
|
|
172
173
|
<div v-if="!q.isFetching.value && visibleOptions.length === 0" class="px-2.5 py-1.5 text-sm text-muted-foreground">
|
|
173
|
-
{{ search ?
|
|
174
|
+
{{ search ? t("integration.nodeConfig.dynamicField.noMatch", { query: search }) : t("integration.nodeConfig.dynamicField.empty") }}
|
|
174
175
|
</div>
|
|
175
176
|
</div>
|
|
176
177
|
</div>
|
|
177
178
|
<p v-if="currentValue !== '' && !currentKnown" class="mt-1 text-sm text-amber-600">
|
|
178
|
-
|
|
179
|
+
{{ t("integration.nodeConfig.dynamicField.currentNotResolved") }}
|
|
179
180
|
</p>
|
|
180
|
-
<p v-if="q.isFetching.value && q.hasData.value && !focused" class="mt-1 text-sm text-muted-foreground">reloading
|
|
181
|
+
<p v-if="q.isFetching.value && q.hasData.value && !focused" class="mt-1 text-sm text-muted-foreground">{{ t("integration.nodeConfig.dynamicField.reloading") }}</p>
|
|
181
182
|
</template>
|
|
182
183
|
</div>
|
|
183
184
|
</template>
|
|
@@ -13,6 +13,7 @@ const props = defineProps({
|
|
|
13
13
|
depth: { type: Number, required: false }
|
|
14
14
|
});
|
|
15
15
|
const emit = defineEmits(["set"]);
|
|
16
|
+
const t = useIntegrationsT();
|
|
16
17
|
void IntegrationsApiError;
|
|
17
18
|
const MAX_DEPTH = 5;
|
|
18
19
|
const q = useNodeConfigResolve({
|
|
@@ -29,17 +30,17 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
|
|
|
29
30
|
<div>
|
|
30
31
|
<!-- gated: required dependencies still unset -->
|
|
31
32
|
<p v-if="q.gated.value" class="text-sm text-amber-600">
|
|
32
|
-
|
|
33
|
+
{{ t("integration.nodeConfig.dynamicField.gated", { keys: q.unsatisfied.value.join(", ") }) }}
|
|
33
34
|
</p>
|
|
34
35
|
|
|
35
36
|
<!-- first load: no fields yet -->
|
|
36
37
|
<p v-else-if="q.isFetching.value && !resolvedFields.length" class="text-sm text-muted-foreground">
|
|
37
|
-
|
|
38
|
+
{{ t("integration.nodeConfig.dynamicSchema.resolving") }}
|
|
38
39
|
</p>
|
|
39
40
|
|
|
40
41
|
<!-- error: build/runtime/etc -->
|
|
41
42
|
<div v-else-if="q.error.value" class="flex items-center gap-2 text-sm text-amber-600">
|
|
42
|
-
<span>{{ q.error.value.kind === "building" ? "
|
|
43
|
+
<span>{{ q.error.value.kind === "building" ? t("integration.nodeConfig.resolve.error.building") : q.error.value.message }}</span>
|
|
43
44
|
<Button
|
|
44
45
|
type="button"
|
|
45
46
|
variant="outline"
|
|
@@ -47,13 +48,13 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
|
|
|
47
48
|
class="h-6 px-1.5 text-mono text-muted-foreground"
|
|
48
49
|
@click="q.refetch()"
|
|
49
50
|
>
|
|
50
|
-
|
|
51
|
+
{{ t("integration.nodeConfig.resolve.action.retry") }}
|
|
51
52
|
</Button>
|
|
52
53
|
</div>
|
|
53
54
|
|
|
54
55
|
<!-- empty: resolved but no fields -->
|
|
55
56
|
<p v-else-if="!resolvedFields.length" class="text-sm text-muted-foreground">
|
|
56
|
-
|
|
57
|
+
{{ t("integration.nodeConfig.dynamicSchema.empty") }}
|
|
57
58
|
</p>
|
|
58
59
|
|
|
59
60
|
<!-- fieldset: the resolved child fields, rendered into the flat node config -->
|
|
@@ -69,12 +70,12 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
|
|
|
69
70
|
<template v-for="f in resolvedFields" :key="f.key">
|
|
70
71
|
<!-- collision guard: a resolved key that shadows a static config field is skipped -->
|
|
71
72
|
<p v-if="staticKeys.includes(f.key)" class="text-sm text-amber-600">
|
|
72
|
-
|
|
73
|
+
{{ t("integration.nodeConfig.dynamicSchema.fieldCollision", { key: f.key }) }}
|
|
73
74
|
</p>
|
|
74
75
|
|
|
75
76
|
<!-- depth cap: don't expand nested dynamic-schema past MAX_DEPTH -->
|
|
76
77
|
<p v-else-if="f.type === 'dynamic-schema' && (depth ?? 0) >= MAX_DEPTH" class="text-sm text-muted-foreground">
|
|
77
|
-
|
|
78
|
+
{{ t("integration.nodeConfig.dynamicSchema.depthCapped") }}
|
|
78
79
|
</p>
|
|
79
80
|
|
|
80
81
|
<!-- a resolved field, rendered exactly like a static node config field -->
|
|
@@ -92,7 +93,7 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
|
|
|
92
93
|
</template>
|
|
93
94
|
|
|
94
95
|
<p v-if="q.isFetching.value && q.hasData.value" class="text-sm text-muted-foreground">
|
|
95
|
-
reloading
|
|
96
|
+
{{ t("integration.nodeConfig.dynamicSchema.reloading") }}
|
|
96
97
|
</p>
|
|
97
98
|
</fieldset>
|
|
98
99
|
</div>
|