@revenexx/studio-integrations 1.3.0 → 1.4.1
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 +457 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue +53 -0
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +61 -29
- package/dist/runtime/components/Integrations/EditorTip.vue +2 -15
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +10 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +94 -49
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +10 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +37 -60
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
- package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
- package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +165 -56
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
- package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
- package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
- package/dist/runtime/composables/useCredentialTypes.js +3 -9
- package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
- package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
- package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
- package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
- package/dist/runtime/composables/useIntegrationsTemplates.js +5 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
- package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -7
- package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
- package/dist/runtime/data/canvasShortcuts.js +68 -0
- package/dist/runtime/i18n/messages/de.json +30 -16
- package/dist/runtime/i18n/messages/en.json +30 -16
- package/dist/runtime/pages/integrations/credentials.vue +40 -18
- package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
- package/dist/runtime/pages/integrations/workflows/[id].vue +128 -33
- package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
- package/dist/runtime/utils/apiEnvelope.js +20 -0
- package/dist/runtime/utils/apiError.d.ts +35 -8
- package/dist/runtime/utils/apiError.js +12 -12
- package/dist/runtime/utils/configDefaults.d.ts +18 -0
- package/dist/runtime/utils/configDefaults.js +7 -0
- package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
- package/dist/runtime/utils/errorPathAdvice.js +19 -0
- package/dist/runtime/utils/notify.d.ts +2 -2
- package/dist/runtime/utils/runEnvelope.d.ts +12 -0
- package/dist/runtime/utils/runEnvelope.js +2 -1
- package/dist/runtime/utils/selectOptions.d.ts +48 -0
- package/dist/runtime/utils/selectOptions.js +19 -0
- package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
- package/dist/runtime/utils/shortcutLabel.js +48 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, reactive, ref, watch } from "vue";
|
|
2
|
+
import { computed, nextTick, reactive, ref, watch } from "vue";
|
|
3
3
|
import {
|
|
4
4
|
CheckCircle as CheckCircle2,
|
|
5
5
|
CloseCircle as XCircle,
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
SelectValue
|
|
31
31
|
} from "@revenexx/studio";
|
|
32
32
|
import { toast } from "../../utils/notify";
|
|
33
|
-
import {
|
|
33
|
+
import { apiRefusal, errorReason } from "../../utils/apiError";
|
|
34
34
|
import { credentialState, relativeTime } from "../../utils/credentialState";
|
|
35
35
|
import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
|
|
36
36
|
import { resolveLocalized } from "../../utils/localize";
|
|
@@ -53,14 +53,31 @@ const saving = ref(false);
|
|
|
53
53
|
const deleting = ref(false);
|
|
54
54
|
const testing = ref(false);
|
|
55
55
|
const liveTest = ref(null);
|
|
56
|
-
const
|
|
57
|
-
const
|
|
56
|
+
const saveRefusal = ref(null);
|
|
57
|
+
const drawnKeys = computed(() => [
|
|
58
|
+
"name",
|
|
59
|
+
...sourceActive.value ? [] : fieldsOf(props.type).map((f) => `config.${f.key}`)
|
|
60
|
+
]);
|
|
61
|
+
const refusal = computed(() => apiRefusal(saveRefusal.value, drawnKeys.value));
|
|
62
|
+
const fieldErrors = computed(() => refusal.value.atFields);
|
|
63
|
+
const summaryReasons = computed(() => {
|
|
64
|
+
if (!saveRefusal.value) return [];
|
|
65
|
+
if (refusal.value.unshown.length) return refusal.value.unshown.map((u) => u.message);
|
|
66
|
+
if (Object.keys(refusal.value.atFields).length) return [];
|
|
67
|
+
return [errorReason(saveRefusal.value, t("integration.credentials.error.save"))];
|
|
68
|
+
});
|
|
58
69
|
function configFieldAria(key) {
|
|
59
70
|
return fieldErrors.value[`config.${key}`] ? { "aria-invalid": "true", "aria-describedby": `detail-field-${key}-error` } : {};
|
|
60
71
|
}
|
|
72
|
+
async function focusFirstRefusedField() {
|
|
73
|
+
await nextTick();
|
|
74
|
+
const key = drawnKeys.value.find((k) => k in fieldErrors.value);
|
|
75
|
+
if (!key) return;
|
|
76
|
+
const id = key === "name" ? "detail-cred-name" : `detail-field-${key.slice("config.".length)}`;
|
|
77
|
+
document.getElementById(id)?.focus();
|
|
78
|
+
}
|
|
61
79
|
function clearRefusal() {
|
|
62
|
-
|
|
63
|
-
fieldErrors.value = {};
|
|
80
|
+
saveRefusal.value = null;
|
|
64
81
|
}
|
|
65
82
|
const connecting = ref(false);
|
|
66
83
|
const typeName = computed(() => resolveLocalized(props.type?.name) || props.credential?.credential_type_slug || "");
|
|
@@ -103,6 +120,7 @@ async function runTest() {
|
|
|
103
120
|
if (!props.credential) return;
|
|
104
121
|
testing.value = true;
|
|
105
122
|
liveTest.value = null;
|
|
123
|
+
clearRefusal();
|
|
106
124
|
try {
|
|
107
125
|
liveTest.value = await test(props.credential.id);
|
|
108
126
|
await load(true);
|
|
@@ -121,9 +139,9 @@ async function save() {
|
|
|
121
139
|
emit("update:open", false);
|
|
122
140
|
toast.success(t("integration.credentials.toast.updated"));
|
|
123
141
|
} catch (e) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
toast.error(
|
|
142
|
+
saveRefusal.value = e;
|
|
143
|
+
void focusFirstRefusedField();
|
|
144
|
+
toast.error(errorReason(e, t("integration.credentials.error.save")));
|
|
127
145
|
} finally {
|
|
128
146
|
saving.value = false;
|
|
129
147
|
}
|
|
@@ -182,8 +200,11 @@ async function connect() {
|
|
|
182
200
|
<!-- Standard cockpit tab bar (violet underline) -->
|
|
183
201
|
<TabBar :model-value="tab" :tabs="tabs" @update:model-value="(k) => tab = k" />
|
|
184
202
|
|
|
185
|
-
<!-- Tab 1: Credentials
|
|
186
|
-
|
|
203
|
+
<!-- Tab 1: Credentials. A `template` rather than a wrapping element: the
|
|
204
|
+
scrolling pane and the block below it are siblings, as they were when the
|
|
205
|
+
pane was the whole tab, so nothing about the spacing around them moves. -->
|
|
206
|
+
<template v-if="tab === 'credentials'">
|
|
207
|
+
<div class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
|
|
187
208
|
<!-- Connection state — the headline answer: are these credentials correct? -->
|
|
188
209
|
<div class="rounded-lg border p-3" :class="state.toneClass">
|
|
189
210
|
<div class="flex items-center gap-2">
|
|
@@ -243,7 +264,10 @@ async function connect() {
|
|
|
243
264
|
<IntegrationsCredentialRevenexxSource
|
|
244
265
|
v-if="hasRevenexxSource(type?.slug)"
|
|
245
266
|
:type="type"
|
|
246
|
-
@update:active="(v) =>
|
|
267
|
+
@update:active="(v) => {
|
|
268
|
+
sourceActive = v;
|
|
269
|
+
clearRefusal();
|
|
270
|
+
}"
|
|
247
271
|
@apply="(c) => Object.assign(form.config, c)"
|
|
248
272
|
/>
|
|
249
273
|
|
|
@@ -313,27 +337,35 @@ async function connect() {
|
|
|
313
337
|
{{ liveTest.ok ? liveTest.message ?? t("integration.credentials.shared.test.ok") : liveTest.error ?? liveTest.message ?? t("integration.credentials.shared.test.failed") }}
|
|
314
338
|
</div>
|
|
315
339
|
|
|
316
|
-
|
|
317
|
-
the field where the service keyed it. -->
|
|
318
|
-
<div v-if="saveError" role="alert" class="rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
|
|
319
|
-
{{ saveError }}
|
|
320
|
-
</div>
|
|
340
|
+
</div>
|
|
321
341
|
|
|
322
|
-
<div class="flex items-center gap-2 pt-1">
|
|
323
|
-
<div class="flex-1" />
|
|
324
|
-
<Button variant="outline" :disabled="testing || saving" @click="runTest">
|
|
325
|
-
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
326
|
-
<TestIcon v-else :size="14" class="mr-1" />
|
|
327
|
-
{{ t("integration.credentials.shared.action.testConnection") }}
|
|
328
|
-
</Button>
|
|
329
|
-
<Button :disabled="saving || !form.name.trim()" @click="save">
|
|
330
|
-
<Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
|
|
331
|
-
{{ t("integration.common.action.save") }}
|
|
332
|
-
</Button>
|
|
333
|
-
</div>
|
|
334
342
|
</div>
|
|
335
343
|
|
|
344
|
+
<!-- Outside the scrolling pane on purpose, the way the create dialog has it:
|
|
345
|
+
a refusal puts the operator in the first marked field, which scrolls that
|
|
346
|
+
pane back to the top — and a summary rendered at the end of the form would
|
|
347
|
+
be carried out of sight by the very focus meant to make the refusal
|
|
348
|
+
visible. What is marked at a field is not repeated here; what has no field
|
|
349
|
+
to be marked at would otherwise be silent. -->
|
|
350
|
+
<div v-if="summaryReasons.length" role="alert" class="mt-3 space-y-1 rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
|
|
351
|
+
<!-- Keyed by position: two undrawn keys can be refused with the same
|
|
352
|
+
sentence, and a duplicate key is how one of them goes unrendered. -->
|
|
353
|
+
<p v-for="(reason, i) in summaryReasons" :key="i">{{ reason }}</p>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<div class="flex items-center gap-2 pt-3">
|
|
357
|
+
<div class="flex-1" />
|
|
358
|
+
<Button variant="outline" :disabled="testing || saving" @click="runTest">
|
|
359
|
+
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
360
|
+
<TestIcon v-else :size="14" class="mr-1" />
|
|
361
|
+
{{ t("integration.credentials.shared.action.testConnection") }}
|
|
362
|
+
</Button>
|
|
363
|
+
<Button :disabled="saving || !form.name.trim()" @click="save">
|
|
364
|
+
<Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
|
|
365
|
+
{{ t("integration.common.action.save") }}
|
|
366
|
+
</Button>
|
|
336
367
|
</div>
|
|
368
|
+
</template>
|
|
337
369
|
|
|
338
370
|
<!-- Tab 2: Details -->
|
|
339
371
|
<div v-else class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed } from "vue";
|
|
3
3
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@revenexx/studio";
|
|
4
|
+
import { formatShortcut } from "../../utils/shortcutLabel";
|
|
4
5
|
const props = defineProps({
|
|
5
6
|
label: { type: String, required: true },
|
|
6
7
|
shortcut: { type: String, required: false }
|
|
7
8
|
});
|
|
8
|
-
const
|
|
9
|
-
const keys = computed(() => {
|
|
10
|
-
if (!props.shortcut) return "";
|
|
11
|
-
const out = [];
|
|
12
|
-
for (const raw of props.shortcut.split("+")) {
|
|
13
|
-
const t = raw.toLowerCase();
|
|
14
|
-
if (t === "mod") out.push(isMac ? "\u2318" : "Ctrl");
|
|
15
|
-
else if (t === "shift") out.push("\u21E7");
|
|
16
|
-
else if (t === "alt") out.push(isMac ? "\u2325" : "Alt");
|
|
17
|
-
else if (t === "enter") out.push("\u21B5");
|
|
18
|
-
else if (t === "escape") out.push("Esc");
|
|
19
|
-
else out.push(t.toUpperCase());
|
|
20
|
-
}
|
|
21
|
-
return isMac ? out.join("") : out.join("+");
|
|
22
|
-
});
|
|
9
|
+
const keys = computed(() => props.shortcut ? formatShortcut(props.shortcut) : "");
|
|
23
10
|
</script>
|
|
24
11
|
|
|
25
12
|
<template>
|
|
@@ -21,6 +21,16 @@ type __VLS_Props = {
|
|
|
21
21
|
}>;
|
|
22
22
|
/** Why the last save refused this setting, if it did. */
|
|
23
23
|
refused?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The 1-based row this setting sits in, when it is a sub-setting of a list
|
|
26
|
+
* row — set only by the list branch below, rendering itself.
|
|
27
|
+
*
|
|
28
|
+
* It is what keeps a repeating control nameable: the visible label repeats
|
|
29
|
+
* per row, so every row's control would otherwise answer to the same name
|
|
30
|
+
* (specs/accessibility.md AC-1). With it, each says which setting AND which
|
|
31
|
+
* row it belongs to.
|
|
32
|
+
*/
|
|
33
|
+
rowIndex?: number;
|
|
24
34
|
};
|
|
25
35
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
26
36
|
set: (key: string, value: unknown) => any;
|
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
Textarea
|
|
16
16
|
} from "@revenexx/studio";
|
|
17
17
|
import { resolveLocalized } from "../../utils/localize";
|
|
18
|
+
import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
|
|
18
19
|
import { credentialState } from "../../utils/credentialState";
|
|
20
|
+
import { defaultsFor } from "../../utils/configDefaults";
|
|
21
|
+
import { applyingFields, settingApplies, staleKeys } from "../../utils/settingCondition";
|
|
19
22
|
const props = defineProps({
|
|
20
23
|
field: { type: Object, required: true },
|
|
21
24
|
config: { type: Object, required: true },
|
|
@@ -28,7 +31,8 @@ const props = defineProps({
|
|
|
28
31
|
variables: { type: Object, required: false },
|
|
29
32
|
stateNamespaces: { type: Array, required: false },
|
|
30
33
|
upstreamOutputs: { type: Array, required: false },
|
|
31
|
-
refused: { type: String, required: false }
|
|
34
|
+
refused: { type: String, required: false },
|
|
35
|
+
rowIndex: { type: Number, required: false }
|
|
32
36
|
});
|
|
33
37
|
const emit = defineEmits(["set", "declare-state"]);
|
|
34
38
|
const t = useIntegrationsT();
|
|
@@ -38,6 +42,11 @@ function write(v) {
|
|
|
38
42
|
}
|
|
39
43
|
const exprHint = "${{ \u2026 }}";
|
|
40
44
|
const descriptionText = computed(() => resolveLocalized(props.field.description));
|
|
45
|
+
const inRow = computed(() => props.rowIndex !== void 0);
|
|
46
|
+
const controlName = computed(() => {
|
|
47
|
+
const label = resolveLocalized(props.field.label);
|
|
48
|
+
return inRow.value ? t("integration.nodeConfig.array.subFieldAria", { label, n: props.rowIndex }) : label;
|
|
49
|
+
});
|
|
41
50
|
function isEmpty(v) {
|
|
42
51
|
return v === void 0 || v === null || v === "" || Array.isArray(v) && v.length === 0;
|
|
43
52
|
}
|
|
@@ -107,12 +116,12 @@ const credOptions = computed(
|
|
|
107
116
|
() => credsForField(props.field).map((c) => ({ credential: c, state: credentialState(c) }))
|
|
108
117
|
);
|
|
109
118
|
const UNKNOWN_CREDENTIAL = computed(() => t("integration.nodeConfig.credential.unknown"));
|
|
110
|
-
|
|
111
|
-
const label =
|
|
119
|
+
const credTriggerLabel = computed(() => {
|
|
120
|
+
const label = controlName.value;
|
|
112
121
|
if (bound.value) return t("integration.nodeConfig.credential.trigger.bound", { label, name: bound.value.credential.name, state: bound.value.state.label });
|
|
113
122
|
if (value.value && credentialsLoaded.value) return t("integration.nodeConfig.credential.trigger.unknown", { label, unknown: UNKNOWN_CREDENTIAL.value });
|
|
114
123
|
return label;
|
|
115
|
-
}
|
|
124
|
+
});
|
|
116
125
|
const credCreateOpen = ref(false);
|
|
117
126
|
const sftpImportOpen = ref(false);
|
|
118
127
|
const CRED_ACTION_NEW = "__action_new_credential__";
|
|
@@ -267,17 +276,36 @@ function writeJson(text) {
|
|
|
267
276
|
function readArray() {
|
|
268
277
|
return Array.isArray(value.value) ? value.value : [];
|
|
269
278
|
}
|
|
279
|
+
const rowKeys = ref([]);
|
|
280
|
+
let nextRowKey = 0;
|
|
281
|
+
watch(() => readArray().length, (n) => {
|
|
282
|
+
while (rowKeys.value.length < n) rowKeys.value.push(nextRowKey++);
|
|
283
|
+
if (rowKeys.value.length > n) rowKeys.value.splice(n);
|
|
284
|
+
}, { immediate: true });
|
|
270
285
|
function pushArrayItem(defaultItem) {
|
|
271
286
|
write([...readArray(), defaultItem]);
|
|
272
287
|
}
|
|
288
|
+
function newRow() {
|
|
289
|
+
const declared = props.field.items?.properties ?? [];
|
|
290
|
+
const row = defaultsFor(declared);
|
|
291
|
+
for (; ; ) {
|
|
292
|
+
const withdrawn = declared.filter((f) => f.key in row && !settingApplies(f, row));
|
|
293
|
+
if (!withdrawn.length) return row;
|
|
294
|
+
for (const f of withdrawn) delete row[f.key];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
273
297
|
function removeArrayItem(index) {
|
|
274
298
|
const arr = readArray().slice();
|
|
275
299
|
arr.splice(index, 1);
|
|
300
|
+
rowKeys.value.splice(index, 1);
|
|
276
301
|
write(arr);
|
|
277
302
|
}
|
|
278
303
|
function setArrayItemField(index, fieldKey, v) {
|
|
279
304
|
const arr = readArray().slice();
|
|
280
|
-
|
|
305
|
+
const after = { ...arr[index] ?? {}, [fieldKey]: v };
|
|
306
|
+
for (const key of staleKeys(props.field.items?.properties ?? [], fieldKey, after))
|
|
307
|
+
delete after[key];
|
|
308
|
+
arr[index] = after;
|
|
281
309
|
write(arr);
|
|
282
310
|
}
|
|
283
311
|
</script>
|
|
@@ -351,7 +379,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
351
379
|
|
|
352
380
|
<template v-else-if="isJsonField">
|
|
353
381
|
<Textarea
|
|
354
|
-
:aria-label="
|
|
382
|
+
:aria-label="controlName"
|
|
355
383
|
:model-value="jsonDraft"
|
|
356
384
|
:rows="8"
|
|
357
385
|
spellcheck="false"
|
|
@@ -368,14 +396,14 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
368
396
|
<!-- A repeating row has no room for visible labels, so each control
|
|
369
397
|
names itself and says which row of which field it belongs to. -->
|
|
370
398
|
<Input
|
|
371
|
-
:aria-label="t('integration.nodeConfig.object.row.keyAria', { label:
|
|
399
|
+
:aria-label="t('integration.nodeConfig.object.row.keyAria', { label: controlName, n: idx + 1 })"
|
|
372
400
|
:model-value="row.key"
|
|
373
401
|
:placeholder="t('integration.nodeConfig.object.row.keyPlaceholder')"
|
|
374
402
|
class="h-7 w-[38%] rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
|
|
375
403
|
@update:model-value="(v) => setKvRow(idx, { key: String(v) })"
|
|
376
404
|
/>
|
|
377
405
|
<Input
|
|
378
|
-
:aria-label="t('integration.nodeConfig.object.row.valueAria', { label:
|
|
406
|
+
:aria-label="t('integration.nodeConfig.object.row.valueAria', { label: controlName, n: idx + 1 })"
|
|
379
407
|
:model-value="row.value"
|
|
380
408
|
:placeholder="t('integration.nodeConfig.object.row.valuePlaceholder')"
|
|
381
409
|
class="h-7 min-w-0 flex-1 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
|
|
@@ -383,7 +411,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
383
411
|
/>
|
|
384
412
|
<button
|
|
385
413
|
type="button"
|
|
386
|
-
:aria-label="t('integration.nodeConfig.object.row.removeAria', { label:
|
|
414
|
+
:aria-label="t('integration.nodeConfig.object.row.removeAria', { label: controlName, n: idx + 1 })"
|
|
387
415
|
class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
|
|
388
416
|
@click="removeKvRow(idx)"
|
|
389
417
|
>
|
|
@@ -410,16 +438,22 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
410
438
|
:variables="variables"
|
|
411
439
|
:upstream-outputs="upstreamOutputs"
|
|
412
440
|
:invalid="invalid"
|
|
413
|
-
:label="
|
|
441
|
+
:label="controlName"
|
|
414
442
|
:multiline="isMultiline"
|
|
415
443
|
@update:model-value="(v) => write(v)"
|
|
416
444
|
/>
|
|
417
445
|
|
|
418
446
|
<!-- dynamic options — resolved at author time via config:resolve. Ahead of
|
|
419
447
|
the generic expression branch so a dynamic select with
|
|
420
|
-
expressionAllowed still renders as a dropdown by default.
|
|
448
|
+
expressionAllowed still renders as a dropdown by default.
|
|
449
|
+
|
|
450
|
+
Not inside a list row: the resolve request carries the config the
|
|
451
|
+
setting belongs to, and a row is not that config — the platform would
|
|
452
|
+
be asked about a set of values it has never heard of. No published node
|
|
453
|
+
declares one there; a row's setting is drawn from what it declares
|
|
454
|
+
instead (specs/node-configuration.md, ## Gaps). -->
|
|
421
455
|
<IntegrationsNodeConfigDynamicField
|
|
422
|
-
v-else-if="field.dynamic"
|
|
456
|
+
v-else-if="field.dynamic && !inRow"
|
|
423
457
|
:slug="slug"
|
|
424
458
|
:version="version"
|
|
425
459
|
:field="field"
|
|
@@ -437,7 +471,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
437
471
|
:variables="variables"
|
|
438
472
|
:upstream-outputs="upstreamOutputs"
|
|
439
473
|
:invalid="invalid"
|
|
440
|
-
:label="
|
|
474
|
+
:label="controlName"
|
|
441
475
|
:multiline="isMultiline"
|
|
442
476
|
@update:model-value="(v) => write(v)"
|
|
443
477
|
/>
|
|
@@ -451,7 +485,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
451
485
|
shared control sizes itself to its content and ignores it. -->
|
|
452
486
|
<Textarea
|
|
453
487
|
v-else-if="field.type === 'string' && isMultiline"
|
|
454
|
-
:aria-label="
|
|
488
|
+
:aria-label="controlName"
|
|
455
489
|
:model-value="value ?? ''"
|
|
456
490
|
:placeholder="resolveLocalized(field.placeholder)"
|
|
457
491
|
:rows="4"
|
|
@@ -463,7 +497,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
463
497
|
<!-- plain string (no expressions) -->
|
|
464
498
|
<Input
|
|
465
499
|
v-else-if="field.type === 'string'"
|
|
466
|
-
:aria-label="
|
|
500
|
+
:aria-label="controlName"
|
|
467
501
|
:model-value="value ?? ''"
|
|
468
502
|
:placeholder="resolveLocalized(field.placeholder)"
|
|
469
503
|
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
|
|
@@ -474,7 +508,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
474
508
|
<!-- number -->
|
|
475
509
|
<Input
|
|
476
510
|
v-else-if="field.type === 'number'"
|
|
477
|
-
:aria-label="
|
|
511
|
+
:aria-label="controlName"
|
|
478
512
|
type="number"
|
|
479
513
|
:model-value="value ?? ''"
|
|
480
514
|
class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
|
|
@@ -485,7 +519,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
485
519
|
<!-- boolean -->
|
|
486
520
|
<div v-else-if="field.type === 'boolean'" class="flex items-center gap-2 pt-1">
|
|
487
521
|
<Switch
|
|
488
|
-
:aria-label="
|
|
522
|
+
:aria-label="controlName"
|
|
489
523
|
class="data-[state=unchecked]:bg-slate-300"
|
|
490
524
|
:model-value="Boolean(value)"
|
|
491
525
|
@update:model-value="(v) => write(v)"
|
|
@@ -495,9 +529,12 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
495
529
|
</span>
|
|
496
530
|
</div>
|
|
497
531
|
|
|
498
|
-
<!-- dynamic schema — child fields resolved at author time, written into the
|
|
532
|
+
<!-- dynamic schema — child fields resolved at author time, written into the
|
|
533
|
+
flat config. Withheld inside a list row for the same reason as the
|
|
534
|
+
branch above, and one more: the fields it resolves are written into the
|
|
535
|
+
node's flat config, which a row is not. -->
|
|
499
536
|
<IntegrationsNodeConfigDynamicSchema
|
|
500
|
-
v-else-if="field.type === 'dynamic-schema'"
|
|
537
|
+
v-else-if="field.type === 'dynamic-schema' && !inRow"
|
|
501
538
|
:slug="slug"
|
|
502
539
|
:version="version"
|
|
503
540
|
:field="field"
|
|
@@ -511,21 +548,21 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
511
548
|
<!-- select -->
|
|
512
549
|
<Select
|
|
513
550
|
v-else-if="field.type === 'select'"
|
|
514
|
-
:model-value="value
|
|
515
|
-
@update:model-value="(v) => write(v)"
|
|
551
|
+
:model-value="toModelValue(value, field.options)"
|
|
552
|
+
@update:model-value="(v) => write(fromModelValue(v))"
|
|
516
553
|
>
|
|
517
|
-
<SelectTrigger :aria-label="
|
|
554
|
+
<SelectTrigger :aria-label="controlName" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
518
555
|
<SelectValue :placeholder="resolveLocalized(field.placeholder) || t('integration.nodeConfig.select.placeholder')" />
|
|
519
556
|
</SelectTrigger>
|
|
520
557
|
<SelectContent>
|
|
521
|
-
<SelectItem v-for="opt in field.options ?? []" :key="String(opt.value)" :value="
|
|
558
|
+
<SelectItem v-for="opt in field.options ?? []" :key="String(opt.value)" :value="toItemValue(opt.value)">
|
|
522
559
|
{{ resolveLocalized(opt.label) }}
|
|
523
560
|
</SelectItem>
|
|
524
561
|
</SelectContent>
|
|
525
562
|
</Select>
|
|
526
563
|
|
|
527
564
|
<!-- multiselect — pill toggles for now -->
|
|
528
|
-
<div v-else-if="field.type === 'multiselect'" role="group" :aria-label="
|
|
565
|
+
<div v-else-if="field.type === 'multiselect'" role="group" :aria-label="controlName" class="flex flex-wrap gap-1">
|
|
529
566
|
<button
|
|
530
567
|
v-for="opt in field.options ?? []"
|
|
531
568
|
:key="String(opt.value)"
|
|
@@ -548,7 +585,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
548
585
|
:model-value="value ?? ''"
|
|
549
586
|
@update:model-value="(v) => write(v)"
|
|
550
587
|
>
|
|
551
|
-
<SelectTrigger :aria-label="
|
|
588
|
+
<SelectTrigger :aria-label="controlName" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
552
589
|
<SelectValue :placeholder="t('integration.nodeConfig.secretRef.placeholder')" />
|
|
553
590
|
</SelectTrigger>
|
|
554
591
|
<SelectContent>
|
|
@@ -564,7 +601,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
564
601
|
:model-value="value ?? ''"
|
|
565
602
|
@update:model-value="(v) => onCredentialSelect(v)"
|
|
566
603
|
>
|
|
567
|
-
<SelectTrigger :aria-label="credTriggerLabel
|
|
604
|
+
<SelectTrigger :aria-label="credTriggerLabel" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
568
605
|
<!-- The trigger renders the bound credential itself instead of letting
|
|
569
606
|
`SelectValue` echo the chosen row. Left to itself it shows that row's
|
|
570
607
|
`textContent`, which glues the name and the state pill into one word —
|
|
@@ -653,7 +690,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
653
690
|
:model-value="value ?? ''"
|
|
654
691
|
@update:model-value="(v) => onStateSelect(v)"
|
|
655
692
|
>
|
|
656
|
-
<SelectTrigger :aria-label="
|
|
693
|
+
<SelectTrigger :aria-label="controlName" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
657
694
|
<SelectValue :placeholder="t('integration.nodeConfig.state.placeholder')" />
|
|
658
695
|
</SelectTrigger>
|
|
659
696
|
<SelectContent>
|
|
@@ -687,7 +724,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
687
724
|
<div v-else-if="field.type === 'array' && field.items?.type === 'object'" class="space-y-2">
|
|
688
725
|
<div
|
|
689
726
|
v-for="(item, idx) in readArray()"
|
|
690
|
-
:key="idx"
|
|
727
|
+
:key="rowKeys[idx] ?? idx"
|
|
691
728
|
class="rounded-md border border-black/[0.06] bg-muted/20 p-2 space-y-1.5"
|
|
692
729
|
>
|
|
693
730
|
<div class="flex items-center justify-between">
|
|
@@ -696,39 +733,47 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
696
733
|
</span>
|
|
697
734
|
<button
|
|
698
735
|
type="button"
|
|
699
|
-
:aria-label="t('integration.nodeConfig.array.removeItemAria', { label:
|
|
736
|
+
:aria-label="t('integration.nodeConfig.array.removeItemAria', { label: controlName, n: idx + 1 })"
|
|
700
737
|
class="flex size-5 items-center justify-center rounded text-muted-foreground hover:text-rose-600"
|
|
701
738
|
@click="removeArrayItem(idx)"
|
|
702
739
|
>
|
|
703
740
|
<TrashBin :size="11" />
|
|
704
741
|
</button>
|
|
705
742
|
</div>
|
|
706
|
-
|
|
707
|
-
|
|
743
|
+
<!-- Each sub-setting is drawn by this same component, one level down,
|
|
744
|
+
the way a dynamic-schema child is: the row is its config, its own
|
|
745
|
+
`key` addresses its value in it, and what it declares — a list of
|
|
746
|
+
options, a switch, a description, a required marker — decides the
|
|
747
|
+
control, instead of every one of them being a text box.
|
|
748
|
+
|
|
749
|
+
`refused` is deliberately not handed down: it says why the LAST
|
|
750
|
+
SAVE refused this setting as a whole, so passing it on would mark
|
|
751
|
+
every sub-control of every row red with a sentence about a
|
|
752
|
+
different value. -->
|
|
753
|
+
<IntegrationsNodeConfigField
|
|
754
|
+
v-for="sub in applyingFields(field.items.properties ?? [], item ?? {})"
|
|
708
755
|
:key="sub.key"
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
/>
|
|
724
|
-
</div>
|
|
756
|
+
:field="sub"
|
|
757
|
+
:config="item"
|
|
758
|
+
:slug="slug"
|
|
759
|
+
:version="version"
|
|
760
|
+
:secret-keys="secretKeys"
|
|
761
|
+
:static-keys="staticKeys"
|
|
762
|
+
:depth="(depth ?? 0) + 1"
|
|
763
|
+
:variables="variables"
|
|
764
|
+
:state-namespaces="stateNamespaces"
|
|
765
|
+
:upstream-outputs="upstreamOutputs"
|
|
766
|
+
:row-index="idx + 1"
|
|
767
|
+
@set="(k, v) => setArrayItemField(idx, k, v)"
|
|
768
|
+
@declare-state="(ns, role, visibility) => emit('declare-state', ns, role, visibility)"
|
|
769
|
+
/>
|
|
725
770
|
</div>
|
|
726
771
|
<Button
|
|
727
772
|
type="button"
|
|
728
773
|
variant="outline"
|
|
729
774
|
size="sm"
|
|
730
775
|
class="h-7 text-sm w-full"
|
|
731
|
-
@click="pushArrayItem(
|
|
776
|
+
@click="pushArrayItem(newRow())"
|
|
732
777
|
>
|
|
733
778
|
<Plus :size="11" />
|
|
734
779
|
{{ t("integration.nodeConfig.action.addRow") }}
|
|
@@ -738,7 +783,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
738
783
|
<!-- fallback: JSON textarea for complex types -->
|
|
739
784
|
<Textarea
|
|
740
785
|
v-else
|
|
741
|
-
:aria-label="
|
|
786
|
+
:aria-label="controlName"
|
|
742
787
|
:model-value="value === void 0 ? '' : JSON.stringify(value, null, 2)"
|
|
743
788
|
rows="3"
|
|
744
789
|
class="rounded-md border border-black/[0.08] ring-0 shadow-none text-sm font-mono bg-white"
|
|
@@ -21,6 +21,16 @@ type __VLS_Props = {
|
|
|
21
21
|
}>;
|
|
22
22
|
/** Why the last save refused this setting, if it did. */
|
|
23
23
|
refused?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The 1-based row this setting sits in, when it is a sub-setting of a list
|
|
26
|
+
* row — set only by the list branch below, rendering itself.
|
|
27
|
+
*
|
|
28
|
+
* It is what keeps a repeating control nameable: the visible label repeats
|
|
29
|
+
* per row, so every row's control would otherwise answer to the same name
|
|
30
|
+
* (specs/accessibility.md AC-1). With it, each says which setting AND which
|
|
31
|
+
* row it belongs to.
|
|
32
|
+
*/
|
|
33
|
+
rowIndex?: number;
|
|
24
34
|
};
|
|
25
35
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
26
36
|
set: (key: string, value: unknown) => any;
|