@revenexx/studio-integrations 2.2.0 → 2.4.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 +26 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +5 -4
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +4 -3
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +2 -1
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +2 -2
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +2 -7
- package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +3 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +7 -3
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +6 -3
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +3 -23
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +6 -3
- package/dist/runtime/composables/useIntegrationsRuns.js +3 -0
- package/dist/runtime/data/nodeIcons.d.ts +8 -5
- package/dist/runtime/data/nodeIcons.js +5 -1
- package/dist/runtime/i18n/messages/de.json +2 -2
- package/dist/runtime/i18n/messages/en.json +2 -2
- package/dist/runtime/pages/integrations/credentials.vue +4 -3
- package/dist/runtime/pages/integrations/templates/[slug].vue +4 -7
- package/dist/runtime/pages/integrations/workflows/[id].vue +29 -12
- package/dist/runtime/types/integrations.d.ts +6 -0
- package/dist/runtime/utils/runFeedQuery.d.ts +14 -0
- package/dist/runtime/utils/runFeedQuery.js +3 -0
- package/dist/runtime/utils/templateDescription.d.ts +1 -0
- package/dist/runtime/utils/templateDescription.js +38 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @revenexx/studio-integrations
|
|
2
2
|
|
|
3
|
+
## 2.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 22eb158: Run in the workflow editor no longer starts the saved version while the canvas shows unsaved changes (PO-567). A changed setting used to be run as its old value, with nothing on screen to say so. Run now stands in the header beside Save rather than in the canvas's control pane, and while there is unsaved work it reads **Save & run** (German **Speichern & Run**): the press saves first and runs the workflow as it then stands. A save that is refused, or waits on the question whether to keep the work as paused, starts no run. The canvas's right-click menu names the save the same way. The header's dispatch badge now shows the mode as entered in the settings, as the name beside it already did, because that is the mode the next run goes out in. `WorkflowEditor` loses its `runWaiting` prop and gains `runSaves`.
|
|
8
|
+
|
|
9
|
+
## 2.3.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 95d8a13: A run somebody terminated is no longer offered a resume from its row (PO-355). The studio shows a terminated run as failed, and the row's actions menu offered it **Resume**, which the platform refuses: it resumes only a run that failed on its own. The row now asks the platform's own status word, as the run's record already did, so the resume appears only where it can work. The run still reads as failed, and a retry is still offered. `WorkflowRun` gains an optional `apiStatus` carrying that word.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 903b632: A credential form no longer dies on a list option meaning "no choice" (PO-424). A system that offered an option with an empty value in one of its lists stopped that list opening, in the New credential dialog (from the credentials list or from a node's credential setting) and in a credential's detail view, and nothing in the dialog could be closed afterwards. Those lists now carry their values the way a node's settings have since PO-422: the option is offered and can be chosen like any other, and the credential stores what the system declared.
|
|
18
|
+
- 76ec989: A template's description can no longer run script in the studio (PO-524). The "About" section of a template's page rendered the registry's Markdown as whatever HTML it carried, so an image with an `onerror` handler or a `javascript:` link in a description ran inside the tenant's session. The description is now sanitised with DOMPurify after it is rendered: headings, lists, emphasis, code, links and images still show, while script, event handlers, script links, style sheets, inline styles and form controls are removed. A task-list checkbox in a description now shows as its text alone. The module gains `dompurify` as a runtime dependency.
|
|
19
|
+
- 1231fcc: PO-552: the Linear nodes draw their own icons instead of an empty box.
|
|
20
|
+
|
|
21
|
+
Create Issue, Get Issue and Add Comment named icons the studio's table did not
|
|
22
|
+
know, so all three showed the generic box under Linear → Issues. They now draw a
|
|
23
|
+
ticket with a plus, a ticket and a speech bubble with text, and List Projects,
|
|
24
|
+
new in the same package, draws a folder.
|
|
25
|
+
|
|
26
|
+
- 1070ae7: The blank form of the New workflow dialog opens with empty fields (PO-562): the example name and description are gone, so nothing reads as already filled in. The name is marked as required from the start, with the same red asterisk the node settings use.
|
|
27
|
+
- 44ced2f: A node icon declared as a Lucide name (`lucide:<kebab-name>`) is drawn as named, without the studio's translation table, so a new node package shows its icons without a studio change (PO-566). The `mdi:*` names older packages declare are still translated; that table takes no new entries. A template's flow preview now draws node icons the same way as the canvas and the palette — it used to skip the table and draw most of them as a box.
|
|
28
|
+
|
|
3
29
|
## 2.2.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
package/dist/module.json
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "@revenexx/studio";
|
|
21
21
|
import { toast } from "vue-sonner";
|
|
22
22
|
import { resolveLocalized } from "../../utils/localize";
|
|
23
|
+
import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
|
|
23
24
|
import { errorReason } from "../../utils/apiError";
|
|
24
25
|
const props = defineProps({
|
|
25
26
|
open: { type: Boolean, required: true },
|
|
@@ -89,7 +90,7 @@ function buildConfigPayload() {
|
|
|
89
90
|
const out = {};
|
|
90
91
|
for (const f of fieldsOf(pickedType.value)) {
|
|
91
92
|
const v = form.config[f.key];
|
|
92
|
-
if (v !== void 0
|
|
93
|
+
if (v !== void 0) out[f.key] = v;
|
|
93
94
|
}
|
|
94
95
|
return out;
|
|
95
96
|
}
|
|
@@ -217,14 +218,14 @@ async function submit() {
|
|
|
217
218
|
</div>
|
|
218
219
|
<Select
|
|
219
220
|
v-else-if="f.type === 'select'"
|
|
220
|
-
:model-value="form.config[f.key]
|
|
221
|
-
@update:model-value="(v) => form.config[f.key] = v"
|
|
221
|
+
:model-value="toModelValue(form.config[f.key], f.options)"
|
|
222
|
+
@update:model-value="(v) => form.config[f.key] = fromModelValue(v)"
|
|
222
223
|
>
|
|
223
224
|
<SelectTrigger :id="`create-field-${f.key}`" class="bg-white">
|
|
224
225
|
<SelectValue :placeholder="t('integration.credentials.shared.field.select.placeholder')" />
|
|
225
226
|
</SelectTrigger>
|
|
226
227
|
<SelectContent>
|
|
227
|
-
<SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="
|
|
228
|
+
<SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="toItemValue(opt.value)">
|
|
228
229
|
{{ resolveLocalized(opt.label) }}
|
|
229
230
|
</SelectItem>
|
|
230
231
|
</SelectContent>
|
|
@@ -36,6 +36,7 @@ import { relativeTime } from "../../utils/relativeTime";
|
|
|
36
36
|
import { useNow } from "../../composables/useNow";
|
|
37
37
|
import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
|
|
38
38
|
import { resolveLocalized } from "../../utils/localize";
|
|
39
|
+
import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
|
|
39
40
|
const props = defineProps({
|
|
40
41
|
open: { type: Boolean, required: true },
|
|
41
42
|
credential: { type: [Object, null], required: true },
|
|
@@ -305,14 +306,14 @@ async function connect() {
|
|
|
305
306
|
</div>
|
|
306
307
|
<Select
|
|
307
308
|
v-else-if="f.type === 'select'"
|
|
308
|
-
:model-value="form.config[f.key]
|
|
309
|
-
@update:model-value="(v) => form.config[f.key] = v"
|
|
309
|
+
:model-value="toModelValue(form.config[f.key], f.options)"
|
|
310
|
+
@update:model-value="(v) => form.config[f.key] = fromModelValue(v)"
|
|
310
311
|
>
|
|
311
312
|
<SelectTrigger :id="`detail-field-${f.key}`" class="bg-white" v-bind="configFieldAria(f.key)">
|
|
312
313
|
<SelectValue :placeholder="t('integration.credentials.shared.field.select.placeholder')" />
|
|
313
314
|
</SelectTrigger>
|
|
314
315
|
<SelectContent>
|
|
315
|
-
<SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="
|
|
316
|
+
<SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="toItemValue(opt.value)">
|
|
316
317
|
{{ resolveLocalized(opt.label) }}
|
|
317
318
|
</SelectItem>
|
|
318
319
|
</SelectContent>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@revenexx/studio";
|
|
12
12
|
import { toast } from "vue-sonner";
|
|
13
13
|
import { errorReason } from "../../utils/apiError";
|
|
14
|
+
import { acceptsResume } from "../../utils/runFeedQuery";
|
|
14
15
|
const props = defineProps({
|
|
15
16
|
run: { type: Object, required: true }
|
|
16
17
|
});
|
|
@@ -27,7 +28,7 @@ function viewReplay() {
|
|
|
27
28
|
const hasExecution = computed(() => !!props.run.temporalRunId);
|
|
28
29
|
const isActive = computed(() => (props.run.status === "running" || props.run.status === "queued") && hasExecution.value);
|
|
29
30
|
const isTerminal = computed(() => ["succeeded", "failed", "cancelled"].includes(props.run.status));
|
|
30
|
-
const canResume = computed(() => props.run.status
|
|
31
|
+
const canResume = computed(() => acceptsResume(props.run.apiStatus, props.run.status) && hasExecution.value);
|
|
31
32
|
const hasAny = computed(() => isActive.value || isTerminal.value);
|
|
32
33
|
const busy = ref(false);
|
|
33
34
|
function actionLabel(a) {
|
|
@@ -7,6 +7,7 @@ import { IntegrationsApiError, errorReason } from "../../utils/apiError";
|
|
|
7
7
|
import { createPollBudget, SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
8
8
|
import { formatDuration } from "../../utils/formatDuration";
|
|
9
9
|
import { unwrapRun } from "../../utils/runEnvelope";
|
|
10
|
+
import { acceptsResume } from "../../utils/runFeedQuery";
|
|
10
11
|
import { splitRunOutputs } from "../../utils/runOutputs";
|
|
11
12
|
const props = defineProps({
|
|
12
13
|
run: { type: Object, required: true },
|
|
@@ -527,8 +528,7 @@ const apiStatusRaw = computed(() => pickStr(runData.value, ["status"]));
|
|
|
527
528
|
const isActive = computed(() => !isTerminal.value);
|
|
528
529
|
const canStop = computed(() => isActive.value && hasTemporal.value);
|
|
529
530
|
const canResume = computed(() => {
|
|
530
|
-
|
|
531
|
-
return failed && hasTemporal.value;
|
|
531
|
+
return acceptsResume(apiStatusRaw.value || props.run.apiStatus, props.run.status) && hasTemporal.value;
|
|
532
532
|
});
|
|
533
533
|
const hasActions = computed(() => canStop.value || canResume.value);
|
|
534
534
|
const armed = ref(null);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, onMounted } from "vue";
|
|
3
|
-
import
|
|
3
|
+
import { nodeIcon } from "../../data/nodeIcons";
|
|
4
4
|
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
5
5
|
import { findManifest, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
6
6
|
import { resolveLocalized } from "../../utils/localize";
|
|
@@ -64,7 +64,7 @@ const boxes = computed(() => {
|
|
|
64
64
|
label: manifest ? resolveLocalized(manifest.name) : prettify(n.nodeSlug),
|
|
65
65
|
subtitle: groups[groups.length - 1] ?? "",
|
|
66
66
|
manifest,
|
|
67
|
-
icon:
|
|
67
|
+
icon: nodeIcon(manifest?.icon),
|
|
68
68
|
categoryColor: NODE_CATEGORIES.find((c) => c.key === manifest?.category)?.color ?? "bg-muted text-foreground"
|
|
69
69
|
});
|
|
70
70
|
});
|
|
@@ -74,11 +74,6 @@ function prettify(slug) {
|
|
|
74
74
|
const tail = slug.includes(":") ? slug.split(":").pop() : slug;
|
|
75
75
|
return tail.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
|
|
76
76
|
}
|
|
77
|
-
function iconFor(name) {
|
|
78
|
-
if (!name) return Lucide.Box;
|
|
79
|
-
const cleaned = (name.includes(":") ? name.split(":").pop() : name).split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
80
|
-
return Lucide[cleaned] || Lucide.Box;
|
|
81
|
-
}
|
|
82
77
|
function stepPath(x1, y1, x2, y2) {
|
|
83
78
|
if (y1 === y2) return `M ${x1} ${y1} L ${x2} ${y2}`;
|
|
84
79
|
if (x2 - x1 >= 48) {
|
|
@@ -11,6 +11,8 @@ type __VLS_Props = {
|
|
|
11
11
|
targetIsTrigger?: boolean;
|
|
12
12
|
/** Busy flag for the Run item. */
|
|
13
13
|
runBusy?: boolean;
|
|
14
|
+
/** The workflow has unsaved work, so the Run item saves it first — and says so. */
|
|
15
|
+
runSaves?: boolean;
|
|
14
16
|
};
|
|
15
17
|
declare var __VLS_14: {};
|
|
16
18
|
type __VLS_Slots = {} & {
|
|
@@ -16,7 +16,8 @@ const props = defineProps({
|
|
|
16
16
|
selectionCount: { type: Number, required: true },
|
|
17
17
|
totalNodes: { type: Number, required: true },
|
|
18
18
|
targetIsTrigger: { type: Boolean, required: false },
|
|
19
|
-
runBusy: { type: Boolean, required: false }
|
|
19
|
+
runBusy: { type: Boolean, required: false },
|
|
20
|
+
runSaves: { type: Boolean, required: false }
|
|
20
21
|
});
|
|
21
22
|
const emit = defineEmits(["action", "update:open"]);
|
|
22
23
|
const t = useIntegrationsT();
|
|
@@ -33,7 +34,7 @@ const sections = computed(() => {
|
|
|
33
34
|
{ id: "duplicate", label: t("integration.canvas.contextMenu.item.duplicate"), shortcut: primaryShortcut("duplicate"), disabled: props.targetIsTrigger }
|
|
34
35
|
],
|
|
35
36
|
[
|
|
36
|
-
{ id: "run", label: t("integration.canvas.contextMenu.item.runWorkflow"), shortcut: primaryShortcut("run"), disabled: props.runBusy },
|
|
37
|
+
{ id: "run", label: t(props.runSaves ? "integration.canvas.contextMenu.item.saveAndRunWorkflow" : "integration.canvas.contextMenu.item.runWorkflow"), shortcut: primaryShortcut("run"), disabled: props.runBusy },
|
|
37
38
|
{ id: "tidy-up", label: t("integration.canvas.contextMenu.item.tidyUp"), shortcut: primaryShortcut("tidyUp") }
|
|
38
39
|
],
|
|
39
40
|
[
|
|
@@ -11,6 +11,8 @@ type __VLS_Props = {
|
|
|
11
11
|
targetIsTrigger?: boolean;
|
|
12
12
|
/** Busy flag for the Run item. */
|
|
13
13
|
runBusy?: boolean;
|
|
14
|
+
/** The workflow has unsaved work, so the Run item saves it first — and says so. */
|
|
15
|
+
runSaves?: boolean;
|
|
14
16
|
};
|
|
15
17
|
declare var __VLS_14: {};
|
|
16
18
|
type __VLS_Slots = {} & {
|
|
@@ -161,8 +161,13 @@ async function installed(id) {
|
|
|
161
161
|
<form v-if="mode === 'blank'" class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1" @submit.prevent="submit">
|
|
162
162
|
<!-- Two-column rows: label rail left, field right. -->
|
|
163
163
|
<div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
<!-- The asterisk sits beside the label, not in it: it is for the eye,
|
|
165
|
+
aria-required says it to everyone else, and the field's name stays "Name". -->
|
|
166
|
+
<div class="flex items-center">
|
|
167
|
+
<Label for="wf-name">{{ t("integration.workflows.create.field.name.label") }}</Label>
|
|
168
|
+
<span class="text-sm text-rose-500" aria-hidden="true" :title="t('integration.nodeConfig.field.requiredMark')"> *</span>
|
|
169
|
+
</div>
|
|
170
|
+
<Input id="wf-name" v-model="name" aria-required="true" class="bg-white" />
|
|
166
171
|
</div>
|
|
167
172
|
|
|
168
173
|
<div class="grid grid-cols-[130px_1fr] items-start gap-x-4">
|
|
@@ -171,7 +176,6 @@ async function installed(id) {
|
|
|
171
176
|
id="wf-desc"
|
|
172
177
|
v-model="description"
|
|
173
178
|
rows="2"
|
|
174
|
-
:placeholder="t('integration.workflows.create.field.description.placeholder')"
|
|
175
179
|
class="bg-white"
|
|
176
180
|
/>
|
|
177
181
|
</div>
|
|
@@ -20,10 +20,13 @@ type __VLS_Props = {
|
|
|
20
20
|
* keystrokes.
|
|
21
21
|
*/
|
|
22
22
|
draft?: WorkflowBlob | null;
|
|
23
|
-
/**
|
|
23
|
+
/** A run is being triggered — the context menu's Run item and the shortcut stand down. */
|
|
24
24
|
runBusy?: boolean;
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The workflow has unsaved work, so Run saves it first. Only the context menu
|
|
27
|
+
* reads it, to say so: Run itself stands in the page header, beside Save.
|
|
28
|
+
*/
|
|
29
|
+
runSaves?: boolean;
|
|
27
30
|
/**
|
|
28
31
|
* One of the page's own lightboxes is open above the editor (settings,
|
|
29
32
|
* duplicate, run payload, the sync response, the deactivation prompt). The
|
|
@@ -13,10 +13,8 @@ import { MiniMap } from "@vue-flow/minimap";
|
|
|
13
13
|
import {
|
|
14
14
|
Widget as LayoutGrid,
|
|
15
15
|
MagicStick as Sparkles,
|
|
16
|
-
Play,
|
|
17
16
|
MinusCircle as ZoomOut,
|
|
18
|
-
AddCircle as ZoomIn
|
|
19
|
-
RefreshCircle as Loader2
|
|
17
|
+
AddCircle as ZoomIn
|
|
20
18
|
} from "#solar-icons";
|
|
21
19
|
import {
|
|
22
20
|
Dialog,
|
|
@@ -65,7 +63,7 @@ const props = defineProps({
|
|
|
65
63
|
workflow: { type: Object, required: true },
|
|
66
64
|
draft: { type: [Object, null], required: false },
|
|
67
65
|
runBusy: { type: Boolean, required: false },
|
|
68
|
-
|
|
66
|
+
runSaves: { type: Boolean, required: false },
|
|
69
67
|
overlayOpen: { type: Boolean, required: false }
|
|
70
68
|
});
|
|
71
69
|
const emit = defineEmits(["change", "run", "toggleActive", "openSettings"]);
|
|
@@ -1420,6 +1418,7 @@ const edgeTypes = {
|
|
|
1420
1418
|
:total-nodes="vfNodes.length"
|
|
1421
1419
|
:target-is-trigger="!!menu.targetId && blob.triggers.some((t2) => t2.id === menu.targetId)"
|
|
1422
1420
|
:run-busy="runBusy"
|
|
1421
|
+
:run-saves="runSaves"
|
|
1423
1422
|
@action="onMenuAction"
|
|
1424
1423
|
>
|
|
1425
1424
|
<!-- `pan-activation-key-code` is null rather than left at its default, which
|
|
@@ -1695,25 +1694,6 @@ const edgeTypes = {
|
|
|
1695
1694
|
<TooltipProvider :delay-duration="250">
|
|
1696
1695
|
<div class="rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm">
|
|
1697
1696
|
<div class="flex items-center gap-0.5 whitespace-nowrap border border-white bg-white rounded-lg bg-gradient-to-br from-white via-violet-50/40 to-white px-2 py-1">
|
|
1698
|
-
<!-- Smart Run: the page decides what "run" means — a workflow that
|
|
1699
|
-
consumes trigger.payload, or leaves the mode to the caller, opens
|
|
1700
|
-
the run dialog; one that asks neither fires immediately. One
|
|
1701
|
-
button either way. -->
|
|
1702
|
-
<IntegrationsEditorTip :label="runWaiting ? t('integration.editor.tool.run.tooltipWaiting') : t('integration.editor.tool.run.tooltip')" :shortcut="primaryShortcut('run')">
|
|
1703
|
-
<button
|
|
1704
|
-
type="button"
|
|
1705
|
-
class="relative flex h-8 items-center gap-1.5 rounded-lg bg-gradient-to-b from-emerald-500 to-emerald-600 px-3.5 text-sm font-medium text-white transition after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_1px_0_rgba(255,255,255,0.3),inset_0_-2px_0_rgba(0,0,0,0.25)] hover:from-emerald-600 hover:to-emerald-600 active:translate-y-px disabled:opacity-60"
|
|
1706
|
-
:disabled="runBusy"
|
|
1707
|
-
@click="emit('run')"
|
|
1708
|
-
>
|
|
1709
|
-
<Loader2 v-if="runBusy" :size="14" class="animate-spin" />
|
|
1710
|
-
<Play v-else :size="14" />
|
|
1711
|
-
{{ runWaiting ? t("integration.editor.run.waitingForBuild") : t("integration.editor.run.button") }}
|
|
1712
|
-
</button>
|
|
1713
|
-
</IntegrationsEditorTip>
|
|
1714
|
-
|
|
1715
|
-
<div class="mx-1 h-5 w-px bg-black/10" />
|
|
1716
|
-
|
|
1717
1697
|
<div class="flex items-center gap-0.5 rounded-lg bg-black/[0.05] p-0.5 shadow-[inset_0_1px_2px_rgba(15,23,42,0.12)]">
|
|
1718
1698
|
<!-- Icon-only, and the tooltip is not an accessible name: each one
|
|
1719
1699
|
says what it does itself, and whether it is the active mode. -->
|
|
@@ -20,10 +20,13 @@ type __VLS_Props = {
|
|
|
20
20
|
* keystrokes.
|
|
21
21
|
*/
|
|
22
22
|
draft?: WorkflowBlob | null;
|
|
23
|
-
/**
|
|
23
|
+
/** A run is being triggered — the context menu's Run item and the shortcut stand down. */
|
|
24
24
|
runBusy?: boolean;
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The workflow has unsaved work, so Run saves it first. Only the context menu
|
|
27
|
+
* reads it, to say so: Run itself stands in the page header, beside Save.
|
|
28
|
+
*/
|
|
29
|
+
runSaves?: boolean;
|
|
27
30
|
/**
|
|
28
31
|
* One of the page's own lightboxes is open above the editor (settings,
|
|
29
32
|
* duplicate, run payload, the sync response, the deactivation prompt). The
|
|
@@ -49,6 +49,9 @@ export function useIntegrationsRuns() {
|
|
|
49
49
|
workflowId: r.workflow_id,
|
|
50
50
|
workflowName: r.workflow_name ?? workflowName,
|
|
51
51
|
status: mapRunStatus(r.status),
|
|
52
|
+
// Kept beside the mapped word, because the mapping folds `terminated` into
|
|
53
|
+
// `failed` and the platform resumes only the literal `failed` (PO-355).
|
|
54
|
+
apiStatus: r.status,
|
|
52
55
|
temporalRunId: r.temporal_run_id ?? null,
|
|
53
56
|
triggerId: r.trigger_handle ?? "",
|
|
54
57
|
// The platform reports what started the run. This was hardcoded to
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Node manifest icon → Lucide component.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Node icons are the studio's one deliberate Lucide exception (`docs/icons.md`):
|
|
5
|
+
* a package declares `lucide:<kebab-name>`, and that name is drawn as declared —
|
|
6
|
+
* no table, no studio change for a new package (PO-566).
|
|
7
|
+
*
|
|
8
|
+
* `MDI_TO_LUCIDE` is for packages still published with `mdi:*` names, the
|
|
9
|
+
* convention before PO-566. It is frozen: a missing name is fixed by the package
|
|
10
|
+
* declaring its Lucide name, not by a new entry here. Unmapped names still try
|
|
11
|
+
* the PascalCase lookup before falling back to Box.
|
|
9
12
|
*
|
|
10
13
|
* Exported for its test, which checks every entry against Lucide's exports: a
|
|
11
14
|
* test that found the entries by reading this file's text stopped seeing every
|
|
@@ -24,6 +24,7 @@ export const MDI_TO_LUCIDE = {
|
|
|
24
24
|
"code-brackets": "Brackets",
|
|
25
25
|
"code-json": "Braces",
|
|
26
26
|
"code-tags": "Code",
|
|
27
|
+
"comment-text-outline": "MessageSquareText",
|
|
27
28
|
"compare-horizontal": "ArrowLeftRight",
|
|
28
29
|
"content-duplicate": "Copy",
|
|
29
30
|
"currency-eur": "Euro",
|
|
@@ -51,6 +52,7 @@ export const MDI_TO_LUCIDE = {
|
|
|
51
52
|
filter: "Filter",
|
|
52
53
|
"find-replace": "Replace",
|
|
53
54
|
"folder-download-outline": "FolderDown",
|
|
55
|
+
"folder-outline": "Folder",
|
|
54
56
|
"folder-plus-outline": "FolderPlus",
|
|
55
57
|
"folder-search-outline": "FolderSearch",
|
|
56
58
|
"folder-upload-outline": "FolderUp",
|
|
@@ -93,6 +95,8 @@ export const MDI_TO_LUCIDE = {
|
|
|
93
95
|
"tag-multiple-outline": "Tags",
|
|
94
96
|
"tag-plus-outline": "Tag",
|
|
95
97
|
telegram: "Send",
|
|
98
|
+
"ticket-confirmation-outline": "TicketPlus",
|
|
99
|
+
"ticket-outline": "Ticket",
|
|
96
100
|
"timeline-text-outline": "Activity",
|
|
97
101
|
transfer: "ArrowRightLeft",
|
|
98
102
|
translate: "Languages",
|
|
@@ -107,7 +111,7 @@ export const MDI_TO_LUCIDE = {
|
|
|
107
111
|
export function nodeIcon(name) {
|
|
108
112
|
if (!name) return Lucide.Box;
|
|
109
113
|
const bare = name.includes(":") ? name.split(":").pop() : name;
|
|
110
|
-
const mapped = MDI_TO_LUCIDE[bare];
|
|
114
|
+
const mapped = name.startsWith("lucide:") ? void 0 : MDI_TO_LUCIDE[bare];
|
|
111
115
|
if (mapped && Lucide[mapped]) {
|
|
112
116
|
return Lucide[mapped];
|
|
113
117
|
}
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"integration.canvas.contextMenu.item.paste": "Einfügen",
|
|
54
54
|
"integration.canvas.contextMenu.item.rename": "Umbenennen",
|
|
55
55
|
"integration.canvas.contextMenu.item.runWorkflow": "Workflow ausführen",
|
|
56
|
+
"integration.canvas.contextMenu.item.saveAndRunWorkflow": "Speichern & Workflow ausführen",
|
|
56
57
|
"integration.canvas.contextMenu.item.selectAll": "Alle auswählen",
|
|
57
58
|
"integration.canvas.contextMenu.item.tidyUp": "Aufräumen",
|
|
58
59
|
"integration.canvas.edge.action.delete": "Verbindung löschen",
|
|
@@ -439,6 +440,7 @@
|
|
|
439
440
|
"integration.editor.run.error.syncFailed": "Synchroner Run fehlgeschlagen",
|
|
440
441
|
"integration.editor.run.error.syncTimeout": "Synchroner Run: Zeitüberschreitung",
|
|
441
442
|
"integration.editor.run.loading": "Run wird geladen…",
|
|
443
|
+
"integration.editor.run.saveAndRun": "Speichern & Run",
|
|
442
444
|
"integration.editor.run.waitingForBuild": "Warten auf Build…",
|
|
443
445
|
"integration.editor.runPayload.action.latestEvent": "Letztes reales Event",
|
|
444
446
|
"integration.editor.runPayload.action.latestEvent.title": "Letztes empfangenes Event dieses Topics einfügen",
|
|
@@ -1201,11 +1203,9 @@
|
|
|
1201
1203
|
"integration.workflows.create.executionMode.syncOnly.hint": "Wartet auf das Ergebnis.",
|
|
1202
1204
|
"integration.workflows.create.executionMode.syncOnly.label": "Nur synchron",
|
|
1203
1205
|
"integration.workflows.create.field.description.label": "Beschreibung",
|
|
1204
|
-
"integration.workflows.create.field.description.placeholder": "Optional: Was dieser Workflow macht und warum es ihn gibt.",
|
|
1205
1206
|
"integration.workflows.create.field.executionMode.label": "Ausführungsmodus",
|
|
1206
1207
|
"integration.workflows.create.field.icon.label": "Symbol",
|
|
1207
1208
|
"integration.workflows.create.field.name.label": "Name",
|
|
1208
|
-
"integration.workflows.create.field.name.placeholder": "Nächtliche Bestellsynchronisierung",
|
|
1209
1209
|
"integration.workflows.create.field.tags.label": "Tags",
|
|
1210
1210
|
"integration.workflows.create.field.trigger.label": "Trigger",
|
|
1211
1211
|
"integration.workflows.create.tab.blank": "Leer starten",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"integration.canvas.contextMenu.item.paste": "Paste",
|
|
54
54
|
"integration.canvas.contextMenu.item.rename": "Rename",
|
|
55
55
|
"integration.canvas.contextMenu.item.runWorkflow": "Run workflow",
|
|
56
|
+
"integration.canvas.contextMenu.item.saveAndRunWorkflow": "Save & run workflow",
|
|
56
57
|
"integration.canvas.contextMenu.item.selectAll": "Select all",
|
|
57
58
|
"integration.canvas.contextMenu.item.tidyUp": "Tidy up",
|
|
58
59
|
"integration.canvas.edge.action.delete": "Delete this connection",
|
|
@@ -439,6 +440,7 @@
|
|
|
439
440
|
"integration.editor.run.error.syncFailed": "Sync run failed",
|
|
440
441
|
"integration.editor.run.error.syncTimeout": "Sync run timed out",
|
|
441
442
|
"integration.editor.run.loading": "Loading run…",
|
|
443
|
+
"integration.editor.run.saveAndRun": "Save & run",
|
|
442
444
|
"integration.editor.run.waitingForBuild": "Waiting for build…",
|
|
443
445
|
"integration.editor.runPayload.action.latestEvent": "Latest real event",
|
|
444
446
|
"integration.editor.runPayload.action.latestEvent.title": "Insert the latest received event of this topic",
|
|
@@ -1201,11 +1203,9 @@
|
|
|
1201
1203
|
"integration.workflows.create.executionMode.syncOnly.hint": "Waits for the result.",
|
|
1202
1204
|
"integration.workflows.create.executionMode.syncOnly.label": "Sync only",
|
|
1203
1205
|
"integration.workflows.create.field.description.label": "Description",
|
|
1204
|
-
"integration.workflows.create.field.description.placeholder": "Optional: what this workflow does and why it exists.",
|
|
1205
1206
|
"integration.workflows.create.field.executionMode.label": "Execution mode",
|
|
1206
1207
|
"integration.workflows.create.field.icon.label": "Icon",
|
|
1207
1208
|
"integration.workflows.create.field.name.label": "Name",
|
|
1208
|
-
"integration.workflows.create.field.name.placeholder": "Sync orders nightly",
|
|
1209
1209
|
"integration.workflows.create.field.tags.label": "Tags",
|
|
1210
1210
|
"integration.workflows.create.field.trigger.label": "Trigger",
|
|
1211
1211
|
"integration.workflows.create.tab.blank": "Start blank",
|
|
@@ -35,6 +35,7 @@ import { apiRefusal, errorReason } from "../../utils/apiError";
|
|
|
35
35
|
import { CREDENTIAL_STATES, credentialState, credentialStateLabel, providerStepWord } from "../../utils/credentialState";
|
|
36
36
|
import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
|
|
37
37
|
import { resolveLocalized } from "../../utils/localize";
|
|
38
|
+
import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
|
|
38
39
|
const t = useIntegrationsT();
|
|
39
40
|
useHead({ title: () => t("integration.credentials.list.head.title") });
|
|
40
41
|
const { credentials, loading, error, load, create, update, remove, testInline, test, oauthAuthorizeUrl } = useIntegrationsCredentials();
|
|
@@ -422,14 +423,14 @@ async function confirmDelete() {
|
|
|
422
423
|
<!-- select -->
|
|
423
424
|
<Select
|
|
424
425
|
v-else-if="f.type === 'select'"
|
|
425
|
-
:model-value="form.config[f.key]
|
|
426
|
-
@update:model-value="(v) => form.config[f.key] = v"
|
|
426
|
+
:model-value="toModelValue(form.config[f.key], f.options)"
|
|
427
|
+
@update:model-value="(v) => form.config[f.key] = fromModelValue(v)"
|
|
427
428
|
>
|
|
428
429
|
<SelectTrigger :id="`cred-field-${f.key}`" class="bg-white" v-bind="configFieldAria(f.key)">
|
|
429
430
|
<SelectValue :placeholder="t('integration.credentials.shared.field.select.placeholder')" />
|
|
430
431
|
</SelectTrigger>
|
|
431
432
|
<SelectContent>
|
|
432
|
-
<SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="
|
|
433
|
+
<SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="toItemValue(opt.value)">
|
|
433
434
|
{{ resolveLocalized(opt.label) }}
|
|
434
435
|
</SelectItem>
|
|
435
436
|
</SelectContent>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref } from "vue";
|
|
3
|
-
import { marked } from "marked";
|
|
4
3
|
import { AltArrowRight as Chevron, CheckCircle as CheckCircle2, Key, RefreshCircle as Loader2, Shield as ShieldAlert, User } from "#solar-icons";
|
|
5
4
|
import { triggerGlyph as glyphForTrigger } from "../../../data/triggerGlyphs";
|
|
6
5
|
import {
|
|
@@ -18,6 +17,7 @@ import LoadingAnnouncement from "../../../components/Integrations/LoadingAnnounc
|
|
|
18
17
|
import NodeMark from "../../../components/Integrations/NodeMark.vue";
|
|
19
18
|
import { templateCategoryLabel, templatePackageLabel } from "../../../data/integrationTemplates";
|
|
20
19
|
import { describeCron } from "../../../utils/schedule";
|
|
20
|
+
import { renderTemplateDescription } from "../../../utils/templateDescription";
|
|
21
21
|
const t = useIntegrationsT();
|
|
22
22
|
const route = useRoute();
|
|
23
23
|
const router = useRouter();
|
|
@@ -34,12 +34,7 @@ onMounted(async () => {
|
|
|
34
34
|
await Promise.all([fetchDetail(slug.value), loadNodeCatalog(), loadCredentials(), loadSecrets()]);
|
|
35
35
|
reqs.value = await requirements(slug.value);
|
|
36
36
|
});
|
|
37
|
-
const descriptionHtml = computed(() =>
|
|
38
|
-
const md = tpl.value?.descriptionMd ?? "";
|
|
39
|
-
if (!md.trim()) return "";
|
|
40
|
-
const withoutH1 = md.replace(/^#\s[^\n]*\n+/, "");
|
|
41
|
-
return marked.parse(withoutH1);
|
|
42
|
-
});
|
|
37
|
+
const descriptionHtml = computed(() => renderTemplateDescription(tpl.value?.descriptionMd));
|
|
43
38
|
const steps = computed(() => {
|
|
44
39
|
const blob = tpl.value?.blob;
|
|
45
40
|
if (!blob?.nodes.length) return [];
|
|
@@ -221,10 +216,12 @@ function levelDot(level) {
|
|
|
221
216
|
class="rounded-lg border border-black/[0.06] bg-white p-4"
|
|
222
217
|
>
|
|
223
218
|
<h3 id="tpl-about-heading" class="text-sm font-semibold mb-2">{{ t("integration.templates.detail.about.title") }}</h3>
|
|
219
|
+
<!-- eslint-disable vue/no-v-html -- renderTemplateDescription sanitises marked's output with DOMPurify (PO-524) -->
|
|
224
220
|
<div
|
|
225
221
|
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"
|
|
226
222
|
v-html="descriptionHtml"
|
|
227
223
|
/>
|
|
224
|
+
<!-- eslint-enable vue/no-v-html -->
|
|
228
225
|
</div>
|
|
229
226
|
|
|
230
227
|
<!-- Step by step, humanized from the actual nodes -->
|
|
@@ -400,7 +400,11 @@ async function run(mode) {
|
|
|
400
400
|
}
|
|
401
401
|
const payloadDriven = computed(() => payloadFieldsFromBlob(shownBlob.value ?? { nodes: [] }).length > 0);
|
|
402
402
|
async function runFromEditor() {
|
|
403
|
-
if (!workflow.value) return;
|
|
403
|
+
if (!workflow.value || saving.value) return;
|
|
404
|
+
if (isDirty.value) {
|
|
405
|
+
await save();
|
|
406
|
+
if (isDirty.value) return;
|
|
407
|
+
}
|
|
404
408
|
if (payloadDriven.value || workflow.value.execution_mode === "caller_decides") {
|
|
405
409
|
runPayloadOpen.value = true;
|
|
406
410
|
return;
|
|
@@ -817,14 +821,12 @@ async function confirmDeactivate() {
|
|
|
817
821
|
<WorkflowIconView :icon="shownIcon" :size="16" />
|
|
818
822
|
</span>
|
|
819
823
|
<span class="truncate" :title="shownName">{{ shownName }}</span>
|
|
820
|
-
<!-- The
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
<Badge variant="outline" class="ml-1.5 shrink-0 border text-xs font-medium uppercase text-muted-foreground" :title="t('integration.editor.header.executionMode.title', { mode: modeLabel(workflow.execution_mode) })">
|
|
827
|
-
{{ modeLabel(workflow.execution_mode) }}
|
|
824
|
+
<!-- The DRAFTED mode, like the name beside it: Run over a draft saves
|
|
825
|
+
it first (PO-567), so this is the mode the next run goes out in. The
|
|
826
|
+
saved one would read ASYNC over a button about to fire a sync run;
|
|
827
|
+
the unsaved marker says the platform does not have it yet. -->
|
|
828
|
+
<Badge variant="outline" class="ml-1.5 shrink-0 border text-xs font-medium uppercase text-muted-foreground" :title="t('integration.editor.header.executionMode.title', { mode: modeLabel(draftedMode) })">
|
|
829
|
+
{{ modeLabel(draftedMode) }}
|
|
828
830
|
</Badge>
|
|
829
831
|
</template>
|
|
830
832
|
</span>
|
|
@@ -927,8 +929,23 @@ async function confirmDeactivate() {
|
|
|
927
929
|
<Save v-else :size="12" />
|
|
928
930
|
{{ saving ? t("integration.editor.action.save.saving") : t("integration.editor.action.save.label") }}
|
|
929
931
|
</Button>
|
|
932
|
+
<!-- Run stands beside Save because a run goes to the workflow the
|
|
933
|
+
platform holds, not to the draft on the canvas: with the two at
|
|
934
|
+
opposite ends of the screen, a changed value was run as the old one
|
|
935
|
+
and nothing said so. While there is a draft the press saves it first,
|
|
936
|
+
and the name says that it will (PO-567). -->
|
|
937
|
+
<Button
|
|
938
|
+
class="h-7 bg-emerald-600 text-sm text-white hover:bg-emerald-700"
|
|
939
|
+
:disabled="saving || running || syncLoading || waitingForBuild"
|
|
940
|
+
:title="waitingForBuild ? t('integration.editor.tool.run.tooltipWaiting') : t('integration.editor.tool.run.tooltip')"
|
|
941
|
+
@click="runFromEditor"
|
|
942
|
+
>
|
|
943
|
+
<Loader2 v-if="running || syncLoading || waitingForBuild" :size="12" class="animate-spin" />
|
|
944
|
+
<Play v-else :size="12" />
|
|
945
|
+
{{ waitingForBuild ? t("integration.editor.run.waitingForBuild") : isDirty ? t("integration.editor.run.saveAndRun") : t("integration.editor.run.button") }}
|
|
946
|
+
</Button>
|
|
930
947
|
<!-- Activation is a standing state, not a transport control. The play
|
|
931
|
-
symbol belongs to Run,
|
|
948
|
+
symbol belongs to Run, the button just before this one,
|
|
932
949
|
where pressing it makes one thing happen now; arming a workflow's
|
|
933
950
|
triggers looks like nothing happening at all. So this states the
|
|
934
951
|
state in the words the workflows list uses — Active, Paused — and
|
|
@@ -1064,8 +1081,8 @@ async function confirmDeactivate() {
|
|
|
1064
1081
|
ref="editorRef"
|
|
1065
1082
|
:workflow="workflow"
|
|
1066
1083
|
:draft="draft?.blob ?? null"
|
|
1067
|
-
:run-busy="running || syncLoading || waitingForBuild"
|
|
1068
|
-
:run-
|
|
1084
|
+
:run-busy="saving || running || syncLoading || waitingForBuild"
|
|
1085
|
+
:run-saves="isDirty"
|
|
1069
1086
|
:overlay-open="settingsOpen || duplicateOpen || importOpen || !!exporter?.open || deleteOpen || syncOpen || runPayloadOpen || deactivationOpen || leaveOpen"
|
|
1070
1087
|
@change="onChange"
|
|
1071
1088
|
@run="runFromEditor"
|
|
@@ -695,6 +695,12 @@ export interface WorkflowRun {
|
|
|
695
695
|
workflowId: number;
|
|
696
696
|
workflowName: string;
|
|
697
697
|
status: RunStatus;
|
|
698
|
+
/**
|
|
699
|
+
* The platform's own status word, as it sent it (`failed`, `terminated`, …).
|
|
700
|
+
* For deciding what the platform will accept (`acceptsResume`), never for
|
|
701
|
+
* drawing the outcome — that is `status`, which folds several of these into one.
|
|
702
|
+
*/
|
|
703
|
+
apiStatus?: string;
|
|
698
704
|
/** Temporal run id for this execution; null when the row predates Temporal. */
|
|
699
705
|
temporalRunId?: string | null;
|
|
700
706
|
triggerId: string;
|
|
@@ -47,6 +47,20 @@ export declare const API_RUN_STATUSES: readonly ["pending", "running", "complete
|
|
|
47
47
|
* are deliberate, and only the table is shared.
|
|
48
48
|
*/
|
|
49
49
|
export declare function mapRunStatus(status: string): RunStatus;
|
|
50
|
+
/**
|
|
51
|
+
* Would the platform accept a resume of this run? Its own status word decides,
|
|
52
|
+
* not the studio's.
|
|
53
|
+
*
|
|
54
|
+
* `mapRunStatus` folds `terminated` into the studio's `failed`, which is right for
|
|
55
|
+
* the outcome a row draws and wrong for this question: the platform resumes only a
|
|
56
|
+
* run whose stored status is literally `failed`, and answers a terminated one with
|
|
57
|
+
* a 409 (PO-355). So the platform's word is read where it is known, and the
|
|
58
|
+
* studio's only as the fallback for a run that arrived without one.
|
|
59
|
+
*
|
|
60
|
+
* Whether there is an execution to resume from is a separate condition, which each
|
|
61
|
+
* caller already asks in its own terms (specs/run-row.md AC-12).
|
|
62
|
+
*/
|
|
63
|
+
export declare function acceptsResume(apiStatus: string | null | undefined, status: RunStatus): boolean;
|
|
50
64
|
/**
|
|
51
65
|
* The platform statuses behind the studio's words.
|
|
52
66
|
*
|
|
@@ -11,6 +11,9 @@ export function mapRunStatus(status) {
|
|
|
11
11
|
const word = normalisedRunStatus(status);
|
|
12
12
|
return isRunStatusWord(word) ? word : "failed";
|
|
13
13
|
}
|
|
14
|
+
export function acceptsResume(apiStatus, status) {
|
|
15
|
+
return apiStatus ? apiStatus === "failed" : status === "failed";
|
|
16
|
+
}
|
|
14
17
|
export function apiStatusesFor(words) {
|
|
15
18
|
if (!words.length) return [];
|
|
16
19
|
return API_RUN_STATUSES.filter((status) => words.includes(mapRunStatus(status)));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderTemplateDescription(md: string | null | undefined): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import DOMPurify from "dompurify";
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
const ALLOWED_TAGS = [
|
|
4
|
+
"h1",
|
|
5
|
+
"h2",
|
|
6
|
+
"h3",
|
|
7
|
+
"h4",
|
|
8
|
+
"h5",
|
|
9
|
+
"h6",
|
|
10
|
+
"p",
|
|
11
|
+
"br",
|
|
12
|
+
"hr",
|
|
13
|
+
"strong",
|
|
14
|
+
"em",
|
|
15
|
+
"del",
|
|
16
|
+
"code",
|
|
17
|
+
"pre",
|
|
18
|
+
"blockquote",
|
|
19
|
+
"ul",
|
|
20
|
+
"ol",
|
|
21
|
+
"li",
|
|
22
|
+
"table",
|
|
23
|
+
"thead",
|
|
24
|
+
"tbody",
|
|
25
|
+
"tr",
|
|
26
|
+
"th",
|
|
27
|
+
"td",
|
|
28
|
+
"a",
|
|
29
|
+
"img"
|
|
30
|
+
];
|
|
31
|
+
const ALLOWED_ATTR = ["href", "title", "src", "alt", "align", "start"];
|
|
32
|
+
export function renderTemplateDescription(md) {
|
|
33
|
+
if (!md?.trim()) return "";
|
|
34
|
+
if (!DOMPurify.isSupported) return "";
|
|
35
|
+
const withoutH1 = md.replace(/^#\s[^\n]*\n+/, "");
|
|
36
|
+
const html = marked.parse(withoutH1, { async: false });
|
|
37
|
+
return DOMPurify.sanitize(html, { ALLOWED_TAGS, ALLOWED_ATTR });
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/studio-integrations",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Integration Studio (core: workflows, node editor, runs, schedules, credentials, secrets, audit) as a standalone, publishable Nuxt module.",
|
|
6
6
|
"repository": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"@revenexx/studio-shared": "^0.3.2",
|
|
30
30
|
"@solar-icons/nuxt": "^1.2.0",
|
|
31
31
|
"@solar-icons/vue": "^1.2.0",
|
|
32
|
+
"dompurify": "^3.4.16",
|
|
32
33
|
"marked": "^18.0.5"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|