@revenexx/studio-integrations 0.6.0 → 1.0.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 +643 -0
- package/README.md +31 -0
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +12 -3
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +32 -28
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +45 -38
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
- package/dist/runtime/components/Integrations/ExpressionInput.vue +21 -15
- package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
- package/dist/runtime/components/Integrations/Layout.vue +28 -3
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +30 -16
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -8
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +45 -42
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +315 -221
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +37 -10
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +40 -35
- package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
- package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
- package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
- package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +82 -34
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +18 -8
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +22 -12
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +41 -39
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +131 -87
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +27 -24
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +76 -46
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +55 -43
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +55 -43
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -13
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +79 -39
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +4 -2
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
- package/dist/runtime/composables/useIntegrationsCredentials.js +8 -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 +14 -2
- package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
- package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +3 -2
- package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
- package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
- package/dist/runtime/composables/useIntegrationsT.js +9 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
- package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
- package/dist/runtime/composables/useIntegrationsTriggers.d.ts +23 -3
- package/dist/runtime/composables/useIntegrationsTriggers.js +21 -8
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +30 -7
- package/dist/runtime/composables/useNodeConfigResolve.js +4 -2
- package/dist/runtime/data/integrationNodeCatalog.d.ts +63 -1
- package/dist/runtime/data/integrationNodeCatalog.js +22 -4
- package/dist/runtime/data/integrationTemplates.d.ts +9 -0
- package/dist/runtime/data/integrationTemplates.js +7 -1
- package/dist/runtime/data/nodeInputData.d.ts +58 -1
- package/dist/runtime/data/nodeInputData.js +30 -5
- package/dist/runtime/data/payloadForm.js +2 -1
- package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
- package/dist/runtime/data/triggerGlyphs.js +11 -0
- package/dist/runtime/data/workflowIconCatalog.js +2 -1
- package/dist/runtime/i18n/messages/de.json +1224 -0
- package/dist/runtime/i18n/messages/en.json +1224 -0
- package/dist/runtime/i18n/translate.d.ts +83 -0
- package/dist/runtime/i18n/translate.js +41 -0
- package/dist/runtime/pages/integrations/audit.vue +52 -34
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
- package/dist/runtime/pages/integrations/credentials.vue +73 -52
- package/dist/runtime/pages/integrations/index.vue +66 -73
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +49 -28
- package/dist/runtime/pages/integrations/secrets.vue +34 -34
- package/dist/runtime/pages/integrations/templates/[slug].vue +89 -72
- package/dist/runtime/pages/integrations/templates/index.vue +35 -27
- package/dist/runtime/pages/integrations/workflows/[id].vue +351 -197
- package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
- package/dist/runtime/plugins/locale.d.ts +10 -0
- package/dist/runtime/plugins/locale.js +6 -0
- package/dist/runtime/plugins/realtime.client.d.ts +32 -0
- package/dist/runtime/plugins/realtime.client.js +4 -0
- package/dist/runtime/types/integrations.d.ts +25 -1
- package/dist/runtime/utils/cockpitSources.d.ts +22 -3
- package/dist/runtime/utils/credentialState.d.ts +30 -2
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/localize.d.ts +52 -5
- package/dist/runtime/utils/localize.js +47 -9
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/schedule.d.ts +0 -25
- package/dist/runtime/utils/schedule.js +37 -25
- package/dist/runtime/utils/servedPaths.d.ts +22 -0
- package/dist/runtime/utils/servedPaths.js +3 -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 +2 -2
- package/dist/runtime/utils/validationFindings.js +4 -3
- package/dist/types.d.mts +2 -6
- package/package.json +3 -2
|
@@ -29,18 +29,19 @@ const props = defineProps({
|
|
|
29
29
|
type: { type: [Object, null], required: true }
|
|
30
30
|
});
|
|
31
31
|
const emit = defineEmits(["update:active", "apply"]);
|
|
32
|
+
const t = useIntegrationsT();
|
|
32
33
|
const kind = computed(() => props.type ? REVENEXX_SOURCE_KIND_BY_SLUG[props.type.slug] ?? null : null);
|
|
33
34
|
const uid = useId();
|
|
34
|
-
const SWITCH_LABEL = {
|
|
35
|
-
sftp: "
|
|
36
|
-
smtp: "
|
|
37
|
-
api: "
|
|
38
|
-
};
|
|
39
|
-
const NEW_LABEL = {
|
|
40
|
-
sftp: "
|
|
41
|
-
smtp: "
|
|
42
|
-
api: "
|
|
43
|
-
};
|
|
35
|
+
const SWITCH_LABEL = computed(() => ({
|
|
36
|
+
sftp: t("integration.credentials.revenexxSource.switch.sftp"),
|
|
37
|
+
smtp: t("integration.credentials.revenexxSource.switch.smtp"),
|
|
38
|
+
api: t("integration.credentials.revenexxSource.switch.api")
|
|
39
|
+
}));
|
|
40
|
+
const NEW_LABEL = computed(() => ({
|
|
41
|
+
sftp: t("integration.credentials.revenexxSource.new.sftp"),
|
|
42
|
+
smtp: t("integration.credentials.revenexxSource.new.smtp"),
|
|
43
|
+
api: t("integration.credentials.revenexxSource.new.api")
|
|
44
|
+
}));
|
|
44
45
|
const sources = cockpitSources();
|
|
45
46
|
const available = sources !== null;
|
|
46
47
|
const tenants = computed(() => sources?.tenants.tenants.value ?? []);
|
|
@@ -74,7 +75,7 @@ watch(kind, () => {
|
|
|
74
75
|
});
|
|
75
76
|
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 })));
|
|
76
77
|
const marketOptions = computed(() => [
|
|
77
|
-
{ code: GLOBAL_MARKET_CODE, label: "
|
|
78
|
+
{ code: GLOBAL_MARKET_CODE, label: t("integration.credentials.revenexxSource.market.global") },
|
|
78
79
|
...markets.value.map((m) => ({ code: m.code, label: `${m.name} (${m.code})` }))
|
|
79
80
|
]);
|
|
80
81
|
async function ensureLists() {
|
|
@@ -92,7 +93,7 @@ async function ensureLists() {
|
|
|
92
93
|
await fetchMarkets();
|
|
93
94
|
}
|
|
94
95
|
} catch (e) {
|
|
95
|
-
toast.error(e?.message ?? "
|
|
96
|
+
toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.loadResources"));
|
|
96
97
|
} finally {
|
|
97
98
|
loadingList.value = false;
|
|
98
99
|
}
|
|
@@ -105,14 +106,14 @@ function marketSlug() {
|
|
|
105
106
|
}
|
|
106
107
|
async function applySftp() {
|
|
107
108
|
if (!sftp) return;
|
|
108
|
-
const
|
|
109
|
+
const tenantSlug = tenant.value;
|
|
109
110
|
const acc = sftp.accounts.value.find((a) => String(a.id) === selectedId.value);
|
|
110
|
-
if (!
|
|
111
|
+
if (!tenantSlug || !acc) return;
|
|
111
112
|
applying.value = true;
|
|
112
113
|
try {
|
|
113
|
-
const creds = await sftp.revealPassword(
|
|
114
|
+
const creds = await sftp.revealPassword(tenantSlug, acc.id);
|
|
114
115
|
if (!creds) {
|
|
115
|
-
toast.error("
|
|
116
|
+
toast.error(t("integration.credentials.revenexxSource.error.accountProvisioning"));
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
118
119
|
emit("apply", {
|
|
@@ -123,7 +124,7 @@ async function applySftp() {
|
|
|
123
124
|
});
|
|
124
125
|
applied.value = `${acc.login_username}@${sftpHost}:${sftpPort}`;
|
|
125
126
|
} catch (e) {
|
|
126
|
-
toast.error(e?.message ?? "
|
|
127
|
+
toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.readSftpPassword"));
|
|
127
128
|
} finally {
|
|
128
129
|
applying.value = false;
|
|
129
130
|
}
|
|
@@ -134,7 +135,7 @@ async function applySmtp() {
|
|
|
134
135
|
try {
|
|
135
136
|
const cred = await mail.getCredential(selectedId.value);
|
|
136
137
|
if (!cred) {
|
|
137
|
-
toast.error("
|
|
138
|
+
toast.error(t("integration.credentials.revenexxSource.error.mailCredentialGone"));
|
|
138
139
|
return;
|
|
139
140
|
}
|
|
140
141
|
emit("apply", {
|
|
@@ -146,7 +147,7 @@ async function applySmtp() {
|
|
|
146
147
|
});
|
|
147
148
|
applied.value = `${cred.smtp_username}@${mailSmtpHost}:${mailSmtpPort}`;
|
|
148
149
|
} catch (e) {
|
|
149
|
-
toast.error(e?.message ?? "
|
|
150
|
+
toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.readMailCredential"));
|
|
150
151
|
} finally {
|
|
151
152
|
applying.value = false;
|
|
152
153
|
}
|
|
@@ -178,30 +179,30 @@ function openCreate() {
|
|
|
178
179
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
179
180
|
async function createSftp() {
|
|
180
181
|
if (!sftp) return;
|
|
181
|
-
const
|
|
182
|
-
if (!
|
|
183
|
-
toast.error("
|
|
182
|
+
const tenantSlug = tenant.value;
|
|
183
|
+
if (!tenantSlug) {
|
|
184
|
+
toast.error(t("integration.credentials.revenexxSource.error.noTenant"));
|
|
184
185
|
return;
|
|
185
186
|
}
|
|
186
187
|
creating.value = true;
|
|
187
188
|
try {
|
|
188
|
-
let acc = await sftp.create(
|
|
189
|
+
let acc = await sftp.create(tenantSlug, createName.value.trim() || void 0);
|
|
189
190
|
for (let i = 0; i < 24 && !(acc.is_active || acc.status === "active"); i++) {
|
|
190
|
-
provisioningNote.value = "
|
|
191
|
+
provisioningNote.value = t("integration.credentials.revenexxSource.status.provisioning");
|
|
191
192
|
await sleep(2500);
|
|
192
|
-
acc = await sftp.refresh(
|
|
193
|
-
if (acc.status === "failed") throw new Error(acc.error_message || "
|
|
193
|
+
acc = await sftp.refresh(tenantSlug, acc.id);
|
|
194
|
+
if (acc.status === "failed") throw new Error(acc.error_message || t("integration.credentials.revenexxSource.error.provisioningFailed"));
|
|
194
195
|
}
|
|
195
196
|
if (!(acc.is_active || acc.status === "active")) {
|
|
196
|
-
throw new Error("
|
|
197
|
+
throw new Error(t("integration.credentials.revenexxSource.error.stillProvisioning"));
|
|
197
198
|
}
|
|
198
|
-
await sftp.load(
|
|
199
|
+
await sftp.load(tenantSlug);
|
|
199
200
|
selectedId.value = String(acc.id);
|
|
200
201
|
await applySftp();
|
|
201
202
|
createOpen.value = false;
|
|
202
|
-
toast.success("
|
|
203
|
+
toast.success(t("integration.credentials.revenexxSource.toast.sftpCreated"));
|
|
203
204
|
} catch (e) {
|
|
204
|
-
toast.error(e?.message ?? "
|
|
205
|
+
toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createSftp"));
|
|
205
206
|
} finally {
|
|
206
207
|
creating.value = false;
|
|
207
208
|
provisioningNote.value = "";
|
|
@@ -213,7 +214,7 @@ async function createSmtp() {
|
|
|
213
214
|
try {
|
|
214
215
|
const cred = await mail.createCredential({ name: createName.value.trim() || "SMTP", type: "SMTP" });
|
|
215
216
|
if (!cred) {
|
|
216
|
-
toast.error("
|
|
217
|
+
toast.error(t("integration.credentials.revenexxSource.error.mailCredentialNotReturned"));
|
|
217
218
|
return;
|
|
218
219
|
}
|
|
219
220
|
emit("apply", {
|
|
@@ -227,9 +228,9 @@ async function createSmtp() {
|
|
|
227
228
|
smtpCreds.value = [{ uuid: cred.uuid, name: cred.name }, ...smtpCreds.value];
|
|
228
229
|
selectedId.value = cred.uuid;
|
|
229
230
|
createOpen.value = false;
|
|
230
|
-
toast.success("
|
|
231
|
+
toast.success(t("integration.credentials.revenexxSource.toast.smtpCreated"));
|
|
231
232
|
} catch (e) {
|
|
232
|
-
toast.error(e?.message ?? "
|
|
233
|
+
toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createSmtp"));
|
|
233
234
|
} finally {
|
|
234
235
|
creating.value = false;
|
|
235
236
|
}
|
|
@@ -241,11 +242,11 @@ async function createApiKey() {
|
|
|
241
242
|
const { secret } = await consoleKeys.create(createName.value.trim() || "Integrations key", ["*"]);
|
|
242
243
|
emit("apply", { apiKey: secret, tenant: tenant.value ?? "", market: marketSlug() });
|
|
243
244
|
apiKeyReady.value = true;
|
|
244
|
-
applied.value =
|
|
245
|
+
applied.value = marketSlug() ? t("integration.credentials.revenexxSource.status.keyForMarket", { tenant: tenant.value, market: marketSlug() }) : t("integration.credentials.revenexxSource.status.keyForGlobal", { tenant: tenant.value });
|
|
245
246
|
createOpen.value = false;
|
|
246
|
-
toast.success("
|
|
247
|
+
toast.success(t("integration.credentials.revenexxSource.toast.apiKeyCreated"));
|
|
247
248
|
} catch (e) {
|
|
248
|
-
toast.error(e?.message ?? "
|
|
249
|
+
toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createApiKey"));
|
|
249
250
|
} finally {
|
|
250
251
|
creating.value = false;
|
|
251
252
|
}
|
|
@@ -269,11 +270,10 @@ function submitCreate() {
|
|
|
269
270
|
belongs somewhere else. The manual fields stay on screen either way. -->
|
|
270
271
|
<p class="text-xs text-muted-foreground">
|
|
271
272
|
<template v-if="available">
|
|
272
|
-
|
|
273
|
+
{{ t("integration.credentials.revenexxSource.hint.available") }}
|
|
273
274
|
</template>
|
|
274
275
|
<template v-else>
|
|
275
|
-
|
|
276
|
-
Enter the access data by hand instead.
|
|
276
|
+
{{ t("integration.credentials.revenexxSource.hint.unavailable") }}
|
|
277
277
|
</template>
|
|
278
278
|
</p>
|
|
279
279
|
</div>
|
|
@@ -291,14 +291,14 @@ function submitCreate() {
|
|
|
291
291
|
<div v-if="active" class="mt-3 space-y-3">
|
|
292
292
|
<!-- Tenant (preselected) -->
|
|
293
293
|
<div class="space-y-1.5">
|
|
294
|
-
<Label :for="`${uid}-tenant`">
|
|
294
|
+
<Label :for="`${uid}-tenant`">{{ t("integration.credentials.revenexxSource.field.tenant.label") }}</Label>
|
|
295
295
|
<Select :model-value="tenant ?? ''" @update:model-value="(v) => tenant = v">
|
|
296
296
|
<SelectTrigger :id="`${uid}-tenant`" class="bg-white">
|
|
297
|
-
<SelectValue placeholder="
|
|
297
|
+
<SelectValue :placeholder="t('integration.credentials.revenexxSource.field.tenant.placeholder')" />
|
|
298
298
|
</SelectTrigger>
|
|
299
299
|
<SelectContent>
|
|
300
|
-
<SelectItem v-for="
|
|
301
|
-
{{
|
|
300
|
+
<SelectItem v-for="tn in tenants" :key="tn.slug" :value="tn.slug">
|
|
301
|
+
{{ tn.name }} ({{ tn.slug }})
|
|
302
302
|
</SelectItem>
|
|
303
303
|
</SelectContent>
|
|
304
304
|
</Select>
|
|
@@ -306,10 +306,10 @@ function submitCreate() {
|
|
|
306
306
|
|
|
307
307
|
<!-- Market scope — only the Revenexx API credential is market-scoped -->
|
|
308
308
|
<div v-if="kind === 'api'" class="space-y-1.5">
|
|
309
|
-
<Label :for="`${uid}-market`">
|
|
309
|
+
<Label :for="`${uid}-market`">{{ t("integration.credentials.revenexxSource.field.market.label") }}</Label>
|
|
310
310
|
<Select :model-value="marketCode" @update:model-value="(v) => marketCode = v">
|
|
311
311
|
<SelectTrigger :id="`${uid}-market`" class="bg-white">
|
|
312
|
-
<SelectValue placeholder="
|
|
312
|
+
<SelectValue :placeholder="t('integration.credentials.revenexxSource.field.market.placeholder')" />
|
|
313
313
|
</SelectTrigger>
|
|
314
314
|
<SelectContent>
|
|
315
315
|
<SelectItem v-for="m in marketOptions" :key="m.code" :value="m.code">
|
|
@@ -321,11 +321,11 @@ function submitCreate() {
|
|
|
321
321
|
|
|
322
322
|
<!-- Resource select (SFTP / Mail) -->
|
|
323
323
|
<div v-if="kind === 'sftp' || kind === 'smtp'" class="space-y-1.5">
|
|
324
|
-
<Label :for="`${uid}-resource`">{{ kind === "sftp" ? "
|
|
324
|
+
<Label :for="`${uid}-resource`">{{ kind === "sftp" ? t("integration.credentials.revenexxSource.field.resource.sftpLabel") : t("integration.credentials.revenexxSource.field.resource.mailLabel") }}</Label>
|
|
325
325
|
<div class="flex items-center gap-2">
|
|
326
326
|
<Select :model-value="selectedId" class="flex-1" @update:model-value="(v) => onSelect(v)">
|
|
327
327
|
<SelectTrigger :id="`${uid}-resource`" class="bg-white">
|
|
328
|
-
<SelectValue :placeholder="loadingList ? '
|
|
328
|
+
<SelectValue :placeholder="loadingList ? t('integration.common.state.loading') : t('integration.credentials.revenexxSource.field.resource.placeholder')" />
|
|
329
329
|
</SelectTrigger>
|
|
330
330
|
<SelectContent>
|
|
331
331
|
<template v-if="kind === 'sftp'">
|
|
@@ -338,7 +338,7 @@ function submitCreate() {
|
|
|
338
338
|
</Select>
|
|
339
339
|
<Button variant="outline" size="sm" class="shrink-0 bg-white" :disabled="!tenant" @click="openCreate">
|
|
340
340
|
<Plus :size="14" class="mr-1" />
|
|
341
|
-
|
|
341
|
+
{{ t("integration.credentials.revenexxSource.action.new") }}
|
|
342
342
|
</Button>
|
|
343
343
|
</div>
|
|
344
344
|
</div>
|
|
@@ -347,7 +347,7 @@ function submitCreate() {
|
|
|
347
347
|
<div v-else-if="kind === 'api'">
|
|
348
348
|
<Button variant="outline" size="sm" class="bg-white" :disabled="!tenant" @click="openCreate">
|
|
349
349
|
<Plus :size="14" class="mr-1" />
|
|
350
|
-
|
|
350
|
+
{{ t("integration.credentials.revenexxSource.action.createApiKey") }}
|
|
351
351
|
</Button>
|
|
352
352
|
</div>
|
|
353
353
|
|
|
@@ -355,7 +355,7 @@ function submitCreate() {
|
|
|
355
355
|
<div v-if="applied || applying" class="flex items-center gap-2 rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
|
|
356
356
|
<Loader2 v-if="applying" :size="14" class="animate-spin" />
|
|
357
357
|
<Check v-else :size="14" />
|
|
358
|
-
<span class="truncate">{{ applying ? "
|
|
358
|
+
<span class="truncate">{{ applying ? t("integration.credentials.revenexxSource.status.copying") : t("integration.credentials.revenexxSource.status.using", { value: applied }) }}</span>
|
|
359
359
|
</div>
|
|
360
360
|
</div>
|
|
361
361
|
|
|
@@ -363,36 +363,36 @@ function submitCreate() {
|
|
|
363
363
|
<Dialog v-model:open="createOpen">
|
|
364
364
|
<DialogContent class="sm:max-w-md">
|
|
365
365
|
<DialogHeader>
|
|
366
|
-
<DialogTitle>{{ kind ? NEW_LABEL[kind] : "
|
|
366
|
+
<DialogTitle>{{ kind ? NEW_LABEL[kind] : t("integration.credentials.revenexxSource.new.fallback") }}</DialogTitle>
|
|
367
367
|
<DialogDescription class="text-sm">
|
|
368
368
|
<template v-if="kind === 'api'">
|
|
369
|
-
|
|
369
|
+
{{ t("integration.credentials.revenexxSource.createDialog.description.api", { tenant }) }}
|
|
370
370
|
</template>
|
|
371
371
|
<template v-else-if="kind === 'sftp'">
|
|
372
|
-
|
|
372
|
+
{{ t("integration.credentials.revenexxSource.createDialog.description.sftp", { tenant }) }}
|
|
373
373
|
</template>
|
|
374
374
|
<template v-else>
|
|
375
|
-
|
|
375
|
+
{{ t("integration.credentials.revenexxSource.createDialog.description.smtp") }}
|
|
376
376
|
</template>
|
|
377
377
|
</DialogDescription>
|
|
378
378
|
</DialogHeader>
|
|
379
379
|
|
|
380
380
|
<div class="space-y-1.5 py-1">
|
|
381
|
-
<Label for="rvnxx-new-name">{{ kind === "sftp" ? "
|
|
381
|
+
<Label for="rvnxx-new-name">{{ kind === "sftp" ? t("integration.credentials.revenexxSource.createDialog.field.label") : t("integration.credentials.revenexxSource.createDialog.field.name") }}</Label>
|
|
382
382
|
<Input
|
|
383
383
|
id="rvnxx-new-name"
|
|
384
384
|
v-model="createName"
|
|
385
385
|
class="bg-white"
|
|
386
|
-
:placeholder="kind === 'sftp' ? '
|
|
386
|
+
:placeholder="kind === 'sftp' ? t('integration.credentials.revenexxSource.createDialog.field.placeholder.sftp') : t('integration.credentials.revenexxSource.createDialog.field.placeholder.default')"
|
|
387
387
|
/>
|
|
388
388
|
<p v-if="provisioningNote" class="text-xs text-muted-foreground">{{ provisioningNote }}</p>
|
|
389
389
|
</div>
|
|
390
390
|
|
|
391
391
|
<DialogFooter>
|
|
392
|
-
<Button variant="ghost" :disabled="creating" @click="createOpen = false">
|
|
392
|
+
<Button variant="ghost" :disabled="creating" @click="createOpen = false">{{ t("integration.common.action.cancel") }}</Button>
|
|
393
393
|
<Button :disabled="creating" @click="submitCreate">
|
|
394
394
|
<Loader2 v-if="creating" :size="14" class="mr-1 animate-spin" />
|
|
395
|
-
|
|
395
|
+
{{ t("integration.credentials.revenexxSource.action.create") }}
|
|
396
396
|
</Button>
|
|
397
397
|
</DialogFooter>
|
|
398
398
|
</DialogContent>
|
|
@@ -7,20 +7,21 @@ const props = defineProps({
|
|
|
7
7
|
types: { type: Array, required: true }
|
|
8
8
|
});
|
|
9
9
|
const emit = defineEmits(["pick"]);
|
|
10
|
+
const t = useIntegrationsT();
|
|
10
11
|
const query = ref("");
|
|
11
12
|
function authKindLabel(k) {
|
|
12
13
|
switch (k) {
|
|
13
14
|
case "api-key":
|
|
14
|
-
return "
|
|
15
|
+
return t("integration.credentials.shared.authKind.apiKey");
|
|
15
16
|
case "oauth2-authcode":
|
|
16
17
|
case "oauth2-client-credentials":
|
|
17
|
-
return "
|
|
18
|
+
return t("integration.credentials.shared.authKind.oauth2");
|
|
18
19
|
case "basic-auth":
|
|
19
|
-
return "
|
|
20
|
+
return t("integration.credentials.shared.authKind.basicAuth");
|
|
20
21
|
case "static":
|
|
21
|
-
return "
|
|
22
|
+
return t("integration.credentials.shared.authKind.static");
|
|
22
23
|
case "secret":
|
|
23
|
-
return "
|
|
24
|
+
return t("integration.credentials.shared.authKind.secret");
|
|
24
25
|
default:
|
|
25
26
|
return k ?? "";
|
|
26
27
|
}
|
|
@@ -28,7 +29,7 @@ function authKindLabel(k) {
|
|
|
28
29
|
const filtered = computed(() => {
|
|
29
30
|
const q = query.value.trim().toLowerCase();
|
|
30
31
|
if (!q) return props.types;
|
|
31
|
-
return props.types.filter((
|
|
32
|
+
return props.types.filter((t2) => resolveLocalized(t2.name).toLowerCase().includes(q));
|
|
32
33
|
});
|
|
33
34
|
</script>
|
|
34
35
|
|
|
@@ -38,16 +39,16 @@ const filtered = computed(() => {
|
|
|
38
39
|
<Search :size="15" class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
39
40
|
<Input
|
|
40
41
|
v-model="query"
|
|
41
|
-
aria-label="
|
|
42
|
+
:aria-label="t('integration.credentials.typePicker.search.aria')"
|
|
42
43
|
class="bg-white pl-9"
|
|
43
|
-
placeholder="
|
|
44
|
+
:placeholder="t('integration.credentials.typePicker.search.placeholder')"
|
|
44
45
|
autofocus
|
|
45
46
|
/>
|
|
46
47
|
</div>
|
|
47
48
|
|
|
48
49
|
<div class="mt-2 max-h-80 overflow-y-auto -mx-1 px-1">
|
|
49
50
|
<p v-if="!filtered.length" class="px-2 py-6 text-center text-sm text-muted-foreground">
|
|
50
|
-
|
|
51
|
+
{{ t("integration.credentials.typePicker.empty", { query }) }}
|
|
51
52
|
</p>
|
|
52
53
|
<button
|
|
53
54
|
v-for="t in filtered"
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { onMounted, ref, watch } from "vue";
|
|
3
|
-
import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as
|
|
3
|
+
import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
4
4
|
import { Badge, Button } from "@revenexx/studio";
|
|
5
5
|
import { toast } from "vue-sonner";
|
|
6
6
|
const props = defineProps({
|
|
7
7
|
workflowId: { type: Number, required: true }
|
|
8
8
|
});
|
|
9
|
+
const t = useIntegrationsT();
|
|
9
10
|
const { list, replay, discard } = useIntegrationsDeadLetters();
|
|
10
11
|
const PER_PAGE = 25;
|
|
11
12
|
const items = ref([]);
|
|
@@ -25,7 +26,7 @@ async function load(p = page.value) {
|
|
|
25
26
|
lastPage.value = res.lastPage;
|
|
26
27
|
page.value = res.page;
|
|
27
28
|
} catch (e) {
|
|
28
|
-
error.value = e?.message ?? "
|
|
29
|
+
error.value = e?.message ?? t("integration.deadLetters.error.load");
|
|
29
30
|
} finally {
|
|
30
31
|
loading.value = false;
|
|
31
32
|
}
|
|
@@ -41,10 +42,10 @@ async function onReplay(dl) {
|
|
|
41
42
|
try {
|
|
42
43
|
const res = await replay(dl.id);
|
|
43
44
|
const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
|
|
44
|
-
toast.success(newId ?
|
|
45
|
+
toast.success(newId ? t("integration.deadLetters.toast.replay.started", { id: newId }) : t("integration.deadLetters.toast.replay.done"));
|
|
45
46
|
await load();
|
|
46
47
|
} catch (e) {
|
|
47
|
-
toast.error(e?.message ?? "
|
|
48
|
+
toast.error(e?.message ?? t("integration.deadLetters.toast.replay.error"));
|
|
48
49
|
} finally {
|
|
49
50
|
busyId.value = null;
|
|
50
51
|
}
|
|
@@ -53,10 +54,10 @@ async function onDiscard(dl) {
|
|
|
53
54
|
busyId.value = dl.id;
|
|
54
55
|
try {
|
|
55
56
|
await discard(dl.id);
|
|
56
|
-
toast.success("
|
|
57
|
+
toast.success(t("integration.deadLetters.toast.discard.done"));
|
|
57
58
|
await load();
|
|
58
59
|
} catch (e) {
|
|
59
|
-
toast.error(e?.message ?? "
|
|
60
|
+
toast.error(e?.message ?? t("integration.deadLetters.toast.discard.error"));
|
|
60
61
|
} finally {
|
|
61
62
|
busyId.value = null;
|
|
62
63
|
}
|
|
@@ -80,14 +81,14 @@ function fmt(iso) {
|
|
|
80
81
|
<div class="space-y-3">
|
|
81
82
|
<div class="flex items-center justify-between">
|
|
82
83
|
<div>
|
|
83
|
-
<h3 class="text-sm font-semibold">
|
|
84
|
+
<h3 class="text-sm font-semibold">{{ t("integration.deadLetters.panel.title") }}</h3>
|
|
84
85
|
<p class="mt-0.5 text-sm text-muted-foreground">
|
|
85
|
-
|
|
86
|
+
{{ t("integration.deadLetters.panel.description") }}
|
|
86
87
|
</p>
|
|
87
88
|
</div>
|
|
88
89
|
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="loading" @click="() => load()">
|
|
89
90
|
<Loader2 v-if="loading" :size="12" class="animate-spin" />
|
|
90
|
-
|
|
91
|
+
{{ t("integration.common.action.refresh") }}
|
|
91
92
|
</Button>
|
|
92
93
|
</div>
|
|
93
94
|
|
|
@@ -107,7 +108,7 @@ function fmt(iso) {
|
|
|
107
108
|
</div>
|
|
108
109
|
</div>
|
|
109
110
|
<div v-else-if="!items.length" class="rounded-lg border border-black/[0.06] bg-white p-6 text-center text-sm text-muted-foreground">
|
|
110
|
-
|
|
111
|
+
{{ t("integration.deadLetters.state.empty") }}
|
|
111
112
|
</div>
|
|
112
113
|
|
|
113
114
|
<div v-else class="rounded-lg border border-black/[0.06] bg-white divide-y divide-black/[0.04]">
|
|
@@ -118,7 +119,7 @@ function fmt(iso) {
|
|
|
118
119
|
>
|
|
119
120
|
<Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0 mt-0.5', statusClass(dl.status)]">{{ dl.status }}</Badge>
|
|
120
121
|
<div class="min-w-0 flex-1">
|
|
121
|
-
<div class="text-sm break-words">{{ dl.reason || "
|
|
122
|
+
<div class="text-sm break-words">{{ dl.reason || t("integration.deadLetters.reason.fallback") }}</div>
|
|
122
123
|
<div class="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground font-mono">
|
|
123
124
|
<span>#{{ dl.id }}</span>
|
|
124
125
|
<span v-if="dl.source">{{ dl.source }}</span>
|
|
@@ -130,21 +131,21 @@ function fmt(iso) {
|
|
|
130
131
|
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="busyId === dl.id" @click="onReplay(dl)">
|
|
131
132
|
<Loader2 v-if="busyId === dl.id" :size="12" class="animate-spin" />
|
|
132
133
|
<ReplayIcon v-else :size="12" />
|
|
133
|
-
|
|
134
|
+
{{ t("integration.deadLetters.action.replay") }}
|
|
134
135
|
</Button>
|
|
135
136
|
<Button variant="ghost" size="sm" class="h-7 text-sm text-rose-600 hover:bg-rose-500/10" :disabled="busyId === dl.id" @click="onDiscard(dl)">
|
|
136
|
-
<
|
|
137
|
-
|
|
137
|
+
<TrashBin :size="12" />
|
|
138
|
+
{{ t("integration.deadLetters.action.discard") }}
|
|
138
139
|
</Button>
|
|
139
140
|
</div>
|
|
140
141
|
</div>
|
|
141
142
|
</div>
|
|
142
143
|
|
|
143
144
|
<div v-if="lastPage > 1" class="flex items-center justify-between text-sm text-muted-foreground">
|
|
144
|
-
<span class="tabular-nums">
|
|
145
|
+
<span class="tabular-nums">{{ t("integration.deadLetters.pagination.summary", { page, lastPage, total }) }}</span>
|
|
145
146
|
<div class="flex gap-2">
|
|
146
|
-
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page <= 1 || loading" @click="goPage(page - 1)"
|
|
147
|
-
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page >= lastPage || loading" @click="goPage(page + 1)">
|
|
147
|
+
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page <= 1 || loading" @click="goPage(page - 1)">{{ t("integration.deadLetters.pagination.newer") }}</Button>
|
|
148
|
+
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page >= lastPage || loading" @click="goPage(page + 1)">{{ t("integration.deadLetters.pagination.older") }}</Button>
|
|
148
149
|
</div>
|
|
149
150
|
</div>
|
|
150
151
|
</div>
|
|
@@ -6,6 +6,7 @@ const props = defineProps({
|
|
|
6
6
|
inputId: { type: String, required: false }
|
|
7
7
|
});
|
|
8
8
|
const emit = defineEmits(["update:modelValue"]);
|
|
9
|
+
const t = useIntegrationsT();
|
|
9
10
|
const catalog = typeof useDefinedEvents === "function" ? useDefinedEvents() : { emits: ref([]), listens: ref([]), loading: ref(false), load: async () => {
|
|
10
11
|
} };
|
|
11
12
|
onMounted(() => {
|
|
@@ -15,8 +16,8 @@ const focused = ref(false);
|
|
|
15
16
|
const options = computed(() => {
|
|
16
17
|
const out = /* @__PURE__ */ new Map();
|
|
17
18
|
for (const e of catalog.emits.value) out.set(e.topic, { topic: e.topic, source: e.app });
|
|
18
|
-
for (const
|
|
19
|
-
if (!out.has(
|
|
19
|
+
for (const t2 of catalog.listens.value) {
|
|
20
|
+
if (!out.has(t2)) out.set(t2, { topic: t2, source: null });
|
|
20
21
|
}
|
|
21
22
|
return [...out.values()].sort((a, b) => a.topic.localeCompare(b.topic));
|
|
22
23
|
});
|
|
@@ -48,7 +49,7 @@ function pick(topic) {
|
|
|
48
49
|
class="absolute left-0 right-0 top-full z-30 mt-1 max-h-56 overflow-y-auto rounded-md border border-black/[0.08] bg-white py-1 shadow-md"
|
|
49
50
|
>
|
|
50
51
|
<div v-if="catalog.loading.value && !filtered.length" class="px-2.5 py-1.5 text-sm text-muted-foreground">
|
|
51
|
-
|
|
52
|
+
{{ t("integration.canvas.eventCombobox.loading") }}
|
|
52
53
|
</div>
|
|
53
54
|
<button
|
|
54
55
|
v-for="o in filtered"
|
|
@@ -17,6 +17,7 @@ const props = defineProps({
|
|
|
17
17
|
label: { type: String, required: false }
|
|
18
18
|
});
|
|
19
19
|
const emit = defineEmits(["update:modelValue"]);
|
|
20
|
+
const t = useIntegrationsT();
|
|
20
21
|
const text = computed(() => String(props.modelValue ?? ""));
|
|
21
22
|
const OPEN = "${{ ";
|
|
22
23
|
const CLOSE = " }}";
|
|
@@ -34,12 +35,12 @@ function pillLabel(expr) {
|
|
|
34
35
|
}
|
|
35
36
|
if (path[0] === "trigger" && path[1] === "payload") {
|
|
36
37
|
const rest = path.slice(2).join(".");
|
|
37
|
-
return rest ? "
|
|
38
|
+
return rest ? t("integration.nodeConfig.expression.pill.triggerPath", { rest }) : t("integration.nodeConfig.expression.pill.triggerPayload");
|
|
38
39
|
}
|
|
39
|
-
if (path[0] === "vars") return "
|
|
40
|
+
if (path[0] === "vars") return t("integration.nodeConfig.expression.pill.variable", { path: path.slice(1).join(".") });
|
|
40
41
|
if (path[0] === "in") {
|
|
41
42
|
const rest = path.slice(1).join(".");
|
|
42
|
-
return rest ? "
|
|
43
|
+
return rest ? t("integration.nodeConfig.expression.pill.inputPath", { rest }) : t("integration.nodeConfig.expression.pill.inputPayload");
|
|
43
44
|
}
|
|
44
45
|
return expr;
|
|
45
46
|
}
|
|
@@ -104,9 +105,9 @@ onBeforeUnmount(() => {
|
|
|
104
105
|
if (insertHandler?.value === insertToken) insertHandler.value = null;
|
|
105
106
|
editor.destroy();
|
|
106
107
|
});
|
|
107
|
-
watch(text, (
|
|
108
|
-
if (docToText(editor.getJSON()) !==
|
|
109
|
-
editor.commands.setContent(textToDoc(
|
|
108
|
+
watch(text, (t2) => {
|
|
109
|
+
if (docToText(editor.getJSON()) !== t2) {
|
|
110
|
+
editor.commands.setContent(textToDoc(t2));
|
|
110
111
|
}
|
|
111
112
|
});
|
|
112
113
|
function insertToken(expr) {
|
|
@@ -126,7 +127,7 @@ const sections = computed(() => {
|
|
|
126
127
|
label: key,
|
|
127
128
|
expr: tok("vars." + key)
|
|
128
129
|
}));
|
|
129
|
-
if (varItems.length) out.push({ heading: "
|
|
130
|
+
if (varItems.length) out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.variables"), items: varItems });
|
|
130
131
|
const upstream = props.upstreamOutputs ?? [];
|
|
131
132
|
const nodeItems = upstream.flatMap((u) => {
|
|
132
133
|
const base = "nodes." + u.nodeId + ".outputs." + u.output;
|
|
@@ -139,13 +140,13 @@ const sections = computed(() => {
|
|
|
139
140
|
}
|
|
140
141
|
return rows;
|
|
141
142
|
});
|
|
142
|
-
if (nodeItems.length) out.push({ heading: "
|
|
143
|
+
if (nodeItems.length) out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.nodeOutputs"), items: nodeItems });
|
|
143
144
|
const primary = upstream[0];
|
|
144
|
-
const inItems = [{ label: "
|
|
145
|
+
const inItems = [{ label: t("integration.nodeConfig.expression.suggestion.inputPayload"), expr: tok("in") }];
|
|
145
146
|
for (const f of primary?.fields ?? []) {
|
|
146
147
|
inItems.push({ label: "input." + f, expr: tok("in." + f) });
|
|
147
148
|
}
|
|
148
|
-
out.push({ heading: "
|
|
149
|
+
out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.input"), items: inItems });
|
|
149
150
|
return out;
|
|
150
151
|
});
|
|
151
152
|
const hasSuggestions = computed(() => sections.value.some((s) => s.items.length > 0));
|
|
@@ -201,7 +202,7 @@ function insert(expr) {
|
|
|
201
202
|
</div>
|
|
202
203
|
<button
|
|
203
204
|
type="button"
|
|
204
|
-
title="
|
|
205
|
+
:title="t('integration.nodeConfig.expression.insertTitle')"
|
|
205
206
|
class="flex h-8 shrink-0 items-center rounded-md border border-black/[0.08] bg-white px-1.5 font-mono text-sm text-muted-foreground transition hover:text-foreground hover:bg-muted/30"
|
|
206
207
|
:class="open ? 'ring-2 ring-emerald-500/40' : ''"
|
|
207
208
|
@mousedown.prevent="toggle"
|
|
@@ -213,28 +214,33 @@ function insert(expr) {
|
|
|
213
214
|
<p
|
|
214
215
|
v-if="preview"
|
|
215
216
|
class="mt-1 flex min-w-0 items-baseline gap-1 text-[11px]"
|
|
216
|
-
:title="preview.misses ? '
|
|
217
|
+
:title="preview.misses ? t('integration.nodeConfig.expression.preview.missTitle') : preview.resolved"
|
|
217
218
|
>
|
|
218
219
|
<span class="shrink-0 text-muted-foreground">→</span>
|
|
219
220
|
<span class="min-w-0 truncate font-mono" :class="preview.misses ? 'text-amber-600' : 'text-emerald-700'">{{ preview.resolved }}</span>
|
|
220
221
|
</p>
|
|
221
222
|
|
|
223
|
+
<!-- Named, because the same expressions are offered twice on this surface —
|
|
224
|
+
here and in the panel beside the form — and a popup nothing can address
|
|
225
|
+
is a popup nothing can test either. -->
|
|
222
226
|
<div
|
|
223
227
|
v-if="open && hasSuggestions"
|
|
228
|
+
role="group"
|
|
229
|
+
:aria-label="t('integration.nodeConfig.expression.picker.aria')"
|
|
224
230
|
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"
|
|
225
231
|
>
|
|
226
232
|
<div class="shrink-0 border-b border-black/[0.06] p-1">
|
|
227
233
|
<input
|
|
228
234
|
v-model="query"
|
|
229
|
-
aria-label="
|
|
230
|
-
placeholder="
|
|
235
|
+
:aria-label="t('integration.nodeConfig.expression.picker.filterAria')"
|
|
236
|
+
:placeholder="t('integration.nodeConfig.expression.picker.filterPlaceholder')"
|
|
231
237
|
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
238
|
@mousedown.stop
|
|
233
239
|
>
|
|
234
240
|
</div>
|
|
235
241
|
<div class="min-h-0 flex-1 overflow-y-auto py-1">
|
|
236
242
|
<p v-if="!filteredSections.length" class="px-2 py-1.5 text-muted-foreground">
|
|
237
|
-
|
|
243
|
+
{{ t("integration.nodeConfig.expression.picker.empty", { query }) }}
|
|
238
244
|
</p>
|
|
239
245
|
<template v-for="section in filteredSections" :key="section.heading">
|
|
240
246
|
<div class="px-2 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|