@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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, reactive, ref } from "vue";
|
|
3
3
|
import { marked } from "marked";
|
|
4
|
-
import { AltArrowRight as Chevron,
|
|
4
|
+
import { AltArrowRight as Chevron, CheckCircle as CheckCircle2, Key, RefreshCircle as Loader2, Shield as ShieldAlert, User } from "#solar-icons";
|
|
5
|
+
import { triggerGlyph as glyphForTrigger } from "../../../data/triggerGlyphs";
|
|
5
6
|
import {
|
|
6
7
|
Badge,
|
|
7
8
|
Button,
|
|
@@ -24,6 +25,7 @@ import NodeMark from "../../../components/Integrations/NodeMark.vue";
|
|
|
24
25
|
import { templateCategoryLabel, templatePackageLabel } from "../../../data/integrationTemplates";
|
|
25
26
|
import { describeCron } from "../../../utils/schedule";
|
|
26
27
|
const SELECT_LATER = "__later__";
|
|
28
|
+
const t = useIntegrationsT();
|
|
27
29
|
const route = useRoute();
|
|
28
30
|
const router = useRouter();
|
|
29
31
|
const { get, load, loaded, fetchDetail, requirements, instantiate } = useIntegrationsTemplates();
|
|
@@ -32,7 +34,7 @@ const { keys: secretKeys, load: loadSecrets } = useIntegrationsSecrets();
|
|
|
32
34
|
const { ofType, load: loadCredentials } = useIntegrationsCredentials();
|
|
33
35
|
const slug = computed(() => String(route.params.slug));
|
|
34
36
|
const tpl = computed(() => get(slug.value));
|
|
35
|
-
useHead({ title: () =>
|
|
37
|
+
useHead({ title: () => t("integration.templates.detail.head.title", { name: tpl.value?.name ?? t("integration.templates.detail.head.fallbackName") }) });
|
|
36
38
|
const reqs = ref(null);
|
|
37
39
|
onMounted(async () => {
|
|
38
40
|
await load();
|
|
@@ -50,7 +52,7 @@ const steps = computed(() => {
|
|
|
50
52
|
if (!blob?.nodes.length) return [];
|
|
51
53
|
const nodeById = new Map(blob.nodes.map((n) => [n.id, n]));
|
|
52
54
|
const happyEdges = (blob.edges ?? []).filter((e) => e.from.output !== "error");
|
|
53
|
-
const triggerHandles = new Set((blob.triggers ?? []).map((
|
|
55
|
+
const triggerHandles = new Set((blob.triggers ?? []).map((t2) => t2.id));
|
|
54
56
|
const incoming = /* @__PURE__ */ new Map();
|
|
55
57
|
for (const e of happyEdges) {
|
|
56
58
|
if (!triggerHandles.has(e.from.nodeId)) incoming.set(e.to.nodeId, (incoming.get(e.to.nodeId) ?? 0) + 1);
|
|
@@ -89,29 +91,28 @@ function firstSentence(text) {
|
|
|
89
91
|
const idx = text.indexOf(". ");
|
|
90
92
|
return idx === -1 ? text : text.slice(0, idx + 1);
|
|
91
93
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
label = `Schedule \xB7 ${when}${t.config.timezone ? ` (${t.config.timezone})` : ""}`;
|
|
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: glyphForTrigger(trig.type), 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,21 +193,29 @@ 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>
|
|
198
207
|
|
|
199
208
|
<div class="p-4 space-y-4">
|
|
200
|
-
<!-- Hero: the live workflow preview on the editor's dotted canvas
|
|
201
|
-
|
|
209
|
+
<!-- Hero: the live workflow preview on the editor's dotted canvas.
|
|
210
|
+
Named like the two panels below it, with `aria-label` because it
|
|
211
|
+
carries no heading to point at: it was the largest thing on the page
|
|
212
|
+
and the only one with no name at all, which also left it the one
|
|
213
|
+
surface drawing the miniature that nothing could address (PO-247). -->
|
|
214
|
+
<div
|
|
215
|
+
role="region"
|
|
216
|
+
:aria-label="t('integration.templates.detail.flow.ariaLabel')"
|
|
217
|
+
class="relative overflow-hidden rounded-lg border border-black/[0.06] bg-slate-50/70 [background-image:radial-gradient(circle,rgba(15,23,42,0.07)_1px,transparent_1px)] [background-size:14px_14px]"
|
|
218
|
+
>
|
|
202
219
|
<TemplateFlowPreview v-if="tpl.detailLoaded" :blob="tpl.blob" :height="400" class="px-4" />
|
|
203
220
|
<div v-else class="flex h-[400px] items-center justify-center">
|
|
204
221
|
<Loader2 :size="18" class="animate-spin text-muted-foreground/50" />
|
|
@@ -236,7 +253,7 @@ function levelDot(level) {
|
|
|
236
253
|
aria-labelledby="tpl-about-heading"
|
|
237
254
|
class="rounded-lg border border-black/[0.06] bg-white p-4"
|
|
238
255
|
>
|
|
239
|
-
<h3 id="tpl-about-heading" class="text-sm font-semibold mb-2">
|
|
256
|
+
<h3 id="tpl-about-heading" class="text-sm font-semibold mb-2">{{ t("integration.templates.detail.about.title") }}</h3>
|
|
240
257
|
<div
|
|
241
258
|
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
259
|
v-html="descriptionHtml"
|
|
@@ -249,7 +266,7 @@ function levelDot(level) {
|
|
|
249
266
|
aria-labelledby="tpl-steps-heading"
|
|
250
267
|
class="rounded-lg border border-black/[0.06] bg-white p-4"
|
|
251
268
|
>
|
|
252
|
-
<h3 id="tpl-steps-heading" class="text-sm font-semibold mb-3">
|
|
269
|
+
<h3 id="tpl-steps-heading" class="text-sm font-semibold mb-3">{{ t("integration.templates.detail.steps.title") }}</h3>
|
|
253
270
|
<ol v-if="steps.length" class="space-y-2.5">
|
|
254
271
|
<li
|
|
255
272
|
v-for="(step, i) in steps"
|
|
@@ -269,7 +286,7 @@ function levelDot(level) {
|
|
|
269
286
|
</li>
|
|
270
287
|
</ol>
|
|
271
288
|
<p v-else class="text-sm text-muted-foreground italic">
|
|
272
|
-
|
|
289
|
+
{{ t("integration.templates.detail.steps.empty") }}
|
|
273
290
|
</p>
|
|
274
291
|
</div>
|
|
275
292
|
|
|
@@ -284,8 +301,8 @@ function levelDot(level) {
|
|
|
284
301
|
aria-labelledby="tpl-variables-heading"
|
|
285
302
|
class="rounded-lg border border-black/[0.06] bg-white p-4"
|
|
286
303
|
>
|
|
287
|
-
<h3 id="tpl-variables-heading" class="text-sm font-semibold mb-1">
|
|
288
|
-
<p class="text-sm text-muted-foreground mb-3">
|
|
304
|
+
<h3 id="tpl-variables-heading" class="text-sm font-semibold mb-1">{{ t("integration.templates.detail.variables.title") }}</h3>
|
|
305
|
+
<p class="text-sm text-muted-foreground mb-3">{{ t("integration.templates.detail.variables.description") }}</p>
|
|
289
306
|
<DataTable
|
|
290
307
|
:data="variables"
|
|
291
308
|
:columns="variableColumns"
|
|
@@ -315,51 +332,51 @@ function levelDot(level) {
|
|
|
315
332
|
shows no heading of its own, so there is no visible string to
|
|
316
333
|
point `aria-labelledby` at. Copy this pattern only for a panel
|
|
317
334
|
that genuinely has none. -->
|
|
318
|
-
<div role="group" aria-label="
|
|
335
|
+
<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
336
|
<div v-if="setupReady" class="flex items-center gap-1.5 text-sm text-emerald-700">
|
|
320
337
|
<CheckCircle2 :size="14" />
|
|
321
|
-
|
|
338
|
+
{{ t("integration.templates.detail.setup.ready") }}
|
|
322
339
|
</div>
|
|
323
340
|
<div v-else class="text-sm text-amber-700 space-y-0.5">
|
|
324
341
|
<div v-if="missingCredentials" class="flex items-center gap-1.5">
|
|
325
342
|
<Key :size="12" />
|
|
326
|
-
{{ missingCredentials
|
|
343
|
+
{{ t("integration.templates.detail.setup.missingCredentials", missingCredentials) }}
|
|
327
344
|
</div>
|
|
328
345
|
<div v-if="missingSecrets" class="flex items-center gap-1.5">
|
|
329
346
|
<ShieldAlert :size="12" />
|
|
330
|
-
{{ missingSecrets
|
|
347
|
+
{{ t("integration.templates.detail.setup.missingSecrets", missingSecrets) }}
|
|
331
348
|
</div>
|
|
332
349
|
</div>
|
|
333
350
|
<Button class="w-full h-8 text-sm" @click="openInstall">
|
|
334
|
-
|
|
351
|
+
{{ t("integration.templates.detail.action.useTemplate") }}
|
|
335
352
|
</Button>
|
|
336
353
|
<p class="text-xs text-muted-foreground">
|
|
337
|
-
|
|
354
|
+
{{ t("integration.templates.detail.setup.hint") }}
|
|
338
355
|
</p>
|
|
339
356
|
</div>
|
|
340
357
|
|
|
341
358
|
<div class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
|
|
342
|
-
<div class="text-mono text-muted-foreground">
|
|
359
|
+
<div class="text-mono text-muted-foreground">{{ t("integration.templates.detail.publisher.title") }}</div>
|
|
343
360
|
<div class="flex items-center gap-2">
|
|
344
361
|
<span class="flex size-6 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
345
362
|
<User :size="12" />
|
|
346
363
|
</span>
|
|
347
364
|
<div class="text-sm font-medium truncate">{{ tpl.publisher.name }}</div>
|
|
348
365
|
<Badge variant="outline" :class="['text-sm border', publisherBadgeClass(tpl.publisher.kind)]">
|
|
349
|
-
{{
|
|
366
|
+
{{ publisherKindLabel(tpl.publisher.kind) }}
|
|
350
367
|
</Badge>
|
|
351
368
|
</div>
|
|
352
369
|
<div v-if="packageLabel" class="border-t border-black/[0.06] pt-2 text-sm">
|
|
353
|
-
<div class="text-muted-foreground">
|
|
370
|
+
<div class="text-muted-foreground">{{ t("integration.templates.detail.publisher.shipsWith") }}</div>
|
|
354
371
|
<div class="font-medium">{{ packageLabel }}</div>
|
|
355
372
|
</div>
|
|
356
373
|
<div class="border-t border-black/[0.06] pt-2 grid grid-cols-2 gap-2 text-sm">
|
|
357
374
|
<div>
|
|
358
|
-
<div class="text-muted-foreground">
|
|
375
|
+
<div class="text-muted-foreground">{{ t("integration.templates.detail.rail.stepsLabel") }}</div>
|
|
359
376
|
<div class="font-medium tabular-nums">{{ steps.length || tpl.nodeCount }}</div>
|
|
360
377
|
</div>
|
|
361
378
|
<div>
|
|
362
|
-
<div class="text-muted-foreground">
|
|
379
|
+
<div class="text-muted-foreground">{{ t("integration.templates.detail.rail.levelLabel") }}</div>
|
|
363
380
|
<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
381
|
<span class="size-1.5 rounded-full" :class="levelDot(tpl.level)" />
|
|
365
382
|
{{ tpl.level }}
|
|
@@ -373,7 +390,7 @@ function levelDot(level) {
|
|
|
373
390
|
what a screen reader hears instead of what is written. -->
|
|
374
391
|
<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
392
|
<div id="tpl-credentials-label" class="flex items-center gap-1.5 text-mono text-muted-foreground">
|
|
376
|
-
<Key :size="11" />
|
|
393
|
+
<Key :size="11" /> {{ t("integration.templates.detail.credentials.title") }}
|
|
377
394
|
</div>
|
|
378
395
|
<ul class="space-y-1">
|
|
379
396
|
<li
|
|
@@ -390,22 +407,22 @@ function levelDot(level) {
|
|
|
390
407
|
class="shrink-0 bg-emerald-500/10 border-emerald-500/20 text-emerald-700 text-sm"
|
|
391
408
|
>
|
|
392
409
|
<CheckCircle2 :size="10" class="mr-0.5" />
|
|
393
|
-
{{ ofType(c.slug).length }}
|
|
410
|
+
{{ t("integration.templates.detail.credentials.readyCount", ofType(c.slug).length) }}
|
|
394
411
|
</Badge>
|
|
395
412
|
<Badge v-else variant="outline" class="shrink-0 bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm">
|
|
396
|
-
|
|
413
|
+
{{ t("integration.templates.detail.credentials.connectFirst") }}
|
|
397
414
|
</Badge>
|
|
398
415
|
</li>
|
|
399
416
|
</ul>
|
|
400
|
-
<NuxtLink to="/integrations/credentials" class="block text-sm text-primary hover:underline">
|
|
417
|
+
<NuxtLink to="/integrations/credentials" class="block text-sm text-primary hover:underline">{{ t("integration.templates.detail.credentials.manageLink") }} →</NuxtLink>
|
|
401
418
|
</div>
|
|
402
419
|
|
|
403
420
|
<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
421
|
<div class="flex items-center gap-2">
|
|
405
|
-
<span id="tpl-secrets-label" class="text-mono text-muted-foreground">
|
|
422
|
+
<span id="tpl-secrets-label" class="text-mono text-muted-foreground">{{ t("integration.templates.detail.secrets.title") }}</span>
|
|
406
423
|
<span v-if="missingSecrets > 0" class="flex items-center gap-1 text-sm text-amber-700">
|
|
407
424
|
<ShieldAlert :size="11" />
|
|
408
|
-
{{ missingSecrets }}
|
|
425
|
+
{{ t("integration.templates.detail.secrets.missingBadge", missingSecrets) }}
|
|
409
426
|
</span>
|
|
410
427
|
</div>
|
|
411
428
|
<ul class="space-y-1">
|
|
@@ -421,20 +438,20 @@ function levelDot(level) {
|
|
|
421
438
|
class="bg-emerald-500/10 border-emerald-500/20 text-emerald-700 text-sm"
|
|
422
439
|
>
|
|
423
440
|
<CheckCircle2 :size="10" class="mr-0.5" />
|
|
424
|
-
|
|
441
|
+
{{ t("integration.templates.detail.secrets.set") }}
|
|
425
442
|
</Badge>
|
|
426
443
|
<Badge v-else variant="outline" class="bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm">
|
|
427
|
-
|
|
444
|
+
{{ t("integration.templates.detail.secrets.missing") }}
|
|
428
445
|
</Badge>
|
|
429
446
|
</li>
|
|
430
447
|
</ul>
|
|
431
448
|
</div>
|
|
432
449
|
|
|
433
450
|
<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">
|
|
451
|
+
<div class="text-mono text-muted-foreground">{{ t("integration.templates.detail.fit.title") }}</div>
|
|
435
452
|
<div class="flex flex-wrap gap-1">
|
|
436
453
|
<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" ? "
|
|
454
|
+
{{ ind === "any" ? t("integration.templates.detail.fit.anyIndustry") : ind }}
|
|
438
455
|
</span>
|
|
439
456
|
<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
457
|
{{ v }}
|
|
@@ -447,54 +464,54 @@ function levelDot(level) {
|
|
|
447
464
|
</template>
|
|
448
465
|
<div v-else-if="!loaded" class="p-12 flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
|
449
466
|
<Loader2 :size="14" class="animate-spin" />
|
|
450
|
-
|
|
467
|
+
{{ t("integration.templates.detail.state.loading") }}
|
|
451
468
|
</div>
|
|
452
469
|
<div v-else class="p-12 text-center text-sm text-muted-foreground">
|
|
453
|
-
|
|
470
|
+
{{ t("integration.templates.detail.state.notFound") }}
|
|
454
471
|
</div>
|
|
455
472
|
|
|
456
473
|
<!-- Install: name + per-node credential binding ------------------------- -->
|
|
457
474
|
<Dialog v-model:open="installOpen">
|
|
458
475
|
<DialogContent class="sm:max-w-lg">
|
|
459
476
|
<DialogHeader>
|
|
460
|
-
<DialogTitle>
|
|
477
|
+
<DialogTitle>{{ t("integration.templates.detail.action.useTemplate") }}</DialogTitle>
|
|
461
478
|
<DialogDescription class="text-sm">
|
|
462
|
-
|
|
479
|
+
{{ t("integration.templates.detail.install.description") }}
|
|
463
480
|
</DialogDescription>
|
|
464
481
|
</DialogHeader>
|
|
465
482
|
|
|
466
483
|
<div class="space-y-4 py-1 max-h-[28rem] overflow-y-auto pr-1">
|
|
467
484
|
<div class="space-y-1.5">
|
|
468
|
-
<Label for="wf-name">
|
|
469
|
-
<Input id="wf-name" v-model="name" class="bg-white" placeholder="
|
|
485
|
+
<Label for="wf-name">{{ t("integration.templates.detail.install.nameLabel") }}</Label>
|
|
486
|
+
<Input id="wf-name" v-model="name" class="bg-white" :placeholder="t('integration.templates.detail.install.namePlaceholder')" />
|
|
470
487
|
</div>
|
|
471
488
|
|
|
472
489
|
<!-- Credentials -->
|
|
473
490
|
<div class="space-y-2">
|
|
474
491
|
<div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
|
|
475
|
-
|
|
492
|
+
{{ t("integration.templates.detail.credentials.title") }}
|
|
476
493
|
</div>
|
|
477
494
|
<p v-if="!credentialReqs.length" class="text-sm text-muted-foreground italic">
|
|
478
|
-
|
|
495
|
+
{{ t("integration.templates.detail.install.noCredentials") }}
|
|
479
496
|
</p>
|
|
480
497
|
<div v-for="req in credentialReqs" :key="req.slug" class="space-y-2">
|
|
481
498
|
<div class="flex items-center gap-2">
|
|
482
499
|
<span class="text-sm font-medium">{{ req.name ?? req.slug }}</span>
|
|
483
500
|
<Badge v-if="!req.registered" variant="outline" class="bg-rose-500/10 border-rose-500/20 text-rose-700 text-sm">
|
|
484
|
-
|
|
501
|
+
{{ t("integration.templates.detail.install.notRegistered") }}
|
|
485
502
|
</Badge>
|
|
486
503
|
<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
|
-
|
|
504
|
+
{{ t("integration.templates.detail.install.noneYet") }}
|
|
488
505
|
</Badge>
|
|
489
506
|
</div>
|
|
490
507
|
|
|
491
508
|
<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
|
-
|
|
509
|
+
{{ t("integration.templates.detail.install.noCredentialsOfType") }}
|
|
510
|
+
<NuxtLink to="/integrations/credentials" class="text-primary hover:underline">{{ t("integration.templates.detail.install.createOneLink") }}</NuxtLink>
|
|
511
|
+
{{ t("integration.templates.detail.install.orInstallLater") }}
|
|
495
512
|
</p>
|
|
496
513
|
<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">
|
|
514
|
+
<Label :for="`tpl-cred-${nodeId}`" class="text-xs text-muted-foreground font-mono">{{ t("integration.templates.detail.install.nodeLabel", { id: nodeId }) }}</Label>
|
|
498
515
|
<Select
|
|
499
516
|
:model-value="bindings[nodeId] ?? SELECT_LATER"
|
|
500
517
|
@update:model-value="(v2) => {
|
|
@@ -502,10 +519,10 @@ function levelDot(level) {
|
|
|
502
519
|
}"
|
|
503
520
|
>
|
|
504
521
|
<SelectTrigger :id="`tpl-cred-${nodeId}`" class="bg-white">
|
|
505
|
-
<SelectValue placeholder="
|
|
522
|
+
<SelectValue :placeholder="t('integration.templates.detail.install.selectLater')" />
|
|
506
523
|
</SelectTrigger>
|
|
507
524
|
<SelectContent>
|
|
508
|
-
<SelectItem :value="SELECT_LATER">
|
|
525
|
+
<SelectItem :value="SELECT_LATER">{{ t("integration.templates.detail.install.selectLater") }}</SelectItem>
|
|
509
526
|
<SelectItem v-for="c in ofType(req.slug)" :key="c.id" :value="c.id">
|
|
510
527
|
{{ c.name }}
|
|
511
528
|
</SelectItem>
|
|
@@ -518,11 +535,11 @@ function levelDot(level) {
|
|
|
518
535
|
<!-- Secrets -->
|
|
519
536
|
<div v-if="secretChecks.length" class="space-y-2">
|
|
520
537
|
<div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
|
|
521
|
-
|
|
538
|
+
{{ t("integration.templates.detail.install.secretsHeader") }}
|
|
522
539
|
</div>
|
|
523
540
|
<p class="text-sm text-muted-foreground">
|
|
524
|
-
|
|
525
|
-
<NuxtLink to="/integrations/secrets" class="text-primary hover:underline">
|
|
541
|
+
{{ t("integration.templates.detail.install.secretsDescription") }}
|
|
542
|
+
<NuxtLink to="/integrations/secrets" class="text-primary hover:underline">{{ t("integration.nav.item.secrets") }}</NuxtLink> {{ t("integration.templates.detail.install.secretsDescriptionSuffix") }}
|
|
526
543
|
</p>
|
|
527
544
|
<ul class="space-y-1">
|
|
528
545
|
<li
|
|
@@ -532,7 +549,7 @@ function levelDot(level) {
|
|
|
532
549
|
>
|
|
533
550
|
<code class="font-mono">{{ s.label }}</code>
|
|
534
551
|
<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 ? "
|
|
552
|
+
{{ s.present ? t("integration.templates.detail.install.secretStored") : t("integration.templates.detail.secrets.missing") }}
|
|
536
553
|
</Badge>
|
|
537
554
|
</li>
|
|
538
555
|
</ul>
|
|
@@ -545,11 +562,11 @@ function levelDot(level) {
|
|
|
545
562
|
|
|
546
563
|
<DialogFooter>
|
|
547
564
|
<Button variant="outline" :disabled="installing" @click="installOpen = false">
|
|
548
|
-
|
|
565
|
+
{{ t("integration.common.action.cancel") }}
|
|
549
566
|
</Button>
|
|
550
567
|
<Button :disabled="installing || !name.trim()" @click="confirmInstall">
|
|
551
568
|
<Loader2 v-if="installing" :size="14" class="mr-1 animate-spin" />
|
|
552
|
-
{{ installing ? "
|
|
569
|
+
{{ installing ? t("integration.templates.detail.install.submitting") : t("integration.templates.detail.install.submit") }}
|
|
553
570
|
</Button>
|
|
554
571
|
</DialogFooter>
|
|
555
572
|
</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>
|