@revenexx/studio-integrations 0.5.1 → 0.6.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 +201 -0
- package/README.md +17 -4
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +15 -14
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +91 -19
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -21
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +4 -8
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -2
- 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 +54 -25
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +12 -12
- package/dist/runtime/components/Integrations/Layout.vue +21 -2
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +10 -11
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +2 -5
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +160 -67
- 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 +1075 -303
- 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 +42 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +100 -34
- package/dist/runtime/components/Integrations/QueryBuilder.vue +8 -1
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -1
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +17 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +2 -1
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +109 -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 +7 -10
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +21 -37
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +23 -7
- 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 +167 -46
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +11 -2
- 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 +189 -46
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +7 -2
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -2
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +30 -17
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +47 -7
- package/dist/runtime/composables/useCredentialTypes.js +4 -4
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +4 -4
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- package/dist/runtime/composables/useIntegrationsNodes.js +4 -4
- package/dist/runtime/composables/useIntegrationsRuns.js +11 -6
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +10 -7
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsTemplates.js +12 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +6 -6
- package/dist/runtime/composables/useIntegrationsWorkflows.js +8 -4
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- 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/integrationTemplates.d.ts +39 -0
- package/dist/runtime/data/integrationTemplates.js +15 -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 +27 -3
- 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 +2 -5
- package/dist/runtime/pages/integrations/audit.vue +91 -6
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +6 -4
- package/dist/runtime/pages/integrations/credentials.vue +119 -21
- package/dist/runtime/pages/integrations/index.vue +49 -44
- package/dist/runtime/pages/integrations/runs.vue +1 -1
- package/dist/runtime/pages/integrations/schedules.vue +51 -7
- package/dist/runtime/pages/integrations/secrets.vue +29 -7
- package/dist/runtime/pages/integrations/templates/[slug].vue +52 -30
- package/dist/runtime/pages/integrations/templates/index.vue +57 -86
- package/dist/runtime/pages/integrations/workflows/[id].vue +196 -8
- package/dist/runtime/pages/integrations/workflows/index.vue +15 -2
- 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/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +29 -0
- package/dist/runtime/utils/localize.js +4 -0
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -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 +141 -0
- package/dist/runtime/utils/schedule.js +207 -0
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +53 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/package.json +8 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, reactive, ref, watch } from "vue";
|
|
2
|
+
import { computed, reactive, ref, useId, watch } from "vue";
|
|
3
3
|
import {
|
|
4
4
|
AddCircle as Plus,
|
|
5
5
|
CheckCircle as Check,
|
|
@@ -23,16 +23,14 @@ import {
|
|
|
23
23
|
SelectValue
|
|
24
24
|
} from "@revenexx/studio";
|
|
25
25
|
import { toast } from "vue-sonner";
|
|
26
|
+
import { REVENEXX_SOURCE_KIND_BY_SLUG } from "../../utils/revenexxCredentialSource";
|
|
27
|
+
import { cockpitSources } from "../../utils/cockpitSources";
|
|
26
28
|
const props = defineProps({
|
|
27
29
|
type: { type: [Object, null], required: true }
|
|
28
30
|
});
|
|
29
31
|
const emit = defineEmits(["update:active", "apply"]);
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
"revenexx:smtp": "smtp",
|
|
33
|
-
"revenexx:api": "api"
|
|
34
|
-
};
|
|
35
|
-
const kind = computed(() => props.type ? KIND_BY_SLUG[props.type.slug] ?? null : null);
|
|
32
|
+
const kind = computed(() => props.type ? REVENEXX_SOURCE_KIND_BY_SLUG[props.type.slug] ?? null : null);
|
|
33
|
+
const uid = useId();
|
|
36
34
|
const SWITCH_LABEL = {
|
|
37
35
|
sftp: "Use revenexx SFTP Servers",
|
|
38
36
|
smtp: "Use revenexx Mail Servers",
|
|
@@ -43,11 +41,18 @@ const NEW_LABEL = {
|
|
|
43
41
|
smtp: "New mail credential",
|
|
44
42
|
api: "New API key"
|
|
45
43
|
};
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
44
|
+
const sources = cockpitSources();
|
|
45
|
+
const available = sources !== null;
|
|
46
|
+
const tenants = computed(() => sources?.tenants.tenants.value ?? []);
|
|
47
|
+
const onlyTenantSlug = computed(() => sources?.tenants.onlyTenantSlug.value ?? null);
|
|
48
|
+
const loadTenants = () => sources?.tenants.load() ?? Promise.resolve();
|
|
49
|
+
const markets = computed(() => sources?.market.markets.value ?? []);
|
|
50
|
+
const activeCode = computed(() => sources?.market.activeCode.value ?? "");
|
|
51
|
+
const fetchMarkets = () => sources?.market.fetchMarkets() ?? Promise.resolve();
|
|
52
|
+
const GLOBAL_MARKET_CODE = sources?.market.GLOBAL_MARKET_CODE ?? "__global__";
|
|
53
|
+
const sftp = sources?.sftp ?? null;
|
|
54
|
+
const mail = sources?.mail ?? null;
|
|
55
|
+
const consoleKeys = sources?.keys ?? null;
|
|
51
56
|
const config = useRuntimeConfig();
|
|
52
57
|
const sftpHost = config.public.sftpHost ?? "share.revenexx.com";
|
|
53
58
|
const sftpPort = Number(config.public.sftpPort ?? 42);
|
|
@@ -67,12 +72,13 @@ watch(kind, () => {
|
|
|
67
72
|
selectedId.value = "";
|
|
68
73
|
applied.value = null;
|
|
69
74
|
});
|
|
70
|
-
const sftpOptions = computed(() => sftp
|
|
75
|
+
const sftpOptions = computed(() => (sftp?.accounts.value ?? []).filter((a) => (a.is_active || a.status === "active") && a.login_username).map((a) => ({ value: String(a.id), label: a.login_username })));
|
|
71
76
|
const marketOptions = computed(() => [
|
|
72
77
|
{ code: GLOBAL_MARKET_CODE, label: "Global (tenant baseline)" },
|
|
73
78
|
...markets.value.map((m) => ({ code: m.code, label: `${m.name} (${m.code})` }))
|
|
74
79
|
]);
|
|
75
80
|
async function ensureLists() {
|
|
81
|
+
if (!sftp || !mail) return;
|
|
76
82
|
loadingList.value = true;
|
|
77
83
|
try {
|
|
78
84
|
await loadTenants();
|
|
@@ -98,6 +104,7 @@ function marketSlug() {
|
|
|
98
104
|
return marketCode.value === GLOBAL_MARKET_CODE ? "" : marketCode.value;
|
|
99
105
|
}
|
|
100
106
|
async function applySftp() {
|
|
107
|
+
if (!sftp) return;
|
|
101
108
|
const t = tenant.value;
|
|
102
109
|
const acc = sftp.accounts.value.find((a) => String(a.id) === selectedId.value);
|
|
103
110
|
if (!t || !acc) return;
|
|
@@ -122,9 +129,14 @@ async function applySftp() {
|
|
|
122
129
|
}
|
|
123
130
|
}
|
|
124
131
|
async function applySmtp() {
|
|
132
|
+
if (!mail) return;
|
|
125
133
|
applying.value = true;
|
|
126
134
|
try {
|
|
127
135
|
const cred = await mail.getCredential(selectedId.value);
|
|
136
|
+
if (!cred) {
|
|
137
|
+
toast.error("That mail credential is no longer there.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
128
140
|
emit("apply", {
|
|
129
141
|
host: mailSmtpHost,
|
|
130
142
|
port: mailSmtpPort,
|
|
@@ -165,6 +177,7 @@ function openCreate() {
|
|
|
165
177
|
}
|
|
166
178
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
167
179
|
async function createSftp() {
|
|
180
|
+
if (!sftp) return;
|
|
168
181
|
const t = tenant.value;
|
|
169
182
|
if (!t) {
|
|
170
183
|
toast.error("No tenant selected.");
|
|
@@ -195,9 +208,14 @@ async function createSftp() {
|
|
|
195
208
|
}
|
|
196
209
|
}
|
|
197
210
|
async function createSmtp() {
|
|
211
|
+
if (!mail) return;
|
|
198
212
|
creating.value = true;
|
|
199
213
|
try {
|
|
200
214
|
const cred = await mail.createCredential({ name: createName.value.trim() || "SMTP", type: "SMTP" });
|
|
215
|
+
if (!cred) {
|
|
216
|
+
toast.error("The mail credential was not returned \u2014 pick it from the list once it appears.");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
201
219
|
emit("apply", {
|
|
202
220
|
host: mailSmtpHost,
|
|
203
221
|
port: mailSmtpPort,
|
|
@@ -217,6 +235,7 @@ async function createSmtp() {
|
|
|
217
235
|
}
|
|
218
236
|
}
|
|
219
237
|
async function createApiKey() {
|
|
238
|
+
if (!consoleKeys) return;
|
|
220
239
|
creating.value = true;
|
|
221
240
|
try {
|
|
222
241
|
const { secret } = await consoleKeys.create(createName.value.trim() || "Integrations key", ["*"]);
|
|
@@ -244,19 +263,37 @@ function submitCreate() {
|
|
|
244
263
|
<div class="flex items-center justify-between gap-3">
|
|
245
264
|
<div class="min-w-0">
|
|
246
265
|
<div class="text-sm font-medium">{{ SWITCH_LABEL[kind] }}</div>
|
|
266
|
+
<!-- Two sentences, one per state. Saying WHERE the sources are missing is
|
|
267
|
+
the point: without it an operator in the dev studio or another host
|
|
268
|
+
reads a dead switch and cannot tell a broken feature from one that
|
|
269
|
+
belongs somewhere else. The manual fields stay on screen either way. -->
|
|
247
270
|
<p class="text-xs text-muted-foreground">
|
|
248
|
-
|
|
271
|
+
<template v-if="available">
|
|
272
|
+
Reuse an existing revenexx resource instead of typing access data by hand.
|
|
273
|
+
</template>
|
|
274
|
+
<template v-else>
|
|
275
|
+
Not available here — the managed revenexx resources live in the cockpit.
|
|
276
|
+
Enter the access data by hand instead.
|
|
277
|
+
</template>
|
|
249
278
|
</p>
|
|
250
279
|
</div>
|
|
251
|
-
|
|
280
|
+
<!-- Disabled rather than hidden: a control that vanishes leaves nothing to
|
|
281
|
+
read the explanation from, and the operator cannot tell whether the
|
|
282
|
+
feature exists at all. -->
|
|
283
|
+
<Switch
|
|
284
|
+
:aria-label="SWITCH_LABEL[kind]"
|
|
285
|
+
:disabled="!available"
|
|
286
|
+
:model-value="active"
|
|
287
|
+
@update:model-value="(v) => active = v"
|
|
288
|
+
/>
|
|
252
289
|
</div>
|
|
253
290
|
|
|
254
291
|
<div v-if="active" class="mt-3 space-y-3">
|
|
255
292
|
<!-- Tenant (preselected) -->
|
|
256
293
|
<div class="space-y-1.5">
|
|
257
|
-
<Label>Tenant</Label>
|
|
294
|
+
<Label :for="`${uid}-tenant`">Tenant</Label>
|
|
258
295
|
<Select :model-value="tenant ?? ''" @update:model-value="(v) => tenant = v">
|
|
259
|
-
<SelectTrigger class="bg-white">
|
|
296
|
+
<SelectTrigger :id="`${uid}-tenant`" class="bg-white">
|
|
260
297
|
<SelectValue placeholder="Select tenant" />
|
|
261
298
|
</SelectTrigger>
|
|
262
299
|
<SelectContent>
|
|
@@ -269,9 +306,9 @@ function submitCreate() {
|
|
|
269
306
|
|
|
270
307
|
<!-- Market scope — only the Revenexx API credential is market-scoped -->
|
|
271
308
|
<div v-if="kind === 'api'" class="space-y-1.5">
|
|
272
|
-
<Label>Market</Label>
|
|
309
|
+
<Label :for="`${uid}-market`">Market</Label>
|
|
273
310
|
<Select :model-value="marketCode" @update:model-value="(v) => marketCode = v">
|
|
274
|
-
<SelectTrigger class="bg-white">
|
|
311
|
+
<SelectTrigger :id="`${uid}-market`" class="bg-white">
|
|
275
312
|
<SelectValue placeholder="Select market" />
|
|
276
313
|
</SelectTrigger>
|
|
277
314
|
<SelectContent>
|
|
@@ -284,10 +321,10 @@ function submitCreate() {
|
|
|
284
321
|
|
|
285
322
|
<!-- Resource select (SFTP / Mail) -->
|
|
286
323
|
<div v-if="kind === 'sftp' || kind === 'smtp'" class="space-y-1.5">
|
|
287
|
-
<Label>{{ kind === "sftp" ? "SFTP account" : "Mail credential" }}</Label>
|
|
324
|
+
<Label :for="`${uid}-resource`">{{ kind === "sftp" ? "SFTP account" : "Mail credential" }}</Label>
|
|
288
325
|
<div class="flex items-center gap-2">
|
|
289
326
|
<Select :model-value="selectedId" class="flex-1" @update:model-value="(v) => onSelect(v)">
|
|
290
|
-
<SelectTrigger class="bg-white">
|
|
327
|
+
<SelectTrigger :id="`${uid}-resource`" class="bg-white">
|
|
291
328
|
<SelectValue :placeholder="loadingList ? 'Loading\u2026' : 'Select one'" />
|
|
292
329
|
</SelectTrigger>
|
|
293
330
|
<SelectContent>
|
|
@@ -2,17 +2,12 @@
|
|
|
2
2
|
import { computed, ref } from "vue";
|
|
3
3
|
import { Magnifier as Search } from "#solar-icons";
|
|
4
4
|
import { Input } from "@revenexx/studio";
|
|
5
|
+
import { resolveLocalized } from "../../utils/localize";
|
|
5
6
|
const props = defineProps({
|
|
6
7
|
types: { type: Array, required: true }
|
|
7
8
|
});
|
|
8
9
|
const emit = defineEmits(["pick"]);
|
|
9
10
|
const query = ref("");
|
|
10
|
-
function loc(v) {
|
|
11
|
-
if (!v) return "";
|
|
12
|
-
if (typeof v === "string") return v;
|
|
13
|
-
const o = v;
|
|
14
|
-
return o.en ?? o.de ?? Object.values(o)[0] ?? "";
|
|
15
|
-
}
|
|
16
11
|
function authKindLabel(k) {
|
|
17
12
|
switch (k) {
|
|
18
13
|
case "api-key":
|
|
@@ -33,7 +28,7 @@ function authKindLabel(k) {
|
|
|
33
28
|
const filtered = computed(() => {
|
|
34
29
|
const q = query.value.trim().toLowerCase();
|
|
35
30
|
if (!q) return props.types;
|
|
36
|
-
return props.types.filter((t) =>
|
|
31
|
+
return props.types.filter((t) => resolveLocalized(t.name).toLowerCase().includes(q));
|
|
37
32
|
});
|
|
38
33
|
</script>
|
|
39
34
|
|
|
@@ -43,6 +38,7 @@ const filtered = computed(() => {
|
|
|
43
38
|
<Search :size="15" class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
44
39
|
<Input
|
|
45
40
|
v-model="query"
|
|
41
|
+
aria-label="Search for an app"
|
|
46
42
|
class="bg-white pl-9"
|
|
47
43
|
placeholder="Search for an app…"
|
|
48
44
|
autofocus
|
|
@@ -62,7 +58,7 @@ const filtered = computed(() => {
|
|
|
62
58
|
>
|
|
63
59
|
<IntegrationsCredentialLogo :type="t" :size="16" />
|
|
64
60
|
<div class="min-w-0 flex-1 truncate text-sm">
|
|
65
|
-
<span class="font-medium">{{
|
|
61
|
+
<span class="font-medium">{{ resolveLocalized(t.name) || t.slug }}</span>
|
|
66
62
|
<span class="ml-1.5 text-muted-foreground">({{ authKindLabel(t.auth_kind) }})</span>
|
|
67
63
|
</div>
|
|
68
64
|
</button>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
modelValue: string;
|
|
3
|
+
inputId?: string;
|
|
3
4
|
};
|
|
4
5
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
5
6
|
"update:modelValue": (value: string) => any;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { computed, onMounted, ref } from "vue";
|
|
3
3
|
import { Input } from "@revenexx/studio";
|
|
4
4
|
const props = defineProps({
|
|
5
|
-
modelValue: { type: String, required: true }
|
|
5
|
+
modelValue: { type: String, required: true },
|
|
6
|
+
inputId: { type: String, required: false }
|
|
6
7
|
});
|
|
7
8
|
const emit = defineEmits(["update:modelValue"]);
|
|
8
9
|
const catalog = typeof useDefinedEvents === "function" ? useDefinedEvents() : { emits: ref([]), listens: ref([]), loading: ref(false), load: async () => {
|
|
@@ -34,6 +35,7 @@ function pick(topic) {
|
|
|
34
35
|
<template>
|
|
35
36
|
<div class="relative">
|
|
36
37
|
<Input
|
|
38
|
+
:id="props.inputId"
|
|
37
39
|
:model-value="modelValue"
|
|
38
40
|
placeholder="customer.created"
|
|
39
41
|
class="h-8 text-sm bg-white font-mono"
|
|
@@ -46,7 +48,7 @@ function pick(topic) {
|
|
|
46
48
|
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"
|
|
47
49
|
>
|
|
48
50
|
<div v-if="catalog.loading.value && !filtered.length" class="px-2.5 py-1.5 text-sm text-muted-foreground">
|
|
49
|
-
Loading event
|
|
51
|
+
Loading event catalogue…
|
|
50
52
|
</div>
|
|
51
53
|
<button
|
|
52
54
|
v-for="o in filtered"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
modelValue: string;
|
|
3
|
+
inputId?: string;
|
|
3
4
|
};
|
|
4
5
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
5
6
|
"update:modelValue": (value: string) => any;
|
|
@@ -10,6 +10,10 @@ type __VLS_Props = {
|
|
|
10
10
|
placeholder?: string;
|
|
11
11
|
variables?: Record<string, WorkflowVariable>;
|
|
12
12
|
upstreamOutputs?: UpstreamOutput[];
|
|
13
|
+
/** Draw the editor as at fault — the field above owns the reason. */
|
|
14
|
+
invalid?: boolean;
|
|
15
|
+
/** The setting's label, so the editable region has an accessible name. */
|
|
16
|
+
label?: string;
|
|
13
17
|
};
|
|
14
18
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
19
|
"update:modelValue": (value: string) => any;
|
|
@@ -12,7 +12,9 @@ const props = defineProps({
|
|
|
12
12
|
modelValue: { type: null, required: true },
|
|
13
13
|
placeholder: { type: String, required: false },
|
|
14
14
|
variables: { type: Object, required: false },
|
|
15
|
-
upstreamOutputs: { type: Array, required: false }
|
|
15
|
+
upstreamOutputs: { type: Array, required: false },
|
|
16
|
+
invalid: { type: Boolean, required: false },
|
|
17
|
+
label: { type: String, required: false }
|
|
16
18
|
});
|
|
17
19
|
const emit = defineEmits(["update:modelValue"]);
|
|
18
20
|
const text = computed(() => String(props.modelValue ?? ""));
|
|
@@ -78,7 +80,11 @@ const editor = new Editor({
|
|
|
78
80
|
content: textToDoc(text.value),
|
|
79
81
|
editorProps: {
|
|
80
82
|
// Single line: swallow Enter (paragraph splits would break the field).
|
|
81
|
-
handleKeyDown: (_view, event) => event.key === "Enter" && !event.isComposing
|
|
83
|
+
handleKeyDown: (_view, event) => event.key === "Enter" && !event.isComposing,
|
|
84
|
+
// A contenteditable region is not a text box to assistive technology
|
|
85
|
+
// unless it says so, and its label sits in a sibling element — so it would
|
|
86
|
+
// otherwise be an anonymous blob to a screen reader.
|
|
87
|
+
attributes: { role: "textbox", ...props.label ? { "aria-label": props.label } : {} }
|
|
82
88
|
},
|
|
83
89
|
onUpdate({ editor: ed }) {
|
|
84
90
|
const json = ed.getJSON();
|
|
@@ -143,6 +149,12 @@ const sections = computed(() => {
|
|
|
143
149
|
return out;
|
|
144
150
|
});
|
|
145
151
|
const hasSuggestions = computed(() => sections.value.some((s) => s.items.length > 0));
|
|
152
|
+
const query = ref("");
|
|
153
|
+
const filteredSections = computed(() => {
|
|
154
|
+
const q = query.value.trim().toLowerCase();
|
|
155
|
+
if (!q) return sections.value;
|
|
156
|
+
return sections.value.map((s) => ({ ...s, items: s.items.filter((i) => i.label.toLowerCase().includes(q) || i.expr.toLowerCase().includes(q)) })).filter((s) => s.items.length > 0);
|
|
157
|
+
});
|
|
146
158
|
const open = ref(false);
|
|
147
159
|
const wrapRef = ref(null);
|
|
148
160
|
function toggle() {
|
|
@@ -163,6 +175,9 @@ function onKeydown(e) {
|
|
|
163
175
|
function onDocMousedown(e) {
|
|
164
176
|
if (wrapRef.value && !wrapRef.value.contains(e.target)) open.value = false;
|
|
165
177
|
}
|
|
178
|
+
watch(open, (isOpen) => {
|
|
179
|
+
if (!isOpen) query.value = "";
|
|
180
|
+
});
|
|
166
181
|
onMounted(() => document.addEventListener("mousedown", onDocMousedown));
|
|
167
182
|
onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown));
|
|
168
183
|
function insert(expr) {
|
|
@@ -174,7 +189,10 @@ function insert(expr) {
|
|
|
174
189
|
<template>
|
|
175
190
|
<div ref="wrapRef" class="relative" @focusout="onFocusOut">
|
|
176
191
|
<div class="flex items-center gap-1">
|
|
177
|
-
<div
|
|
192
|
+
<div
|
|
193
|
+
class="expr-editor relative h-8 min-w-0 flex-1 overflow-hidden rounded-md border bg-white px-2 text-sm transition focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-ring"
|
|
194
|
+
:class="invalid ? 'border-rose-400' : 'border-black/[0.08]'"
|
|
195
|
+
>
|
|
178
196
|
<span
|
|
179
197
|
v-if="!text"
|
|
180
198
|
class="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 truncate text-muted-foreground/60"
|
|
@@ -203,34 +221,45 @@ function insert(expr) {
|
|
|
203
221
|
|
|
204
222
|
<div
|
|
205
223
|
v-if="open && hasSuggestions"
|
|
206
|
-
class="absolute right-0 top-full z-50 mt-1 max-h-
|
|
224
|
+
class="absolute right-0 top-full z-50 mt-1 flex max-h-72 w-80 flex-col rounded-md border border-black/[0.08] bg-white text-sm shadow-lg"
|
|
207
225
|
>
|
|
208
|
-
<
|
|
209
|
-
<
|
|
210
|
-
v-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
<button
|
|
216
|
-
v-for="item in section.items"
|
|
217
|
-
:key="item.expr"
|
|
218
|
-
type="button"
|
|
219
|
-
class="block w-full px-2 py-1 text-left transition hover:bg-muted/50"
|
|
220
|
-
@mousedown.prevent="insert(item.expr)"
|
|
226
|
+
<div class="shrink-0 border-b border-black/[0.06] p-1">
|
|
227
|
+
<input
|
|
228
|
+
v-model="query"
|
|
229
|
+
aria-label="Filter available data"
|
|
230
|
+
placeholder="Filter…"
|
|
231
|
+
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
|
+
@mousedown.stop
|
|
221
233
|
>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
234
|
+
</div>
|
|
235
|
+
<div class="min-h-0 flex-1 overflow-y-auto py-1">
|
|
236
|
+
<p v-if="!filteredSections.length" class="px-2 py-1.5 text-muted-foreground">
|
|
237
|
+
Nothing matches “{{ query }}”.
|
|
238
|
+
</p>
|
|
239
|
+
<template v-for="section in filteredSections" :key="section.heading">
|
|
240
|
+
<div class="px-2 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
241
|
+
{{ section.heading }}
|
|
227
242
|
</div>
|
|
228
|
-
|
|
229
|
-
|
|
243
|
+
<button
|
|
244
|
+
v-for="item in section.items"
|
|
245
|
+
:key="item.expr"
|
|
246
|
+
type="button"
|
|
247
|
+
class="block w-full px-2 py-1 text-left transition hover:bg-muted/50"
|
|
248
|
+
@mousedown.prevent="insert(item.expr)"
|
|
249
|
+
>
|
|
250
|
+
<div class="truncate text-muted-foreground">
|
|
251
|
+
{{ item.label }}
|
|
252
|
+
</div>
|
|
253
|
+
<div class="truncate font-mono text-emerald-700">
|
|
254
|
+
{{ item.expr }}
|
|
255
|
+
</div>
|
|
256
|
+
</button>
|
|
257
|
+
</template>
|
|
258
|
+
</div>
|
|
230
259
|
</div>
|
|
231
260
|
</div>
|
|
232
261
|
</template>
|
|
233
262
|
|
|
234
263
|
<style>
|
|
235
|
-
.expr-editor .ProseMirror{align-items:center;display:flex;height:100%;outline:none;overflow-x:auto;scrollbar-width:none}.expr-editor .ProseMirror p{line-height:30px;margin:0;white-space:pre}.expr-pill{background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.25);border-radius:6px;color:#047857;display:inline-
|
|
264
|
+
.expr-editor .ProseMirror{align-items:center;display:flex;height:100%;outline:none;overflow-x:auto;scrollbar-width:none}.expr-editor .ProseMirror p{line-height:30px;margin:0;white-space:pre}.expr-pill{align-items:center;background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.25);border-radius:6px;color:#047857;display:inline-flex;font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;height:20px;line-height:1;margin:0 2px;padding:0 7px;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;white-space:nowrap}.expr-pill.ProseMirror-selectednode{outline:2px solid rgba(16,185,129,.5)}
|
|
236
265
|
</style>
|
|
@@ -10,6 +10,10 @@ type __VLS_Props = {
|
|
|
10
10
|
placeholder?: string;
|
|
11
11
|
variables?: Record<string, WorkflowVariable>;
|
|
12
12
|
upstreamOutputs?: UpstreamOutput[];
|
|
13
|
+
/** Draw the editor as at fault — the field above owns the reason. */
|
|
14
|
+
invalid?: boolean;
|
|
15
|
+
/** The setting's label, so the editable region has an accessible name. */
|
|
16
|
+
label?: string;
|
|
13
17
|
};
|
|
14
18
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
19
|
"update:modelValue": (value: string) => any;
|
|
@@ -3,6 +3,7 @@ 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
5
|
import { ChevronsDownUp, ChevronsUpDown, Trash2, Ungroup } from "lucide-vue-next";
|
|
6
|
+
import { resolveDisplayName } from "../../utils/localize";
|
|
6
7
|
const props = defineProps({
|
|
7
8
|
id: { type: String, required: true },
|
|
8
9
|
data: { type: Object, required: true },
|
|
@@ -10,20 +11,16 @@ const props = defineProps({
|
|
|
10
11
|
});
|
|
11
12
|
defineEmits(["updateNodeInternals"]);
|
|
12
13
|
const GROUP_COLORS = [
|
|
13
|
-
{ bg: "rgba(254,243,199,0.45)", border: "#fcd34d", chip: "#f59e0b" },
|
|
14
|
-
{ bg: "rgba(255,237,213,0.45)", border: "#fdba74", chip: "#f97316" },
|
|
15
|
-
{ bg: "rgba(255,228,230,0.45)", border: "#fda4af", chip: "#f43f5e" },
|
|
16
|
-
{ bg: "rgba(220,252,231,0.45)", border: "#86efac", chip: "#10b981" },
|
|
17
|
-
{ bg: "rgba(219,234,254,0.45)", border: "#93c5fd", chip: "#3b82f6" },
|
|
18
|
-
{ bg: "rgba(237,233,254,0.45)", border: "#c4b5fd", chip: "#8b5cf6" },
|
|
19
|
-
{ bg: "rgba(245,245,244,0.6)", border: "#d6d3d1", chip: "#78716c" }
|
|
14
|
+
{ name: "Amber", bg: "rgba(254,243,199,0.45)", border: "#fcd34d", chip: "#f59e0b" },
|
|
15
|
+
{ name: "Orange", bg: "rgba(255,237,213,0.45)", border: "#fdba74", chip: "#f97316" },
|
|
16
|
+
{ name: "Rose", bg: "rgba(255,228,230,0.45)", border: "#fda4af", chip: "#f43f5e" },
|
|
17
|
+
{ name: "Green", bg: "rgba(220,252,231,0.45)", border: "#86efac", chip: "#10b981" },
|
|
18
|
+
{ name: "Blue", bg: "rgba(219,234,254,0.45)", border: "#93c5fd", chip: "#3b82f6" },
|
|
19
|
+
{ name: "Violet", bg: "rgba(237,233,254,0.45)", border: "#c4b5fd", chip: "#8b5cf6" },
|
|
20
|
+
{ name: "Gray", bg: "rgba(245,245,244,0.6)", border: "#d6d3d1", chip: "#78716c" }
|
|
20
21
|
];
|
|
21
22
|
const color = computed(() => GROUP_COLORS[props.data.group.color ?? 4] ?? GROUP_COLORS[4]);
|
|
22
|
-
|
|
23
|
-
if (!v) return "";
|
|
24
|
-
return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
|
|
25
|
-
}
|
|
26
|
-
const label = computed(() => loc(props.data.group.label) || "Group");
|
|
23
|
+
const label = computed(() => resolveDisplayName(props.data.group.label) || "Group");
|
|
27
24
|
const collapsed = computed(() => props.data.group.collapsed === true);
|
|
28
25
|
const editable = computed(() => !!props.data.actions);
|
|
29
26
|
const editing = ref(false);
|
|
@@ -99,6 +96,8 @@ function onResizeEnd() {
|
|
|
99
96
|
class="size-4.5 rounded-full border transition hover:scale-110"
|
|
100
97
|
:class="(data.group.color ?? 4) === i ? 'ring-2 ring-black/25 ring-offset-1' : ''"
|
|
101
98
|
:style="{ backgroundColor: c.chip, borderColor: c.border, width: '16px', height: '16px' }"
|
|
99
|
+
:title="c.name"
|
|
100
|
+
:aria-label="c.name"
|
|
102
101
|
@click="data.actions?.setColor(i)"
|
|
103
102
|
/>
|
|
104
103
|
<div class="mx-0.5 h-4 w-px bg-black/10" />
|
|
@@ -184,6 +183,7 @@ function onResizeEnd() {
|
|
|
184
183
|
v-if="editing"
|
|
185
184
|
ref="inputEl"
|
|
186
185
|
v-model="draft"
|
|
186
|
+
aria-label="Group name"
|
|
187
187
|
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
188
|
@blur="commitRename"
|
|
189
189
|
@keydown.enter.prevent="commitRename"
|
|
@@ -5,6 +5,13 @@ const { focused } = useIntegrationsFocus();
|
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
7
|
<div class="flex h-[calc(100%-0.75rem)] mb-3 mr-3 ml-1 overflow-hidden rounded-lg border border-gray-400/10 outline-1 outline-white/30">
|
|
8
|
+
<!-- First thing in the tab order: jumps past the sidebar into the page. -->
|
|
9
|
+
<a
|
|
10
|
+
href="#integrations-main"
|
|
11
|
+
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
|
+
>
|
|
13
|
+
Skip to content
|
|
14
|
+
</a>
|
|
8
15
|
<div
|
|
9
16
|
class="shrink-0 overflow-hidden transition-[width] duration-200 ease-out border-r border-black/[0.06] bg-white"
|
|
10
17
|
:class="focused ? 'w-0 !border-r-0' : 'w-64'"
|
|
@@ -13,8 +20,20 @@ const { focused } = useIntegrationsFocus();
|
|
|
13
20
|
<Sidebar />
|
|
14
21
|
</div>
|
|
15
22
|
</div>
|
|
16
|
-
|
|
23
|
+
<!-- `tabindex="-1"` is what makes the skip link actually skip: a <main> is
|
|
24
|
+
not focusable on its own, so the browser would move the fragment but
|
|
25
|
+
leave focus on the link — and the next Tab would walk into the sidebar
|
|
26
|
+
the user just asked to be past. It stays out of the tab order (-1, not
|
|
27
|
+
0); only the fragment and a script can put focus here — which is also
|
|
28
|
+
why it draws no focus ring: a ring around the whole page would be noise
|
|
29
|
+
on a region nobody tabs to, and the promise of a visible mark is about
|
|
30
|
+
the fields, buttons and links inside it. -->
|
|
31
|
+
<main
|
|
32
|
+
id="integrations-main"
|
|
33
|
+
tabindex="-1"
|
|
34
|
+
class="flex-1 min-w-0 overflow-y-auto bg-white/40 backdrop-blur-sm outline-none"
|
|
35
|
+
>
|
|
17
36
|
<slot />
|
|
18
|
-
</
|
|
37
|
+
</main>
|
|
19
38
|
</div>
|
|
20
39
|
</template>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed, ref } from "vue";
|
|
3
3
|
import { ChevronDown } from "lucide-vue-next";
|
|
4
4
|
import { Input } from "@revenexx/studio";
|
|
5
|
+
import { resolveLocalized } from "../../utils/localize";
|
|
5
6
|
const props = defineProps({
|
|
6
7
|
slug: { type: String, required: true },
|
|
7
8
|
version: { type: String, required: true },
|
|
@@ -10,10 +11,6 @@ const props = defineProps({
|
|
|
10
11
|
modelValue: { type: null, required: true }
|
|
11
12
|
});
|
|
12
13
|
const emit = defineEmits(["update:modelValue"]);
|
|
13
|
-
function labelText(v) {
|
|
14
|
-
if (!v) return "";
|
|
15
|
-
return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
|
|
16
|
-
}
|
|
17
14
|
const search = ref("");
|
|
18
15
|
const focused = ref(false);
|
|
19
16
|
const q = useNodeConfigResolve({
|
|
@@ -33,14 +30,14 @@ const currentKnown = computed(
|
|
|
33
30
|
);
|
|
34
31
|
const currentLabel = computed(() => {
|
|
35
32
|
const hit = options.value.find((o) => String(o.value) === currentValue.value);
|
|
36
|
-
return hit ?
|
|
33
|
+
return hit ? resolveLocalized(hit.label) || String(hit.value) : currentValue.value;
|
|
37
34
|
});
|
|
38
35
|
const listOpen = computed(() => focused.value && !q.gated.value);
|
|
39
36
|
const visibleOptions = computed(() => {
|
|
40
37
|
const term = search.value.trim().toLowerCase();
|
|
41
38
|
if (!focused.value || !term) return options.value;
|
|
42
39
|
const hits = options.value.filter(
|
|
43
|
-
(o) => (
|
|
40
|
+
(o) => (resolveLocalized(o.label) || "").toLowerCase().includes(term) || String(o.value).toLowerCase().includes(term)
|
|
44
41
|
);
|
|
45
42
|
return hits.length ? hits : options.value;
|
|
46
43
|
});
|
|
@@ -66,7 +63,7 @@ const visibleMultiOptions = computed(() => {
|
|
|
66
63
|
const f = multiFilter.value.trim().toLowerCase();
|
|
67
64
|
if (!f) return options.value;
|
|
68
65
|
return options.value.filter(
|
|
69
|
-
(o) => (
|
|
66
|
+
(o) => (resolveLocalized(o.label) || String(o.value)).toLowerCase().includes(f) || String(o.value).toLowerCase().includes(f)
|
|
70
67
|
);
|
|
71
68
|
});
|
|
72
69
|
function toggleMulti(optValue) {
|
|
@@ -109,10 +106,11 @@ function toggleMulti(optValue) {
|
|
|
109
106
|
<Input
|
|
110
107
|
v-if="options.length > 10"
|
|
111
108
|
v-model="multiFilter"
|
|
109
|
+
:aria-label="`Filter the options for ${resolveLocalized(field.label)}`"
|
|
112
110
|
placeholder="Filter options"
|
|
113
111
|
class="mb-1.5 h-7 bg-white text-sm"
|
|
114
112
|
/>
|
|
115
|
-
<div class="flex flex-wrap gap-1">
|
|
113
|
+
<div role="group" :aria-label="resolveLocalized(field.label)" class="flex flex-wrap gap-1">
|
|
116
114
|
<span v-if="visibleMultiOptions.length === 0" class="text-sm text-muted-foreground">
|
|
117
115
|
{{ options.length === 0 ? "no options available" : "no options match the filter" }}
|
|
118
116
|
</span>
|
|
@@ -125,7 +123,7 @@ function toggleMulti(optValue) {
|
|
|
125
123
|
:class="selectedArray.includes(String(opt.value)) ? 'bg-emerald-500/15 text-emerald-700 border-emerald-500/30' : 'bg-white text-muted-foreground hover:text-foreground'"
|
|
126
124
|
@click="toggleMulti(String(opt.value))"
|
|
127
125
|
>
|
|
128
|
-
{{
|
|
126
|
+
{{ resolveLocalized(opt.label) || String(opt.value) }}
|
|
129
127
|
</button>
|
|
130
128
|
<!-- stored values no longer offered — shown read-only so they stay visible -->
|
|
131
129
|
<span
|
|
@@ -144,7 +142,8 @@ function toggleMulti(optValue) {
|
|
|
144
142
|
<div class="relative">
|
|
145
143
|
<Input
|
|
146
144
|
:model-value="focused ? search : currentLabel"
|
|
147
|
-
:
|
|
145
|
+
:aria-label="resolveLocalized(field.label)"
|
|
146
|
+
:placeholder="resolveLocalized(field.placeholder) || 'Pick one'"
|
|
148
147
|
class="h-8 bg-white pr-7 text-sm"
|
|
149
148
|
@update:model-value="(v) => {
|
|
150
149
|
search = String(v);
|
|
@@ -168,7 +167,7 @@ function toggleMulti(optValue) {
|
|
|
168
167
|
:class="String(opt.value) === currentValue ? 'bg-emerald-500/10 text-emerald-800' : ''"
|
|
169
168
|
@mousedown.prevent="pick(String(opt.value))"
|
|
170
169
|
>
|
|
171
|
-
<span class="min-w-0 truncate">{{
|
|
170
|
+
<span class="min-w-0 truncate">{{ resolveLocalized(opt.label) || String(opt.value) }}</span>
|
|
172
171
|
</button>
|
|
173
172
|
<div v-if="!q.isFetching.value && visibleOptions.length === 0" class="px-2.5 py-1.5 text-sm text-muted-foreground">
|
|
174
173
|
{{ search ? 'nothing matches "' + search + '"' : "no options available" }}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed } from "vue";
|
|
3
3
|
import { Button } from "@revenexx/studio";
|
|
4
4
|
import { IntegrationsApiError } from "../../utils/apiError";
|
|
5
|
+
import { resolveLocalized } from "../../utils/localize";
|
|
5
6
|
const props = defineProps({
|
|
6
7
|
slug: { type: String, required: true },
|
|
7
8
|
version: { type: String, required: true },
|
|
@@ -22,10 +23,6 @@ const q = useNodeConfigResolve({
|
|
|
22
23
|
dependsOn: () => props.field.dependsOn
|
|
23
24
|
});
|
|
24
25
|
const resolvedFields = computed(() => q.data.value?.fields ?? []);
|
|
25
|
-
function labelText(v) {
|
|
26
|
-
if (!v) return "";
|
|
27
|
-
return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
|
|
28
|
-
}
|
|
29
26
|
</script>
|
|
30
27
|
|
|
31
28
|
<template>
|
|
@@ -67,7 +64,7 @@ function labelText(v) {
|
|
|
67
64
|
class="space-y-3 rounded-md border border-black/[0.08] bg-muted/10 p-3 m-0"
|
|
68
65
|
>
|
|
69
66
|
<legend class="px-1 text-mono text-muted-foreground">
|
|
70
|
-
{{
|
|
67
|
+
{{ resolveLocalized(field.label) }}
|
|
71
68
|
</legend>
|
|
72
69
|
<template v-for="f in resolvedFields" :key="f.key">
|
|
73
70
|
<!-- collision guard: a resolved key that shadows a static config field is skipped -->
|
|
@@ -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;
|