@revenexx/studio-integrations 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +227 -0
- package/README.md +31 -0
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +10 -2
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +29 -28
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +38 -37
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
- package/dist/runtime/components/Integrations/ExpressionInput.vue +16 -15
- package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
- package/dist/runtime/components/Integrations/Layout.vue +28 -3
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +15 -14
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +9 -8
- package/dist/runtime/components/Integrations/NodeConfigField.vue +43 -42
- package/dist/runtime/components/Integrations/NodeInspector.vue +225 -216
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +1 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +3 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +1 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +30 -29
- package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
- package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
- package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
- package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +37 -23
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/TemplateCard.vue +16 -6
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +10 -9
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +36 -35
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +82 -79
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +24 -23
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +19 -20
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +52 -39
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +20 -19
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +12 -9
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +27 -26
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +4 -2
- package/dist/runtime/composables/useIntegrationsCredentials.js +6 -4
- package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
- package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
- package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +3 -1
- package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
- package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
- package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
- package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
- package/dist/runtime/composables/useIntegrationsT.js +9 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
- package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +4 -2
- package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -3
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +14 -0
- package/dist/runtime/data/integrationTemplates.d.ts +9 -0
- package/dist/runtime/data/integrationTemplates.js +7 -1
- package/dist/runtime/data/payloadForm.js +2 -1
- package/dist/runtime/data/workflowIconCatalog.js +2 -1
- package/dist/runtime/i18n/messages/de.json +1207 -0
- package/dist/runtime/i18n/messages/en.json +1207 -0
- package/dist/runtime/i18n/translate.d.ts +83 -0
- package/dist/runtime/i18n/translate.js +41 -0
- package/dist/runtime/pages/integrations/audit.vue +52 -34
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
- package/dist/runtime/pages/integrations/credentials.vue +70 -52
- package/dist/runtime/pages/integrations/index.vue +50 -56
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +49 -28
- package/dist/runtime/pages/integrations/secrets.vue +34 -34
- package/dist/runtime/pages/integrations/templates/[slug].vue +77 -68
- package/dist/runtime/pages/integrations/templates/index.vue +35 -27
- package/dist/runtime/pages/integrations/workflows/[id].vue +164 -99
- package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
- package/dist/runtime/plugins/locale.d.ts +10 -0
- package/dist/runtime/plugins/locale.js +6 -0
- package/dist/runtime/plugins/realtime.client.d.ts +32 -0
- package/dist/runtime/plugins/realtime.client.js +4 -0
- package/dist/runtime/utils/credentialState.d.ts +24 -1
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/localize.d.ts +18 -5
- package/dist/runtime/utils/localize.js +32 -9
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/schedule.d.ts +0 -25
- package/dist/runtime/utils/schedule.js +37 -25
- package/dist/runtime/utils/statusStyle.d.ts +26 -2
- package/dist/runtime/utils/statusStyle.js +9 -1
- package/dist/runtime/utils/validationFindings.d.ts +2 -2
- package/dist/runtime/utils/validationFindings.js +4 -3
- package/dist/types.d.mts +2 -6
- package/package.json +3 -2
|
@@ -24,6 +24,7 @@ import NodeMark from "../../../components/Integrations/NodeMark.vue";
|
|
|
24
24
|
import { templateCategoryLabel, templatePackageLabel } from "../../../data/integrationTemplates";
|
|
25
25
|
import { describeCron } from "../../../utils/schedule";
|
|
26
26
|
const SELECT_LATER = "__later__";
|
|
27
|
+
const t = useIntegrationsT();
|
|
27
28
|
const route = useRoute();
|
|
28
29
|
const router = useRouter();
|
|
29
30
|
const { get, load, loaded, fetchDetail, requirements, instantiate } = useIntegrationsTemplates();
|
|
@@ -32,7 +33,7 @@ const { keys: secretKeys, load: loadSecrets } = useIntegrationsSecrets();
|
|
|
32
33
|
const { ofType, load: loadCredentials } = useIntegrationsCredentials();
|
|
33
34
|
const slug = computed(() => String(route.params.slug));
|
|
34
35
|
const tpl = computed(() => get(slug.value));
|
|
35
|
-
useHead({ title: () =>
|
|
36
|
+
useHead({ title: () => t("integration.templates.detail.head.title", { name: tpl.value?.name ?? t("integration.templates.detail.head.fallbackName") }) });
|
|
36
37
|
const reqs = ref(null);
|
|
37
38
|
onMounted(async () => {
|
|
38
39
|
await load();
|
|
@@ -50,7 +51,7 @@ const steps = computed(() => {
|
|
|
50
51
|
if (!blob?.nodes.length) return [];
|
|
51
52
|
const nodeById = new Map(blob.nodes.map((n) => [n.id, n]));
|
|
52
53
|
const happyEdges = (blob.edges ?? []).filter((e) => e.from.output !== "error");
|
|
53
|
-
const triggerHandles = new Set((blob.triggers ?? []).map((
|
|
54
|
+
const triggerHandles = new Set((blob.triggers ?? []).map((t2) => t2.id));
|
|
54
55
|
const incoming = /* @__PURE__ */ new Map();
|
|
55
56
|
for (const e of happyEdges) {
|
|
56
57
|
if (!triggerHandles.has(e.from.nodeId)) incoming.set(e.to.nodeId, (incoming.get(e.to.nodeId) ?? 0) + 1);
|
|
@@ -90,28 +91,28 @@ function firstSentence(text) {
|
|
|
90
91
|
return idx === -1 ? text : text.slice(0, idx + 1);
|
|
91
92
|
}
|
|
92
93
|
const TRIGGER_ICON = { schedule: Clock, webhook: Webhook, manual: Play, event: Zap };
|
|
93
|
-
const triggerChips = computed(() => (tpl.value?.blob.triggers ?? []).map((
|
|
94
|
-
let label =
|
|
95
|
-
if (
|
|
96
|
-
const when = describeCron(String(
|
|
97
|
-
label =
|
|
94
|
+
const triggerChips = computed(() => (tpl.value?.blob.triggers ?? []).map((trig) => {
|
|
95
|
+
let label = trig.type.charAt(0).toUpperCase() + trig.type.slice(1);
|
|
96
|
+
if (trig.type === "schedule" && trig.config?.cron) {
|
|
97
|
+
const when = describeCron(String(trig.config.cron)) ?? String(trig.config.cron);
|
|
98
|
+
label = `${t("integration.templates.detail.trigger.schedule")} \xB7 ${when}${trig.config.timezone ? ` (${trig.config.timezone})` : ""}`;
|
|
98
99
|
}
|
|
99
|
-
if (
|
|
100
|
-
if (
|
|
101
|
-
return { key:
|
|
100
|
+
if (trig.type === "webhook" && trig.config?.path) label = `${t("integration.templates.trigger.webhook")} \xB7 ${trig.config.path}`;
|
|
101
|
+
if (trig.type === "manual") label = t("integration.templates.trigger.manual");
|
|
102
|
+
return { key: trig.id, icon: TRIGGER_ICON[trig.type] ?? Zap, label };
|
|
102
103
|
}));
|
|
103
104
|
const variables = computed(() => Object.entries(tpl.value?.blob.variables ?? {}).map(([key, v]) => ({
|
|
104
105
|
key,
|
|
105
106
|
default: String(v?.default ?? ""),
|
|
106
107
|
description: resolveLocalized(v?.description ?? "")
|
|
107
108
|
})));
|
|
108
|
-
const variableColumns = [
|
|
109
|
-
{ key: "key", label: "
|
|
110
|
-
{ key: "description", label: "
|
|
109
|
+
const variableColumns = computed(() => [
|
|
110
|
+
{ key: "key", label: t("integration.templates.detail.variables.column.key"), width: "minmax(0,180px)", sortable: true },
|
|
111
|
+
{ key: "description", label: t("integration.templates.detail.variables.column.description") },
|
|
111
112
|
// Stored as `default` in the blob, but it *is* the value the workflow runs
|
|
112
113
|
// with — see `specs/workflow-variables.md`.
|
|
113
|
-
{ key: "default", label: "
|
|
114
|
-
];
|
|
114
|
+
{ key: "default", label: t("integration.templates.detail.variables.column.value"), width: "minmax(0,200px)" }
|
|
115
|
+
]);
|
|
115
116
|
const credentialReqs = computed(() => reqs.value?.credentialTypes ?? []);
|
|
116
117
|
const secretReqs = computed(() => reqs.value?.secrets ?? []);
|
|
117
118
|
const secretChecks = computed(() => {
|
|
@@ -119,7 +120,7 @@ const secretChecks = computed(() => {
|
|
|
119
120
|
if (fromApi.length) {
|
|
120
121
|
return fromApi.map((s) => ({
|
|
121
122
|
key: s.key,
|
|
122
|
-
label: s.key ?? "
|
|
123
|
+
label: s.key ?? t("integration.templates.detail.secrets.chosenAtInstall"),
|
|
123
124
|
present: s.exists || (s.key ? secretKeys.value.includes(s.key) : false)
|
|
124
125
|
}));
|
|
125
126
|
}
|
|
@@ -164,12 +165,20 @@ async function confirmInstall() {
|
|
|
164
165
|
});
|
|
165
166
|
if (id) await router.push(`/integrations/workflows/${id}`);
|
|
166
167
|
} catch (e) {
|
|
167
|
-
installError.value = e.message || "
|
|
168
|
+
installError.value = e.message || t("integration.templates.detail.install.error.fallback");
|
|
168
169
|
} finally {
|
|
169
170
|
installing.value = false;
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
|
-
const
|
|
173
|
+
const PUBLISHER_KIND_LABEL_KEY = {
|
|
174
|
+
revenexx: "integration.templates.detail.publisher.kind.revenexx",
|
|
175
|
+
partner: "integration.templates.detail.publisher.kind.partner",
|
|
176
|
+
community: "integration.templates.detail.publisher.kind.community"
|
|
177
|
+
};
|
|
178
|
+
function publisherKindLabel(kind) {
|
|
179
|
+
const key = PUBLISHER_KIND_LABEL_KEY[kind];
|
|
180
|
+
return key ? t(key) : kind;
|
|
181
|
+
}
|
|
173
182
|
function publisherBadgeClass(kind) {
|
|
174
183
|
return kind === "revenexx" ? "bg-violet-500/10 text-violet-700 border-violet-500/20" : kind === "partner" ? "bg-amber-500/10 text-amber-700 border-amber-500/20" : "bg-slate-500/10 text-slate-700 border-slate-500/20";
|
|
175
184
|
}
|
|
@@ -184,14 +193,14 @@ function levelDot(level) {
|
|
|
184
193
|
<PageHeader :title="tpl.name">
|
|
185
194
|
<template #title>
|
|
186
195
|
<span class="inline-flex items-center gap-1.5">
|
|
187
|
-
<NuxtLink to="/integrations/templates" class="font-normal text-muted-foreground/80 hover:text-primary transition-colors">
|
|
196
|
+
<NuxtLink to="/integrations/templates" class="font-normal text-muted-foreground/80 hover:text-primary transition-colors">{{ t("integration.templates.detail.breadcrumb.templates") }}</NuxtLink>
|
|
188
197
|
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
189
198
|
<span class="truncate">{{ tpl.name }}</span>
|
|
190
199
|
</span>
|
|
191
200
|
</template>
|
|
192
201
|
<template #actions>
|
|
193
202
|
<Button class="h-7 text-sm" @click="openInstall">
|
|
194
|
-
|
|
203
|
+
{{ t("integration.templates.detail.action.useTemplate") }}
|
|
195
204
|
</Button>
|
|
196
205
|
</template>
|
|
197
206
|
</PageHeader>
|
|
@@ -236,7 +245,7 @@ function levelDot(level) {
|
|
|
236
245
|
aria-labelledby="tpl-about-heading"
|
|
237
246
|
class="rounded-lg border border-black/[0.06] bg-white p-4"
|
|
238
247
|
>
|
|
239
|
-
<h3 id="tpl-about-heading" class="text-sm font-semibold mb-2">
|
|
248
|
+
<h3 id="tpl-about-heading" class="text-sm font-semibold mb-2">{{ t("integration.templates.detail.about.title") }}</h3>
|
|
240
249
|
<div
|
|
241
250
|
class="text-sm leading-relaxed text-foreground/90 space-y-2 [&_h1]:text-sm [&_h1]:font-semibold [&_h2]:text-sm [&_h2]:font-semibold [&_h3]:text-sm [&_h3]:font-semibold [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5 [&_li]:mt-1 [&_code]:rounded [&_code]:bg-slate-100 [&_code]:px-1 [&_code]:py-px [&_code]:font-mono [&_code]:text-[13px] [&_a]:text-primary [&_a]:underline [&_strong]:font-semibold"
|
|
242
251
|
v-html="descriptionHtml"
|
|
@@ -249,7 +258,7 @@ function levelDot(level) {
|
|
|
249
258
|
aria-labelledby="tpl-steps-heading"
|
|
250
259
|
class="rounded-lg border border-black/[0.06] bg-white p-4"
|
|
251
260
|
>
|
|
252
|
-
<h3 id="tpl-steps-heading" class="text-sm font-semibold mb-3">
|
|
261
|
+
<h3 id="tpl-steps-heading" class="text-sm font-semibold mb-3">{{ t("integration.templates.detail.steps.title") }}</h3>
|
|
253
262
|
<ol v-if="steps.length" class="space-y-2.5">
|
|
254
263
|
<li
|
|
255
264
|
v-for="(step, i) in steps"
|
|
@@ -269,7 +278,7 @@ function levelDot(level) {
|
|
|
269
278
|
</li>
|
|
270
279
|
</ol>
|
|
271
280
|
<p v-else class="text-sm text-muted-foreground italic">
|
|
272
|
-
|
|
281
|
+
{{ t("integration.templates.detail.steps.empty") }}
|
|
273
282
|
</p>
|
|
274
283
|
</div>
|
|
275
284
|
|
|
@@ -284,8 +293,8 @@ function levelDot(level) {
|
|
|
284
293
|
aria-labelledby="tpl-variables-heading"
|
|
285
294
|
class="rounded-lg border border-black/[0.06] bg-white p-4"
|
|
286
295
|
>
|
|
287
|
-
<h3 id="tpl-variables-heading" class="text-sm font-semibold mb-1">
|
|
288
|
-
<p class="text-sm text-muted-foreground mb-3">
|
|
296
|
+
<h3 id="tpl-variables-heading" class="text-sm font-semibold mb-1">{{ t("integration.templates.detail.variables.title") }}</h3>
|
|
297
|
+
<p class="text-sm text-muted-foreground mb-3">{{ t("integration.templates.detail.variables.description") }}</p>
|
|
289
298
|
<DataTable
|
|
290
299
|
:data="variables"
|
|
291
300
|
:columns="variableColumns"
|
|
@@ -315,51 +324,51 @@ function levelDot(level) {
|
|
|
315
324
|
shows no heading of its own, so there is no visible string to
|
|
316
325
|
point `aria-labelledby` at. Copy this pattern only for a panel
|
|
317
326
|
that genuinely has none. -->
|
|
318
|
-
<div role="group" aria-label="
|
|
327
|
+
<div role="group" :aria-label="t('integration.templates.detail.setup.ariaLabel')" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2.5">
|
|
319
328
|
<div v-if="setupReady" class="flex items-center gap-1.5 text-sm text-emerald-700">
|
|
320
329
|
<CheckCircle2 :size="14" />
|
|
321
|
-
|
|
330
|
+
{{ t("integration.templates.detail.setup.ready") }}
|
|
322
331
|
</div>
|
|
323
332
|
<div v-else class="text-sm text-amber-700 space-y-0.5">
|
|
324
333
|
<div v-if="missingCredentials" class="flex items-center gap-1.5">
|
|
325
334
|
<Key :size="12" />
|
|
326
|
-
{{ missingCredentials
|
|
335
|
+
{{ t("integration.templates.detail.setup.missingCredentials", missingCredentials) }}
|
|
327
336
|
</div>
|
|
328
337
|
<div v-if="missingSecrets" class="flex items-center gap-1.5">
|
|
329
338
|
<ShieldAlert :size="12" />
|
|
330
|
-
{{ missingSecrets
|
|
339
|
+
{{ t("integration.templates.detail.setup.missingSecrets", missingSecrets) }}
|
|
331
340
|
</div>
|
|
332
341
|
</div>
|
|
333
342
|
<Button class="w-full h-8 text-sm" @click="openInstall">
|
|
334
|
-
|
|
343
|
+
{{ t("integration.templates.detail.action.useTemplate") }}
|
|
335
344
|
</Button>
|
|
336
345
|
<p class="text-xs text-muted-foreground">
|
|
337
|
-
|
|
346
|
+
{{ t("integration.templates.detail.setup.hint") }}
|
|
338
347
|
</p>
|
|
339
348
|
</div>
|
|
340
349
|
|
|
341
350
|
<div class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
|
|
342
|
-
<div class="text-mono text-muted-foreground">
|
|
351
|
+
<div class="text-mono text-muted-foreground">{{ t("integration.templates.detail.publisher.title") }}</div>
|
|
343
352
|
<div class="flex items-center gap-2">
|
|
344
353
|
<span class="flex size-6 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
345
354
|
<User :size="12" />
|
|
346
355
|
</span>
|
|
347
356
|
<div class="text-sm font-medium truncate">{{ tpl.publisher.name }}</div>
|
|
348
357
|
<Badge variant="outline" :class="['text-sm border', publisherBadgeClass(tpl.publisher.kind)]">
|
|
349
|
-
{{
|
|
358
|
+
{{ publisherKindLabel(tpl.publisher.kind) }}
|
|
350
359
|
</Badge>
|
|
351
360
|
</div>
|
|
352
361
|
<div v-if="packageLabel" class="border-t border-black/[0.06] pt-2 text-sm">
|
|
353
|
-
<div class="text-muted-foreground">
|
|
362
|
+
<div class="text-muted-foreground">{{ t("integration.templates.detail.publisher.shipsWith") }}</div>
|
|
354
363
|
<div class="font-medium">{{ packageLabel }}</div>
|
|
355
364
|
</div>
|
|
356
365
|
<div class="border-t border-black/[0.06] pt-2 grid grid-cols-2 gap-2 text-sm">
|
|
357
366
|
<div>
|
|
358
|
-
<div class="text-muted-foreground">
|
|
367
|
+
<div class="text-muted-foreground">{{ t("integration.templates.detail.rail.stepsLabel") }}</div>
|
|
359
368
|
<div class="font-medium tabular-nums">{{ steps.length || tpl.nodeCount }}</div>
|
|
360
369
|
</div>
|
|
361
370
|
<div>
|
|
362
|
-
<div class="text-muted-foreground">
|
|
371
|
+
<div class="text-muted-foreground">{{ t("integration.templates.detail.rail.levelLabel") }}</div>
|
|
363
372
|
<span class="mt-0.5 inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
|
|
364
373
|
<span class="size-1.5 rounded-full" :class="levelDot(tpl.level)" />
|
|
365
374
|
{{ tpl.level }}
|
|
@@ -373,7 +382,7 @@ function levelDot(level) {
|
|
|
373
382
|
what a screen reader hears instead of what is written. -->
|
|
374
383
|
<div v-if="credentialReqs.length" role="group" aria-labelledby="tpl-credentials-label" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
|
|
375
384
|
<div id="tpl-credentials-label" class="flex items-center gap-1.5 text-mono text-muted-foreground">
|
|
376
|
-
<Key :size="11" />
|
|
385
|
+
<Key :size="11" /> {{ t("integration.templates.detail.credentials.title") }}
|
|
377
386
|
</div>
|
|
378
387
|
<ul class="space-y-1">
|
|
379
388
|
<li
|
|
@@ -390,22 +399,22 @@ function levelDot(level) {
|
|
|
390
399
|
class="shrink-0 bg-emerald-500/10 border-emerald-500/20 text-emerald-700 text-sm"
|
|
391
400
|
>
|
|
392
401
|
<CheckCircle2 :size="10" class="mr-0.5" />
|
|
393
|
-
{{ ofType(c.slug).length }}
|
|
402
|
+
{{ t("integration.templates.detail.credentials.readyCount", ofType(c.slug).length) }}
|
|
394
403
|
</Badge>
|
|
395
404
|
<Badge v-else variant="outline" class="shrink-0 bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm">
|
|
396
|
-
|
|
405
|
+
{{ t("integration.templates.detail.credentials.connectFirst") }}
|
|
397
406
|
</Badge>
|
|
398
407
|
</li>
|
|
399
408
|
</ul>
|
|
400
|
-
<NuxtLink to="/integrations/credentials" class="block text-sm text-primary hover:underline">
|
|
409
|
+
<NuxtLink to="/integrations/credentials" class="block text-sm text-primary hover:underline">{{ t("integration.templates.detail.credentials.manageLink") }} →</NuxtLink>
|
|
401
410
|
</div>
|
|
402
411
|
|
|
403
412
|
<div v-if="secretChecks.length" role="group" aria-labelledby="tpl-secrets-label" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
|
|
404
413
|
<div class="flex items-center gap-2">
|
|
405
|
-
<span id="tpl-secrets-label" class="text-mono text-muted-foreground">
|
|
414
|
+
<span id="tpl-secrets-label" class="text-mono text-muted-foreground">{{ t("integration.templates.detail.secrets.title") }}</span>
|
|
406
415
|
<span v-if="missingSecrets > 0" class="flex items-center gap-1 text-sm text-amber-700">
|
|
407
416
|
<ShieldAlert :size="11" />
|
|
408
|
-
{{ missingSecrets }}
|
|
417
|
+
{{ t("integration.templates.detail.secrets.missingBadge", missingSecrets) }}
|
|
409
418
|
</span>
|
|
410
419
|
</div>
|
|
411
420
|
<ul class="space-y-1">
|
|
@@ -421,20 +430,20 @@ function levelDot(level) {
|
|
|
421
430
|
class="bg-emerald-500/10 border-emerald-500/20 text-emerald-700 text-sm"
|
|
422
431
|
>
|
|
423
432
|
<CheckCircle2 :size="10" class="mr-0.5" />
|
|
424
|
-
|
|
433
|
+
{{ t("integration.templates.detail.secrets.set") }}
|
|
425
434
|
</Badge>
|
|
426
435
|
<Badge v-else variant="outline" class="bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm">
|
|
427
|
-
|
|
436
|
+
{{ t("integration.templates.detail.secrets.missing") }}
|
|
428
437
|
</Badge>
|
|
429
438
|
</li>
|
|
430
439
|
</ul>
|
|
431
440
|
</div>
|
|
432
441
|
|
|
433
442
|
<div v-if="tpl.industries.length || tpl.vendors.length" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
|
|
434
|
-
<div class="text-mono text-muted-foreground">
|
|
443
|
+
<div class="text-mono text-muted-foreground">{{ t("integration.templates.detail.fit.title") }}</div>
|
|
435
444
|
<div class="flex flex-wrap gap-1">
|
|
436
445
|
<span v-for="ind in tpl.industries" :key="`i-${ind}`" class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
|
|
437
|
-
{{ ind === "any" ? "
|
|
446
|
+
{{ ind === "any" ? t("integration.templates.detail.fit.anyIndustry") : ind }}
|
|
438
447
|
</span>
|
|
439
448
|
<span v-for="v in tpl.vendors" :key="`v-${v}`" class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
|
|
440
449
|
{{ v }}
|
|
@@ -447,54 +456,54 @@ function levelDot(level) {
|
|
|
447
456
|
</template>
|
|
448
457
|
<div v-else-if="!loaded" class="p-12 flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
|
449
458
|
<Loader2 :size="14" class="animate-spin" />
|
|
450
|
-
|
|
459
|
+
{{ t("integration.templates.detail.state.loading") }}
|
|
451
460
|
</div>
|
|
452
461
|
<div v-else class="p-12 text-center text-sm text-muted-foreground">
|
|
453
|
-
|
|
462
|
+
{{ t("integration.templates.detail.state.notFound") }}
|
|
454
463
|
</div>
|
|
455
464
|
|
|
456
465
|
<!-- Install: name + per-node credential binding ------------------------- -->
|
|
457
466
|
<Dialog v-model:open="installOpen">
|
|
458
467
|
<DialogContent class="sm:max-w-lg">
|
|
459
468
|
<DialogHeader>
|
|
460
|
-
<DialogTitle>
|
|
469
|
+
<DialogTitle>{{ t("integration.templates.detail.action.useTemplate") }}</DialogTitle>
|
|
461
470
|
<DialogDescription class="text-sm">
|
|
462
|
-
|
|
471
|
+
{{ t("integration.templates.detail.install.description") }}
|
|
463
472
|
</DialogDescription>
|
|
464
473
|
</DialogHeader>
|
|
465
474
|
|
|
466
475
|
<div class="space-y-4 py-1 max-h-[28rem] overflow-y-auto pr-1">
|
|
467
476
|
<div class="space-y-1.5">
|
|
468
|
-
<Label for="wf-name">
|
|
469
|
-
<Input id="wf-name" v-model="name" class="bg-white" placeholder="
|
|
477
|
+
<Label for="wf-name">{{ t("integration.templates.detail.install.nameLabel") }}</Label>
|
|
478
|
+
<Input id="wf-name" v-model="name" class="bg-white" :placeholder="t('integration.templates.detail.install.namePlaceholder')" />
|
|
470
479
|
</div>
|
|
471
480
|
|
|
472
481
|
<!-- Credentials -->
|
|
473
482
|
<div class="space-y-2">
|
|
474
483
|
<div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
|
|
475
|
-
|
|
484
|
+
{{ t("integration.templates.detail.credentials.title") }}
|
|
476
485
|
</div>
|
|
477
486
|
<p v-if="!credentialReqs.length" class="text-sm text-muted-foreground italic">
|
|
478
|
-
|
|
487
|
+
{{ t("integration.templates.detail.install.noCredentials") }}
|
|
479
488
|
</p>
|
|
480
489
|
<div v-for="req in credentialReqs" :key="req.slug" class="space-y-2">
|
|
481
490
|
<div class="flex items-center gap-2">
|
|
482
491
|
<span class="text-sm font-medium">{{ req.name ?? req.slug }}</span>
|
|
483
492
|
<Badge v-if="!req.registered" variant="outline" class="bg-rose-500/10 border-rose-500/20 text-rose-700 text-sm">
|
|
484
|
-
|
|
493
|
+
{{ t("integration.templates.detail.install.notRegistered") }}
|
|
485
494
|
</Badge>
|
|
486
495
|
<Badge v-else-if="!ofType(req.slug).length" variant="outline" class="bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm">
|
|
487
|
-
|
|
496
|
+
{{ t("integration.templates.detail.install.noneYet") }}
|
|
488
497
|
</Badge>
|
|
489
498
|
</div>
|
|
490
499
|
|
|
491
500
|
<p v-if="!ofType(req.slug).length" class="rounded-md border-l-2 border-black/[0.08] bg-muted/30 px-2.5 py-1.5 text-sm text-muted-foreground">
|
|
492
|
-
|
|
493
|
-
<NuxtLink to="/integrations/credentials" class="text-primary hover:underline">
|
|
494
|
-
|
|
501
|
+
{{ t("integration.templates.detail.install.noCredentialsOfType") }}
|
|
502
|
+
<NuxtLink to="/integrations/credentials" class="text-primary hover:underline">{{ t("integration.templates.detail.install.createOneLink") }}</NuxtLink>
|
|
503
|
+
{{ t("integration.templates.detail.install.orInstallLater") }}
|
|
495
504
|
</p>
|
|
496
505
|
<div v-for="nodeId in nodesFor(req.usedBy)" v-else :key="nodeId" class="space-y-1">
|
|
497
|
-
<Label :for="`tpl-cred-${nodeId}`" class="text-xs text-muted-foreground font-mono">
|
|
506
|
+
<Label :for="`tpl-cred-${nodeId}`" class="text-xs text-muted-foreground font-mono">{{ t("integration.templates.detail.install.nodeLabel", { id: nodeId }) }}</Label>
|
|
498
507
|
<Select
|
|
499
508
|
:model-value="bindings[nodeId] ?? SELECT_LATER"
|
|
500
509
|
@update:model-value="(v2) => {
|
|
@@ -502,10 +511,10 @@ function levelDot(level) {
|
|
|
502
511
|
}"
|
|
503
512
|
>
|
|
504
513
|
<SelectTrigger :id="`tpl-cred-${nodeId}`" class="bg-white">
|
|
505
|
-
<SelectValue placeholder="
|
|
514
|
+
<SelectValue :placeholder="t('integration.templates.detail.install.selectLater')" />
|
|
506
515
|
</SelectTrigger>
|
|
507
516
|
<SelectContent>
|
|
508
|
-
<SelectItem :value="SELECT_LATER">
|
|
517
|
+
<SelectItem :value="SELECT_LATER">{{ t("integration.templates.detail.install.selectLater") }}</SelectItem>
|
|
509
518
|
<SelectItem v-for="c in ofType(req.slug)" :key="c.id" :value="c.id">
|
|
510
519
|
{{ c.name }}
|
|
511
520
|
</SelectItem>
|
|
@@ -518,11 +527,11 @@ function levelDot(level) {
|
|
|
518
527
|
<!-- Secrets -->
|
|
519
528
|
<div v-if="secretChecks.length" class="space-y-2">
|
|
520
529
|
<div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
|
|
521
|
-
|
|
530
|
+
{{ t("integration.templates.detail.install.secretsHeader") }}
|
|
522
531
|
</div>
|
|
523
532
|
<p class="text-sm text-muted-foreground">
|
|
524
|
-
|
|
525
|
-
<NuxtLink to="/integrations/secrets" class="text-primary hover:underline">
|
|
533
|
+
{{ t("integration.templates.detail.install.secretsDescription") }}
|
|
534
|
+
<NuxtLink to="/integrations/secrets" class="text-primary hover:underline">{{ t("integration.nav.item.secrets") }}</NuxtLink> {{ t("integration.templates.detail.install.secretsDescriptionSuffix") }}
|
|
526
535
|
</p>
|
|
527
536
|
<ul class="space-y-1">
|
|
528
537
|
<li
|
|
@@ -532,7 +541,7 @@ function levelDot(level) {
|
|
|
532
541
|
>
|
|
533
542
|
<code class="font-mono">{{ s.label }}</code>
|
|
534
543
|
<Badge variant="outline" :class="s.present ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-700 text-sm' : 'bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm'">
|
|
535
|
-
{{ s.present ? "
|
|
544
|
+
{{ s.present ? t("integration.templates.detail.install.secretStored") : t("integration.templates.detail.secrets.missing") }}
|
|
536
545
|
</Badge>
|
|
537
546
|
</li>
|
|
538
547
|
</ul>
|
|
@@ -545,11 +554,11 @@ function levelDot(level) {
|
|
|
545
554
|
|
|
546
555
|
<DialogFooter>
|
|
547
556
|
<Button variant="outline" :disabled="installing" @click="installOpen = false">
|
|
548
|
-
|
|
557
|
+
{{ t("integration.common.action.cancel") }}
|
|
549
558
|
</Button>
|
|
550
559
|
<Button :disabled="installing || !name.trim()" @click="confirmInstall">
|
|
551
560
|
<Loader2 v-if="installing" :size="14" class="mr-1 animate-spin" />
|
|
552
|
-
{{ installing ? "
|
|
561
|
+
{{ installing ? t("integration.templates.detail.install.submitting") : t("integration.templates.detail.install.submit") }}
|
|
553
562
|
</Button>
|
|
554
563
|
</DialogFooter>
|
|
555
564
|
</DialogContent>
|
|
@@ -4,12 +4,13 @@ import { Magnifier as Search, Layers, DangerTriangle as AlertTriangle } from "#s
|
|
|
4
4
|
import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@revenexx/studio";
|
|
5
5
|
import TemplateCard from "../../../components/Integrations/TemplateCard.vue";
|
|
6
6
|
import { templateCategoryLabel, templatePackageLabel } from "../../../data/integrationTemplates";
|
|
7
|
-
|
|
7
|
+
const t = useIntegrationsT();
|
|
8
|
+
useHead({ title: () => t("integration.templates.gallery.head.title") });
|
|
8
9
|
const { templates, loading, error, loaded, load, ensureDetails } = useIntegrationsTemplates();
|
|
9
10
|
const { load: loadNodeCatalog } = useIntegrationsNodes();
|
|
10
11
|
onMounted(async () => {
|
|
11
12
|
await Promise.all([load(true), loadNodeCatalog()]);
|
|
12
|
-
void ensureDetails(templates.value.map((
|
|
13
|
+
void ensureDetails(templates.value.map((t2) => t2.slug));
|
|
13
14
|
});
|
|
14
15
|
const search = ref("");
|
|
15
16
|
const activeCategory = ref("all");
|
|
@@ -17,22 +18,31 @@ const activeLevel = ref("all");
|
|
|
17
18
|
const sortBy = ref("name");
|
|
18
19
|
const categories = computed(() => {
|
|
19
20
|
const counts = /* @__PURE__ */ new Map();
|
|
20
|
-
for (const
|
|
21
|
-
if (!
|
|
22
|
-
counts.set(
|
|
21
|
+
for (const t2 of templates.value) {
|
|
22
|
+
if (!t2.category) continue;
|
|
23
|
+
counts.set(t2.category, (counts.get(t2.category) ?? 0) + 1);
|
|
23
24
|
}
|
|
24
25
|
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([key, count]) => ({ key, label: templateCategoryLabel(key), count }));
|
|
25
26
|
});
|
|
26
27
|
const LEVELS = ["beginner", "intermediate", "advanced"];
|
|
27
28
|
const LEVEL_RANK = { beginner: 0, intermediate: 1, advanced: 2 };
|
|
29
|
+
const levelOptions = computed(() => LEVELS.map((l) => ({
|
|
30
|
+
value: l,
|
|
31
|
+
label: t(`integration.templates.gallery.filter.level.${l}`)
|
|
32
|
+
})));
|
|
33
|
+
const sortOptions = computed(() => [
|
|
34
|
+
{ value: "name", label: t("integration.templates.gallery.sort.name") },
|
|
35
|
+
{ value: "level", label: t("integration.templates.gallery.sort.level") },
|
|
36
|
+
{ value: "nodes", label: t("integration.templates.gallery.sort.nodes") }
|
|
37
|
+
]);
|
|
28
38
|
const filtered = computed(() => {
|
|
29
39
|
const q = search.value.trim().toLowerCase();
|
|
30
40
|
let items = templates.value;
|
|
31
|
-
if (activeCategory.value !== "all") items = items.filter((
|
|
32
|
-
if (activeLevel.value !== "all") items = items.filter((
|
|
41
|
+
if (activeCategory.value !== "all") items = items.filter((t2) => t2.category === activeCategory.value);
|
|
42
|
+
if (activeLevel.value !== "all") items = items.filter((t2) => t2.level === activeLevel.value);
|
|
33
43
|
if (q) {
|
|
34
44
|
items = items.filter(
|
|
35
|
-
(
|
|
45
|
+
(t2) => t2.name.toLowerCase().includes(q) || t2.description.toLowerCase().includes(q) || t2.vendors.some((v) => v.toLowerCase().includes(q)) || (templatePackageLabel(t2) ?? "").toLowerCase().includes(q) || t2.industries.some((i) => i.toLowerCase().includes(q))
|
|
36
46
|
);
|
|
37
47
|
}
|
|
38
48
|
const sorted = [...items];
|
|
@@ -42,10 +52,10 @@ const filtered = computed(() => {
|
|
|
42
52
|
return sorted;
|
|
43
53
|
});
|
|
44
54
|
watch(() => templates.value.length, () => {
|
|
45
|
-
if (templates.value.length) void ensureDetails(templates.value.map((
|
|
55
|
+
if (templates.value.length) void ensureDetails(templates.value.map((t2) => t2.slug));
|
|
46
56
|
});
|
|
47
57
|
const awaitingCatalogue = computed(() => loading.value && !templates.value.length);
|
|
48
|
-
const statusText = computed(() => awaitingCatalogue.value ? "
|
|
58
|
+
const statusText = computed(() => awaitingCatalogue.value ? t("integration.templates.gallery.state.loading") : "");
|
|
49
59
|
</script>
|
|
50
60
|
|
|
51
61
|
<template>
|
|
@@ -53,8 +63,8 @@ const statusText = computed(() => awaitingCatalogue.value ? "Loading templates\u
|
|
|
53
63
|
<!-- The toolbar lives in the header's #tabs slot: PageHeader is the
|
|
54
64
|
sticky element (top-0 z-20), so filters + search stay pinned. -->
|
|
55
65
|
<PageHeader
|
|
56
|
-
title="
|
|
57
|
-
description="
|
|
66
|
+
:title="t('integration.templates.gallery.header.title')"
|
|
67
|
+
:description="t('integration.templates.gallery.header.description')"
|
|
58
68
|
>
|
|
59
69
|
<template #tabs>
|
|
60
70
|
<div class="-mx-3 w-[calc(100%+1.5rem)] flex items-center gap-3 border-t border-black/[0.06] px-4 pt-2.5 pb-1">
|
|
@@ -65,7 +75,7 @@ const statusText = computed(() => awaitingCatalogue.value ? "Loading templates\u
|
|
|
65
75
|
:class="activeCategory === 'all' ? 'border-primary/30 bg-primary/10 text-primary font-medium' : 'border-black/[0.08] text-muted-foreground hover:border-black/20'"
|
|
66
76
|
@click="activeCategory = 'all'"
|
|
67
77
|
>
|
|
68
|
-
|
|
78
|
+
{{ t("integration.templates.gallery.filter.all") }} <span class="opacity-70">({{ templates.length }})</span>
|
|
69
79
|
</button>
|
|
70
80
|
<button
|
|
71
81
|
v-for="cat in categories"
|
|
@@ -80,27 +90,25 @@ const statusText = computed(() => awaitingCatalogue.value ? "Loading templates\u
|
|
|
80
90
|
</div>
|
|
81
91
|
|
|
82
92
|
<Select v-model="activeLevel">
|
|
83
|
-
<SelectTrigger aria-label="
|
|
93
|
+
<SelectTrigger :aria-label="t('integration.templates.gallery.filter.levelAria')" class="h-7 data-[size=default]:h-7 min-h-0 py-0 w-36 rounded-full bg-white text-sm">
|
|
84
94
|
<SelectValue />
|
|
85
95
|
</SelectTrigger>
|
|
86
96
|
<SelectContent>
|
|
87
|
-
<SelectItem value="all">
|
|
88
|
-
<SelectItem v-for="
|
|
97
|
+
<SelectItem value="all">{{ t("integration.templates.gallery.filter.levelAllOption") }}</SelectItem>
|
|
98
|
+
<SelectItem v-for="opt in levelOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectItem>
|
|
89
99
|
</SelectContent>
|
|
90
100
|
</Select>
|
|
91
101
|
<Select v-model="sortBy">
|
|
92
|
-
<SelectTrigger aria-label="
|
|
102
|
+
<SelectTrigger :aria-label="t('integration.templates.gallery.sort.aria')" class="h-7 data-[size=default]:h-7 min-h-0 py-0 w-40 rounded-full bg-white text-sm">
|
|
93
103
|
<SelectValue />
|
|
94
104
|
</SelectTrigger>
|
|
95
105
|
<SelectContent>
|
|
96
|
-
<SelectItem value="
|
|
97
|
-
<SelectItem value="level">Sort: Simplest first</SelectItem>
|
|
98
|
-
<SelectItem value="nodes">Sort: Most steps</SelectItem>
|
|
106
|
+
<SelectItem v-for="opt in sortOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectItem>
|
|
99
107
|
</SelectContent>
|
|
100
108
|
</Select>
|
|
101
109
|
<div class="relative w-60">
|
|
102
110
|
<Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
103
|
-
<Input v-model="search" aria-label="
|
|
111
|
+
<Input v-model="search" :aria-label="t('integration.templates.gallery.search.aria')" :placeholder="t('integration.templates.gallery.search.placeholder')" class="h-7 min-h-0 py-0 rounded-full pl-7 text-sm bg-white" />
|
|
104
112
|
</div>
|
|
105
113
|
</div>
|
|
106
114
|
</template>
|
|
@@ -122,7 +130,7 @@ const statusText = computed(() => awaitingCatalogue.value ? "Loading templates\u
|
|
|
122
130
|
card draws one while its flow miniature loads ("Loading the flow"). Two
|
|
123
131
|
unnamed status regions on one page are two things a reader — and a test —
|
|
124
132
|
cannot tell apart, and this one is about the gallery rather than a card. -->
|
|
125
|
-
<p role="status" aria-live="polite" aria-label="
|
|
133
|
+
<p role="status" aria-live="polite" :aria-label="t('integration.templates.gallery.state.regionLabel')" class="sr-only">{{ statusText }}</p>
|
|
126
134
|
|
|
127
135
|
<!-- The placeholders are taken out of the accessibility tree rather than
|
|
128
136
|
marked busy. `aria-busy` only asks assistive technology to defer
|
|
@@ -155,7 +163,7 @@ const statusText = computed(() => awaitingCatalogue.value ? "Loading templates\u
|
|
|
155
163
|
<div v-else-if="error && !templates.length" role="alert" class="rounded-lg border border-rose-500/20 bg-rose-500/5 p-4 text-sm text-rose-700 flex items-start gap-2">
|
|
156
164
|
<AlertTriangle :size="15" class="mt-0.5 shrink-0" />
|
|
157
165
|
<div>
|
|
158
|
-
<div class="font-medium">
|
|
166
|
+
<div class="font-medium">{{ t("integration.templates.gallery.error.title") }}</div>
|
|
159
167
|
<div class="text-rose-700/80">{{ error }}</div>
|
|
160
168
|
</div>
|
|
161
169
|
</div>
|
|
@@ -165,15 +173,15 @@ const statusText = computed(() => awaitingCatalogue.value ? "Loading templates\u
|
|
|
165
173
|
<span class="mx-auto mb-3 flex size-10 items-center justify-center rounded-lg bg-slate-100 text-slate-400">
|
|
166
174
|
<Layers :size="18" />
|
|
167
175
|
</span>
|
|
168
|
-
<div class="text-sm font-semibold text-foreground">
|
|
176
|
+
<div class="text-sm font-semibold text-foreground">{{ t("integration.templates.gallery.empty.registered.title") }}</div>
|
|
169
177
|
<p class="mx-auto mt-1 max-w-sm text-sm text-muted-foreground">
|
|
170
|
-
|
|
178
|
+
{{ t("integration.templates.gallery.empty.registered.description") }}
|
|
171
179
|
</p>
|
|
172
180
|
</div>
|
|
173
181
|
|
|
174
182
|
<!-- The group is named so the cards can be addressed as a set; a card names
|
|
175
183
|
itself from what it shows, which TemplateCard says more about. -->
|
|
176
|
-
<div v-else role="group" aria-label="
|
|
184
|
+
<div v-else role="group" :aria-label="t('integration.templates.gallery.cards.ariaLabel')" class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
177
185
|
<TemplateCard
|
|
178
186
|
v-for="tpl in filtered"
|
|
179
187
|
:key="tpl.slug"
|
|
@@ -184,7 +192,7 @@ const statusText = computed(() => awaitingCatalogue.value ? "Loading templates\u
|
|
|
184
192
|
</div>
|
|
185
193
|
|
|
186
194
|
<div v-if="templates.length && !filtered.length" class="mt-4 rounded-lg border border-black/[0.06] bg-white p-8 text-center text-sm text-muted-foreground">
|
|
187
|
-
|
|
195
|
+
{{ t("integration.templates.gallery.empty.filtered") }}
|
|
188
196
|
</div>
|
|
189
197
|
</div>
|
|
190
198
|
</IntegrationsLayout>
|