@revenexx/studio-integrations 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +201 -0
- package/README.md +17 -4
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +15 -14
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +91 -19
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -21
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +4 -8
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -2
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +54 -25
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +12 -12
- package/dist/runtime/components/Integrations/Layout.vue +21 -2
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +10 -11
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +2 -5
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +160 -67
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +1075 -303
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
- package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +42 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +100 -34
- package/dist/runtime/components/Integrations/QueryBuilder.vue +8 -1
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -1
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +17 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +2 -1
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +109 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +7 -10
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +21 -37
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +23 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +22 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +170 -48
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +22 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +11 -2
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +189 -46
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +7 -2
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -2
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +30 -17
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +47 -7
- package/dist/runtime/composables/useCredentialTypes.js +4 -4
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +4 -4
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- package/dist/runtime/composables/useIntegrationsNodes.js +4 -4
- package/dist/runtime/composables/useIntegrationsRuns.js +11 -6
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +10 -7
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsTemplates.js +12 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +6 -6
- package/dist/runtime/composables/useIntegrationsWorkflows.js +8 -4
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- package/dist/runtime/data/branchReorder.d.ts +37 -0
- package/dist/runtime/data/branchReorder.js +71 -0
- package/dist/runtime/data/handleId.d.ts +23 -0
- package/dist/runtime/data/handleId.js +6 -0
- package/dist/runtime/data/integrationTemplates.d.ts +39 -0
- package/dist/runtime/data/integrationTemplates.js +15 -0
- package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
- package/dist/runtime/data/nodeIcons.d.ts +2 -0
- package/dist/runtime/data/nodeIcons.js +73 -0
- package/dist/runtime/data/nodeInputData.d.ts +16 -4
- package/dist/runtime/data/nodeInputData.js +4 -0
- package/dist/runtime/data/payloadForm.d.ts +9 -2
- package/dist/runtime/data/payloadForm.js +27 -3
- package/dist/runtime/data/portTone.d.ts +17 -0
- package/dist/runtime/data/portTone.js +7 -0
- package/dist/runtime/data/webhookAuth.d.ts +18 -0
- package/dist/runtime/data/webhookAuth.js +19 -0
- package/dist/runtime/data/workflowIconCatalog.js +2 -5
- package/dist/runtime/pages/integrations/audit.vue +91 -6
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +6 -4
- package/dist/runtime/pages/integrations/credentials.vue +119 -21
- package/dist/runtime/pages/integrations/index.vue +49 -44
- package/dist/runtime/pages/integrations/runs.vue +1 -1
- package/dist/runtime/pages/integrations/schedules.vue +51 -7
- package/dist/runtime/pages/integrations/secrets.vue +29 -7
- package/dist/runtime/pages/integrations/templates/[slug].vue +52 -30
- package/dist/runtime/pages/integrations/templates/index.vue +57 -86
- package/dist/runtime/pages/integrations/workflows/[id].vue +243 -25
- package/dist/runtime/pages/integrations/workflows/index.vue +15 -2
- package/dist/runtime/types/integrations.d.ts +65 -6
- package/dist/runtime/utils/apiError.d.ts +47 -0
- package/dist/runtime/utils/apiError.js +32 -0
- package/dist/runtime/utils/cockpitSources.d.ts +153 -0
- package/dist/runtime/utils/cockpitSources.js +14 -0
- package/dist/runtime/utils/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +29 -0
- package/dist/runtime/utils/localize.js +4 -0
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -0
- package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
- package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
- package/dist/runtime/utils/schedule.d.ts +141 -0
- package/dist/runtime/utils/schedule.js +207 -0
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +53 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/package.json +8 -2
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
SelectValue
|
|
19
19
|
} from "@revenexx/studio";
|
|
20
20
|
import { toast } from "vue-sonner";
|
|
21
|
+
import { resolveDisplayName } from "../../utils/localize";
|
|
21
22
|
const props = defineProps({
|
|
22
23
|
open: { type: Boolean, required: true },
|
|
23
24
|
variables: { type: Object, required: true }
|
|
@@ -89,9 +90,7 @@ function applyJsonDraft(key, type) {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
function descriptionText(v) {
|
|
92
|
-
|
|
93
|
-
if (!d) return "";
|
|
94
|
-
return typeof d === "string" ? d : d.en ?? Object.values(d)[0] ?? "";
|
|
93
|
+
return resolveDisplayName(v.description);
|
|
95
94
|
}
|
|
96
95
|
function writeDescription(key, text) {
|
|
97
96
|
const cur = props.variables[key];
|
|
@@ -148,6 +147,8 @@ async function copyUsage(key) {
|
|
|
148
147
|
<div
|
|
149
148
|
v-for="[key, v] in entries"
|
|
150
149
|
:key="key"
|
|
150
|
+
role="group"
|
|
151
|
+
:aria-label="`Variable ${key}`"
|
|
151
152
|
class="rounded-lg border border-black/[0.06] bg-white p-3"
|
|
152
153
|
>
|
|
153
154
|
<div class="flex items-center gap-2">
|
|
@@ -161,13 +162,6 @@ async function copyUsage(key) {
|
|
|
161
162
|
<Copy :size="12" />
|
|
162
163
|
</button>
|
|
163
164
|
<div class="flex-1" />
|
|
164
|
-
<label class="flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
165
|
-
Required
|
|
166
|
-
<Switch
|
|
167
|
-
:model-value="v.required === true"
|
|
168
|
-
@update:model-value="(val) => patchVar(key, { required: val || void 0 })"
|
|
169
|
-
/>
|
|
170
|
-
</label>
|
|
171
165
|
<button
|
|
172
166
|
type="button"
|
|
173
167
|
class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
|
|
@@ -179,10 +173,15 @@ async function copyUsage(key) {
|
|
|
179
173
|
</div>
|
|
180
174
|
|
|
181
175
|
<div class="mt-2 grid gap-2 sm:grid-cols-[8rem_1fr]">
|
|
176
|
+
<!-- One row per variable, so every id carries its key — otherwise
|
|
177
|
+
the labels of the second row point at the first row's fields. -->
|
|
182
178
|
<div class="space-y-1">
|
|
183
|
-
<Label class="text-mono text-muted-foreground">Type</Label>
|
|
179
|
+
<Label :for="`var-${key}-type`" class="text-mono text-muted-foreground">Type</Label>
|
|
184
180
|
<Select :model-value="v.type" @update:model-value="(t) => changeType(key, t)">
|
|
185
|
-
|
|
181
|
+
<!-- Both: the `id` lets the visible Label's `for` resolve, and the
|
|
182
|
+
`aria-label` names the trigger without relying on an engine
|
|
183
|
+
naming a button from a <label> — see the sweep's docblock. -->
|
|
184
|
+
<SelectTrigger :id="`var-${key}-type`" aria-label="Type" class="h-8 bg-white text-sm">
|
|
186
185
|
<SelectValue />
|
|
187
186
|
</SelectTrigger>
|
|
188
187
|
<SelectContent>
|
|
@@ -192,31 +191,43 @@ async function copyUsage(key) {
|
|
|
192
191
|
</div>
|
|
193
192
|
|
|
194
193
|
<div class="space-y-1">
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
</Label>
|
|
194
|
+
<!-- "Value", not "Default": PO-206. The `for`/`id` still name the
|
|
195
|
+
blob field, which is called `default` and stays that way. -->
|
|
196
|
+
<Label :for="`var-${key}-default`" class="text-mono text-muted-foreground">Value</Label>
|
|
198
197
|
|
|
198
|
+
<!-- Only one of these renders, so they share the one `id` the Label
|
|
199
|
+
points at. The labelable ones take their name from that Label —
|
|
200
|
+
one string, and renaming it turns the tests red instead of
|
|
201
|
+
leaving them green against changed copy. -->
|
|
199
202
|
<Input
|
|
200
203
|
v-if="v.type === 'string'"
|
|
204
|
+
:id="`var-${key}-default`"
|
|
201
205
|
:model-value="typeof v.default === 'string' ? v.default : ''"
|
|
202
206
|
class="h-8 bg-white text-sm"
|
|
203
207
|
@update:model-value="(val) => patchVar(key, { default: String(val) })"
|
|
204
208
|
/>
|
|
205
209
|
<Input
|
|
206
210
|
v-else-if="v.type === 'number'"
|
|
211
|
+
:id="`var-${key}-default`"
|
|
207
212
|
type="number"
|
|
208
213
|
:model-value="typeof v.default === 'number' ? v.default : ''"
|
|
209
214
|
class="h-8 bg-white text-sm"
|
|
210
215
|
@update:model-value="(val) => patchVar(key, { default: val === '' ? void 0 : Number(val) })"
|
|
211
216
|
/>
|
|
212
217
|
<div v-else-if="v.type === 'boolean'" class="flex h-8 items-center">
|
|
218
|
+
<!-- Named out loud: a Switch is a `button role="switch"`, and
|
|
219
|
+
HTML-AAM does not name a button from a <label> — Chromium
|
|
220
|
+
does, other engines need not. -->
|
|
213
221
|
<Switch
|
|
222
|
+
:id="`var-${key}-default`"
|
|
214
223
|
:model-value="v.default === true"
|
|
224
|
+
aria-label="Value"
|
|
215
225
|
@update:model-value="(val) => patchVar(key, { default: val })"
|
|
216
226
|
/>
|
|
217
227
|
</div>
|
|
218
228
|
<div v-else>
|
|
219
229
|
<textarea
|
|
230
|
+
:id="`var-${key}-default`"
|
|
220
231
|
:value="jsonDrafts[key] ?? ''"
|
|
221
232
|
rows="3"
|
|
222
233
|
spellcheck="false"
|
|
@@ -232,8 +243,9 @@ async function copyUsage(key) {
|
|
|
232
243
|
</div>
|
|
233
244
|
|
|
234
245
|
<div class="mt-2 space-y-1">
|
|
235
|
-
<Label class="text-mono text-muted-foreground">Description</Label>
|
|
246
|
+
<Label :for="`var-${key}-desc`" class="text-mono text-muted-foreground">Description</Label>
|
|
236
247
|
<Input
|
|
248
|
+
:id="`var-${key}-desc`"
|
|
237
249
|
:model-value="descriptionText(v)"
|
|
238
250
|
class="h-8 bg-white text-sm"
|
|
239
251
|
placeholder="What this variable is for"
|
|
@@ -244,9 +256,10 @@ async function copyUsage(key) {
|
|
|
244
256
|
|
|
245
257
|
<!-- Add row -->
|
|
246
258
|
<div class="rounded-lg border border-dashed border-black/[0.12] p-3">
|
|
247
|
-
<Label class="text-mono text-muted-foreground">New variable</Label>
|
|
259
|
+
<Label for="var-new-key" class="text-mono text-muted-foreground">New variable</Label>
|
|
248
260
|
<div class="mt-1 flex items-center gap-2">
|
|
249
261
|
<Input
|
|
262
|
+
id="var-new-key"
|
|
250
263
|
v-model="newKey"
|
|
251
264
|
class="h-8 flex-1 bg-white font-mono text-sm"
|
|
252
265
|
placeholder="e.g. base_url"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { RefreshCircle as Loader2, MenuDots as MoreHorizontal } from "#solar-icons";
|
|
3
3
|
import { toast } from "vue-sonner";
|
|
4
|
+
import { apiValidationIssues } from "../../utils/apiError";
|
|
4
5
|
import WorkflowDuplicateDialog from "./WorkflowDuplicateDialog.vue";
|
|
5
6
|
import WorkflowIconView from "./WorkflowIconView.vue";
|
|
6
7
|
import { Layers } from "#solar-icons";
|
|
@@ -48,7 +49,7 @@ watch(groupByTags, (on) => {
|
|
|
48
49
|
} catch {
|
|
49
50
|
}
|
|
50
51
|
});
|
|
51
|
-
const { runs, load: loadRuns, list: listRuns } = useIntegrationsRuns();
|
|
52
|
+
const { runs, loaded: runsLoaded, load: loadRuns, list: listRuns } = useIntegrationsRuns();
|
|
52
53
|
onMounted(() => {
|
|
53
54
|
void loadRuns();
|
|
54
55
|
});
|
|
@@ -86,6 +87,18 @@ async function confirmDelete() {
|
|
|
86
87
|
deleting.value = false;
|
|
87
88
|
}
|
|
88
89
|
}
|
|
90
|
+
async function onToggleActive(wf) {
|
|
91
|
+
try {
|
|
92
|
+
await toggleActive(wf.id);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
const issues = apiValidationIssues(e);
|
|
95
|
+
if (issues.length) {
|
|
96
|
+
toast.error(`Can\u2019t activate \u201C${wf.name}\u201D \u2014 ${issues.length} validation issue${issues.length === 1 ? "" : "s"} to fix. Open the editor to resolve them.`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
toast.error(e?.message ?? "Could not change the workflow status.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
89
102
|
function statusBadge(wf) {
|
|
90
103
|
if (!wf.active) return { label: "Paused", class: "bg-slate-100 text-slate-700 border-slate-200" };
|
|
91
104
|
if (wf.revision === 0) return { label: "Draft", class: "bg-amber-500/10 text-amber-700 border-amber-500/20" };
|
|
@@ -238,14 +251,28 @@ const columns = [
|
|
|
238
251
|
</div>
|
|
239
252
|
</template>
|
|
240
253
|
|
|
254
|
+
<!-- The badge word alone ("Paused", "Failed") also matches a column header
|
|
255
|
+
and the neighbouring column's own words, so each badge carries its
|
|
256
|
+
workflow's name — the rows are plain divs with nothing else to tie a
|
|
257
|
+
value to the workflow it belongs to. -->
|
|
241
258
|
<template #cell-status="{ row: wf }">
|
|
242
|
-
<Badge
|
|
259
|
+
<Badge
|
|
260
|
+
variant="outline"
|
|
261
|
+
:class="['border text-xs font-medium uppercase', statusBadge(wf).class]"
|
|
262
|
+
:aria-label="`${wf.name}: ${statusBadge(wf).label}`"
|
|
263
|
+
>
|
|
243
264
|
{{ statusBadge(wf).label }}
|
|
244
265
|
</Badge>
|
|
245
266
|
</template>
|
|
246
267
|
|
|
268
|
+
<!-- A response without a build state renders `—`; naming that would announce
|
|
269
|
+
"<name> build: —", which claims nothing in more words. No state, no name. -->
|
|
247
270
|
<template #cell-build="{ row: wf }">
|
|
248
|
-
<Badge
|
|
271
|
+
<Badge
|
|
272
|
+
variant="outline"
|
|
273
|
+
:class="['border text-xs font-medium uppercase', buildState(wf).class]"
|
|
274
|
+
:aria-label="wf.buildStatus ? `${wf.name} build: ${buildState(wf).label}` : void 0"
|
|
275
|
+
>
|
|
249
276
|
{{ buildState(wf).label }}
|
|
250
277
|
</Badge>
|
|
251
278
|
</template>
|
|
@@ -273,14 +300,25 @@ const columns = [
|
|
|
273
300
|
|
|
274
301
|
<template #cell-lastRun="{ row: wf }">
|
|
275
302
|
<div v-if="lastRunFor(wf.id)" class="flex items-center gap-2 min-w-0">
|
|
276
|
-
<Badge
|
|
303
|
+
<Badge
|
|
304
|
+
variant="outline"
|
|
305
|
+
:class="['border text-xs font-medium uppercase shrink-0', runBadge(lastRunFor(wf.id).status).class]"
|
|
306
|
+
:aria-label="`${wf.name} last run: ${runBadge(lastRunFor(wf.id).status).label}`"
|
|
307
|
+
>
|
|
277
308
|
{{ runBadge(lastRunFor(wf.id).status).label }}
|
|
278
309
|
</Badge>
|
|
279
310
|
<span class="text-sm tabular-nums text-muted-foreground truncate">
|
|
280
311
|
{{ formatDateTime(lastRunFor(wf.id).startedAt) }}
|
|
281
312
|
</span>
|
|
282
313
|
</div>
|
|
283
|
-
|
|
314
|
+
<!-- Runs arrive after the rows do, so until they are in, "never run" is
|
|
315
|
+
not known — the bare `—` says nothing, and that is the honest answer.
|
|
316
|
+
The name is only claimed once there is something to claim it from. -->
|
|
317
|
+
<span
|
|
318
|
+
v-else
|
|
319
|
+
class="text-sm text-muted-foreground"
|
|
320
|
+
:aria-label="runsLoaded ? `${wf.name} last run: Never run` : void 0"
|
|
321
|
+
>—</span>
|
|
284
322
|
</template>
|
|
285
323
|
|
|
286
324
|
<template #cell-updated="{ row: wf }">
|
|
@@ -293,7 +331,9 @@ const columns = [
|
|
|
293
331
|
<div @click.stop>
|
|
294
332
|
<DropdownMenu>
|
|
295
333
|
<DropdownMenuTrigger as-child>
|
|
296
|
-
|
|
334
|
+
<!-- Icon-only, so it needs a name of its own — the rows are plain
|
|
335
|
+
divs, which leaves nothing else to address this button by. -->
|
|
336
|
+
<Button variant="ghost" size="icon" class="size-7" :aria-label="`Actions for ${wf.name}`">
|
|
297
337
|
<MoreHorizontal :size="14" />
|
|
298
338
|
</Button>
|
|
299
339
|
</DropdownMenuTrigger>
|
|
@@ -306,7 +346,7 @@ const columns = [
|
|
|
306
346
|
<DropdownMenuItem @select="askDuplicate(wf)">
|
|
307
347
|
Duplicate
|
|
308
348
|
</DropdownMenuItem>
|
|
309
|
-
<DropdownMenuItem @select="
|
|
349
|
+
<DropdownMenuItem @select="onToggleActive(wf)">
|
|
310
350
|
{{ wf.active ? "Pause" : "Activate" }}
|
|
311
351
|
</DropdownMenuItem>
|
|
312
352
|
<DropdownMenuSeparator />
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
2
2
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
3
|
+
import { useLoadGate } from "./useLoadGate.js";
|
|
3
4
|
export function useCredentialTypes() {
|
|
4
5
|
const types = useState("integrations.credentialTypes", () => []);
|
|
5
6
|
const loading = useState("integrations.credentialTypes.loading", () => false);
|
|
6
7
|
const error = useState("integrations.credentialTypes.error", () => null);
|
|
7
8
|
const loaded = useState("integrations.credentialTypes.loaded", () => false);
|
|
9
|
+
const gate = useLoadGate("integrations.credentialTypes", { loading, loaded });
|
|
8
10
|
const api = useIntegrationsApi();
|
|
9
11
|
function normalize(res) {
|
|
10
12
|
const obj = res;
|
|
@@ -17,9 +19,7 @@ export function useCredentialTypes() {
|
|
|
17
19
|
return e?.message ?? fallback;
|
|
18
20
|
}
|
|
19
21
|
async function load(force = false) {
|
|
20
|
-
if (
|
|
21
|
-
if (loaded.value && !force) return types.value;
|
|
22
|
-
loading.value = true;
|
|
22
|
+
if (!await gate.claim(force)) return types.value;
|
|
23
23
|
error.value = null;
|
|
24
24
|
try {
|
|
25
25
|
types.value = normalize(await api.get("/credential-types"));
|
|
@@ -27,7 +27,7 @@ export function useCredentialTypes() {
|
|
|
27
27
|
} catch (e) {
|
|
28
28
|
error.value = captureError(e, "Could not load credential types.");
|
|
29
29
|
} finally {
|
|
30
|
-
|
|
30
|
+
gate.release();
|
|
31
31
|
}
|
|
32
32
|
return types.value;
|
|
33
33
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
2
2
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
3
|
+
import { useLoadGate } from "./useLoadGate.js";
|
|
3
4
|
export function useIntegrationsAudit() {
|
|
4
5
|
const api = useIntegrationsApi();
|
|
5
6
|
const entries = useState("int.audit.entries", () => []);
|
|
6
7
|
const nextCursor = useState("int.audit.cursor", () => null);
|
|
7
8
|
const loading = useState("int.audit.loading", () => false);
|
|
8
9
|
const error = useState("int.audit.error", () => null);
|
|
10
|
+
const gate = useLoadGate("int.audit", { loading });
|
|
9
11
|
function qs(filters, cursor) {
|
|
10
12
|
const p = new URLSearchParams();
|
|
11
13
|
if (filters.sub) p.set("sub", filters.sub);
|
|
@@ -16,8 +18,7 @@ export function useIntegrationsAudit() {
|
|
|
16
18
|
return p.toString();
|
|
17
19
|
}
|
|
18
20
|
async function load(filters = {}, append = false) {
|
|
19
|
-
if (
|
|
20
|
-
loading.value = true;
|
|
21
|
+
if (!await gate.claim()) return;
|
|
21
22
|
error.value = null;
|
|
22
23
|
try {
|
|
23
24
|
const res = await api.get(
|
|
@@ -29,7 +30,7 @@ export function useIntegrationsAudit() {
|
|
|
29
30
|
} catch (e) {
|
|
30
31
|
error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? "Could not load the audit log.";
|
|
31
32
|
} finally {
|
|
32
|
-
|
|
33
|
+
gate.release();
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
function loadMore(filters = {}) {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
4
|
+
import { useLoadGate } from "./useLoadGate.js";
|
|
4
5
|
export function useIntegrationsCredentials() {
|
|
5
6
|
const credentials = useState("integrations.credentials", () => []);
|
|
6
7
|
const loading = useState("integrations.credentials.loading", () => false);
|
|
7
8
|
const error = useState("integrations.credentials.error", () => null);
|
|
8
9
|
const loaded = useState("integrations.credentials.loaded", () => false);
|
|
10
|
+
const gate = useLoadGate("integrations.credentials", { loading, loaded });
|
|
9
11
|
const api = useIntegrationsApi();
|
|
10
12
|
function normalize(res) {
|
|
11
13
|
const obj = res;
|
|
@@ -21,9 +23,7 @@ export function useIntegrationsCredentials() {
|
|
|
21
23
|
return e?.message ?? fallback;
|
|
22
24
|
}
|
|
23
25
|
async function load(force = false) {
|
|
24
|
-
if (
|
|
25
|
-
if (loaded.value && !force) return credentials.value;
|
|
26
|
-
loading.value = true;
|
|
26
|
+
if (!await gate.claim(force)) return credentials.value;
|
|
27
27
|
error.value = null;
|
|
28
28
|
try {
|
|
29
29
|
credentials.value = normalize(await api.get("/credentials"));
|
|
@@ -31,7 +31,7 @@ export function useIntegrationsCredentials() {
|
|
|
31
31
|
} catch (e) {
|
|
32
32
|
error.value = captureError(e, "Could not load credentials.");
|
|
33
33
|
} finally {
|
|
34
|
-
|
|
34
|
+
gate.release();
|
|
35
35
|
}
|
|
36
36
|
return credentials.value;
|
|
37
37
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
|
+
import { useLoadGate } from "./useLoadGate.js";
|
|
3
4
|
export function useIntegrationsDeadLetters() {
|
|
4
5
|
const api = useIntegrationsApi();
|
|
5
6
|
async function list(workflowId, opts = {}) {
|
|
@@ -17,15 +18,14 @@ export function useIntegrationsDeadLetters() {
|
|
|
17
18
|
}
|
|
18
19
|
const replay = (id) => api.post(`/dead-letters/${id}/replay`, {});
|
|
19
20
|
const discard = (id) => api.post(`/dead-letters/${id}/discard`, {});
|
|
20
|
-
const { workflows, load: loadWorkflows } = useIntegrationsWorkflows();
|
|
21
|
+
const { workflows, load: loadWorkflows, loaded: workflowsLoaded } = useIntegrationsWorkflows();
|
|
21
22
|
const globalItems = useState("integrations.deadletters.global", () => []);
|
|
22
23
|
const globalLoading = useState("integrations.deadletters.global.loading", () => false);
|
|
23
24
|
const globalLoaded = useState("integrations.deadletters.global.loaded", () => false);
|
|
25
|
+
const gate = useLoadGate("integrations.deadletters.global", { loading: globalLoading, loaded: globalLoaded });
|
|
24
26
|
const pendingCount = computed(() => globalItems.value.filter((i) => i.status === "pending").length);
|
|
25
27
|
async function loadGlobal(force = false) {
|
|
26
|
-
if (
|
|
27
|
-
if (globalLoaded.value && !force) return globalItems.value;
|
|
28
|
-
globalLoading.value = true;
|
|
28
|
+
if (!await gate.claim(force)) return globalItems.value;
|
|
29
29
|
try {
|
|
30
30
|
await loadWorkflows();
|
|
31
31
|
const MAX_PAGES = 20;
|
|
@@ -41,10 +41,11 @@ export function useIntegrationsDeadLetters() {
|
|
|
41
41
|
return out;
|
|
42
42
|
}));
|
|
43
43
|
globalItems.value = per.flatMap((p) => p.status === "fulfilled" ? p.value : []).sort((a, b) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime());
|
|
44
|
-
globalLoaded.value =
|
|
44
|
+
globalLoaded.value = workflowsLoaded.value && per.every((p) => p.status === "fulfilled");
|
|
45
45
|
} finally {
|
|
46
|
-
|
|
46
|
+
gate.release();
|
|
47
47
|
}
|
|
48
|
+
return globalItems.value;
|
|
48
49
|
}
|
|
49
50
|
return { list, replay, discard, loadGlobal, globalItems, globalLoading, pendingCount };
|
|
50
51
|
}
|
|
@@ -2,15 +2,15 @@ import { applyNodeCatalog, findManifest } from "../data/integrationNodeCatalog.j
|
|
|
2
2
|
import { resolveLocalized } from "../utils/localize.js";
|
|
3
3
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
4
4
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
5
|
+
import { useLoadGate } from "./useLoadGate.js";
|
|
5
6
|
export function useIntegrationsNodes() {
|
|
6
7
|
const loading = useState("integrations.nodes.loading", () => false);
|
|
7
8
|
const error = useState("integrations.nodes.error", () => null);
|
|
8
9
|
const loaded = useState("integrations.nodes.loaded", () => false);
|
|
10
|
+
const gate = useLoadGate("integrations.nodes", { loading, loaded });
|
|
9
11
|
const api = useIntegrationsApi();
|
|
10
12
|
async function load(force = false) {
|
|
11
|
-
if (
|
|
12
|
-
if (loaded.value && !force) return;
|
|
13
|
-
loading.value = true;
|
|
13
|
+
if (!await gate.claim(force)) return;
|
|
14
14
|
error.value = null;
|
|
15
15
|
try {
|
|
16
16
|
const res = await api.get("/nodes");
|
|
@@ -29,7 +29,7 @@ export function useIntegrationsNodes() {
|
|
|
29
29
|
applyNodeCatalog([]);
|
|
30
30
|
error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? "Could not load nodes.";
|
|
31
31
|
} finally {
|
|
32
|
-
|
|
32
|
+
gate.release();
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
return { loading, error, loaded, load, findManifest, resolveLocalized };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
4
|
+
import { useLoadGate } from "./useLoadGate.js";
|
|
4
5
|
function mapStatus(status) {
|
|
5
6
|
switch (status) {
|
|
6
7
|
case "completed":
|
|
@@ -29,9 +30,10 @@ export function useIntegrationsRuns() {
|
|
|
29
30
|
const error = useState("integrations.runs.error", () => null);
|
|
30
31
|
const loaded = useState("integrations.runs.loaded", () => false);
|
|
31
32
|
const reconciling = useState("integrations.runs.reconciling", () => false);
|
|
33
|
+
const gate = useLoadGate("integrations.runs", { loading, loaded });
|
|
32
34
|
const SYNC_LIMIT = 5;
|
|
33
35
|
const api = useIntegrationsApi();
|
|
34
|
-
const { workflows, load: loadWorkflows } = useIntegrationsWorkflows();
|
|
36
|
+
const { workflows, load: loadWorkflows, loaded: workflowsLoaded } = useIntegrationsWorkflows();
|
|
35
37
|
function mapRun(r, workflowName) {
|
|
36
38
|
const startedMs = r.started_at ? new Date(r.started_at).getTime() : null;
|
|
37
39
|
const finishedMs = r.completed_at ? new Date(r.completed_at).getTime() : null;
|
|
@@ -45,15 +47,18 @@ export function useIntegrationsRuns() {
|
|
|
45
47
|
triggerType: "manual",
|
|
46
48
|
startedAt: r.started_at ?? "",
|
|
47
49
|
finishedAt: r.completed_at ?? null,
|
|
50
|
+
// The list carries the reason a run failed; dropping it here was why every
|
|
51
|
+
// failure read the same wherever the list is shown. An empty string is not
|
|
52
|
+
// a reason — it has to fall through to whatever the surface says instead,
|
|
53
|
+
// or it renders as a blank line where the sentence belongs.
|
|
54
|
+
errorMessage: r.error || void 0,
|
|
48
55
|
// Derived from the timestamps — the list payload carries no duration.
|
|
49
56
|
durationMs: startedMs != null && finishedMs != null && finishedMs >= startedMs ? finishedMs - startedMs : null,
|
|
50
57
|
steps: []
|
|
51
58
|
};
|
|
52
59
|
}
|
|
53
60
|
async function load(force = false) {
|
|
54
|
-
if (
|
|
55
|
-
if (loaded.value && !force) return runs.value;
|
|
56
|
-
loading.value = true;
|
|
61
|
+
if (!await gate.claim(force)) return runs.value;
|
|
57
62
|
error.value = null;
|
|
58
63
|
try {
|
|
59
64
|
await loadWorkflows();
|
|
@@ -92,11 +97,11 @@ export function useIntegrationsRuns() {
|
|
|
92
97
|
reconciling.value = false;
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
|
-
loaded.value =
|
|
100
|
+
loaded.value = workflowsLoaded.value && perWorkflow.every((p) => p.status === "fulfilled");
|
|
96
101
|
} catch (e) {
|
|
97
102
|
error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? "Could not load runs.";
|
|
98
103
|
} finally {
|
|
99
|
-
|
|
104
|
+
gate.release();
|
|
100
105
|
}
|
|
101
106
|
return runs.value;
|
|
102
107
|
}
|
|
@@ -4,6 +4,10 @@ import type { ScheduleEntry } from '../types/integrations.js';
|
|
|
4
4
|
* sourced from the real triggers sub-resource (useIntegrationsTriggers).
|
|
5
5
|
* Next/last-run timestamps are mocked deterministically (no telemetry
|
|
6
6
|
* endpoint yet).
|
|
7
|
+
*
|
|
8
|
+
* Unordered on purpose: the page's table sorts, by its own default and by
|
|
9
|
+
* whichever column the operator clicks, so an order established here would be
|
|
10
|
+
* replaced before anyone saw it.
|
|
7
11
|
*/
|
|
8
12
|
export declare function useIntegrationsSchedules(): {
|
|
9
13
|
entries: import("vue").ComputedRef<ScheduleEntry[]>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
|
+
import { scheduleTimezone } from "../utils/schedule.js";
|
|
2
3
|
export function useIntegrationsSchedules() {
|
|
3
4
|
const { all, loading, loadAll } = useIntegrationsTriggers();
|
|
4
5
|
const entries = computed(
|
|
@@ -10,13 +11,19 @@ export function useIntegrationsSchedules() {
|
|
|
10
11
|
workflowId: t.workflow_id,
|
|
11
12
|
workflowName: t.workflowName,
|
|
12
13
|
triggerId,
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
// Never invented: a trigger without an expression says so on the page.
|
|
15
|
+
// Standing in `* * * * *` for it would read as "every minute", and with
|
|
16
|
+
// the expression no longer on screen that guess would be invisible.
|
|
17
|
+
cron: typeof cfg.cron === "string" && cfg.cron.trim() !== "" ? cfg.cron : null,
|
|
18
|
+
// The schedule's own zone, or the platform's default where it has none —
|
|
19
|
+
// from the same helper the trigger settings read, so one schedule cannot
|
|
20
|
+
// be shown in two zones on two screens.
|
|
21
|
+
timezone: scheduleTimezone(cfg.timezone),
|
|
15
22
|
active: t.active,
|
|
16
23
|
nextRunAt: t.active ? mockNextRun(t.workflow_id, triggerId) : null,
|
|
17
24
|
lastRunAt: t.active ? mockLastRun(t.workflow_id, triggerId) : null
|
|
18
25
|
};
|
|
19
|
-
})
|
|
26
|
+
})
|
|
20
27
|
);
|
|
21
28
|
const total = computed(() => entries.value.length);
|
|
22
29
|
const activeCount = computed(() => entries.value.filter((e) => e.active).length);
|
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
4
|
+
import { useLoadGate } from "./useLoadGate.js";
|
|
4
5
|
const KEY_PATTERN = /^[A-Z0-9_]+$/;
|
|
5
6
|
export function useIntegrationsSecrets() {
|
|
6
7
|
const secrets = useState("integrations.secrets", () => []);
|
|
7
8
|
const loading = useState("integrations.secrets.loading", () => false);
|
|
8
9
|
const error = useState("integrations.secrets.error", () => null);
|
|
9
10
|
const loaded = useState("integrations.secrets.loaded", () => false);
|
|
11
|
+
const gate = useLoadGate("integrations.secrets", { loading, loaded });
|
|
10
12
|
const api = useIntegrationsApi();
|
|
11
13
|
function isValidKey(key) {
|
|
12
14
|
return KEY_PATTERN.test(key);
|
|
13
15
|
}
|
|
14
16
|
function normalize(res) {
|
|
15
17
|
const obj = res;
|
|
18
|
+
if (Array.isArray(obj?.data)) return obj.data.map(toRecord);
|
|
19
|
+
if (Array.isArray(obj?.secrets)) return obj.secrets.map(toRecord);
|
|
16
20
|
if (Array.isArray(obj?.keys)) return obj.keys.map((key) => ({ key, created_at: "", updated_at: "" }));
|
|
17
|
-
if (Array.isArray(
|
|
18
|
-
if (Array.isArray(obj?.secrets)) return obj.secrets;
|
|
19
|
-
if (Array.isArray(res)) return res;
|
|
21
|
+
if (Array.isArray(res)) return res.map(toRecord);
|
|
20
22
|
return [];
|
|
21
23
|
}
|
|
24
|
+
function toRecord(r) {
|
|
25
|
+
return { key: r.key ?? "", created_at: r.created_at ?? "", updated_at: r.updated_at ?? "" };
|
|
26
|
+
}
|
|
22
27
|
function captureError(e, fallback) {
|
|
23
28
|
if (e instanceof IntegrationsApiError) return e.message;
|
|
24
29
|
return e?.message ?? fallback;
|
|
25
30
|
}
|
|
26
31
|
async function load(force = false) {
|
|
27
|
-
if (
|
|
28
|
-
if (loaded.value && !force) return secrets.value;
|
|
29
|
-
loading.value = true;
|
|
32
|
+
if (!await gate.claim(force)) return secrets.value;
|
|
30
33
|
error.value = null;
|
|
31
34
|
try {
|
|
32
35
|
secrets.value = normalize(await api.get("/secrets"));
|
|
@@ -34,7 +37,7 @@ export function useIntegrationsSecrets() {
|
|
|
34
37
|
} catch (e) {
|
|
35
38
|
error.value = captureError(e, "Could not load secrets.");
|
|
36
39
|
} finally {
|
|
37
|
-
|
|
40
|
+
gate.release();
|
|
38
41
|
}
|
|
39
42
|
return secrets.value;
|
|
40
43
|
}
|
|
@@ -5,7 +5,7 @@ export declare function useIntegrationsTemplates(): {
|
|
|
5
5
|
loading: any;
|
|
6
6
|
error: any;
|
|
7
7
|
total: import("vue").ComputedRef<any>;
|
|
8
|
-
|
|
8
|
+
startingPoints: import("vue").ComputedRef<any>;
|
|
9
9
|
load: (force?: boolean) => Promise<WorkflowTemplate[]>;
|
|
10
10
|
list: () => WorkflowTemplate[];
|
|
11
11
|
get: (slug: string) => WorkflowTemplate | null;
|
|
@@ -2,6 +2,7 @@ import { computed } from "vue";
|
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
4
4
|
import { resolveLocalized } from "../utils/localize.js";
|
|
5
|
+
import { useLoadGate } from "./useLoadGate.js";
|
|
5
6
|
const BLOB_VERSION = "v0-draft";
|
|
6
7
|
const INDUSTRY_VALUES = ["mechanical-engineering", "chemicals", "wholesale", "manufacturing", "retail", "any"];
|
|
7
8
|
function plainExcerpt(md) {
|
|
@@ -34,14 +35,18 @@ function mapListItem(api) {
|
|
|
34
35
|
triggerTypes: api.triggerTypes ?? [],
|
|
35
36
|
images: api.images ?? [],
|
|
36
37
|
package: api.package ? { name: api.package.name, label: api.package.label ?? null } : null,
|
|
37
|
-
|
|
38
|
+
// The registry documents this field as free-form and returns it as nullable,
|
|
39
|
+
// so "no category" is a state and not an error. It used to become `sync` —
|
|
40
|
+
// a category nothing publishes, so the studio was badging a template with a
|
|
41
|
+
// name no publisher had chosen. Empty travels instead, and every surface that
|
|
42
|
+
// draws a chip leaves it out rather than filling it in (PO-243).
|
|
43
|
+
category: api.category || "",
|
|
38
44
|
industries: mapIndustries(api.industries),
|
|
39
45
|
vendors: api.vendors ?? [],
|
|
40
46
|
requiredSecrets: [],
|
|
41
47
|
level: api.level ?? "beginner",
|
|
42
48
|
outline: [],
|
|
43
49
|
nodeCount: 0,
|
|
44
|
-
installs: 0,
|
|
45
50
|
publisher: publisherFor(api.slug, api.vendors ?? []),
|
|
46
51
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
47
52
|
blob: emptyBlob(resolveLocalized(api.name)),
|
|
@@ -83,6 +88,7 @@ export function useIntegrationsTemplates() {
|
|
|
83
88
|
const loaded = useState("integrations.templates.loaded", () => false);
|
|
84
89
|
const loading = useState("integrations.templates.loading", () => false);
|
|
85
90
|
const error = useState("integrations.templates.error", () => null);
|
|
91
|
+
const gate = useLoadGate("integrations.templates", { loading, loaded });
|
|
86
92
|
const api = useIntegrationsApi();
|
|
87
93
|
function unwrap(res) {
|
|
88
94
|
return res?.data ?? res;
|
|
@@ -91,9 +97,7 @@ export function useIntegrationsTemplates() {
|
|
|
91
97
|
return e instanceof IntegrationsApiError ? e.message : e?.message ?? fallback;
|
|
92
98
|
}
|
|
93
99
|
async function load(force = false) {
|
|
94
|
-
if (
|
|
95
|
-
if (loaded.value && !force) return templates.value;
|
|
96
|
-
loading.value = true;
|
|
100
|
+
if (!await gate.claim(force)) return templates.value;
|
|
97
101
|
error.value = null;
|
|
98
102
|
try {
|
|
99
103
|
const list2 = unwrap(await api.get("/templates")) ?? [];
|
|
@@ -104,7 +108,7 @@ export function useIntegrationsTemplates() {
|
|
|
104
108
|
templates.value = [];
|
|
105
109
|
loaded.value = true;
|
|
106
110
|
} finally {
|
|
107
|
-
|
|
111
|
+
gate.release();
|
|
108
112
|
}
|
|
109
113
|
return templates.value;
|
|
110
114
|
}
|
|
@@ -165,6 +169,6 @@ export function useIntegrationsTemplates() {
|
|
|
165
169
|
return instantiate(slug);
|
|
166
170
|
}
|
|
167
171
|
const total = computed(() => templates.value.length);
|
|
168
|
-
const
|
|
169
|
-
return { templates, loaded, loading, error, total,
|
|
172
|
+
const startingPoints = computed(() => templates.value.slice(0, 4));
|
|
173
|
+
return { templates, loaded, loading, error, total, startingPoints, load, list, get, fetchDetail, ensureDetails, requirements, instantiate, install };
|
|
170
174
|
}
|