|
- "use client";
-
- import { memo, useEffect, useState } from "react";
- import {
- Handle,
- Position,
- useReactFlow,
- useUpdateNodeInternals,
- type Node,
- type NodeProps,
- } from "@xyflow/react";
- import {
- Box,
- Chip,
- Divider,
- IconButton,
- Paper,
- Tooltip,
- Typography,
- } from "@mui/material";
- import { alpha } from "@mui/material/styles";
- import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
- import ExpandLessIcon from "@mui/icons-material/ExpandLess";
- import { useTranslation } from "react-i18next";
- import ItemTracingDocLink from "./ItemTracingDocLink";
- import ItemTracingLotTraceLink from "./ItemTracingLotTraceLink";
- import ItemTracingStockTakeLifecycle from "./ItemTracingStockTakeLifecycle";
- import { TraceFlowNodeData } from "./buildReactFlowGraph";
- import { NODE_HEIGHT, NODE_WIDTH, NODE_WIDTH_BRANCH, DO_GROUP_HEADER } from "./traceFlowConstants";
- import { traceFlowHandleTopPercent } from "./traceFlowEdgeLayout";
- import { kindColor, kindLabelKey } from "./traceFlowNodeUtils";
- import { formatQty, formatSignedQty } from "./traceQtyUtils";
- import { buildStockTakeLifecycleStages } from "./traceStockTakeUtils";
- import { pickStatusValueColor } from "./traceLabelUtils";
-
- const PHASE_LABEL_INNER = 96;
- /** Expanded stock-take stage panel (mt + maxHeight + padding) below the fixed card. */
- const STOCK_TAKE_LIFECYCLE_PANEL_EXTRA = 320;
-
- const GroupCollapseButton = ({
- collapsed,
- onToggle,
- collapseLabel,
- expandLabel,
- }: {
- collapsed: boolean;
- onToggle?: () => void;
- collapseLabel: string;
- expandLabel: string;
- }) => (
- <IconButton
- size="small"
- aria-label={collapsed ? expandLabel : collapseLabel}
- aria-expanded={!collapsed}
- onClick={(e) => {
- e.stopPropagation();
- onToggle?.();
- }}
- onMouseDown={(e) => e.stopPropagation()}
- sx={{
- pointerEvents: "all",
- p: 0.25,
- ml: "auto",
- flexShrink: 0,
- color: "warning.dark",
- }}
- >
- {collapsed ? (
- <ExpandMoreIcon fontSize="small" />
- ) : (
- <ExpandLessIcon fontSize="small" />
- )}
- </IconButton>
- );
-
- export const TraceFlowEventNode = memo(function TraceFlowEventNode({
- id,
- data,
- }: NodeProps<Node<TraceFlowNodeData>>) {
- const { t } = useTranslation("itemTracing");
- const { setNodes } = useReactFlow();
- const updateNodeInternals = useUpdateNodeInternals();
- const node = data.layoutNode;
- const color = kindColor(node.kind);
- const isQc = node.kind === "QC" || node.kind === "MATERIAL_QC";
- const isStockTake = node.kind === "STOCK_TAKE";
- const hasLifecycle = isStockTake && node.stockTakeRecordDetail != null;
- const [lifecycleExpanded, setLifecycleExpanded] = useState(false);
- const lifecycleStages = hasLifecycle
- ? buildStockTakeLifecycleStages(node.stockTakeRecordDetail)
- : [];
- const nodeWidth = data.compact ? NODE_WIDTH_BRANCH : NODE_WIDTH;
-
- useEffect(() => {
- if (!hasLifecycle) return;
- const nextH = lifecycleExpanded
- ? NODE_HEIGHT + STOCK_TAKE_LIFECYCLE_PANEL_EXTRA
- : NODE_HEIGHT;
- setNodes((nds) =>
- nds.map((n) => {
- if (n.id !== id) return n;
- return {
- ...n,
- height: nextH,
- style: { ...(n.style ?? {}), width: nodeWidth, height: nextH },
- measured: {
- width: n.measured?.width ?? n.width ?? nodeWidth,
- height: nextH,
- },
- };
- }),
- );
- const raf = requestAnimationFrame(() => updateNodeInternals(id));
- return () => cancelAnimationFrame(raf);
- }, [
- hasLifecycle,
- lifecycleExpanded,
- id,
- nodeWidth,
- setNodes,
- updateNodeInternals,
- ]);
-
- const chipLabel =
- node.kind === "PUTAWAY" && node.putawayStatusLabel?.trim()
- ? node.putawayStatusLabel
- : isQc && node.qcTypeLabel?.trim()
- ? node.qcTypeLabel
- : t(kindLabelKey(node.kind));
- const dateLabel = node.timestamp?.trim() ? node.timestamp : t("noTimestamp");
- const isDoOut = node.kind === "DO_OUT";
- const doOutKindChipSx = {
- height: data.compact ? 18 : 20,
- "& .MuiChip-label": {
- px: 0.75,
- fontSize: data.compact ? "0.65rem" : "0.7rem",
- fontWeight: 600,
- },
- } as const;
- const stopCardClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- };
- const doOutboundKindChips =
- isDoOut && (node.doOutboundIsExtra || node.doOutboundIsReplenish) ? (
- <>
- {node.doOutboundIsExtra ? (
- <Chip
- size="small"
- label={t("doOutboundExtra")}
- color="secondary"
- sx={doOutKindChipSx}
- onClick={stopCardClick}
- onMouseDown={stopCardClick}
- />
- ) : null}
- {node.doOutboundIsReplenish ? (
- <Chip
- size="small"
- label={t("doOutboundReplenish")}
- color="success"
- sx={doOutKindChipSx}
- onClick={stopCardClick}
- onMouseDown={stopCardClick}
- />
- ) : null}
- </>
- ) : null;
- const titleContent = isDoOut ? (
- <Box
- component="span"
- sx={{
- display: "inline-flex",
- flexWrap: "wrap",
- alignItems: "center",
- gap: 0.5,
- maxWidth: "100%",
- }}
- >
- {node.docLinkKind && node.refCode ? (
- <ItemTracingDocLink
- kind={node.docLinkKind}
- code={node.refCode}
- id={node.refId}
- consoCode={node.consoCode ?? node.refCode}
- ticketNo={node.docLinkTicketNo}
- targetDate={node.docLinkTargetDate}
- openInNewTab
- />
- ) : (
- node.refCode || "—"
- )}
- </Box>
- ) : node.docLinkKind && node.refCode ? (
- <ItemTracingDocLink
- kind={node.docLinkKind}
- code={node.refCode}
- id={node.refId}
- consoCode={node.consoCode ?? node.refCode}
- ticketNo={node.docLinkTicketNo}
- targetDate={node.docLinkTargetDate}
- openInNewTab
- />
- ) : (
- node.title
- );
- const incomingCount = data.incomingHandleCount ?? 0;
- const outgoingCount = data.outgoingHandleCount ?? 0;
- const searchActive = data.searchActive ?? false;
- const searchMatch = data.searchMatch ?? false;
- const searchFocused = data.searchFocused ?? false;
- const nodeSelected = data.nodeSelected ?? false;
-
- const hiddenHandleStyle = { opacity: 0, width: 8, height: 8, transform: "none" as const };
-
- const activeCount = lifecycleStages.filter((s) => s.isActive).length;
-
- const card = (
- <Paper
- variant="outlined"
- sx={{
- width: nodeWidth,
- minWidth: nodeWidth,
- maxWidth: nodeWidth,
- height: NODE_HEIGHT,
- display: "flex",
- flexDirection: "column",
- overflow: "hidden",
- position: "relative",
- borderColor: searchFocused
- ? "primary.main"
- : nodeSelected
- ? "primary.main"
- : searchMatch
- ? "warning.main"
- : isQc
- ? `${color}.main`
- : undefined,
- borderWidth: searchFocused || searchMatch || nodeSelected ? 2 : isQc ? 2 : 1,
- opacity: searchActive && !searchMatch ? 0.35 : 1,
- transition: "opacity 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease",
- boxShadow: searchFocused ? 8 : nodeSelected ? 6 : searchMatch ? 4 : undefined,
- cursor: "pointer",
- "&:hover": { boxShadow: searchFocused || searchMatch || nodeSelected ? undefined : 6 },
- }}
- >
- {incomingCount > 0 &&
- Array.from({ length: incomingCount }, (_, i) => (
- <Handle
- key={`in-${i}`}
- id={`in-${i}`}
- type="target"
- position={Position.Left}
- style={{
- ...hiddenHandleStyle,
- top: traceFlowHandleTopPercent(i, incomingCount),
- left: 0,
- }}
- />
- ))}
- <Box sx={{ height: 4, bgcolor: `${color}.main`, flexShrink: 0 }} />
- <Box
- sx={{
- p: data.compact ? 1 : 1.25,
- pb: hasLifecycle ? (data.compact ? 0.5 : 0.75) : undefined,
- flex: 1,
- minHeight: 0,
- overflow: "hidden",
- }}
- >
- <Box
- sx={{
- display: "flex",
- alignItems: "center",
- flexWrap: "wrap",
- gap: 0.5,
- mb: 0.75,
- }}
- >
- {isQc && (
- <Box
- sx={{
- width: 8,
- height: 8,
- bgcolor: `${color}.main`,
- transform: "rotate(45deg)",
- flexShrink: 0,
- }}
- />
- )}
- <Chip size="small" label={chipLabel} color={color} />
- {doOutboundKindChips}
- </Box>
- <Box
- sx={{
- lineHeight: 1.3,
- fontSize: data.compact ? "0.8rem" : undefined,
- fontWeight: 700,
- overflow: "hidden",
- overflowWrap: "anywhere",
- wordBreak: "break-word",
- ...(isDoOut
- ? { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 0.5 }
- : {
- display: "-webkit-box",
- WebkitLineClamp: 2,
- WebkitBoxOrient: "vertical",
- }),
- }}
- title={typeof node.refCode === "string" && node.refCode.trim() ? node.refCode : node.title}
- >
- {titleContent}
- </Box>
- {node.subtitle?.trim() ? (
- <Typography
- variant="caption"
- color="text.secondary"
- display="block"
- title={node.subtitle}
- sx={{
- mt: 0.5,
- overflow: "hidden",
- overflowWrap: "anywhere",
- wordBreak: "break-word",
- display: "-webkit-box",
- WebkitLineClamp: 2,
- WebkitBoxOrient: "vertical",
- }}
- >
- {node.subtitle}
- </Typography>
- ) : null}
- {node.kind === "MATERIAL_PICK" && node.traceItemCode?.trim() ? (
- <Typography
- variant="body2"
- fontWeight={600}
- title={node.meta?.trim() || node.traceItemCode}
- sx={{
- mt: 0.5,
- fontSize: data.compact ? "0.8rem" : undefined,
- overflow: "hidden",
- overflowWrap: "anywhere",
- wordBreak: "break-word",
- display: "-webkit-box",
- WebkitLineClamp: 2,
- WebkitBoxOrient: "vertical",
- }}
- >
- {t("Item")}: {node.meta?.trim() || node.traceItemCode}
- </Typography>
- ) : null}
- {node.qty != null && (
- <Typography
- variant="body2"
- fontWeight={600}
- title={
- node.kind === "ADJUSTMENT"
- ? formatSignedQty(node.qty, node.adjustmentDirection, node.uom)
- : formatQty(node.qty, node.uom)
- }
- sx={{
- mt: 0.5,
- fontSize: data.compact ? "0.8rem" : undefined,
- overflow: "hidden",
- overflowWrap: "anywhere",
- wordBreak: "break-word",
- display: "-webkit-box",
- WebkitLineClamp: 2,
- WebkitBoxOrient: "vertical",
- }}
- >
- {node.kind === "PURCHASE"
- ? t("detailOrderQty")
- : node.kind === "STOCK_TAKE"
- ? t("after")
- : node.kind === "ADJUSTMENT"
- ? t("variance")
- : node.kind === "QC" || node.kind === "MATERIAL_QC" || node.kind === "FAIL"
- ? t("unqualifiedQty")
- : node.kind === "PRODUCTION_STEP"
- ? t("processOutputQty")
- : t("qty")}
- :{" "}
- {node.kind === "ADJUSTMENT"
- ? formatSignedQty(node.qty, node.adjustmentDirection, node.uom)
- : formatQty(node.qty, node.uom)}
- </Typography>
- )}
- {node.traceLotNo ? (
- <ItemTracingLotTraceLink
- label={
- node.kind === "BYPRODUCT"
- ? t("traceByproductLot")
- : node.kind === "REPACK"
- ? t("traceRepackLot")
- : t("traceMaterialLot")
- }
- lotNo={node.traceLotNo}
- itemCode={node.traceItemCode}
- stopPropagation
- sx={{ mt: 0.5, display: "inline-block" }}
- />
- ) : null}
- {node.warehouseCode?.trim() ? (
- <Typography
- variant="caption"
- color="text.secondary"
- display="block"
- title={node.warehouseCode}
- sx={{
- mt: 0.5,
- fontWeight: 600,
- overflow: "hidden",
- overflowWrap: "anywhere",
- wordBreak: "break-word",
- display: "-webkit-box",
- WebkitLineClamp: 2,
- WebkitBoxOrient: "vertical",
- }}
- >
- {t("warehouse")}: {node.warehouseCode}
- </Typography>
- ) : null}
- {(node.kind === "JO_OUT" || node.kind === "MATERIAL_PICK") &&
- node.processingStatusLabel?.trim() ? (
- <Typography
- variant="caption"
- color="text.secondary"
- display="block"
- title={node.processingStatusLabel}
- sx={{
- mt: 0.5,
- fontWeight: 600,
- overflow: "hidden",
- overflowWrap: "anywhere",
- wordBreak: "break-word",
- display: "-webkit-box",
- WebkitLineClamp: 1,
- WebkitBoxOrient: "vertical",
- }}
- >
- {t("processingStatus")}:{" "}
- <Box
- component="span"
- sx={{
- color: (() => {
- const c = pickStatusValueColor(node.processingStatus);
- return c === "default" ? "text.primary" : `${c}.main`;
- })(),
- fontWeight: 700,
- }}
- >
- {node.processingStatusLabel}
- </Box>
- </Typography>
- ) : null}
- {(node.kind === "JO_OUT" || node.kind === "MATERIAL_PICK") &&
- node.matchStatusLabel?.trim() ? (
- <Typography
- variant="caption"
- color="text.secondary"
- display="block"
- title={node.matchStatusLabel}
- sx={{
- mt: 0.5,
- fontWeight: 600,
- overflow: "hidden",
- overflowWrap: "anywhere",
- wordBreak: "break-word",
- display: "-webkit-box",
- WebkitLineClamp: 1,
- WebkitBoxOrient: "vertical",
- }}
- >
- {t("matchStatus")}:{" "}
- <Box
- component="span"
- sx={{
- color: (() => {
- const c = pickStatusValueColor(node.matchStatus);
- return c === "default" ? "text.primary" : `${c}.main`;
- })(),
- fontWeight: 700,
- }}
- >
- {node.matchStatusLabel}
- </Box>
- </Typography>
- ) : null}
- <Typography variant="caption" color="text.secondary" display="block" sx={{ mt: 0.75 }}>
- {dateLabel}
- </Typography>
- </Box>
- {hasLifecycle ? (
- <Box
- sx={{
- flexShrink: 0,
- borderTop: 1,
- borderColor: "divider",
- px: data.compact ? 0.75 : 1,
- py: 0.25,
- }}
- >
- <Box
- role="button"
- tabIndex={0}
- aria-expanded={lifecycleExpanded}
- aria-label={lifecycleExpanded ? t("stockTakeStageCollapse") : t("stockTakeStageExpand")}
- onClick={(e) => {
- e.stopPropagation();
- setLifecycleExpanded((v) => !v);
- }}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- e.stopPropagation();
- setLifecycleExpanded((v) => !v);
- }
- }}
- sx={{
- display: "flex",
- alignItems: "center",
- justifyContent: "space-between",
- gap: 0.5,
- width: "100%",
- py: 0.25,
- px: 0.5,
- borderRadius: 1,
- cursor: "pointer",
- userSelect: "none",
- "&:hover": { bgcolor: "action.hover" },
- }}
- >
- <Typography variant="caption" fontWeight={700} color="secondary.main" noWrap>
- {t("nodeStockTake")} · {t("stockTakeStageDetail", { count: activeCount })}
- </Typography>
- {lifecycleExpanded ? (
- <ExpandLessIcon fontSize="small" color="secondary" />
- ) : (
- <ExpandMoreIcon fontSize="small" color="secondary" />
- )}
- </Box>
- </Box>
- ) : null}
- {outgoingCount > 0 &&
- Array.from({ length: outgoingCount }, (_, i) => (
- <Handle
- key={`out-${i}`}
- id={`out-${i}`}
- type="source"
- position={Position.Right}
- style={{
- ...hiddenHandleStyle,
- top: traceFlowHandleTopPercent(i, outgoingCount),
- right: 0,
- }}
- />
- ))}
- </Paper>
- );
-
- // Inline under the card so pan/zoom keeps the panel attached (Popper portals to body).
- const lifecyclePanel =
- hasLifecycle && lifecycleExpanded ? (
- <Paper
- variant="outlined"
- className="nowheel nodrag nopan"
- onClick={(e) => e.stopPropagation()}
- onMouseDown={(e) => e.stopPropagation()}
- onWheel={(e) => e.stopPropagation()}
- sx={{
- width: nodeWidth,
- maxWidth: nodeWidth,
- mt: 0.75,
- p: 1.25,
- boxShadow: 4,
- borderColor: "secondary.main",
- borderWidth: 1.5,
- maxHeight: STOCK_TAKE_LIFECYCLE_PANEL_EXTRA - 24,
- overflow: "auto",
- overscrollBehavior: "contain",
- bgcolor: "background.paper",
- pointerEvents: "all",
- }}
- >
- <Typography variant="caption" fontWeight={700} color="secondary.main" sx={{ mb: 1, display: "block" }}>
- {t("nodeStockTake")} · {t("stockTakeStageDetail", { count: activeCount })}
- </Typography>
- <Divider sx={{ mb: 1 }} />
- <ItemTracingStockTakeLifecycle stages={lifecycleStages} uom={node.uom} />
- </Paper>
- ) : null;
-
- const wrapped = (
- <Box
- sx={{
- width: nodeWidth,
- height: lifecycleExpanded
- ? NODE_HEIGHT + STOCK_TAKE_LIFECYCLE_PANEL_EXTRA
- : NODE_HEIGHT,
- overflow: "visible",
- }}
- >
- {card}
- {lifecyclePanel}
- </Box>
- );
-
- if (node.meta) {
- return (
- <Tooltip title={node.meta} arrow placement="top">
- {wrapped}
- </Tooltip>
- );
- }
- return wrapped;
- });
-
- export const TraceFlowPhaseLabelNode = memo(function TraceFlowPhaseLabelNode({
- data,
- }: NodeProps<Node<TraceFlowNodeData>>) {
- return (
- <Box
- sx={{
- width: PHASE_LABEL_INNER,
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- px: 0.75,
- py: 1,
- borderLeft: `4px solid ${data.phaseColor ?? "#757575"}`,
- backgroundImage: `linear-gradient(${data.phaseColor ?? "#757575"}14, ${data.phaseColor ?? "#757575"}14)`,
- borderRadius: 1,
- }}
- >
- <Typography
- variant="caption"
- fontWeight={700}
- sx={{
- writingMode: "vertical-rl",
- textOrientation: "mixed",
- color: data.phaseColor ?? "#757575",
- letterSpacing: 0.5,
- }}
- >
- {data.phaseLabel}
- </Typography>
- </Box>
- );
- });
-
- export const TraceFlowDateHeaderNode = memo(function TraceFlowDateHeaderNode({
- data,
- }: NodeProps<Node<TraceFlowNodeData>>) {
- return (
- <Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ width: 80, textAlign: "center" }}>
- {data.dateLabel}
- </Typography>
- );
- });
-
- export const TraceFlowDoGroupNode = memo(function TraceFlowDoGroupNode({
- data,
- }: NodeProps<Node<TraceFlowNodeData>>) {
- const { t } = useTranslation("itemTracing");
- const node = data.layoutNode;
- const incomingCount = data.incomingHandleCount ?? 0;
- const searchActive = data.searchActive ?? false;
- const searchMatch = data.searchMatch ?? false;
- const searchFocused = data.searchFocused ?? false;
- const collapsed = data.groupCollapsed === true;
- const width = node.groupBoxWidth ?? NODE_WIDTH;
- const height = collapsed ? DO_GROUP_HEADER : (node.groupBoxHeight ?? NODE_HEIGHT);
- const hiddenHandleStyle = { opacity: 0, width: 8, height: 8, transform: "none" as const };
-
- return (
- <Box
- sx={{
- width,
- height,
- border: 2,
- borderStyle: "dashed",
- borderColor: searchFocused
- ? "primary.main"
- : searchMatch
- ? "warning.main"
- : "warning.light",
- borderRadius: 1,
- bgcolor: (theme) => alpha(theme.palette.background.paper, 0.45),
- opacity: searchActive && !searchMatch ? 0.35 : 1,
- boxShadow: searchFocused ? 4 : searchMatch ? 2 : undefined,
- position: "relative",
- overflow: "hidden",
- pointerEvents: "none",
- }}
- >
- {incomingCount > 0 &&
- Array.from({ length: incomingCount }, (_, i) => (
- <Handle
- key={`in-${i}`}
- id={`in-${i}`}
- type="target"
- position={Position.Left}
- style={{
- ...hiddenHandleStyle,
- top: traceFlowHandleTopPercent(i, incomingCount),
- left: 0,
- pointerEvents: "all",
- }}
- />
- ))}
- <Box
- sx={{
- height: DO_GROUP_HEADER,
- px: 1.25,
- display: "flex",
- alignItems: "center",
- gap: 1,
- overflow: "hidden",
- bgcolor: (theme) => alpha(theme.palette.warning.light, 0.35),
- borderBottom: collapsed ? 0 : 1,
- borderColor: "warning.light",
- }}
- >
- <Typography variant="caption" fontWeight={700} noWrap sx={{ color: "warning.dark", minWidth: 0 }}>
- {node.title}
- </Typography>
- {node.groupTotalQty != null ? (
- <Typography variant="caption" fontWeight={600} noWrap sx={{ color: "warning.dark", flexShrink: 0 }}>
- {t("flowDoGroupTotalQty", {
- qtyLabel: formatQty(node.groupTotalQty, node.groupUom),
- })}
- </Typography>
- ) : null}
- <GroupCollapseButton
- collapsed={collapsed}
- onToggle={data.onToggleGroupCollapse}
- collapseLabel={t("flowGroupCollapse")}
- expandLabel={t("flowGroupExpand")}
- />
- </Box>
- </Box>
- );
- });
-
- export const TraceFlowPickGroupNode = memo(function TraceFlowPickGroupNode({
- data,
- }: NodeProps<Node<TraceFlowNodeData>>) {
- const { t } = useTranslation("itemTracing");
- const node = data.layoutNode;
- const incomingCount = data.incomingHandleCount ?? 0;
- const outgoingCount = data.outgoingHandleCount ?? 0;
- const searchActive = data.searchActive ?? false;
- const searchMatch = data.searchMatch ?? false;
- const searchFocused = data.searchFocused ?? false;
- const collapsed = data.groupCollapsed === true;
- const width = node.groupBoxWidth ?? NODE_WIDTH;
- const height = collapsed ? DO_GROUP_HEADER : (node.groupBoxHeight ?? NODE_HEIGHT);
- const hiddenHandleStyle = { opacity: 0, width: 8, height: 8, transform: "none" as const };
-
- return (
- <Box
- sx={{
- width,
- height,
- border: 2,
- borderStyle: "dashed",
- borderColor: searchFocused
- ? "primary.main"
- : searchMatch
- ? "warning.main"
- : "warning.light",
- borderRadius: 1,
- bgcolor: (theme) => alpha(theme.palette.background.paper, 0.45),
- opacity: searchActive && !searchMatch ? 0.35 : 1,
- boxShadow: searchFocused ? 4 : searchMatch ? 2 : undefined,
- position: "relative",
- overflow: "hidden",
- pointerEvents: "none",
- }}
- >
- {incomingCount > 0 &&
- Array.from({ length: incomingCount }, (_, i) => (
- <Handle
- key={`in-${i}`}
- id={`in-${i}`}
- type="target"
- position={Position.Left}
- style={{
- ...hiddenHandleStyle,
- top: traceFlowHandleTopPercent(i, incomingCount),
- left: 0,
- pointerEvents: "all",
- }}
- />
- ))}
- <Box
- sx={{
- height: DO_GROUP_HEADER,
- px: 1.25,
- display: "flex",
- alignItems: "center",
- gap: 0.5,
- bgcolor: (theme) => alpha(theme.palette.warning.light, 0.35),
- borderBottom: collapsed ? 0 : 1,
- borderColor: "warning.light",
- }}
- >
- <Typography variant="caption" fontWeight={700} noWrap sx={{ flex: 1, color: "warning.dark", minWidth: 0 }}>
- {node.title}
- </Typography>
- <GroupCollapseButton
- collapsed={collapsed}
- onToggle={data.onToggleGroupCollapse}
- collapseLabel={t("flowGroupCollapse")}
- expandLabel={t("flowGroupExpand")}
- />
- </Box>
- {outgoingCount > 0 &&
- Array.from({ length: outgoingCount }, (_, i) => (
- <Handle
- key={`out-${i}`}
- id={`out-${i}`}
- type="source"
- position={Position.Right}
- style={{
- ...hiddenHandleStyle,
- top: traceFlowHandleTopPercent(i, outgoingCount),
- right: 0,
- pointerEvents: "all",
- }}
- />
- ))}
- </Box>
- );
- });
-
- export const traceFlowNodeTypes = {
- traceEvent: TraceFlowEventNode,
- doGroup: TraceFlowDoGroupNode,
- pickGroup: TraceFlowPickGroupNode,
- phaseLabel: TraceFlowPhaseLabelNode,
- dateHeader: TraceFlowDateHeaderNode,
- };
|