@revenexx/studio-integrations 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +428 -0
- package/README.md +48 -4
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +10 -2
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
- package/dist/runtime/components/Integrations/Layout.vue +48 -4
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
- package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
- package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
- package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
- package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
- package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +8 -6
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
- package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
- package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
- package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
- package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
- package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
- package/dist/runtime/composables/useIntegrationsT.js +9 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
- package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
- package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
- package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/branchReorder.d.ts +37 -0
- package/dist/runtime/data/branchReorder.js +71 -0
- package/dist/runtime/data/handleId.d.ts +23 -0
- package/dist/runtime/data/handleId.js +6 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +14 -0
- package/dist/runtime/data/integrationTemplates.d.ts +48 -0
- package/dist/runtime/data/integrationTemplates.js +21 -0
- package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
- package/dist/runtime/data/nodeIcons.d.ts +2 -0
- package/dist/runtime/data/nodeIcons.js +73 -0
- package/dist/runtime/data/nodeInputData.d.ts +16 -4
- package/dist/runtime/data/nodeInputData.js +4 -0
- package/dist/runtime/data/payloadForm.d.ts +9 -2
- package/dist/runtime/data/payloadForm.js +29 -4
- package/dist/runtime/data/portTone.d.ts +17 -0
- package/dist/runtime/data/portTone.js +7 -0
- package/dist/runtime/data/webhookAuth.d.ts +18 -0
- package/dist/runtime/data/webhookAuth.js +19 -0
- package/dist/runtime/data/workflowIconCatalog.js +4 -6
- package/dist/runtime/i18n/messages/de.json +1207 -0
- package/dist/runtime/i18n/messages/en.json +1207 -0
- package/dist/runtime/i18n/translate.d.ts +83 -0
- package/dist/runtime/i18n/translate.js +41 -0
- package/dist/runtime/pages/integrations/audit.vue +140 -37
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
- package/dist/runtime/pages/integrations/credentials.vue +180 -64
- package/dist/runtime/pages/integrations/index.vue +91 -92
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +88 -23
- package/dist/runtime/pages/integrations/secrets.vue +57 -35
- package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
- package/dist/runtime/pages/integrations/templates/index.vue +82 -103
- package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
- package/dist/runtime/plugins/locale.d.ts +10 -0
- package/dist/runtime/plugins/locale.js +6 -0
- package/dist/runtime/plugins/realtime.client.d.ts +32 -0
- package/dist/runtime/plugins/realtime.client.js +4 -0
- package/dist/runtime/types/integrations.d.ts +65 -6
- package/dist/runtime/utils/apiError.d.ts +47 -0
- package/dist/runtime/utils/apiError.js +32 -0
- package/dist/runtime/utils/cockpitSources.d.ts +153 -0
- package/dist/runtime/utils/cockpitSources.js +14 -0
- package/dist/runtime/utils/credentialState.d.ts +24 -1
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +46 -4
- package/dist/runtime/utils/localize.js +34 -7
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -0
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
- package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
- package/dist/runtime/utils/schedule.d.ts +116 -0
- package/dist/runtime/utils/schedule.js +219 -0
- package/dist/runtime/utils/statusStyle.d.ts +26 -2
- package/dist/runtime/utils/statusStyle.js +9 -1
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +54 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/dist/types.d.mts +2 -6
- package/package.json +10 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, onMounted, ref, watch } from "vue";
|
|
3
|
-
import { AddCircle as Plus, TrashBinMinimalistic as
|
|
3
|
+
import { AddCircle as Plus, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
4
4
|
import {
|
|
5
5
|
Button,
|
|
6
6
|
Input,
|
|
@@ -8,11 +8,14 @@ import {
|
|
|
8
8
|
Select,
|
|
9
9
|
SelectContent,
|
|
10
10
|
SelectItem,
|
|
11
|
+
SelectSeparator,
|
|
11
12
|
SelectTrigger,
|
|
12
13
|
SelectValue,
|
|
13
14
|
Switch,
|
|
14
15
|
Textarea
|
|
15
16
|
} from "@revenexx/studio";
|
|
17
|
+
import { resolveLocalized } from "../../utils/localize";
|
|
18
|
+
import { credentialState } from "../../utils/credentialState";
|
|
16
19
|
const props = defineProps({
|
|
17
20
|
field: { type: Object, required: true },
|
|
18
21
|
config: { type: Object, required: true },
|
|
@@ -22,45 +25,43 @@ const props = defineProps({
|
|
|
22
25
|
staticKeys: { type: Array, required: false },
|
|
23
26
|
depth: { type: Number, required: false },
|
|
24
27
|
variables: { type: Object, required: false },
|
|
25
|
-
upstreamOutputs: { type: Array, required: false }
|
|
28
|
+
upstreamOutputs: { type: Array, required: false },
|
|
29
|
+
refused: { type: String, required: false }
|
|
26
30
|
});
|
|
27
31
|
const emit = defineEmits(["set"]);
|
|
32
|
+
const t = useIntegrationsT();
|
|
28
33
|
const value = computed(() => props.config[props.field.key]);
|
|
29
34
|
function write(v) {
|
|
30
35
|
emit("set", props.field.key, v);
|
|
31
36
|
}
|
|
32
|
-
function labelText(v) {
|
|
33
|
-
if (!v) return "";
|
|
34
|
-
return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
|
|
35
|
-
}
|
|
36
37
|
const exprHint = "${{ \u2026 }}";
|
|
37
|
-
const descriptionText = computed(() =>
|
|
38
|
+
const descriptionText = computed(() => resolveLocalized(props.field.description));
|
|
38
39
|
function isEmpty(v) {
|
|
39
40
|
return v === void 0 || v === null || v === "" || Array.isArray(v) && v.length === 0;
|
|
40
41
|
}
|
|
41
|
-
const requiredMissing = computed(() => !!props.field.required && isEmpty(value.value));
|
|
42
42
|
const formatError = computed(() => {
|
|
43
43
|
const v = value.value;
|
|
44
44
|
if (isEmpty(v)) return null;
|
|
45
45
|
const val = props.field.validation;
|
|
46
46
|
if (!val) return null;
|
|
47
47
|
if (typeof v === "string") {
|
|
48
|
-
if (val.minLength != null && v.length < val.minLength) return
|
|
49
|
-
if (val.maxLength != null && v.length > val.maxLength) return
|
|
48
|
+
if (val.minLength != null && v.length < val.minLength) return t("integration.nodeConfig.validation.minLength", { min: val.minLength });
|
|
49
|
+
if (val.maxLength != null && v.length > val.maxLength) return t("integration.nodeConfig.validation.maxLength", { max: val.maxLength });
|
|
50
50
|
if (val.pattern) {
|
|
51
51
|
try {
|
|
52
|
-
if (!new RegExp(val.pattern).test(v)) return "
|
|
52
|
+
if (!new RegExp(val.pattern).test(v)) return t("integration.nodeConfig.validation.pattern");
|
|
53
53
|
} catch {
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
if (typeof v === "number") {
|
|
58
|
-
if (val.min != null && v < val.min) return
|
|
59
|
-
if (val.max != null && v > val.max) return
|
|
58
|
+
if (val.min != null && v < val.min) return t("integration.nodeConfig.validation.min", { min: val.min });
|
|
59
|
+
if (val.max != null && v > val.max) return t("integration.nodeConfig.validation.max", { max: val.max });
|
|
60
60
|
}
|
|
61
61
|
return null;
|
|
62
62
|
});
|
|
63
|
-
const
|
|
63
|
+
const fieldError = computed(() => formatError.value ?? props.refused ?? null);
|
|
64
|
+
const invalid = computed(() => !!fieldError.value);
|
|
64
65
|
const constraintHint = computed(() => {
|
|
65
66
|
const val = props.field.validation;
|
|
66
67
|
if (!val) return "";
|
|
@@ -69,13 +70,13 @@ const constraintHint = computed(() => {
|
|
|
69
70
|
if (val.min != null) return `\u2265 ${val.min}`;
|
|
70
71
|
if (val.max != null) return `\u2264 ${val.max}`;
|
|
71
72
|
} else {
|
|
72
|
-
if (val.minLength != null && val.maxLength != null) return
|
|
73
|
-
if (val.minLength != null) return
|
|
74
|
-
if (val.maxLength != null) return
|
|
73
|
+
if (val.minLength != null && val.maxLength != null) return t("integration.nodeConfig.hint.charRange", { min: val.minLength, max: val.maxLength });
|
|
74
|
+
if (val.minLength != null) return t("integration.nodeConfig.hint.charMin", { min: val.minLength });
|
|
75
|
+
if (val.maxLength != null) return t("integration.nodeConfig.hint.charMax", { max: val.maxLength });
|
|
75
76
|
}
|
|
76
77
|
return "";
|
|
77
78
|
});
|
|
78
|
-
const { credentials, load: loadCredentials } = useIntegrationsCredentials();
|
|
79
|
+
const { credentials, loaded: credentialsLoaded, load: loadCredentials } = useIntegrationsCredentials();
|
|
79
80
|
onMounted(() => {
|
|
80
81
|
void loadCredentials();
|
|
81
82
|
});
|
|
@@ -95,14 +96,39 @@ function credsForField(field) {
|
|
|
95
96
|
}
|
|
96
97
|
return list;
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
const bound = computed(() => {
|
|
100
|
+
const id = value.value;
|
|
101
|
+
const c = typeof id === "string" && id ? credentials.value.find((x) => x.id === id) : void 0;
|
|
102
|
+
return c ? { credential: c, state: credentialState(c) } : null;
|
|
103
|
+
});
|
|
104
|
+
const credOptions = computed(
|
|
105
|
+
() => credsForField(props.field).map((c) => ({ credential: c, state: credentialState(c) }))
|
|
106
|
+
);
|
|
107
|
+
const UNKNOWN_CREDENTIAL = computed(() => t("integration.nodeConfig.credential.unknown"));
|
|
108
|
+
function credTriggerLabel(field) {
|
|
109
|
+
const label = resolveLocalized(field.label);
|
|
110
|
+
if (bound.value) return t("integration.nodeConfig.credential.trigger.bound", { label, name: bound.value.credential.name, state: bound.value.state.label });
|
|
111
|
+
if (value.value && credentialsLoaded.value) return t("integration.nodeConfig.credential.trigger.unknown", { label, unknown: UNKNOWN_CREDENTIAL.value });
|
|
112
|
+
return label;
|
|
100
113
|
}
|
|
101
114
|
const credCreateOpen = ref(false);
|
|
102
115
|
const sftpImportOpen = ref(false);
|
|
116
|
+
const CRED_ACTION_NEW = "__action_new_credential__";
|
|
117
|
+
const CRED_ACTION_SFTP = "__action_import_sftp__";
|
|
118
|
+
function onCredentialSelect(v) {
|
|
119
|
+
if (v === CRED_ACTION_NEW) {
|
|
120
|
+
credCreateOpen.value = true;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (v === CRED_ACTION_SFTP) {
|
|
124
|
+
sftpImportOpen.value = true;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
write(v);
|
|
128
|
+
}
|
|
103
129
|
function acceptsSftp(field) {
|
|
104
|
-
const
|
|
105
|
-
return
|
|
130
|
+
const t2 = field.credentialType;
|
|
131
|
+
return t2 === "revenexx:sftp" || Array.isArray(t2) && t2.includes("revenexx:sftp");
|
|
106
132
|
}
|
|
107
133
|
const EXPR_TOKEN = "${{";
|
|
108
134
|
const canToggleExpression = computed(
|
|
@@ -206,7 +232,7 @@ function writeJson(text) {
|
|
|
206
232
|
jsonError.value = null;
|
|
207
233
|
emit("set", props.field.key, parsed);
|
|
208
234
|
} catch {
|
|
209
|
-
jsonError.value = "
|
|
235
|
+
jsonError.value = t("integration.nodeConfig.json.invalid");
|
|
210
236
|
}
|
|
211
237
|
}
|
|
212
238
|
function readArray() {
|
|
@@ -230,29 +256,39 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
230
256
|
<template>
|
|
231
257
|
<div class="space-y-1.5">
|
|
232
258
|
<div class="flex items-center justify-between gap-2">
|
|
233
|
-
|
|
234
|
-
|
|
259
|
+
<!-- The label carries the fault as well as the control does. Only a few
|
|
260
|
+
of the widget types below are a bare input that can take a red
|
|
261
|
+
border, so a credential picker or a switch at fault would otherwise
|
|
262
|
+
be marked only by the sentence underneath.
|
|
263
|
+
|
|
264
|
+
No `for`: which control this heads depends on the field's type — one
|
|
265
|
+
of a dozen branches, some of them a group rather than a single
|
|
266
|
+
element. Each branch instead carries the same text as its own name,
|
|
267
|
+
without the required marker, which is a state and not part of a
|
|
268
|
+
name. -->
|
|
269
|
+
<Label class="text-mono" :class="invalid ? 'text-rose-600' : 'text-muted-foreground'">
|
|
270
|
+
{{ resolveLocalized(field.label) }}<span v-if="field.required" class="text-rose-500" :title="t('integration.nodeConfig.field.requiredMark')"> *</span>
|
|
235
271
|
</Label>
|
|
236
272
|
<div class="flex shrink-0 items-center gap-1.5">
|
|
237
273
|
<div v-if="isObjectField && !showExpression" class="flex items-center rounded border border-black/[0.08] bg-white p-px">
|
|
238
274
|
<button
|
|
239
275
|
type="button"
|
|
240
276
|
:disabled="!flatCapable"
|
|
241
|
-
title="
|
|
277
|
+
:title="t('integration.nodeConfig.object.toggle.fieldsTitle')"
|
|
242
278
|
class="rounded px-1 py-px text-[10px] font-medium transition disabled:pointer-events-none disabled:opacity-40"
|
|
243
279
|
:class="objectMode === 'form' ? 'bg-slate-100 text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
|
244
280
|
@click="objectMode = 'form'"
|
|
245
281
|
>
|
|
246
|
-
|
|
282
|
+
{{ t("integration.nodeConfig.object.toggle.fields") }}
|
|
247
283
|
</button>
|
|
248
284
|
<button
|
|
249
285
|
type="button"
|
|
250
|
-
title="
|
|
286
|
+
:title="t('integration.nodeConfig.object.toggle.jsonTitle')"
|
|
251
287
|
class="rounded px-1 py-px text-[10px] font-medium transition"
|
|
252
288
|
:class="objectMode === 'json' ? 'bg-slate-100 text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
|
253
289
|
@click="objectMode = 'json'"
|
|
254
290
|
>
|
|
255
|
-
|
|
291
|
+
{{ t("integration.nodeConfig.object.toggle.json") }}
|
|
256
292
|
</button>
|
|
257
293
|
</div>
|
|
258
294
|
<button
|
|
@@ -260,7 +296,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
260
296
|
type="button"
|
|
261
297
|
class="rounded border px-1 py-0.5 font-mono text-[10px] transition"
|
|
262
298
|
:class="showExpression ? 'border-emerald-500/40 bg-emerald-500/15 text-emerald-700' : 'border-black/[0.08] bg-white text-muted-foreground hover:text-foreground'"
|
|
263
|
-
:title="showExpression ? '
|
|
299
|
+
:title="showExpression ? t('integration.nodeConfig.expression.toggle.off') : t('integration.nodeConfig.expression.toggle.on')"
|
|
264
300
|
@click="toggleExpression"
|
|
265
301
|
>
|
|
266
302
|
{{ exprHint }}
|
|
@@ -268,17 +304,12 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
268
304
|
<span
|
|
269
305
|
v-else-if="field.expressionAllowed"
|
|
270
306
|
class="rounded border border-emerald-500/25 bg-emerald-500/10 px-1 py-0.5 font-mono text-[10px] text-emerald-600"
|
|
271
|
-
title="
|
|
307
|
+
:title="t('integration.nodeConfig.expression.badge.title')"
|
|
272
308
|
>
|
|
273
309
|
{{ exprHint }}
|
|
274
310
|
</span>
|
|
275
311
|
</div>
|
|
276
312
|
</div>
|
|
277
|
-
<!-- inline help (boolean uses its description as the toggle label instead) -->
|
|
278
|
-
<p v-if="descriptionText && field.type !== 'boolean'" class="text-sm text-muted-foreground leading-snug">
|
|
279
|
-
{{ descriptionText }}
|
|
280
|
-
</p>
|
|
281
|
-
|
|
282
313
|
<!-- object / plain-array fields: JSON editor (expressions stay usable
|
|
283
314
|
inside string values). Wins over expressionAllowed — an object value
|
|
284
315
|
in a text input would render as [object Object]. -->
|
|
@@ -291,11 +322,12 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
291
322
|
|
|
292
323
|
<template v-else-if="isJsonField">
|
|
293
324
|
<Textarea
|
|
325
|
+
:aria-label="resolveLocalized(field.label)"
|
|
294
326
|
:model-value="jsonDraft"
|
|
295
327
|
:rows="8"
|
|
296
328
|
spellcheck="false"
|
|
297
|
-
class="font-mono text-xs"
|
|
298
|
-
:placeholder="
|
|
329
|
+
class="rounded-md border border-black/[0.08] ring-0 shadow-none bg-white font-mono text-xs"
|
|
330
|
+
:placeholder="resolveLocalized(field.placeholder) || '{ }'"
|
|
299
331
|
@update:model-value="(v) => writeJson(String(v))"
|
|
300
332
|
/>
|
|
301
333
|
<p v-if="jsonError" class="text-sm text-amber-600">{{ jsonError }}</p>
|
|
@@ -304,24 +336,29 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
304
336
|
<!-- object field in key/value mode -->
|
|
305
337
|
<div v-else-if="isObjectField && objectMode === 'form'" class="space-y-1.5">
|
|
306
338
|
<div v-for="(row, idx) in kvRows" :key="idx" class="flex items-center gap-1.5">
|
|
339
|
+
<!-- A repeating row has no room for visible labels, so each control
|
|
340
|
+
names itself and says which row of which field it belongs to. -->
|
|
307
341
|
<Input
|
|
342
|
+
:aria-label="t('integration.nodeConfig.object.row.keyAria', { label: resolveLocalized(field.label), n: idx + 1 })"
|
|
308
343
|
:model-value="row.key"
|
|
309
|
-
placeholder="
|
|
310
|
-
class="h-7 w-[38%] bg-white
|
|
344
|
+
:placeholder="t('integration.nodeConfig.object.row.keyPlaceholder')"
|
|
345
|
+
class="h-7 w-[38%] rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
|
|
311
346
|
@update:model-value="(v) => setKvRow(idx, { key: String(v) })"
|
|
312
347
|
/>
|
|
313
348
|
<Input
|
|
349
|
+
:aria-label="t('integration.nodeConfig.object.row.valueAria', { label: resolveLocalized(field.label), n: idx + 1 })"
|
|
314
350
|
:model-value="row.value"
|
|
315
|
-
placeholder="
|
|
316
|
-
class="h-7 min-w-0 flex-1 bg-white text-sm"
|
|
351
|
+
:placeholder="t('integration.nodeConfig.object.row.valuePlaceholder')"
|
|
352
|
+
class="h-7 min-w-0 flex-1 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
|
|
317
353
|
@update:model-value="(v) => setKvRow(idx, { value: String(v) })"
|
|
318
354
|
/>
|
|
319
355
|
<button
|
|
320
356
|
type="button"
|
|
357
|
+
:aria-label="t('integration.nodeConfig.object.row.removeAria', { label: resolveLocalized(field.label), n: idx + 1 })"
|
|
321
358
|
class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
|
|
322
359
|
@click="removeKvRow(idx)"
|
|
323
360
|
>
|
|
324
|
-
<
|
|
361
|
+
<TrashBin :size="11" />
|
|
325
362
|
</button>
|
|
326
363
|
</div>
|
|
327
364
|
<Button
|
|
@@ -332,7 +369,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
332
369
|
@click="addKvRow"
|
|
333
370
|
>
|
|
334
371
|
<Plus :size="11" />
|
|
335
|
-
|
|
372
|
+
{{ t("integration.nodeConfig.action.addRow") }}
|
|
336
373
|
</Button>
|
|
337
374
|
</div>
|
|
338
375
|
|
|
@@ -340,9 +377,11 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
340
377
|
<IntegrationsExpressionInput
|
|
341
378
|
v-else-if="showExpression"
|
|
342
379
|
:model-value="typeof value === 'string' ? value : ''"
|
|
343
|
-
:placeholder="
|
|
380
|
+
:placeholder="resolveLocalized(field.placeholder)"
|
|
344
381
|
:variables="variables"
|
|
345
382
|
:upstream-outputs="upstreamOutputs"
|
|
383
|
+
:invalid="invalid"
|
|
384
|
+
:label="resolveLocalized(field.label)"
|
|
346
385
|
@update:model-value="(v) => write(v)"
|
|
347
386
|
/>
|
|
348
387
|
|
|
@@ -363,18 +402,21 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
363
402
|
<IntegrationsExpressionInput
|
|
364
403
|
v-else-if="field.type === 'expression' || field.expressionAllowed"
|
|
365
404
|
:model-value="value"
|
|
366
|
-
:placeholder="
|
|
405
|
+
:placeholder="resolveLocalized(field.placeholder)"
|
|
367
406
|
:variables="variables"
|
|
368
407
|
:upstream-outputs="upstreamOutputs"
|
|
408
|
+
:invalid="invalid"
|
|
409
|
+
:label="resolveLocalized(field.label)"
|
|
369
410
|
@update:model-value="(v) => write(v)"
|
|
370
411
|
/>
|
|
371
412
|
|
|
372
413
|
<!-- plain string (no expressions) -->
|
|
373
414
|
<Input
|
|
374
415
|
v-else-if="field.type === 'string'"
|
|
416
|
+
:aria-label="resolveLocalized(field.label)"
|
|
375
417
|
:model-value="value ?? ''"
|
|
376
|
-
:placeholder="
|
|
377
|
-
class="h-8 text-sm bg-white"
|
|
418
|
+
:placeholder="resolveLocalized(field.placeholder)"
|
|
419
|
+
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
|
|
378
420
|
:class="invalid ? 'border-rose-400 focus-visible:ring-rose-400/40' : ''"
|
|
379
421
|
@update:model-value="(v) => write(v)"
|
|
380
422
|
/>
|
|
@@ -382,9 +424,10 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
382
424
|
<!-- number -->
|
|
383
425
|
<Input
|
|
384
426
|
v-else-if="field.type === 'number'"
|
|
427
|
+
:aria-label="resolveLocalized(field.label)"
|
|
385
428
|
type="number"
|
|
386
429
|
:model-value="value ?? ''"
|
|
387
|
-
class="h-8 text-sm bg-white"
|
|
430
|
+
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
|
|
388
431
|
:class="invalid ? 'border-rose-400 focus-visible:ring-rose-400/40' : ''"
|
|
389
432
|
@update:model-value="(v) => write(v === '' ? null : Number(v))"
|
|
390
433
|
/>
|
|
@@ -392,11 +435,13 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
392
435
|
<!-- boolean -->
|
|
393
436
|
<div v-else-if="field.type === 'boolean'" class="flex items-center gap-2 pt-1">
|
|
394
437
|
<Switch
|
|
438
|
+
:aria-label="resolveLocalized(field.label)"
|
|
439
|
+
class="data-[state=unchecked]:bg-slate-300"
|
|
395
440
|
:model-value="Boolean(value)"
|
|
396
441
|
@update:model-value="(v) => write(v)"
|
|
397
442
|
/>
|
|
398
443
|
<span class="text-sm text-muted-foreground">
|
|
399
|
-
{{
|
|
444
|
+
{{ resolveLocalized(field.description) }}
|
|
400
445
|
</span>
|
|
401
446
|
</div>
|
|
402
447
|
|
|
@@ -419,18 +464,18 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
419
464
|
:model-value="value ?? ''"
|
|
420
465
|
@update:model-value="(v) => write(v)"
|
|
421
466
|
>
|
|
422
|
-
<SelectTrigger class="h-8 text-sm bg-white">
|
|
423
|
-
<SelectValue :placeholder="
|
|
467
|
+
<SelectTrigger :aria-label="resolveLocalized(field.label)" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
468
|
+
<SelectValue :placeholder="resolveLocalized(field.placeholder) || t('integration.nodeConfig.select.placeholder')" />
|
|
424
469
|
</SelectTrigger>
|
|
425
470
|
<SelectContent>
|
|
426
471
|
<SelectItem v-for="opt in field.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
|
|
427
|
-
{{
|
|
472
|
+
{{ resolveLocalized(opt.label) }}
|
|
428
473
|
</SelectItem>
|
|
429
474
|
</SelectContent>
|
|
430
475
|
</Select>
|
|
431
476
|
|
|
432
477
|
<!-- multiselect — pill toggles for now -->
|
|
433
|
-
<div v-else-if="field.type === 'multiselect'" class="flex flex-wrap gap-1">
|
|
478
|
+
<div v-else-if="field.type === 'multiselect'" role="group" :aria-label="resolveLocalized(field.label)" class="flex flex-wrap gap-1">
|
|
434
479
|
<button
|
|
435
480
|
v-for="opt in field.options ?? []"
|
|
436
481
|
:key="String(opt.value)"
|
|
@@ -443,7 +488,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
443
488
|
write(has ? current.filter((v) => v !== opt.value) : [...current, opt.value]);
|
|
444
489
|
}"
|
|
445
490
|
>
|
|
446
|
-
{{
|
|
491
|
+
{{ resolveLocalized(opt.label) }}
|
|
447
492
|
</button>
|
|
448
493
|
</div>
|
|
449
494
|
|
|
@@ -453,52 +498,90 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
453
498
|
:model-value="value ?? ''"
|
|
454
499
|
@update:model-value="(v) => write(v)"
|
|
455
500
|
>
|
|
456
|
-
<SelectTrigger class="h-8 text-sm bg-white">
|
|
457
|
-
<SelectValue placeholder="
|
|
501
|
+
<SelectTrigger :aria-label="resolveLocalized(field.label)" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
502
|
+
<SelectValue :placeholder="t('integration.nodeConfig.secretRef.placeholder')" />
|
|
458
503
|
</SelectTrigger>
|
|
459
504
|
<SelectContent>
|
|
460
505
|
<SelectItem v-for="k in secretKeys" :key="k" :value="k">{{ k }}</SelectItem>
|
|
461
506
|
</SelectContent>
|
|
462
507
|
</Select>
|
|
463
508
|
|
|
464
|
-
<!-- credentials-ref — pick a credential of the required type
|
|
509
|
+
<!-- credentials-ref — pick a credential of the required type. Compact:
|
|
510
|
+
creating and importing live INSIDE the picker as action rows; no
|
|
511
|
+
Manage link (it navigated out of the editor mid-edit). -->
|
|
465
512
|
<template v-else-if="field.type === 'credentials-ref'">
|
|
466
513
|
<Select
|
|
467
514
|
:model-value="value ?? ''"
|
|
468
|
-
@update:model-value="(v) =>
|
|
515
|
+
@update:model-value="(v) => onCredentialSelect(v)"
|
|
469
516
|
>
|
|
470
|
-
<SelectTrigger class="h-8 text-sm bg-white">
|
|
471
|
-
|
|
517
|
+
<SelectTrigger :aria-label="credTriggerLabel(field)" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
518
|
+
<!-- The trigger renders the bound credential itself instead of letting
|
|
519
|
+
`SelectValue` echo the chosen row. Left to itself it shows that row's
|
|
520
|
+
`textContent`, which glues the name and the state pill into one word —
|
|
521
|
+
"Revenexx APIConnected". Rendering it here keeps the pill a pill, and
|
|
522
|
+
the placeholder has to be drawn by hand because a slot replaces the
|
|
523
|
+
fallback that would otherwise show it. -->
|
|
524
|
+
<SelectValue :placeholder="t('integration.nodeConfig.credential.placeholder')">
|
|
525
|
+
<span v-if="bound" class="flex min-w-0 items-center gap-1.5">
|
|
526
|
+
<span class="min-w-0 truncate">{{ bound.credential.name }}</span>
|
|
527
|
+
<!-- No `title` on this badge: `SelectValue` renders itself with
|
|
528
|
+
`pointer-events: none`, so nothing inside it is ever hovered and a
|
|
529
|
+
tooltip here would be a reason nobody can reach. A failed test's
|
|
530
|
+
reason is prose beneath the field instead. -->
|
|
531
|
+
<span
|
|
532
|
+
class="shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium"
|
|
533
|
+
:class="bound.state.toneClass"
|
|
534
|
+
>
|
|
535
|
+
{{ bound.state.label }}
|
|
536
|
+
</span>
|
|
537
|
+
</span>
|
|
538
|
+
<!-- A stored id the tenant's credentials no longer contain: the
|
|
539
|
+
credential was deleted, and the field would otherwise look untouched
|
|
540
|
+
while the workflow still runs into the dead reference. `loaded`
|
|
541
|
+
keeps the wait before the first response out of it. -->
|
|
542
|
+
<span v-else-if="value && credentialsLoaded" class="text-rose-600">{{ UNKNOWN_CREDENTIAL }}</span>
|
|
543
|
+
<span v-else class="text-muted-foreground">{{ t("integration.nodeConfig.credential.placeholder") }}</span>
|
|
544
|
+
</SelectValue>
|
|
472
545
|
</SelectTrigger>
|
|
473
546
|
<SelectContent>
|
|
474
|
-
|
|
547
|
+
<!-- The state comes from `credentialState`, the same derivation the
|
|
548
|
+
credentials table and the detail dialog use. This picker used to read
|
|
549
|
+
`status` alone and call everything that was not `needs_reauth`
|
|
550
|
+
"Connected" — including a credential nobody had ever tested, which is
|
|
551
|
+
a pass claimed for a test that never ran. -->
|
|
552
|
+
<SelectItem v-for="o in credOptions" :key="o.credential.id" :value="o.credential.id">
|
|
475
553
|
<span class="flex items-center gap-1.5">
|
|
476
|
-
<span class="
|
|
477
|
-
|
|
478
|
-
|
|
554
|
+
<span class="min-w-0 truncate">{{ o.credential.name }}</span>
|
|
555
|
+
<!-- `title` carries why a failed test failed, the way the credentials
|
|
556
|
+
table does it. Without it "Failed" is a verdict with no appeal. A
|
|
557
|
+
row is a real hit target, unlike the closed trigger above. -->
|
|
558
|
+
<span
|
|
559
|
+
class="shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium"
|
|
560
|
+
:class="o.state.toneClass"
|
|
561
|
+
:title="o.state.message ?? void 0"
|
|
562
|
+
>{{ o.state.label }}</span>
|
|
479
563
|
</span>
|
|
480
564
|
</SelectItem>
|
|
565
|
+
<div v-if="!credOptions.length" class="px-2 py-1.5 text-sm text-muted-foreground">
|
|
566
|
+
{{ t("integration.nodeConfig.credential.empty") }}
|
|
567
|
+
</div>
|
|
568
|
+
<SelectSeparator />
|
|
569
|
+
<SelectItem :value="CRED_ACTION_NEW" class="font-medium text-violet-600 data-[highlighted]:text-violet-700">
|
|
570
|
+
{{ t("integration.nodeConfig.credential.action.new") }}
|
|
571
|
+
</SelectItem>
|
|
572
|
+
<SelectItem v-if="acceptsSftp(field)" :value="CRED_ACTION_SFTP" class="font-medium text-violet-600 data-[highlighted]:text-violet-700">
|
|
573
|
+
{{ t("integration.nodeConfig.credential.action.importSftp") }}
|
|
574
|
+
</SelectItem>
|
|
481
575
|
</SelectContent>
|
|
482
576
|
</Select>
|
|
483
|
-
|
|
484
|
-
|
|
577
|
+
<!-- What the state has to add, as text rather than as a tooltip — the shape
|
|
578
|
+
the detail dialog uses beneath its own badge. -->
|
|
579
|
+
<p v-if="bound?.state.key === 'needs_reauth'" class="text-sm text-amber-600">
|
|
580
|
+
{{ t("integration.nodeConfig.credential.state.needsReauth") }}
|
|
581
|
+
</p>
|
|
582
|
+
<p v-else-if="bound?.state.key === 'failed' && bound.state.message" class="text-sm break-words text-rose-600">
|
|
583
|
+
{{ t("integration.nodeConfig.credential.state.failed", { message: bound.state.message }) }}
|
|
485
584
|
</p>
|
|
486
|
-
<div class="flex items-center gap-3">
|
|
487
|
-
<button type="button" class="text-sm font-medium text-violet-600 hover:underline" @click="credCreateOpen = true">
|
|
488
|
-
+ New credential
|
|
489
|
-
</button>
|
|
490
|
-
<button
|
|
491
|
-
v-if="acceptsSftp(field)"
|
|
492
|
-
type="button"
|
|
493
|
-
class="text-sm font-medium text-violet-600 hover:underline"
|
|
494
|
-
@click="sftpImportOpen = true"
|
|
495
|
-
>
|
|
496
|
-
Import SFTP account
|
|
497
|
-
</button>
|
|
498
|
-
<NuxtLink to="/integrations/credentials" class="text-sm text-muted-foreground hover:underline">
|
|
499
|
-
Manage →
|
|
500
|
-
</NuxtLink>
|
|
501
|
-
</div>
|
|
502
585
|
<IntegrationsCredentialCreateDialog
|
|
503
586
|
v-model:open="credCreateOpen"
|
|
504
587
|
:credential-type="field.credentialType"
|
|
@@ -524,10 +607,11 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
524
607
|
</span>
|
|
525
608
|
<button
|
|
526
609
|
type="button"
|
|
610
|
+
:aria-label="t('integration.nodeConfig.array.removeItemAria', { label: resolveLocalized(field.label), n: idx + 1 })"
|
|
527
611
|
class="flex size-5 items-center justify-center rounded text-muted-foreground hover:text-rose-600"
|
|
528
612
|
@click="removeArrayItem(idx)"
|
|
529
613
|
>
|
|
530
|
-
<
|
|
614
|
+
<TrashBin :size="11" />
|
|
531
615
|
</button>
|
|
532
616
|
</div>
|
|
533
617
|
<div
|
|
@@ -536,11 +620,14 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
536
620
|
class="space-y-1"
|
|
537
621
|
>
|
|
538
622
|
<Label class="text-mono text-muted-foreground">
|
|
539
|
-
{{
|
|
623
|
+
{{ resolveLocalized(sub.label) }}
|
|
540
624
|
</Label>
|
|
625
|
+
<!-- The label above repeats per item, so it cannot own an id; the
|
|
626
|
+
input carries its own name including the item number. -->
|
|
541
627
|
<Input
|
|
628
|
+
:aria-label="t('integration.nodeConfig.array.subFieldAria', { label: resolveLocalized(sub.label), n: idx + 1 })"
|
|
542
629
|
:model-value="item[sub.key] ?? ''"
|
|
543
|
-
:placeholder="
|
|
630
|
+
:placeholder="resolveLocalized(sub.placeholder)"
|
|
544
631
|
class="h-7 text-sm bg-white"
|
|
545
632
|
:class="sub.type === 'expression' ? 'font-mono' : ''"
|
|
546
633
|
@update:model-value="(v) => setArrayItemField(idx, sub.key, v)"
|
|
@@ -555,16 +642,17 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
555
642
|
@click="pushArrayItem(Object.fromEntries((field.items?.properties ?? []).map((p) => [p.key, ''])))"
|
|
556
643
|
>
|
|
557
644
|
<Plus :size="11" />
|
|
558
|
-
|
|
645
|
+
{{ t("integration.nodeConfig.action.addRow") }}
|
|
559
646
|
</Button>
|
|
560
647
|
</div>
|
|
561
648
|
|
|
562
649
|
<!-- fallback: JSON textarea for complex types -->
|
|
563
650
|
<Textarea
|
|
564
651
|
v-else
|
|
652
|
+
:aria-label="resolveLocalized(field.label)"
|
|
565
653
|
:model-value="value === void 0 ? '' : JSON.stringify(value, null, 2)"
|
|
566
654
|
rows="3"
|
|
567
|
-
class="text-sm font-mono bg-white"
|
|
655
|
+
class="rounded-md border border-black/[0.08] ring-0 shadow-none text-sm font-mono bg-white"
|
|
568
656
|
@update:model-value="(v) => {
|
|
569
657
|
try {
|
|
570
658
|
write(v ? JSON.parse(String(v)) : void 0);
|
|
@@ -573,12 +661,18 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
573
661
|
}"
|
|
574
662
|
/>
|
|
575
663
|
|
|
576
|
-
<!--
|
|
577
|
-
|
|
578
|
-
|
|
664
|
+
<!-- inline help BELOW the control, quiet — same pattern as the Settings
|
|
665
|
+
tab (boolean uses its description as the toggle label instead) -->
|
|
666
|
+
<p v-if="descriptionText && field.type !== 'boolean'" class="text-xs text-muted-foreground leading-snug">
|
|
667
|
+
{{ descriptionText }}
|
|
579
668
|
</p>
|
|
580
|
-
|
|
581
|
-
|
|
669
|
+
|
|
670
|
+
<!-- validation feedback: the error (local format violation, else what the
|
|
671
|
+
last save refused), otherwise the constraint hint. A required field the
|
|
672
|
+
user has simply not reached yet says nothing beyond the * on its
|
|
673
|
+
label — the nagging would outnumber the help. -->
|
|
674
|
+
<p v-if="fieldError" class="text-sm text-rose-600">
|
|
675
|
+
{{ fieldError }}
|
|
582
676
|
</p>
|
|
583
677
|
<p v-else-if="constraintHint" class="text-sm text-muted-foreground">
|
|
584
678
|
{{ constraintHint }}
|
|
@@ -14,6 +14,8 @@ type __VLS_Props = {
|
|
|
14
14
|
output: string;
|
|
15
15
|
fields?: string[];
|
|
16
16
|
}>;
|
|
17
|
+
/** Why the last save refused this setting, if it did. */
|
|
18
|
+
refused?: string;
|
|
17
19
|
};
|
|
18
20
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
19
21
|
set: (key: string, value: unknown) => any;
|
|
@@ -20,6 +20,12 @@ type __VLS_Props = {
|
|
|
20
20
|
dynamicOutputs?: Record<string, NodeManifestOutput[]>;
|
|
21
21
|
/** Workflow id — enables sampling real values from the latest run. */
|
|
22
22
|
workflowId?: number | string;
|
|
23
|
+
/**
|
|
24
|
+
* Config key → why the LAST SAVE refused it. Not live validation: the server
|
|
25
|
+
* is the only judge, so a fix shows once it is saved. Absent for a node the
|
|
26
|
+
* last save had no complaint about.
|
|
27
|
+
*/
|
|
28
|
+
refusedFields?: Record<string, string>;
|
|
23
29
|
};
|
|
24
30
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
25
31
|
close: () => any;
|