@revenexx/studio-integrations 1.1.0 → 1.2.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 +520 -0
- package/README.md +3 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +77 -22
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
- package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
- package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
- package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +23 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +25 -32
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +5 -3
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
- package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
- package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
- package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
- package/dist/runtime/components/Integrations/RunsTable.vue +143 -20
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
- package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +39 -28
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
- package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
- package/dist/runtime/components/Integrations/canvasSides.js +22 -0
- package/dist/runtime/composables/useCredentialTypes.js +2 -6
- package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
- package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
- package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
- package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
- package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
- package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
- package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
- package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
- package/dist/runtime/composables/useIntegrationsTemplates.js +2 -5
- package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
- package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -7
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
- package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
- package/dist/runtime/composables/useOAuthConnect.js +33 -0
- package/dist/runtime/data/expressionEditor.d.ts +17 -5
- package/dist/runtime/data/expressionEditor.js +11 -5
- package/dist/runtime/data/handleId.d.ts +61 -1
- package/dist/runtime/data/handleId.js +8 -1
- package/dist/runtime/data/payloadForm.js +3 -2
- package/dist/runtime/i18n/messages/de.json +30 -11
- package/dist/runtime/i18n/messages/en.json +27 -8
- package/dist/runtime/pages/integrations/audit.vue +7 -6
- package/dist/runtime/pages/integrations/credentials.vue +9 -21
- package/dist/runtime/pages/integrations/index.vue +70 -12
- package/dist/runtime/pages/integrations/runs.vue +116 -7
- package/dist/runtime/pages/integrations/secrets.vue +2 -1
- package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
- package/dist/runtime/pages/integrations/templates/index.vue +18 -15
- package/dist/runtime/pages/integrations/workflows/[id].vue +177 -28
- package/dist/runtime/types/integrations.d.ts +5 -0
- package/dist/runtime/utils/apiError.d.ts +21 -1
- package/dist/runtime/utils/apiError.js +1 -1
- package/dist/runtime/utils/clipboard.d.ts +33 -0
- package/dist/runtime/utils/clipboard.js +11 -0
- package/dist/runtime/utils/deepClone.d.ts +41 -0
- package/dist/runtime/utils/deepClone.js +3 -0
- package/dist/runtime/utils/formatDuration.d.ts +44 -0
- package/dist/runtime/utils/formatDuration.js +13 -0
- package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
- package/dist/runtime/utils/nodeDisplayName.js +6 -0
- package/dist/runtime/utils/notify.d.ts +1 -1
- package/dist/runtime/utils/runEnvelope.d.ts +31 -0
- package/dist/runtime/utils/runEnvelope.js +12 -0
- package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
- package/dist/runtime/utils/runFeedQuery.js +66 -0
- package/dist/runtime/utils/runOutputs.d.ts +33 -0
- package/dist/runtime/utils/runOutputs.js +10 -0
- package/dist/runtime/utils/statusStyle.d.ts +24 -0
- package/dist/runtime/utils/statusStyle.js +1 -0
- package/dist/runtime/utils/validationFindings.d.ts +26 -3
- package/dist/runtime/utils/validationFindings.js +18 -3
- package/package.json +1 -1
|
@@ -50,7 +50,10 @@ import { layoutWorkflowNodes } from "./workflowLayout";
|
|
|
50
50
|
import { TRIGGER_TEMPLATES, findManifest } from "../../data/integrationNodeCatalog";
|
|
51
51
|
import { isTerminalManifest, portsForNode } from "../../data/nodeInputData";
|
|
52
52
|
import { computeLoopBodies, iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
|
|
53
|
-
import { portFromHandleId } from "../../data/handleId";
|
|
53
|
+
import { inputHandleId, outputHandleId, portFromHandleId } from "../../data/handleId";
|
|
54
|
+
import { OPPOSITE_SIDE } from "./canvasSides";
|
|
55
|
+
import { deepClone } from "../../utils/deepClone";
|
|
56
|
+
import { copyToClipboard } from "../../utils/clipboard";
|
|
54
57
|
import { groupFindingsByNode } from "../../utils/validationFindings";
|
|
55
58
|
import { reorderBranchSubtrees } from "../../data/branchReorder";
|
|
56
59
|
import { findingsByNodeField } from "../../utils/validationFindings";
|
|
@@ -65,14 +68,11 @@ const props = defineProps({
|
|
|
65
68
|
const emit = defineEmits(["change", "run", "toggleActive", "openSettings"]);
|
|
66
69
|
const t = useIntegrationsT();
|
|
67
70
|
const { keys: secretKeys } = useIntegrationsSecrets();
|
|
68
|
-
const blob = ref(
|
|
71
|
+
const blob = ref(deepClone(props.draft ?? props.workflow.blob));
|
|
69
72
|
watch(() => props.workflow.id, () => {
|
|
70
|
-
blob.value =
|
|
73
|
+
blob.value = deepClone(props.workflow.blob);
|
|
71
74
|
syncFromBlob();
|
|
72
75
|
});
|
|
73
|
-
function clone(v) {
|
|
74
|
-
return JSON.parse(JSON.stringify(v));
|
|
75
|
-
}
|
|
76
76
|
const vfNodes = ref([]);
|
|
77
77
|
const vfEdges = ref([]);
|
|
78
78
|
const flow = useVueFlow();
|
|
@@ -80,7 +80,7 @@ const invalidNodeMessages = computed(() => findingsByNodeField(props.workflow.wa
|
|
|
80
80
|
const dynamicOutputs = ref({});
|
|
81
81
|
const invalidNodeFields = computed(() => {
|
|
82
82
|
const out = {};
|
|
83
|
-
for (const g of groupFindingsByNode(props.workflow.warnings ?? [], props.workflow.blob.nodes, () => "")) {
|
|
83
|
+
for (const g of groupFindingsByNode(props.workflow.warnings ?? [], props.workflow.blob.nodes, () => "", (_n, key) => key)) {
|
|
84
84
|
if (g.nodeId) out[g.nodeId] = g.fields;
|
|
85
85
|
}
|
|
86
86
|
return out;
|
|
@@ -160,9 +160,6 @@ function nodePosition(id) {
|
|
|
160
160
|
function pickSourceSide(_sourceId, _targetId) {
|
|
161
161
|
return "bottom";
|
|
162
162
|
}
|
|
163
|
-
function oppositeSide(s) {
|
|
164
|
-
return s === "top" ? "bottom" : s === "right" ? "left" : s === "bottom" ? "top" : "right";
|
|
165
|
-
}
|
|
166
163
|
function stickyArea(s) {
|
|
167
164
|
return (s.size?.width ?? 240) * (s.size?.height ?? 160);
|
|
168
165
|
}
|
|
@@ -252,6 +249,7 @@ function markBorn(...ids) {
|
|
|
252
249
|
function bornClass(id) {
|
|
253
250
|
return bornIds.value.has(id) ? "node-pop-in" : void 0;
|
|
254
251
|
}
|
|
252
|
+
const NODE_FALLBACK_POSITION = { x: 240, y: 80 };
|
|
255
253
|
function syncFromBlob() {
|
|
256
254
|
const loops = nodeLoopData(blob.value.nodes, blob.value.edges);
|
|
257
255
|
const stickies = [...blob.value.stickies ?? []].sort((a, b) => stickyArea(b) - stickyArea(a));
|
|
@@ -304,7 +302,7 @@ function syncFromBlob() {
|
|
|
304
302
|
...blob.value.nodes.filter((n) => !collapsedBy.has(n.id)).map((n) => ({
|
|
305
303
|
id: n.id,
|
|
306
304
|
type: "workflow",
|
|
307
|
-
position: n.position ??
|
|
305
|
+
position: n.position ?? NODE_FALLBACK_POSITION,
|
|
308
306
|
class: bornClass(n.id),
|
|
309
307
|
data: {
|
|
310
308
|
...n,
|
|
@@ -330,8 +328,8 @@ function syncFromBlob() {
|
|
|
330
328
|
...vf,
|
|
331
329
|
source,
|
|
332
330
|
target,
|
|
333
|
-
sourceHandle: srcGroup ? "
|
|
334
|
-
targetHandle: tgtGroup ? "
|
|
331
|
+
sourceHandle: srcGroup ? outputHandleId("default", "bottom") : vf.sourceHandle,
|
|
332
|
+
targetHandle: tgtGroup ? inputHandleId("top") : vf.targetHandle
|
|
335
333
|
});
|
|
336
334
|
}
|
|
337
335
|
const bySource = /* @__PURE__ */ new Map();
|
|
@@ -355,15 +353,15 @@ function makeVfEdge(id, sourceId, targetId, output) {
|
|
|
355
353
|
const manifest = wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
|
|
356
354
|
const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"].includes(output);
|
|
357
355
|
const srcSide = pickSourceSide(sourceId, targetId);
|
|
358
|
-
const tgtSide =
|
|
356
|
+
const tgtSide = OPPOSITE_SIDE[srcSide];
|
|
359
357
|
const order = orderedOutputPorts(sourceId);
|
|
360
358
|
const portIndex = order.indexOf(output);
|
|
361
359
|
return {
|
|
362
360
|
id,
|
|
363
361
|
source: sourceId,
|
|
364
362
|
target: targetId,
|
|
365
|
-
sourceHandle:
|
|
366
|
-
targetHandle:
|
|
363
|
+
sourceHandle: outputHandleId(output, srcSide),
|
|
364
|
+
targetHandle: inputHandleId(tgtSide),
|
|
367
365
|
type: "flow",
|
|
368
366
|
data: {
|
|
369
367
|
kind,
|
|
@@ -561,7 +559,7 @@ function removeBlobNode(id) {
|
|
|
561
559
|
push();
|
|
562
560
|
}
|
|
563
561
|
function push() {
|
|
564
|
-
emit("change",
|
|
562
|
+
emit("change", deepClone(blob.value));
|
|
565
563
|
}
|
|
566
564
|
const STICKY_DEFAULT_SIZE = { width: 240, height: 160 };
|
|
567
565
|
function updateSticky(id, patch) {
|
|
@@ -622,7 +620,7 @@ function duplicateBlobNodes(ids) {
|
|
|
622
620
|
src.forEach((n, i) => idMap.set(n.id, `n-${stamp}${i}`));
|
|
623
621
|
const offset = 40;
|
|
624
622
|
const copies = src.map((n) => ({
|
|
625
|
-
...
|
|
623
|
+
...deepClone(n),
|
|
626
624
|
id: idMap.get(n.id),
|
|
627
625
|
position: { x: (n.position?.x ?? 0) + offset, y: (n.position?.y ?? 0) + offset }
|
|
628
626
|
}));
|
|
@@ -661,12 +659,10 @@ async function copyNodes(ids) {
|
|
|
661
659
|
if (!nodes.length) return;
|
|
662
660
|
const inner = new Set(nodes.map((n) => n.id));
|
|
663
661
|
const edges = blob.value.edges.filter((e) => inner.has(e.from.nodeId) && inner.has(e.to.nodeId));
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
toast.error(t("integration.editor.clipboard.error.write"));
|
|
669
|
-
}
|
|
662
|
+
await copyToClipboard(JSON.stringify({ kind: CLIP_KIND, version: 1, nodes, edges }, null, 2), {
|
|
663
|
+
copied: t("integration.editor.clipboard.toast.copied", nodes.length),
|
|
664
|
+
failed: t("integration.editor.clipboard.error.write")
|
|
665
|
+
});
|
|
670
666
|
}
|
|
671
667
|
async function pasteNodes(at) {
|
|
672
668
|
let text = "";
|
|
@@ -695,7 +691,7 @@ async function pasteNodes(at) {
|
|
|
695
691
|
const dx = at ? Math.round(at.x - minX) : 40;
|
|
696
692
|
const dy = at ? Math.round(at.y - minY) : 40;
|
|
697
693
|
blob.value.nodes.push(...payload.nodes.map((n) => ({
|
|
698
|
-
...
|
|
694
|
+
...deepClone(n),
|
|
699
695
|
id: idMap.get(n.id),
|
|
700
696
|
position: { x: (n.position?.x ?? 0) + dx, y: (n.position?.y ?? 0) + dy }
|
|
701
697
|
})));
|
|
@@ -906,10 +902,10 @@ function groupAtPoint(pos) {
|
|
|
906
902
|
function addGroupFrame() {
|
|
907
903
|
const rect = flow.vueFlowRef.value?.getBoundingClientRect();
|
|
908
904
|
const center = rect ? flow.screenToFlowCoordinate({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }) : { x: 120, y: 120 };
|
|
909
|
-
addFromPayload({ kind: "group" }, {
|
|
905
|
+
addFromPayload({ kind: "group" }, freeSpotNear({
|
|
910
906
|
x: center.x - EMPTY_GROUP_SIZE.width / 2,
|
|
911
907
|
y: center.y - EMPTY_GROUP_SIZE.height / 2
|
|
912
|
-
});
|
|
908
|
+
}));
|
|
913
909
|
}
|
|
914
910
|
function adoptIntoGroupAt(id, pos) {
|
|
915
911
|
const g = groupAtPoint(pos);
|
|
@@ -970,6 +966,21 @@ function onDrop(ev) {
|
|
|
970
966
|
const payload = JSON.parse(raw);
|
|
971
967
|
addFromPayload(payload, flow.screenToFlowCoordinate({ x: ev.clientX, y: ev.clientY }));
|
|
972
968
|
}
|
|
969
|
+
const CASCADE_STEP = 2 * GRID;
|
|
970
|
+
function freeSpotNear(at) {
|
|
971
|
+
const taken = [
|
|
972
|
+
...vfNodes.value.map((n) => n.position),
|
|
973
|
+
...blob.value.nodes.map((n) => n.position ?? NODE_FALLBACK_POSITION),
|
|
974
|
+
...Object.values(blob.value.triggerPositions ?? {})
|
|
975
|
+
];
|
|
976
|
+
const occupied = (p) => taken.some((q) => Math.abs(q.x - p.x) < GRID && Math.abs(q.y - p.y) < GRID);
|
|
977
|
+
const spot = { x: snap(at.x), y: snap(at.y) };
|
|
978
|
+
for (let i = 0; i <= taken.length && occupied(spot); i++) {
|
|
979
|
+
spot.x += CASCADE_STEP;
|
|
980
|
+
spot.y += CASCADE_STEP;
|
|
981
|
+
}
|
|
982
|
+
return spot;
|
|
983
|
+
}
|
|
973
984
|
function onPaletteInsert(payload) {
|
|
974
985
|
if (pendingConnect.value && payload.kind === "manifest") {
|
|
975
986
|
const pc = pendingConnect.value;
|
|
@@ -1003,7 +1014,7 @@ function onPaletteInsert(payload) {
|
|
|
1003
1014
|
pendingSpliceEdgeId.value = null;
|
|
1004
1015
|
const rect = flow.vueFlowRef.value?.getBoundingClientRect();
|
|
1005
1016
|
const center = rect ? flow.screenToFlowCoordinate({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }) : { x: 120, y: 120 };
|
|
1006
|
-
addFromPayload(payload, center);
|
|
1017
|
+
addFromPayload(payload, freeSpotNear(center));
|
|
1007
1018
|
}
|
|
1008
1019
|
const pendingSpliceEdgeId = ref(null);
|
|
1009
1020
|
provide("workflow-edge-actions", {
|
|
@@ -11,6 +11,7 @@ import { Badge, Button } from "@revenexx/studio";
|
|
|
11
11
|
import { toast } from "vue-sonner";
|
|
12
12
|
import "@vue-flow/core/dist/style.css";
|
|
13
13
|
import "@vue-flow/core/dist/theme-default.css";
|
|
14
|
+
import LoadingAnnouncement from "./LoadingAnnouncement.vue";
|
|
14
15
|
import WorkflowNodeView from "./WorkflowNodeView.vue";
|
|
15
16
|
import WorkflowTriggerView from "./WorkflowTriggerView.vue";
|
|
16
17
|
import StickyNoteView from "./StickyNoteView.vue";
|
|
@@ -18,8 +19,11 @@ import GroupFrameView from "./GroupFrameView.vue";
|
|
|
18
19
|
import WorkflowEdgeView from "./WorkflowEdgeView.vue";
|
|
19
20
|
import NodeDynamicOutputsResolver from "./NodeDynamicOutputsResolver.vue";
|
|
20
21
|
import { findManifest } from "../../data/integrationNodeCatalog";
|
|
22
|
+
import { OPPOSITE_SIDE } from "./canvasSides";
|
|
23
|
+
import { inputHandleId, outputHandleId } from "../../data/handleId";
|
|
21
24
|
import { iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
|
|
22
25
|
import { useIntegrationsRevisions } from "../../composables/useIntegrationsRevisions";
|
|
26
|
+
import { errorReason } from "../../utils/apiError";
|
|
23
27
|
const props = defineProps({
|
|
24
28
|
workflow: { type: Object, required: true },
|
|
25
29
|
draftPending: { type: Boolean, required: false }
|
|
@@ -118,7 +122,6 @@ function buildPreview(blob) {
|
|
|
118
122
|
return { x: 0, y: 0 };
|
|
119
123
|
};
|
|
120
124
|
const pickSourceSide = (_sourceId, _targetId) => "bottom";
|
|
121
|
-
const opposite = (s) => s === "top" ? "bottom" : s === "right" ? "left" : s === "bottom" ? "top" : "right";
|
|
122
125
|
const edgeKind = (sourceNodeId, output) => {
|
|
123
126
|
const wn = blob.nodes.find((n) => n.id === sourceNodeId);
|
|
124
127
|
if (!wn) return "default";
|
|
@@ -182,8 +185,8 @@ function buildPreview(blob) {
|
|
|
182
185
|
id: e.id,
|
|
183
186
|
source: e.from.nodeId,
|
|
184
187
|
target: e.to.nodeId,
|
|
185
|
-
sourceHandle:
|
|
186
|
-
targetHandle:
|
|
188
|
+
sourceHandle: outputHandleId(e.from.output, srcSide),
|
|
189
|
+
targetHandle: inputHandleId(OPPOSITE_SIDE[srcSide]),
|
|
187
190
|
type: "flow",
|
|
188
191
|
selectable: false,
|
|
189
192
|
data: { kind, label: isBranchPort ? e.from.output : void 0 },
|
|
@@ -222,7 +225,7 @@ async function select(revision) {
|
|
|
222
225
|
buildPreview(detail.blob);
|
|
223
226
|
}
|
|
224
227
|
} catch (e) {
|
|
225
|
-
toast.error(e
|
|
228
|
+
toast.error(errorReason(e, t("integration.history.toast.loadRevisionFailed")));
|
|
226
229
|
} finally {
|
|
227
230
|
detailLoading.value = false;
|
|
228
231
|
}
|
|
@@ -239,7 +242,7 @@ async function restoreRevision(rev) {
|
|
|
239
242
|
await loadList(true);
|
|
240
243
|
void select(null);
|
|
241
244
|
} catch (e) {
|
|
242
|
-
toast.error(e
|
|
245
|
+
toast.error(errorReason(e, t("integration.history.toast.restoreFailed")));
|
|
243
246
|
} finally {
|
|
244
247
|
restoring.value = null;
|
|
245
248
|
}
|
|
@@ -328,6 +331,14 @@ onMounted(() => {
|
|
|
328
331
|
</p>
|
|
329
332
|
</div>
|
|
330
333
|
|
|
334
|
+
<!-- In the aside's chrome rather than in the scrolling list, so it is on the
|
|
335
|
+
page while the revisions are on their way (PO-251). -->
|
|
336
|
+
<LoadingAnnouncement
|
|
337
|
+
:label="t('integration.history.heading.title')"
|
|
338
|
+
:loading="listLoading && !entries.length"
|
|
339
|
+
:text="t('integration.history.state.loading')"
|
|
340
|
+
/>
|
|
341
|
+
|
|
331
342
|
<div class="flex-1 overflow-y-auto p-2 space-y-1">
|
|
332
343
|
<!-- Current (live) version -->
|
|
333
344
|
<button
|
|
@@ -385,7 +396,7 @@ onMounted(() => {
|
|
|
385
396
|
{{ revisionsApi.error.value }}
|
|
386
397
|
</p>
|
|
387
398
|
|
|
388
|
-
<div v-if="listLoading && !entries.length" class="space-y-2 px-2 py-2">
|
|
399
|
+
<div v-if="listLoading && !entries.length" aria-hidden="true" class="space-y-2 px-2 py-2">
|
|
389
400
|
<div v-for="n in 5" :key="`sk-rev-${n}`" class="flex items-center gap-3 py-1.5">
|
|
390
401
|
<div class="size-7 shrink-0 rounded-md bg-black/[0.07] animate-pulse" />
|
|
391
402
|
<div class="min-w-0 flex-1 space-y-1.5">
|
|
@@ -5,8 +5,12 @@ import { NodeToolbar } from "@vue-flow/node-toolbar";
|
|
|
5
5
|
import * as Lucide from "lucide-vue-next";
|
|
6
6
|
import { Copy, DangerTriangle as AlertTriangle, RefreshCircle as Loader2, Repeat, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
7
7
|
import { NODE_CATEGORIES, findManifest, isVersionExecutable } from "../../data/integrationNodeCatalog";
|
|
8
|
+
import { inputHandleId, outputHandleId } from "../../data/handleId";
|
|
8
9
|
import { isTerminalManifest } from "../../data/nodeInputData";
|
|
9
|
-
import {
|
|
10
|
+
import { resolveLocalized } from "../../utils/localize";
|
|
11
|
+
import { SIDE_POSITION, distributedStyle } from "./canvasSides";
|
|
12
|
+
import { fieldDisplayLabel } from "../../utils/validationFindings";
|
|
13
|
+
import { nodeDisplayName } from "../../utils/nodeDisplayName";
|
|
10
14
|
const props = defineProps({
|
|
11
15
|
id: { type: String, required: true },
|
|
12
16
|
data: { type: Object, required: true },
|
|
@@ -17,11 +21,11 @@ defineEmits(["updateNodeInternals"]);
|
|
|
17
21
|
const t = useIntegrationsT();
|
|
18
22
|
const manifest = computed(() => findManifest(props.data.nodeSlug, props.data.nodeVersion));
|
|
19
23
|
const category = computed(() => NODE_CATEGORIES.find((c) => c.key === manifest.value?.category));
|
|
20
|
-
const title = computed(
|
|
21
|
-
() => resolveDisplayName(props.data.name) || resolveLocalized(manifest.value?.name) || props.data.nodeSlug
|
|
22
|
-
);
|
|
24
|
+
const title = computed(() => nodeDisplayName(props.data));
|
|
23
25
|
const invalidFields = computed(() => props.data.__invalidFields ?? []);
|
|
24
|
-
const invalidLabel = computed(() => t("integration.canvas.node.badge.invalidConfig", invalidFields.value.length, {
|
|
26
|
+
const invalidLabel = computed(() => t("integration.canvas.node.badge.invalidConfig", invalidFields.value.length, {
|
|
27
|
+
fields: invalidFields.value.map((key) => fieldDisplayLabel(props.data, key)).join(", ")
|
|
28
|
+
}));
|
|
25
29
|
const insideLoops = computed(() => props.data.__loop?.inside ?? []);
|
|
26
30
|
const repeatsLabel = computed(() => t("integration.canvas.node.badge.repeats", insideLoops.value.length, { iterators: insideLoops.value.join(", ") }));
|
|
27
31
|
const bodySize = computed(() => props.data.__loop?.bodySize);
|
|
@@ -111,14 +115,6 @@ function portTone(p) {
|
|
|
111
115
|
const showPortLabels = computed(() => resolvePending.value || outputs.value.length > 1 || outputs.value.some((p) => p.kind !== "default"));
|
|
112
116
|
const IN_SIDES = ["top"];
|
|
113
117
|
const OUT_SIDES = ["bottom"];
|
|
114
|
-
function sideToPosition(s) {
|
|
115
|
-
return s === "top" ? Position.Top : s === "right" ? Position.Right : s === "bottom" ? Position.Bottom : Position.Left;
|
|
116
|
-
}
|
|
117
|
-
function distributedStyle(side, i, total) {
|
|
118
|
-
const pct = `${(i + 1) / (total + 1) * 100}%`;
|
|
119
|
-
if (side === "top" || side === "bottom") return { left: pct };
|
|
120
|
-
return { top: pct };
|
|
121
|
-
}
|
|
122
118
|
const flow = useVueFlow();
|
|
123
119
|
const hovered = ref(false);
|
|
124
120
|
let hoverTimer = null;
|
|
@@ -192,22 +188,22 @@ function remove() {
|
|
|
192
188
|
<NodeToolbar v-if="nodeActions" :is-visible="hovered || selected" :position="Position.Top" :offset="8" class="flex items-center gap-1 rounded-md border border-black/[0.08] bg-white px-1 py-1 shadow-md" @mouseenter="onHoverEnter" @mouseleave="onHoverLeave">
|
|
193
189
|
<button
|
|
194
190
|
type="button"
|
|
195
|
-
class="flex
|
|
191
|
+
class="flex size-6 items-center justify-center rounded text-sm text-foreground/80 hover:bg-muted/50"
|
|
196
192
|
:title="t('integration.canvas.node.action.duplicate')"
|
|
197
193
|
:aria-label="t('integration.canvas.node.action.duplicateAria')"
|
|
198
194
|
@click="duplicate"
|
|
199
195
|
>
|
|
200
|
-
<Copy :size="
|
|
196
|
+
<Copy :size="13" />
|
|
201
197
|
</button>
|
|
202
198
|
<div class="h-4 w-px bg-black/10" />
|
|
203
199
|
<button
|
|
204
200
|
type="button"
|
|
205
|
-
class="flex
|
|
201
|
+
class="flex size-6 items-center justify-center rounded text-sm text-rose-600 hover:bg-rose-50"
|
|
206
202
|
:title="t('integration.common.action.delete')"
|
|
207
203
|
:aria-label="t('integration.canvas.node.action.deleteAria')"
|
|
208
204
|
@click="remove"
|
|
209
205
|
>
|
|
210
|
-
<TrashBin :size="
|
|
206
|
+
<TrashBin :size="13" />
|
|
211
207
|
</button>
|
|
212
208
|
</NodeToolbar>
|
|
213
209
|
|
|
@@ -239,10 +235,10 @@ function remove() {
|
|
|
239
235
|
dots feed the same `__in` port. Clearly visible, ring + shadow. -->
|
|
240
236
|
<Handle
|
|
241
237
|
v-for="side in IN_SIDES"
|
|
242
|
-
:id="
|
|
238
|
+
:id="inputHandleId(side)"
|
|
243
239
|
:key="`in-${side}`"
|
|
244
240
|
type="target"
|
|
245
|
-
:position="
|
|
241
|
+
:position="SIDE_POSITION[side]"
|
|
246
242
|
:title="t('integration.canvas.node.handle.input')"
|
|
247
243
|
class="!size-3 !rounded-full !border-2 !border-white !bg-slate-500 !shadow-sm transition-all hover:!size-3.5 hover:!bg-slate-700"
|
|
248
244
|
/>
|
|
@@ -252,10 +248,10 @@ function remove() {
|
|
|
252
248
|
<template v-for="side in OUT_SIDES" :key="`side-${side}`">
|
|
253
249
|
<Handle
|
|
254
250
|
v-for="(p, i) in outputs"
|
|
255
|
-
:id="
|
|
251
|
+
:id="outputHandleId(p.name, side)"
|
|
256
252
|
:key="`${p.name}-${side}`"
|
|
257
253
|
type="source"
|
|
258
|
-
:position="
|
|
254
|
+
:position="SIDE_POSITION[side]"
|
|
259
255
|
:title="handleTitle(p)"
|
|
260
256
|
:style="distributedStyle(side, i, outputs.length)"
|
|
261
257
|
class="!size-3 !rounded-full !border-2 !border-white !shadow-sm transition-all hover:!size-3.5"
|
|
@@ -24,10 +24,14 @@ import StickyNoteView from "./StickyNoteView.vue";
|
|
|
24
24
|
import GroupFrameView from "./GroupFrameView.vue";
|
|
25
25
|
import WorkflowEdgeView from "./WorkflowEdgeView.vue";
|
|
26
26
|
import { findManifest } from "../../data/integrationNodeCatalog";
|
|
27
|
+
import { OPPOSITE_SIDE } from "./canvasSides";
|
|
28
|
+
import { inputHandleId, outputHandleId } from "../../data/handleId";
|
|
27
29
|
import { nodeLoopData } from "../../data/loopBody";
|
|
28
30
|
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
31
|
+
import { formatDuration } from "../../utils/formatDuration";
|
|
29
32
|
import { SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
30
33
|
import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
34
|
+
import { errorReason } from "../../utils/apiError";
|
|
31
35
|
const props = defineProps({
|
|
32
36
|
workflow: { type: Object, required: true },
|
|
33
37
|
initialRunId: { type: [String, null], required: false },
|
|
@@ -59,7 +63,7 @@ async function loadRuns(reset = false) {
|
|
|
59
63
|
runsPage.value = res.page;
|
|
60
64
|
runsLastPage.value = res.lastPage;
|
|
61
65
|
} catch (e) {
|
|
62
|
-
toast.error(e
|
|
66
|
+
toast.error(errorReason(e, t("integration.replay.toast.loadRunsFailed")));
|
|
63
67
|
} finally {
|
|
64
68
|
runsLoading.value = false;
|
|
65
69
|
}
|
|
@@ -101,7 +105,7 @@ async function loadSteps(runId, isPoll = false) {
|
|
|
101
105
|
}
|
|
102
106
|
} catch (e) {
|
|
103
107
|
if (selectedRunId.value === runId) {
|
|
104
|
-
stepsError.value = e
|
|
108
|
+
stepsError.value = errorReason(e, t("integration.replay.error.loadStepsFailed"));
|
|
105
109
|
}
|
|
106
110
|
} finally {
|
|
107
111
|
if (!isPoll) stepsLoading.value = false;
|
|
@@ -293,7 +297,6 @@ const vfEdges = computed(() => {
|
|
|
293
297
|
return { x: 0, y: 0 };
|
|
294
298
|
};
|
|
295
299
|
const pickSourceSide = (_sourceId, _targetId) => "bottom";
|
|
296
|
-
const opposite = (s) => s === "top" ? "bottom" : s === "right" ? "left" : s === "bottom" ? "top" : "right";
|
|
297
300
|
const edgeKind = (sourceNodeId, output) => {
|
|
298
301
|
const wn = blob.nodes.find((n) => n.id === sourceNodeId);
|
|
299
302
|
if (!wn) return "default";
|
|
@@ -331,8 +334,8 @@ const vfEdges = computed(() => {
|
|
|
331
334
|
id: e.id,
|
|
332
335
|
source: e.from.nodeId,
|
|
333
336
|
target: e.to.nodeId,
|
|
334
|
-
sourceHandle:
|
|
335
|
-
targetHandle:
|
|
337
|
+
sourceHandle: outputHandleId(e.from.output, srcSide),
|
|
338
|
+
targetHandle: inputHandleId(OPPOSITE_SIDE[srcSide]),
|
|
336
339
|
type: "flow",
|
|
337
340
|
selectable: false,
|
|
338
341
|
animated: traversed && !targetReached,
|
|
@@ -395,12 +398,6 @@ function formatTime(iso) {
|
|
|
395
398
|
return String(iso);
|
|
396
399
|
}
|
|
397
400
|
}
|
|
398
|
-
function formatDuration(ms) {
|
|
399
|
-
if (ms == null) return "\u2014";
|
|
400
|
-
if (ms < 1e3) return `${ms} ms`;
|
|
401
|
-
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)} s`;
|
|
402
|
-
return `${Math.floor(ms / 6e4)}m ${Math.round(ms % 6e4 / 1e3)}s`;
|
|
403
|
-
}
|
|
404
401
|
function pretty(v) {
|
|
405
402
|
try {
|
|
406
403
|
return JSON.stringify(v, null, 2);
|
|
@@ -3,6 +3,8 @@ import { computed, ref } from "vue";
|
|
|
3
3
|
import { Handle, Position, useVueFlow } from "@vue-flow/core";
|
|
4
4
|
import { NodeToolbar } from "@vue-flow/node-toolbar";
|
|
5
5
|
import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
6
|
+
import { SIDE_POSITION } from "./canvasSides";
|
|
7
|
+
import { outputHandleId } from "../../data/handleId";
|
|
6
8
|
import { triggerGlyph as glyphForTrigger } from "../../data/triggerGlyphs";
|
|
7
9
|
import { resolveDisplayName } from "../../utils/localize";
|
|
8
10
|
import { triggerTypeLabel } from "../../data/integrationNodeCatalog";
|
|
@@ -45,21 +47,18 @@ function remove() {
|
|
|
45
47
|
flow.removeNodes([props.id]);
|
|
46
48
|
}
|
|
47
49
|
const OUT_SIDES = ["bottom"];
|
|
48
|
-
function sideToPosition(s) {
|
|
49
|
-
return s === "top" ? Position.Top : s === "right" ? Position.Right : s === "bottom" ? Position.Bottom : Position.Left;
|
|
50
|
-
}
|
|
51
50
|
</script>
|
|
52
51
|
|
|
53
52
|
<template>
|
|
54
53
|
<NodeToolbar :is-visible="hovered || selected" :position="Position.Top" :offset="8" class="flex items-center gap-1 rounded-md border border-black/[0.08] bg-white px-1 py-1 shadow-md" @mouseenter="onHoverEnter" @mouseleave="onHoverLeave">
|
|
55
54
|
<button
|
|
56
55
|
type="button"
|
|
57
|
-
class="flex
|
|
56
|
+
class="flex size-6 items-center justify-center rounded text-sm text-rose-600 hover:bg-rose-50"
|
|
58
57
|
:title="t('integration.common.action.delete')"
|
|
59
58
|
:aria-label="t('integration.canvas.trigger.action.deleteAria')"
|
|
60
59
|
@click="remove"
|
|
61
60
|
>
|
|
62
|
-
<TrashBin :size="
|
|
61
|
+
<TrashBin :size="13" />
|
|
63
62
|
</button>
|
|
64
63
|
</NodeToolbar>
|
|
65
64
|
|
|
@@ -97,10 +96,10 @@ function sideToPosition(s) {
|
|
|
97
96
|
"default"; the side suffix is stripped on persist. -->
|
|
98
97
|
<Handle
|
|
99
98
|
v-for="side in OUT_SIDES"
|
|
100
|
-
:id="
|
|
99
|
+
:id="outputHandleId('default', side)"
|
|
101
100
|
:key="side"
|
|
102
101
|
type="source"
|
|
103
|
-
:position="
|
|
102
|
+
:position="SIDE_POSITION[side]"
|
|
104
103
|
class="!size-3 !rounded-full !border-2 !border-white !bg-emerald-500 !shadow-sm transition-all hover:!size-3.5 hover:!bg-emerald-600"
|
|
105
104
|
/>
|
|
106
105
|
</template>
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
SelectValue
|
|
20
20
|
} from "@revenexx/studio";
|
|
21
21
|
import { toast } from "vue-sonner";
|
|
22
|
+
import { deepClone } from "../../utils/deepClone";
|
|
23
|
+
import { copyToClipboard } from "../../utils/clipboard";
|
|
22
24
|
import { languageKeyRead, languageKeyToWrite, resolveDisplayName } from "../../utils/localize";
|
|
23
25
|
const props = defineProps({
|
|
24
26
|
open: { type: Boolean, required: true },
|
|
@@ -34,7 +36,7 @@ const jsonErrors = reactive({});
|
|
|
34
36
|
const descDrafts = reactive({});
|
|
35
37
|
watch(() => props.open, (o) => {
|
|
36
38
|
if (!o) return;
|
|
37
|
-
working.value =
|
|
39
|
+
working.value = deepClone(props.variables);
|
|
38
40
|
for (const k of Object.keys(jsonDrafts)) delete jsonDrafts[k];
|
|
39
41
|
for (const k of Object.keys(jsonErrors)) delete jsonErrors[k];
|
|
40
42
|
for (const k of Object.keys(descDrafts)) delete descDrafts[k];
|
|
@@ -146,12 +148,10 @@ function addVariable() {
|
|
|
146
148
|
newKey.value = "";
|
|
147
149
|
}
|
|
148
150
|
async function copyUsage(key) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
toast.error(t("integration.workflows.variables.toast.copyFailed"));
|
|
154
|
-
}
|
|
151
|
+
await copyToClipboard(`\${{ vars.${key} }}`, {
|
|
152
|
+
copied: t("integration.workflows.variables.toast.copied"),
|
|
153
|
+
failed: t("integration.workflows.variables.toast.copyFailed")
|
|
154
|
+
});
|
|
155
155
|
}
|
|
156
156
|
</script>
|
|
157
157
|
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import {
|
|
2
|
+
import { MenuDots as MoreHorizontal } from "#solar-icons";
|
|
3
3
|
import { toast } from "vue-sonner";
|
|
4
|
-
import { apiValidationIssues } from "../../utils/apiError";
|
|
4
|
+
import { apiValidationIssues, errorReason } from "../../utils/apiError";
|
|
5
|
+
import WorkflowDeleteDialog from "./WorkflowDeleteDialog.vue";
|
|
5
6
|
import WorkflowDuplicateDialog from "./WorkflowDuplicateDialog.vue";
|
|
6
7
|
import WorkflowIconView from "./WorkflowIconView.vue";
|
|
7
8
|
import { Layers } from "#solar-icons";
|
|
8
9
|
import {
|
|
9
|
-
AlertDialog,
|
|
10
|
-
AlertDialogContent,
|
|
11
|
-
AlertDialogDescription,
|
|
12
|
-
AlertDialogFooter,
|
|
13
|
-
AlertDialogHeader,
|
|
14
|
-
AlertDialogTitle,
|
|
15
10
|
Badge,
|
|
16
11
|
Button,
|
|
17
12
|
DropdownMenu,
|
|
@@ -27,7 +22,7 @@ defineProps({
|
|
|
27
22
|
});
|
|
28
23
|
const t = useIntegrationsT();
|
|
29
24
|
const router = useRouter();
|
|
30
|
-
const {
|
|
25
|
+
const { toggleActive } = useIntegrationsWorkflows();
|
|
31
26
|
const duplicateTarget = ref(null);
|
|
32
27
|
const duplicateOpen = ref(false);
|
|
33
28
|
function askDuplicate(wf) {
|
|
@@ -77,21 +72,11 @@ function runBadge(status) {
|
|
|
77
72
|
return { label: t("integration.workflows.table.status.run.cancelled"), class: "bg-slate-100 text-slate-700 border-slate-200" };
|
|
78
73
|
}
|
|
79
74
|
}
|
|
80
|
-
const
|
|
81
|
-
const
|
|
75
|
+
const deleteOpen = ref(false);
|
|
76
|
+
const deleteTarget = ref(null);
|
|
82
77
|
function askDelete(wf) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
async function confirmDelete() {
|
|
86
|
-
const wf = pendingDelete.value;
|
|
87
|
-
if (!wf) return;
|
|
88
|
-
deleting.value = true;
|
|
89
|
-
try {
|
|
90
|
-
await remove(wf.id);
|
|
91
|
-
pendingDelete.value = null;
|
|
92
|
-
} finally {
|
|
93
|
-
deleting.value = false;
|
|
94
|
-
}
|
|
78
|
+
deleteTarget.value = { id: wf.id, name: wf.name };
|
|
79
|
+
deleteOpen.value = true;
|
|
95
80
|
}
|
|
96
81
|
async function onToggleActive(wf) {
|
|
97
82
|
try {
|
|
@@ -102,7 +87,7 @@ async function onToggleActive(wf) {
|
|
|
102
87
|
toast.error(t("integration.workflows.table.toast.activateBlocked", issues.length, { name: wf.name }));
|
|
103
88
|
return;
|
|
104
89
|
}
|
|
105
|
-
toast.error(e
|
|
90
|
+
toast.error(errorReason(e, t("integration.workflows.table.toast.statusChangeFailed")));
|
|
106
91
|
}
|
|
107
92
|
}
|
|
108
93
|
function lifecycleOf(wf) {
|
|
@@ -405,30 +390,7 @@ const columns = computed(() => [
|
|
|
405
390
|
</template>
|
|
406
391
|
</DataTable>
|
|
407
392
|
|
|
408
|
-
<
|
|
409
|
-
if (!v) pendingDelete = null;
|
|
410
|
-
}">
|
|
411
|
-
<AlertDialogContent>
|
|
412
|
-
<AlertDialogHeader>
|
|
413
|
-
<AlertDialogTitle>{{ t("integration.workflows.table.deleteDialog.title") }}</AlertDialogTitle>
|
|
414
|
-
<AlertDialogDescription>
|
|
415
|
-
<span v-if="pendingDelete">
|
|
416
|
-
<span class="font-medium text-foreground">{{ pendingDelete.name }}</span>
|
|
417
|
-
{{ t("integration.workflows.table.deleteDialog.description") }}
|
|
418
|
-
</span>
|
|
419
|
-
</AlertDialogDescription>
|
|
420
|
-
</AlertDialogHeader>
|
|
421
|
-
<AlertDialogFooter>
|
|
422
|
-
<Button variant="outline" :disabled="deleting" @click="pendingDelete = null">
|
|
423
|
-
{{ t("integration.common.action.cancel") }}
|
|
424
|
-
</Button>
|
|
425
|
-
<Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="deleting" @click="confirmDelete">
|
|
426
|
-
<Loader2 v-if="deleting" :size="14" class="animate-spin" />
|
|
427
|
-
{{ deleting ? t("integration.workflows.table.deleteDialog.action.deleting") : t("integration.common.action.delete") }}
|
|
428
|
-
</Button>
|
|
429
|
-
</AlertDialogFooter>
|
|
430
|
-
</AlertDialogContent>
|
|
431
|
-
</AlertDialog>
|
|
393
|
+
<WorkflowDeleteDialog v-model:open="deleteOpen" :workflow="deleteTarget" />
|
|
432
394
|
|
|
433
395
|
<WorkflowDuplicateDialog v-model:open="duplicateOpen" :workflow="duplicateTarget" @duplicated="onDuplicated" />
|
|
434
396
|
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Position } from '@vue-flow/core';
|
|
2
|
+
import type { Side } from '../../data/handleId.js';
|
|
3
|
+
/**
|
|
4
|
+
* What a connection dot's side means to the canvas: where the library draws it, what
|
|
5
|
+
* faces it, and where along the edge it sits.
|
|
6
|
+
*
|
|
7
|
+
* `data/handleId.ts` owns what a side *is* — the closed set of four, and the id a dot
|
|
8
|
+
* carries. This owns what the canvas does with one. The split is the import: that
|
|
9
|
+
* module has none, so its round trip can be proved without the canvas library, and
|
|
10
|
+
* `Position` is Vue Flow's. Keeping the two apart is what stops a string-shape contract
|
|
11
|
+
* from depending on a rendering library.
|
|
12
|
+
*
|
|
13
|
+
* **Every map here is a `Record<Side, …>`, and that is the point of the module.** Each
|
|
14
|
+
* was a four-branch ternary ending in an `else`, in a component, and two components had
|
|
15
|
+
* their own copy of the first one. An `else` absorbs a fifth side silently: a `centre`
|
|
16
|
+
* dot would have been drawn on the **left**, called `right` its opposite, and spread
|
|
17
|
+
* along the wrong axis — with no type error and nothing on screen to notice, because
|
|
18
|
+
* every one of those is a plausible answer for *some* side.
|
|
19
|
+
*
|
|
20
|
+
* A `Record<Side, …>` cannot do that. Add a name to `SIDES` and all three of these stop
|
|
21
|
+
* compiling until they say what it means. That is weaker than PO-258's "one edit" and
|
|
22
|
+
* better than it: it is still more than one edit, but none of them can be forgotten.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* The side a dot is on → the position Vue Flow draws it at.
|
|
26
|
+
*
|
|
27
|
+
* The two vocabularies happen to be the same four strings today — `Position.Top` is
|
|
28
|
+
* `'top'` — so this could be a cast, and is not. A cast would tie us to that
|
|
29
|
+
* coincidence holding in a library we do not own, and it would say nothing when it
|
|
30
|
+
* stopped: the studio's sides are its own, and this is where they are translated.
|
|
31
|
+
*/
|
|
32
|
+
export declare const SIDE_POSITION: Record<Side, Position>;
|
|
33
|
+
/**
|
|
34
|
+
* The side facing a given one, which is where an edge arriving from it should land.
|
|
35
|
+
*
|
|
36
|
+
* "Opposite" is only meaningful because the four sides are two axes. A fifth position
|
|
37
|
+
* that is not on an axis — a centre dot — has no opposite at all, and the honest answer
|
|
38
|
+
* then is to change this type rather than to invent one; the compiler will ask.
|
|
39
|
+
*/
|
|
40
|
+
export declare const OPPOSITE_SIDE: Record<Side, Side>;
|
|
41
|
+
/**
|
|
42
|
+
* Place the `i`th of `total` output dots evenly along a side.
|
|
43
|
+
*
|
|
44
|
+
* Evenly *between* the corners rather than across them — `(i + 1) / (total + 1)` — so
|
|
45
|
+
* the first and last dot sit inside the card's edge instead of on it, and a single
|
|
46
|
+
* output lands in the middle.
|
|
47
|
+
*/
|
|
48
|
+
export declare function distributedStyle(side: Side, i: number, total: number): Record<string, string>;
|