@codemation/next-host 0.0.1
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/README.md +25 -0
- package/app/(shell)/credentials/page.tsx +5 -0
- package/app/(shell)/dashboard/page.tsx +14 -0
- package/app/(shell)/layout.tsx +11 -0
- package/app/(shell)/page.tsx +5 -0
- package/app/(shell)/users/page.tsx +5 -0
- package/app/(shell)/workflows/[workflowId]/page.tsx +19 -0
- package/app/(shell)/workflows/page.tsx +5 -0
- package/app/api/[[...path]]/route.ts +40 -0
- package/app/api/auth/[...nextauth]/route.ts +3 -0
- package/app/globals.css +997 -0
- package/app/invite/[token]/page.tsx +10 -0
- package/app/layout.tsx +65 -0
- package/app/login/layout.tsx +25 -0
- package/app/login/page.tsx +22 -0
- package/components.json +21 -0
- package/docs/FORMS.md +46 -0
- package/docs/TAILWIND_SHADCN_MIGRATION.md +89 -0
- package/eslint.config.mjs +56 -0
- package/middleware.ts +29 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +34 -0
- package/package.json +76 -0
- package/postcss.config.mjs +7 -0
- package/public/canvas-icons/builtin/openai.svg +5 -0
- package/src/api/CodemationApiClient.ts +107 -0
- package/src/api/CodemationApiHttpError.ts +17 -0
- package/src/auth/CodemationNextAuthConfigResolver.ts +14 -0
- package/src/auth/CodemationNextAuthOAuthProviderDescriptorMapper.ts +30 -0
- package/src/auth/CodemationNextAuthOAuthProviderSnapshotResolver.ts +17 -0
- package/src/auth/CodemationNextAuthProviderCatalog.ts +107 -0
- package/src/auth/codemationEdgeAuth.ts +25 -0
- package/src/auth/codemationNextAuth.ts +32 -0
- package/src/components/Codemation.tsx +6 -0
- package/src/components/CodemationDataTable.tsx +37 -0
- package/src/components/CodemationDialog.tsx +137 -0
- package/src/components/CodemationFormattedDateTime.tsx +46 -0
- package/src/components/GoogleColorGIcon.tsx +39 -0
- package/src/components/OauthProviderIcon.tsx +33 -0
- package/src/components/PasswordStrengthMeter.tsx +59 -0
- package/src/components/forms/index.ts +28 -0
- package/src/components/json/JsonMonacoEditor.tsx +75 -0
- package/src/components/oauthProviderIconData.ts +17 -0
- package/src/components/ui/alert.tsx +56 -0
- package/src/components/ui/badge.tsx +40 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +70 -0
- package/src/components/ui/collapsible.tsx +26 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/dropdown-menu.tsx +238 -0
- package/src/components/ui/form.tsx +147 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/scroll-area.tsx +47 -0
- package/src/components/ui/select.tsx +169 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/switch.tsx +28 -0
- package/src/components/ui/table.tsx +72 -0
- package/src/components/ui/tabs.tsx +76 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle.tsx +41 -0
- package/src/features/credentials/components/CredentialConfirmDialog.tsx +58 -0
- package/src/features/credentials/components/CredentialDialog.tsx +252 -0
- package/src/features/credentials/components/CredentialDialogFeedback.tsx +36 -0
- package/src/features/credentials/components/CredentialDialogFieldRows.tsx +257 -0
- package/src/features/credentials/components/CredentialDialogFormSections.tsx +230 -0
- package/src/features/credentials/components/CredentialEnvFieldStatusRow.tsx +64 -0
- package/src/features/credentials/components/CredentialFieldCopyButton.tsx +48 -0
- package/src/features/credentials/components/CredentialsScreenHealthBadge.tsx +21 -0
- package/src/features/credentials/components/CredentialsScreenInstancesTable.tsx +108 -0
- package/src/features/credentials/components/CredentialsScreenTestFailureAlert.tsx +33 -0
- package/src/features/credentials/hooks/useCredentialCreateDialog.ts +33 -0
- package/src/features/credentials/hooks/useCredentialDialogSession.ts +616 -0
- package/src/features/credentials/hooks/useCredentialsScreen.ts +213 -0
- package/src/features/credentials/lib/credentialFieldHelpers.ts +35 -0
- package/src/features/credentials/lib/credentialFormTypes.ts +1 -0
- package/src/features/credentials/lib/credentialInstanceTestPayloadParser.ts +10 -0
- package/src/features/credentials/screens/CredentialsScreen.tsx +187 -0
- package/src/features/invite/screens/InviteAcceptScreen.tsx +190 -0
- package/src/features/users/components/UsersInviteDialog.tsx +121 -0
- package/src/features/users/components/UsersRegenerateDialog.tsx +81 -0
- package/src/features/users/components/UsersScreenUserStatusBadge.tsx +19 -0
- package/src/features/users/schemas/usersInviteFormSchema.ts +7 -0
- package/src/features/users/screens/UsersScreen.tsx +240 -0
- package/src/features/workflows/components/WorkflowListFolderSection.tsx +91 -0
- package/src/features/workflows/components/WorkflowListItemCard.tsx +67 -0
- package/src/features/workflows/components/WorkflowListRoot.tsx +39 -0
- package/src/features/workflows/components/WorkflowsListTree.tsx +28 -0
- package/src/features/workflows/components/canvas/CanvasNodeChromeTooltip.tsx +96 -0
- package/src/features/workflows/components/canvas/CanvasNodeIconSlot.tsx +25 -0
- package/src/features/workflows/components/canvas/VisibleNodeStatusResolver.tsx +84 -0
- package/src/features/workflows/components/canvas/WorkflowCanvas.tsx +248 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNode.tsx +182 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAccents.tsx +73 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAgentBottomSourceHandles.tsx +43 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAgentLabels.tsx +47 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeCard.tsx +202 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeHandles.tsx +77 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeLabelBelow.tsx +51 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeMainGlyph.tsx +64 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeToolbar.tsx +95 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasLoadingPlaceholder.tsx +69 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasNodeIcon.tsx +102 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasSimpleIconGlyph.tsx +21 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasStraightCountEdge.tsx +33 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasStructureSignature.tsx +7 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasSymmetricForkEdge.tsx +32 -0
- package/src/features/workflows/components/canvas/WorkflowCanvasToolbarIconButton.tsx +95 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasBuiltinIconRegistry.ts +26 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasEdgeCountResolver.ts +51 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasEdgeStyleResolver.ts +35 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasLabelLayoutEstimator.ts +42 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasOverlapResolver.ts +78 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasPortOrderResolver.ts +25 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasRoundedOrthogonalPathPlanner.ts +56 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasSiIconRegistry.ts +18 -0
- package/src/features/workflows/components/canvas/lib/WorkflowCanvasSymmetricForkPathPlanner.ts +43 -0
- package/src/features/workflows/components/canvas/lib/layoutWorkflow.ts +315 -0
- package/src/features/workflows/components/canvas/lib/workflowCanvasEdgeGeometry.ts +3 -0
- package/src/features/workflows/components/canvas/lib/workflowCanvasEmbeddedStyles.ts +62 -0
- package/src/features/workflows/components/canvas/lib/workflowCanvasFlowTypes.ts +10 -0
- package/src/features/workflows/components/canvas/lib/workflowCanvasNodeData.ts +41 -0
- package/src/features/workflows/components/canvas/lib/workflowCanvasNodeGeometry.ts +99 -0
- package/src/features/workflows/components/canvas/workflowCanvasNodeChrome.tsx +46 -0
- package/src/features/workflows/components/realtime/RealtimeContext.tsx +14 -0
- package/src/features/workflows/components/realtime/WorkflowRealtimeProvider.tsx +15 -0
- package/src/features/workflows/components/workflowDetail/NodeCredentialBindingRow.tsx +209 -0
- package/src/features/workflows/components/workflowDetail/NodeCredentialBindingsSection.tsx +227 -0
- package/src/features/workflows/components/workflowDetail/NodePropertiesConfigSection.tsx +51 -0
- package/src/features/workflows/components/workflowDetail/NodePropertiesPanelHeader.tsx +50 -0
- package/src/features/workflows/components/workflowDetail/NodePropertiesSlidePanel.tsx +134 -0
- package/src/features/workflows/components/workflowDetail/WorkflowActivationErrorDialog.tsx +71 -0
- package/src/features/workflows/components/workflowDetail/WorkflowActivationHeaderControl.tsx +64 -0
- package/src/features/workflows/components/workflowDetail/WorkflowDetailIcons.tsx +52 -0
- package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspector.tsx +110 -0
- package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorDetailBody.tsx +213 -0
- package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorPanes.tsx +239 -0
- package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorSidebarResizer.tsx +31 -0
- package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorTreePanel.tsx +133 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorAttachmentGroupingPresenter.tsx +31 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorAttachmentList.tsx +118 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorBinaryView.tsx +15 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorErrorView.tsx +107 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorJsonView.tsx +114 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyTreePresenter.tsx +132 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyTreeViewRenderer.tsx +147 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyView.tsx +65 -0
- package/src/features/workflows/components/workflowDetail/WorkflowInspectorViews.tsx +5 -0
- package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorBinaryAttachmentRow.tsx +74 -0
- package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorBinaryUploadRow.tsx +69 -0
- package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorDialog.tsx +254 -0
- package/src/features/workflows/components/workflowDetail/WorkflowRunsList.tsx +89 -0
- package/src/features/workflows/components/workflowDetail/WorkflowRunsSidebar.tsx +50 -0
- package/src/features/workflows/hooks/canvas/useWorkflowCanvasVisibleNodeStatuses.ts +14 -0
- package/src/features/workflows/hooks/realtime/realtime.tsx +271 -0
- package/src/features/workflows/hooks/realtime/runQueryPolling.ts +34 -0
- package/src/features/workflows/hooks/realtime/useWorkflowRealtimeInfrastructure.ts +541 -0
- package/src/features/workflows/hooks/realtime/useWorkflowRealtimeShowDisconnectedBadge.ts +9 -0
- package/src/features/workflows/hooks/workflowDetail/useWorkflowDetailController.tsx +1300 -0
- package/src/features/workflows/lib/realtime/realtimeApi.ts +78 -0
- package/src/features/workflows/lib/realtime/realtimeClientBridge.ts +52 -0
- package/src/features/workflows/lib/realtime/realtimeDomainTypes.ts +191 -0
- package/src/features/workflows/lib/realtime/realtimeQueryKeys.ts +15 -0
- package/src/features/workflows/lib/realtime/realtimeRunMutations.ts +167 -0
- package/src/features/workflows/lib/realtime/workflowTypes.ts +5 -0
- package/src/features/workflows/lib/workflowDetail/PersistedWorkflowSnapshotMapper.ts +205 -0
- package/src/features/workflows/lib/workflowDetail/WorkflowActivationHttpErrorFormat.ts +32 -0
- package/src/features/workflows/lib/workflowDetail/WorkflowDetailPresenter.ts +1017 -0
- package/src/features/workflows/lib/workflowDetail/WorkflowDetailUrlCodec.ts +70 -0
- package/src/features/workflows/lib/workflowDetail/workflowDetailTypes.ts +152 -0
- package/src/features/workflows/lib/workflowDetailTreeStyles.ts +65 -0
- package/src/features/workflows/screens/WorkflowDetailScreen.tsx +236 -0
- package/src/features/workflows/screens/WorkflowDetailScreenInspectorPanel.tsx +55 -0
- package/src/features/workflows/screens/WorkflowsList.tsx +35 -0
- package/src/features/workflows/screens/WorkflowsScreen.tsx +31 -0
- package/src/index.ts +1 -0
- package/src/lib/utils.ts +6 -0
- package/src/middleware/CodemationNextHostMiddlewarePathRules.ts +31 -0
- package/src/providers/CodemationSessionProvider.tsx +23 -0
- package/src/providers/Providers.tsx +36 -0
- package/src/providers/RealtimeBoundary.tsx +17 -0
- package/src/providers/WhitelabelProvider.tsx +22 -0
- package/src/server/CodemationAuthPrismaClient.ts +21 -0
- package/src/server/CodemationNextHost.ts +379 -0
- package/src/shell/AppLayout.tsx +141 -0
- package/src/shell/AppLayoutNavItems.tsx +129 -0
- package/src/shell/AppLayoutPageHeader.tsx +79 -0
- package/src/shell/AppLayoutSidebarBrand.tsx +33 -0
- package/src/shell/AppMainContent.tsx +17 -0
- package/src/shell/AppShellHeaderActions.tsx +12 -0
- package/src/shell/AppShellHeaderActionsAuthenticated.tsx +51 -0
- package/src/shell/CodemationNextClientShell.tsx +17 -0
- package/src/shell/CredentialsSignInRedirectResolver.ts +21 -0
- package/src/shell/LoginPageClient.tsx +231 -0
- package/src/shell/WorkflowDetailChromeContext.tsx +42 -0
- package/src/shell/WorkflowFolderTreeBuilder.ts +62 -0
- package/src/shell/WorkflowFolderUi.ts +42 -0
- package/src/shell/WorkflowSidebarNavFolder.tsx +112 -0
- package/src/shell/WorkflowSidebarNavTree.tsx +68 -0
- package/src/shell/appLayoutPageTitle.ts +16 -0
- package/src/shell/appLayoutSidebarIcons.tsx +108 -0
- package/src/whitelabel/CodemationWhitelabelSnapshot.ts +4 -0
- package/src/whitelabel/CodemationWhitelabelSnapshotFactory.ts +18 -0
- package/tsconfig.json +40 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +34 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function WorkflowCanvasLoadingPlaceholder(props: Readonly<{ isInitialViewportReady: boolean }>) {
|
|
2
|
+
const { isInitialViewportReady } = props;
|
|
3
|
+
return (
|
|
4
|
+
<div
|
|
5
|
+
aria-hidden={isInitialViewportReady}
|
|
6
|
+
style={{
|
|
7
|
+
position: "absolute",
|
|
8
|
+
inset: 0,
|
|
9
|
+
display: "grid",
|
|
10
|
+
placeItems: "center",
|
|
11
|
+
pointerEvents: "none",
|
|
12
|
+
opacity: isInitialViewportReady ? 0 : 1,
|
|
13
|
+
transition: "opacity 180ms ease-out",
|
|
14
|
+
background:
|
|
15
|
+
"linear-gradient(rgba(251,251,252,0.96), rgba(251,251,252,0.96)), radial-gradient(circle at center, rgba(15,23,42,0.04) 1px, transparent 1px)",
|
|
16
|
+
backgroundSize: "auto, 18px 18px",
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<div
|
|
20
|
+
style={{
|
|
21
|
+
minWidth: 220,
|
|
22
|
+
padding: "16px 18px",
|
|
23
|
+
border: "1px solid #e5e7eb",
|
|
24
|
+
background: "rgba(255,255,255,0.94)",
|
|
25
|
+
boxShadow: "0 10px 28px rgba(15,23,42,0.06)",
|
|
26
|
+
display: "grid",
|
|
27
|
+
gap: 10,
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
31
|
+
<div
|
|
32
|
+
style={{
|
|
33
|
+
width: 10,
|
|
34
|
+
height: 10,
|
|
35
|
+
background: "#2563eb",
|
|
36
|
+
animation: "codemationCanvasLoaderPulse 1s ease-in-out infinite",
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
39
|
+
<div
|
|
40
|
+
style={{ fontSize: 12, fontWeight: 800, letterSpacing: 0.45, textTransform: "uppercase", color: "#475569" }}
|
|
41
|
+
>
|
|
42
|
+
Workflow diagram
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div style={{ fontSize: 14, fontWeight: 700, color: "#111827" }}>Loading...</div>
|
|
46
|
+
<div style={{ display: "grid", gap: 6 }}>
|
|
47
|
+
<div
|
|
48
|
+
style={{
|
|
49
|
+
height: 8,
|
|
50
|
+
width: 176,
|
|
51
|
+
background: "linear-gradient(90deg, #e5e7eb, #f8fafc, #e5e7eb)",
|
|
52
|
+
backgroundSize: "200% 100%",
|
|
53
|
+
animation: "codemationCanvasLoaderShimmer 1.4s linear infinite",
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
<div
|
|
57
|
+
style={{
|
|
58
|
+
height: 8,
|
|
59
|
+
width: 132,
|
|
60
|
+
background: "linear-gradient(90deg, #e5e7eb, #f8fafc, #e5e7eb)",
|
|
61
|
+
backgroundSize: "200% 100%",
|
|
62
|
+
animation: "codemationCanvasLoaderShimmer 1.4s linear infinite",
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Boxes } from "lucide-react";
|
|
2
|
+
import { DynamicIcon } from "lucide-react/dynamic";
|
|
3
|
+
import type { IconName } from "lucide-react/dynamic";
|
|
4
|
+
import { Suspense, type CSSProperties, type ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
import { CanvasNodeIconSlot } from "./CanvasNodeIconSlot";
|
|
7
|
+
import { WorkflowCanvasBuiltinIconRegistry } from "./lib/WorkflowCanvasBuiltinIconRegistry";
|
|
8
|
+
import { WorkflowCanvasSiIconRegistry } from "./lib/WorkflowCanvasSiIconRegistry";
|
|
9
|
+
import { WorkflowCanvasSimpleIconGlyph } from "./WorkflowCanvasSimpleIconGlyph";
|
|
10
|
+
|
|
11
|
+
/** Main node glyph: always contained in the slot, no opaque backing (card paints the tile). */
|
|
12
|
+
const IMG_STYLE: CSSProperties = {
|
|
13
|
+
display: "block",
|
|
14
|
+
width: "100%",
|
|
15
|
+
height: "100%",
|
|
16
|
+
maxWidth: "100%",
|
|
17
|
+
maxHeight: "100%",
|
|
18
|
+
objectFit: "contain",
|
|
19
|
+
objectPosition: "center",
|
|
20
|
+
backgroundColor: "transparent",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function isHttpOrDataUrl(value: string): boolean {
|
|
24
|
+
const v = value.trim().toLowerCase();
|
|
25
|
+
return v.startsWith("http://") || v.startsWith("https://") || v.startsWith("data:") || v.startsWith("/");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function builtinAssetImg(url: string, sizePx: number): ReactNode {
|
|
29
|
+
return (
|
|
30
|
+
<CanvasNodeIconSlot sizePx={sizePx}>
|
|
31
|
+
<img src={url} alt="" style={{ ...IMG_STYLE, width: "100%", height: "100%" }} />
|
|
32
|
+
</CanvasNodeIconSlot>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Canvas node icon resolution:
|
|
38
|
+
* - **URLs** — `http(s):`, `data:`, or root-relative `/…`
|
|
39
|
+
* - **`builtin:<id>`** — SVG under `public/canvas-icons/builtin/` (see {@link WorkflowCanvasBuiltinIconRegistry})
|
|
40
|
+
* - **`si:<slug>`** — cherry-picked Simple Icons, or same builtin asset when slug matches a registered builtin (e.g. `si:openai`)
|
|
41
|
+
* - **`lucide:<name>`** or legacy kebab name — Lucide dynamic icon
|
|
42
|
+
*
|
|
43
|
+
* Node configs set {@link import("@codemation/core").NodeConfigBase.icon}.
|
|
44
|
+
*/
|
|
45
|
+
export function WorkflowCanvasNodeIcon(props: Readonly<{ icon?: string; sizePx: number; strokeWidth?: number }>) {
|
|
46
|
+
const { icon, sizePx, strokeWidth = 2 } = props;
|
|
47
|
+
const raw = icon?.trim();
|
|
48
|
+
if (!raw) {
|
|
49
|
+
return (
|
|
50
|
+
<CanvasNodeIconSlot sizePx={sizePx}>
|
|
51
|
+
<Boxes size={sizePx} strokeWidth={strokeWidth} />
|
|
52
|
+
</CanvasNodeIconSlot>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (isHttpOrDataUrl(raw)) {
|
|
56
|
+
return (
|
|
57
|
+
<CanvasNodeIconSlot sizePx={sizePx}>
|
|
58
|
+
<img src={raw} alt="" style={{ ...IMG_STYLE, width: "100%", height: "100%" }} />
|
|
59
|
+
</CanvasNodeIconSlot>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (raw.startsWith("builtin:")) {
|
|
63
|
+
const id = raw.slice("builtin:".length).trim().toLowerCase();
|
|
64
|
+
const url = WorkflowCanvasBuiltinIconRegistry.resolveUrl(id);
|
|
65
|
+
if (url) {
|
|
66
|
+
return builtinAssetImg(url, sizePx);
|
|
67
|
+
}
|
|
68
|
+
return (
|
|
69
|
+
<CanvasNodeIconSlot sizePx={sizePx}>
|
|
70
|
+
<Boxes size={sizePx} strokeWidth={strokeWidth} />
|
|
71
|
+
</CanvasNodeIconSlot>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (raw.startsWith("si:")) {
|
|
75
|
+
const slug = raw.slice("si:".length).trim().toLowerCase();
|
|
76
|
+
const builtinUrl = WorkflowCanvasBuiltinIconRegistry.resolveUrl(slug);
|
|
77
|
+
if (builtinUrl) {
|
|
78
|
+
return builtinAssetImg(builtinUrl, sizePx);
|
|
79
|
+
}
|
|
80
|
+
const data = WorkflowCanvasSiIconRegistry.resolve(slug);
|
|
81
|
+
if (data) {
|
|
82
|
+
return (
|
|
83
|
+
<CanvasNodeIconSlot sizePx={sizePx}>
|
|
84
|
+
<WorkflowCanvasSimpleIconGlyph title={data.title} path={data.path} hex={data.hex} sizePx={sizePx} />
|
|
85
|
+
</CanvasNodeIconSlot>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return (
|
|
89
|
+
<CanvasNodeIconSlot sizePx={sizePx}>
|
|
90
|
+
<Boxes size={sizePx} strokeWidth={strokeWidth} />
|
|
91
|
+
</CanvasNodeIconSlot>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const lucideName = raw.startsWith("lucide:") ? raw.slice("lucide:".length).trim().toLowerCase() : raw.toLowerCase();
|
|
95
|
+
return (
|
|
96
|
+
<CanvasNodeIconSlot sizePx={sizePx}>
|
|
97
|
+
<Suspense fallback={<Boxes size={sizePx} strokeWidth={strokeWidth} />}>
|
|
98
|
+
<DynamicIcon name={lucideName as IconName} size={sizePx} strokeWidth={strokeWidth} />
|
|
99
|
+
</Suspense>
|
|
100
|
+
</CanvasNodeIconSlot>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a Simple Icons path in a 24×24 viewBox (same coordinate space as Lucide for sizing).
|
|
3
|
+
*/
|
|
4
|
+
export function WorkflowCanvasSimpleIconGlyph(
|
|
5
|
+
props: Readonly<{ title: string; path: string; hex: string; sizePx: number }>,
|
|
6
|
+
) {
|
|
7
|
+
const { title, path, hex, sizePx } = props;
|
|
8
|
+
return (
|
|
9
|
+
<svg
|
|
10
|
+
role="img"
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
width={sizePx}
|
|
13
|
+
height={sizePx}
|
|
14
|
+
aria-hidden
|
|
15
|
+
style={{ display: "block", backgroundColor: "transparent" }}
|
|
16
|
+
>
|
|
17
|
+
<title>{title}</title>
|
|
18
|
+
<path fill={`#${hex}`} d={path} />
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseEdge,
|
|
3
|
+
getStraightPath,
|
|
4
|
+
type Edge as ReactFlowEdge,
|
|
5
|
+
type EdgeProps as ReactFlowEdgeProps,
|
|
6
|
+
} from "@xyflow/react";
|
|
7
|
+
|
|
8
|
+
export function StraightCountEdge(props: ReactFlowEdgeProps<ReactFlowEdge>) {
|
|
9
|
+
const [edgePath, labelX, labelY] = getStraightPath({
|
|
10
|
+
sourceX: props.sourceX,
|
|
11
|
+
sourceY: props.sourceY,
|
|
12
|
+
targetX: props.targetX,
|
|
13
|
+
targetY: props.targetY,
|
|
14
|
+
});
|
|
15
|
+
return (
|
|
16
|
+
<BaseEdge
|
|
17
|
+
id={props.id}
|
|
18
|
+
path={edgePath}
|
|
19
|
+
markerEnd={props.markerEnd}
|
|
20
|
+
markerStart={props.markerStart}
|
|
21
|
+
style={props.style}
|
|
22
|
+
label={props.label}
|
|
23
|
+
labelX={labelX}
|
|
24
|
+
labelY={labelY + 16}
|
|
25
|
+
labelStyle={props.labelStyle}
|
|
26
|
+
labelShowBg
|
|
27
|
+
labelBgStyle={props.labelBgStyle}
|
|
28
|
+
labelBgPadding={props.labelBgPadding}
|
|
29
|
+
labelBgBorderRadius={props.labelBgBorderRadius}
|
|
30
|
+
interactionWidth={props.interactionWidth}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BaseEdge, type Edge, type EdgeProps } from "@xyflow/react";
|
|
2
|
+
|
|
3
|
+
import { WORKFLOW_CANVAS_MAIN_EDGE_OFFSET } from "./lib/workflowCanvasEdgeGeometry";
|
|
4
|
+
import { WorkflowCanvasSymmetricForkPathPlanner } from "./lib/WorkflowCanvasSymmetricForkPathPlanner";
|
|
5
|
+
|
|
6
|
+
export function WorkflowCanvasSymmetricForkEdge(props: EdgeProps<Edge>) {
|
|
7
|
+
const { path, labelX, labelY } = WorkflowCanvasSymmetricForkPathPlanner.build({
|
|
8
|
+
sourceX: props.sourceX,
|
|
9
|
+
sourceY: props.sourceY,
|
|
10
|
+
targetX: props.targetX,
|
|
11
|
+
targetY: props.targetY,
|
|
12
|
+
offset: WORKFLOW_CANVAS_MAIN_EDGE_OFFSET,
|
|
13
|
+
});
|
|
14
|
+
return (
|
|
15
|
+
<BaseEdge
|
|
16
|
+
id={props.id}
|
|
17
|
+
path={path}
|
|
18
|
+
markerEnd={props.markerEnd}
|
|
19
|
+
markerStart={props.markerStart}
|
|
20
|
+
style={props.style}
|
|
21
|
+
label={props.label}
|
|
22
|
+
labelX={labelX}
|
|
23
|
+
labelY={labelY + 16}
|
|
24
|
+
labelStyle={props.labelStyle}
|
|
25
|
+
labelShowBg
|
|
26
|
+
labelBgStyle={props.labelBgStyle}
|
|
27
|
+
labelBgPadding={props.labelBgPadding}
|
|
28
|
+
labelBgBorderRadius={props.labelBgBorderRadius}
|
|
29
|
+
interactionWidth={props.interactionWidth}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type MouseEvent, type ReactNode, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function WorkflowCanvasToolbarIconButton(
|
|
4
|
+
args: Readonly<{
|
|
5
|
+
testId: string;
|
|
6
|
+
ariaLabel: string;
|
|
7
|
+
tooltip: string;
|
|
8
|
+
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
9
|
+
onAfterClick?: () => void;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
accentColor?: string;
|
|
13
|
+
}>,
|
|
14
|
+
) {
|
|
15
|
+
const {
|
|
16
|
+
accentColor = "#111827",
|
|
17
|
+
ariaLabel,
|
|
18
|
+
children,
|
|
19
|
+
disabled = false,
|
|
20
|
+
onAfterClick,
|
|
21
|
+
onClick,
|
|
22
|
+
testId,
|
|
23
|
+
tooltip,
|
|
24
|
+
} = args;
|
|
25
|
+
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
|
|
26
|
+
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
|
|
27
|
+
onClick(event);
|
|
28
|
+
event.currentTarget.blur();
|
|
29
|
+
onAfterClick?.();
|
|
30
|
+
};
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
style={{ position: "relative", display: "grid", placeItems: "center" }}
|
|
34
|
+
onPointerEnter={() => setIsTooltipVisible(true)}
|
|
35
|
+
onPointerLeave={() => setIsTooltipVisible(false)}
|
|
36
|
+
onFocusCapture={() => setIsTooltipVisible(true)}
|
|
37
|
+
onBlurCapture={(event) => {
|
|
38
|
+
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
|
|
39
|
+
setIsTooltipVisible(false);
|
|
40
|
+
}
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
data-testid={testId}
|
|
46
|
+
aria-label={ariaLabel}
|
|
47
|
+
onMouseDown={(event) => {
|
|
48
|
+
if (!disabled) {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
}
|
|
51
|
+
}}
|
|
52
|
+
onClick={handleClick}
|
|
53
|
+
disabled={disabled}
|
|
54
|
+
style={{
|
|
55
|
+
width: 24,
|
|
56
|
+
height: 24,
|
|
57
|
+
border: "1px solid #d1d5db",
|
|
58
|
+
background: "white",
|
|
59
|
+
color: accentColor,
|
|
60
|
+
display: "grid",
|
|
61
|
+
placeItems: "center",
|
|
62
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
63
|
+
opacity: disabled ? 0.45 : 1,
|
|
64
|
+
padding: 0,
|
|
65
|
+
boxShadow: "0 1px 2px rgba(15,23,42,0.05)",
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
{children}
|
|
69
|
+
</button>
|
|
70
|
+
<div
|
|
71
|
+
role="tooltip"
|
|
72
|
+
aria-hidden={!isTooltipVisible}
|
|
73
|
+
style={{
|
|
74
|
+
position: "absolute",
|
|
75
|
+
bottom: "calc(100% + 8px)",
|
|
76
|
+
left: "50%",
|
|
77
|
+
transform: isTooltipVisible ? "translateX(-50%) translateY(0)" : "translateX(-50%) translateY(3px)",
|
|
78
|
+
opacity: isTooltipVisible ? 1 : 0,
|
|
79
|
+
transition: "opacity 110ms ease-out, transform 110ms ease-out",
|
|
80
|
+
pointerEvents: "none",
|
|
81
|
+
padding: "6px 8px",
|
|
82
|
+
background: "rgba(15,23,42,0.94)",
|
|
83
|
+
color: "white",
|
|
84
|
+
fontSize: 11,
|
|
85
|
+
fontWeight: 700,
|
|
86
|
+
whiteSpace: "nowrap",
|
|
87
|
+
boxShadow: "0 10px 24px rgba(15,23,42,0.2)",
|
|
88
|
+
zIndex: 40,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{tooltip}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in canvas icons (brand marks, etc.) shipped as **SVG files** under `public/canvas-icons/builtin/`.
|
|
3
|
+
* Same idea as n8n’s `file:icon.svg`: one asset per id, square or near-square artboard, no per-brand TSX.
|
|
4
|
+
*
|
|
5
|
+
* To add a builtin:
|
|
6
|
+
* 1. Add `public/canvas-icons/builtin/<id>.svg` (prefer square viewBox; optimize with SVGO).
|
|
7
|
+
* 2. Register the URL in {@link BUILTIN_CANVAS_ICON_URLS}.
|
|
8
|
+
*/
|
|
9
|
+
const CANVAS_BUILTIN_ICON_BASE = "/canvas-icons/builtin";
|
|
10
|
+
|
|
11
|
+
export const BUILTIN_CANVAS_ICON_URLS: Readonly<Record<string, string>> = {
|
|
12
|
+
openai: `${CANVAS_BUILTIN_ICON_BASE}/openai.svg`,
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export type BuiltinCanvasIconId = keyof typeof BUILTIN_CANVAS_ICON_URLS;
|
|
16
|
+
|
|
17
|
+
export class WorkflowCanvasBuiltinIconRegistry {
|
|
18
|
+
static resolveUrl(id: string): string | undefined {
|
|
19
|
+
const key = id.trim().toLowerCase();
|
|
20
|
+
return BUILTIN_CANVAS_ICON_URLS[key];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static has(id: string): boolean {
|
|
24
|
+
return WorkflowCanvasBuiltinIconRegistry.resolveUrl(id) !== undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ConnectionInvocationRecord, NodeExecutionSnapshot } from "../../../lib/realtime/realtimeDomainTypes";
|
|
2
|
+
|
|
3
|
+
export class WorkflowCanvasEdgeCountResolver {
|
|
4
|
+
static resolveCount(
|
|
5
|
+
args: Readonly<{
|
|
6
|
+
targetNodeId: string;
|
|
7
|
+
targetNodeRole: string | undefined;
|
|
8
|
+
targetInput: string;
|
|
9
|
+
sourceOutput: string;
|
|
10
|
+
sourceSnapshot: NodeExecutionSnapshot | undefined;
|
|
11
|
+
targetSnapshot: NodeExecutionSnapshot | undefined;
|
|
12
|
+
nodeSnapshotsByNodeId: Readonly<Record<string, NodeExecutionSnapshot>>;
|
|
13
|
+
connectionInvocations: ReadonlyArray<ConnectionInvocationRecord>;
|
|
14
|
+
}>,
|
|
15
|
+
): number {
|
|
16
|
+
if (args.targetNodeRole === "languageModel" || args.targetNodeRole === "tool") {
|
|
17
|
+
const attachmentInvocationCount = this.resolveAttachmentInvocationCount(
|
|
18
|
+
args.targetNodeId,
|
|
19
|
+
args.targetNodeRole,
|
|
20
|
+
args.nodeSnapshotsByNodeId,
|
|
21
|
+
args.connectionInvocations,
|
|
22
|
+
);
|
|
23
|
+
if (attachmentInvocationCount > 0) return attachmentInvocationCount;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const targetInputItems = args.targetSnapshot?.inputsByPort?.[args.targetInput];
|
|
27
|
+
const sourceOutputItems = args.sourceSnapshot?.outputs?.[args.sourceOutput];
|
|
28
|
+
return targetInputItems?.length ?? sourceOutputItems?.length ?? 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private static resolveAttachmentInvocationCount(
|
|
32
|
+
targetNodeId: string,
|
|
33
|
+
targetNodeRole: string,
|
|
34
|
+
nodeSnapshotsByNodeId: Readonly<Record<string, NodeExecutionSnapshot>>,
|
|
35
|
+
connectionInvocations: ReadonlyArray<ConnectionInvocationRecord>,
|
|
36
|
+
): number {
|
|
37
|
+
const fromHistory = connectionInvocations.filter((inv) => inv.connectionNodeId === targetNodeId).length;
|
|
38
|
+
if (fromHistory > 0) {
|
|
39
|
+
return fromHistory;
|
|
40
|
+
}
|
|
41
|
+
return Object.values(nodeSnapshotsByNodeId).filter((snapshot) => {
|
|
42
|
+
if (targetNodeRole === "languageModel") {
|
|
43
|
+
return snapshot.nodeId === targetNodeId;
|
|
44
|
+
}
|
|
45
|
+
if (targetNodeRole === "tool") {
|
|
46
|
+
return snapshot.nodeId === targetNodeId;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}).length;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class WorkflowCanvasEdgeStyleResolver {
|
|
2
|
+
private static readonly activeMainStroke = "#111827";
|
|
3
|
+
private static readonly activeAttachmentStroke = "#94a3b8";
|
|
4
|
+
private static readonly inactiveMainStroke = "#9ca3af";
|
|
5
|
+
private static readonly inactiveAttachmentStroke = "#cbd5e1";
|
|
6
|
+
private static readonly activeMainLabelFill = "#111827";
|
|
7
|
+
private static readonly activeAttachmentLabelFill = "#475569";
|
|
8
|
+
private static readonly inactiveMainLabelFill = "#6b7280";
|
|
9
|
+
private static readonly inactiveAttachmentLabelFill = "#94a3b8";
|
|
10
|
+
private static readonly activeMainLabelBackground = "rgba(255,253,245,0.96)";
|
|
11
|
+
private static readonly activeAttachmentLabelBackground = "rgba(248,250,252,0.92)";
|
|
12
|
+
private static readonly inactiveMainLabelBackground = "rgba(249,250,251,0.96)";
|
|
13
|
+
private static readonly inactiveAttachmentLabelBackground = "rgba(248,250,252,0.72)";
|
|
14
|
+
|
|
15
|
+
static resolveStrokeColor(args: Readonly<{ edgeItemCount: number; isAttachmentEdge: boolean }>): string {
|
|
16
|
+
if (args.edgeItemCount > 0) {
|
|
17
|
+
return args.isAttachmentEdge ? this.activeAttachmentStroke : this.activeMainStroke;
|
|
18
|
+
}
|
|
19
|
+
return args.isAttachmentEdge ? this.inactiveAttachmentStroke : this.inactiveMainStroke;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static resolveLabelFill(args: Readonly<{ edgeItemCount: number; isAttachmentEdge: boolean }>): string {
|
|
23
|
+
if (args.edgeItemCount > 0) {
|
|
24
|
+
return args.isAttachmentEdge ? this.activeAttachmentLabelFill : this.activeMainLabelFill;
|
|
25
|
+
}
|
|
26
|
+
return args.isAttachmentEdge ? this.inactiveAttachmentLabelFill : this.inactiveMainLabelFill;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static resolveLabelBackground(args: Readonly<{ edgeItemCount: number; isAttachmentEdge: boolean }>): string {
|
|
30
|
+
if (args.edgeItemCount > 0) {
|
|
31
|
+
return args.isAttachmentEdge ? this.activeAttachmentLabelBackground : this.activeMainLabelBackground;
|
|
32
|
+
}
|
|
33
|
+
return args.isAttachmentEdge ? this.inactiveAttachmentLabelBackground : this.inactiveMainLabelBackground;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estimates wrapped line count for workflow canvas labels so Dagre layout height
|
|
3
|
+
* matches rendered text (no ellipsis; conservative wrap).
|
|
4
|
+
*/
|
|
5
|
+
export class WorkflowCanvasLabelLayoutEstimator {
|
|
6
|
+
private static readonly maxLinesCap = 80;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Word-wrap simulation in pixel space (approximate average Latin character width).
|
|
10
|
+
*/
|
|
11
|
+
static estimateLineCount(text: string, maxContentWidthPx: number, fontSizePx: number): number {
|
|
12
|
+
const t = text.trim();
|
|
13
|
+
if (t.length === 0) {
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
const avgCharWidthPx = fontSizePx * 0.52;
|
|
17
|
+
const spaceWidthPx = fontSizePx * 0.35;
|
|
18
|
+
const words = t.split(/\s+/).filter((w) => w.length > 0);
|
|
19
|
+
let lines = 1;
|
|
20
|
+
let lineWidthPx = 0;
|
|
21
|
+
for (const word of words) {
|
|
22
|
+
const wordWidthPx = word.length * avgCharWidthPx;
|
|
23
|
+
if (wordWidthPx > maxContentWidthPx) {
|
|
24
|
+
if (lineWidthPx > 0) {
|
|
25
|
+
lines += 1;
|
|
26
|
+
}
|
|
27
|
+
const wordLines = Math.ceil(wordWidthPx / maxContentWidthPx);
|
|
28
|
+
lines += wordLines - 1;
|
|
29
|
+
lineWidthPx = wordWidthPx % maxContentWidthPx || avgCharWidthPx * 0.5;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const gapPx = lineWidthPx > 0 ? spaceWidthPx : 0;
|
|
33
|
+
if (lineWidthPx + gapPx + wordWidthPx <= maxContentWidthPx + 0.5) {
|
|
34
|
+
lineWidthPx += gapPx + wordWidthPx;
|
|
35
|
+
} else {
|
|
36
|
+
lines += 1;
|
|
37
|
+
lineWidthPx = wordWidthPx;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return Math.min(Math.max(1, lines), this.maxLinesCap);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-layout pass that removes axis-aligned overlaps between workflow nodes.
|
|
3
|
+
* Dagre (layered Sugiyama-style) provides the primary structure; this resolver
|
|
4
|
+
* nudges centers so bounding boxes never intersect—important when attachment
|
|
5
|
+
* nodes sit under parents or when manual offsets interact with dense graphs.
|
|
6
|
+
*/
|
|
7
|
+
export class WorkflowCanvasOverlapResolver {
|
|
8
|
+
static resolve(
|
|
9
|
+
args: Readonly<{
|
|
10
|
+
positionsByNodeId: ReadonlyMap<string, { x: number; y: number }>;
|
|
11
|
+
widthByNodeId: ReadonlyMap<string, number>;
|
|
12
|
+
heightByNodeId: ReadonlyMap<string, number>;
|
|
13
|
+
gap: number;
|
|
14
|
+
maxIterations?: number;
|
|
15
|
+
}>,
|
|
16
|
+
): Map<string, { x: number; y: number }> {
|
|
17
|
+
const maxIterations = args.maxIterations ?? 160;
|
|
18
|
+
const gap = args.gap;
|
|
19
|
+
const out = new Map<string, { x: number; y: number }>();
|
|
20
|
+
for (const [id, p] of args.positionsByNodeId) {
|
|
21
|
+
out.set(id, { x: p.x, y: p.y });
|
|
22
|
+
}
|
|
23
|
+
const ids = [...out.keys()].sort();
|
|
24
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
25
|
+
let moved = false;
|
|
26
|
+
for (let i = 0; i < ids.length; i++) {
|
|
27
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
28
|
+
const idA = ids[i];
|
|
29
|
+
const idB = ids[j];
|
|
30
|
+
if (idA === undefined || idB === undefined) continue;
|
|
31
|
+
const pa = out.get(idA);
|
|
32
|
+
const pb = out.get(idB);
|
|
33
|
+
if (!pa || !pb) continue;
|
|
34
|
+
const wa = args.widthByNodeId.get(idA) ?? 0;
|
|
35
|
+
const ha = args.heightByNodeId.get(idA) ?? 0;
|
|
36
|
+
const wb = args.widthByNodeId.get(idB) ?? 0;
|
|
37
|
+
const hb = args.heightByNodeId.get(idB) ?? 0;
|
|
38
|
+
const minDx = (wa + wb) / 2 + gap;
|
|
39
|
+
const minDy = (ha + hb) / 2 + gap;
|
|
40
|
+
const dx = Math.abs(pa.x - pb.x);
|
|
41
|
+
const dy = Math.abs(pa.y - pb.y);
|
|
42
|
+
if (dx >= minDx || dy >= minDy) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const overlapX = minDx - dx;
|
|
46
|
+
const overlapY = minDy - dy;
|
|
47
|
+
const moveId = idA > idB ? idA : idB;
|
|
48
|
+
const pm = out.get(moveId);
|
|
49
|
+
const otherId = moveId === idA ? idB : idA;
|
|
50
|
+
const po = out.get(otherId);
|
|
51
|
+
if (!pm || !po) continue;
|
|
52
|
+
const wm = moveId === idA ? wa : wb;
|
|
53
|
+
const hm = moveId === idA ? ha : hb;
|
|
54
|
+
const wo = moveId === idA ? wb : wa;
|
|
55
|
+
const ho = moveId === idA ? hb : ha;
|
|
56
|
+
const pairMinDx = (wm + wo) / 2 + gap;
|
|
57
|
+
const pairMinDy = (hm + ho) / 2 + gap;
|
|
58
|
+
if (overlapY <= overlapX) {
|
|
59
|
+
if (pm.y <= po.y) {
|
|
60
|
+
out.set(moveId, { x: pm.x, y: po.y - pairMinDy });
|
|
61
|
+
} else {
|
|
62
|
+
out.set(moveId, { x: pm.x, y: po.y + pairMinDy });
|
|
63
|
+
}
|
|
64
|
+
} else if (pm.x <= po.x) {
|
|
65
|
+
out.set(moveId, { x: po.x - pairMinDx, y: pm.y });
|
|
66
|
+
} else {
|
|
67
|
+
out.set(moveId, { x: po.x + pairMinDx, y: pm.y });
|
|
68
|
+
}
|
|
69
|
+
moved = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!moved) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable ordering for workflow I/O ports on canvas handles so branches (e.g. true/false)
|
|
3
|
+
* stack predictably: true above false, merge inputs aligned with branch geometry.
|
|
4
|
+
*/
|
|
5
|
+
export class WorkflowCanvasPortOrderResolver {
|
|
6
|
+
static sortSourceOutputs(ports: readonly string[]): string[] {
|
|
7
|
+
const rank = (p: string): number => {
|
|
8
|
+
if (p === "true") return 0;
|
|
9
|
+
if (p === "false") return 1;
|
|
10
|
+
if (p === "main") return 2;
|
|
11
|
+
return 3;
|
|
12
|
+
};
|
|
13
|
+
return [...new Set(ports)].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static sortTargetInputs(ports: readonly string[]): string[] {
|
|
17
|
+
const rank = (p: string): number => {
|
|
18
|
+
if (p === "true") return 0;
|
|
19
|
+
if (p === "false") return 1;
|
|
20
|
+
if (p === "in") return 2;
|
|
21
|
+
return 3;
|
|
22
|
+
};
|
|
23
|
+
return [...new Set(ports)].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/features/workflows/components/canvas/lib/WorkflowCanvasRoundedOrthogonalPathPlanner.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { WORKFLOW_CANVAS_MAIN_EDGE_CORNER_RADIUS } from "./workflowCanvasEdgeGeometry";
|
|
2
|
+
|
|
3
|
+
type XY = Readonly<{ x: number; y: number }>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Same corner treatment as React Flow's {@link getSmoothStepPath} (quadratic bends at
|
|
7
|
+
* orthogonal joints) applied to an explicit polyline—used for symmetric fork edges.
|
|
8
|
+
*/
|
|
9
|
+
export class WorkflowCanvasRoundedOrthogonalPathPlanner {
|
|
10
|
+
static buildPathFromPoints(
|
|
11
|
+
points: ReadonlyArray<XY>,
|
|
12
|
+
borderRadius: number = WORKFLOW_CANVAS_MAIN_EDGE_CORNER_RADIUS,
|
|
13
|
+
): string {
|
|
14
|
+
if (points.length === 0) {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
return points.reduce((res, p, i) => {
|
|
18
|
+
let segment = "";
|
|
19
|
+
if (i > 0 && i < points.length - 1) {
|
|
20
|
+
const prev = points[i - 1];
|
|
21
|
+
const next = points[i + 1];
|
|
22
|
+
if (prev && next) {
|
|
23
|
+
segment = WorkflowCanvasRoundedOrthogonalPathPlanner.bend(prev, p, next, borderRadius);
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
segment = `${i === 0 ? "M" : "L"}${p.x} ${p.y}`;
|
|
27
|
+
}
|
|
28
|
+
return res + segment;
|
|
29
|
+
}, "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private static distance(a: XY, b: XY): number {
|
|
33
|
+
return Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Ported from @xyflow/system getBend (smoothstep-edge). */
|
|
37
|
+
private static bend(a: XY, b: XY, c: XY, size: number): string {
|
|
38
|
+
const bendSize = Math.min(
|
|
39
|
+
WorkflowCanvasRoundedOrthogonalPathPlanner.distance(a, b) / 2,
|
|
40
|
+
WorkflowCanvasRoundedOrthogonalPathPlanner.distance(b, c) / 2,
|
|
41
|
+
size,
|
|
42
|
+
);
|
|
43
|
+
const { x, y } = b;
|
|
44
|
+
if ((a.x === x && x === c.x) || (a.y === y && y === c.y)) {
|
|
45
|
+
return `L${x} ${y}`;
|
|
46
|
+
}
|
|
47
|
+
if (a.y === y) {
|
|
48
|
+
const xDir = a.x < c.x ? -1 : 1;
|
|
49
|
+
const yDir = a.y < c.y ? 1 : -1;
|
|
50
|
+
return `L ${x + bendSize * xDir},${y}Q ${x},${y} ${x},${y + bendSize * yDir}`;
|
|
51
|
+
}
|
|
52
|
+
const xDir = a.x < c.x ? 1 : -1;
|
|
53
|
+
const yDir = a.y < c.y ? -1 : 1;
|
|
54
|
+
return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`;
|
|
55
|
+
}
|
|
56
|
+
}
|