diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index f02125d0832a..59fad0a910ba 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -2855,6 +2855,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/histories/{history_id}/graph": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Returns a history-scoped structural graph. */ + get: operations["graph_api_histories__history_id__graph_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/histories/{history_id}/jobs_summary": { parameters: { query?: never; @@ -13106,6 +13123,46 @@ export interface components { */ type: "genomebuild"; }; + /** GraphEdge */ + GraphEdge: { + /** Source */ + source: string; + /** Target */ + target: string; + /** + * Type + * @enum {string} + */ + type: "dataset_input" | "dataset_output" | "collection_input" | "collection_output"; + }; + /** GraphNode */ + GraphNode: { + /** Collection Type */ + collection_type?: string | null; + /** Deleted */ + deleted?: boolean | null; + /** Extension */ + extension?: string | null; + /** Hid */ + hid?: number | null; + /** Id */ + id: string; + /** Name */ + name?: string | null; + /** State */ + state?: string | null; + /** Tool Id */ + tool_id?: string | null; + /** Tool Name */ + tool_name?: string | null; + /** + * Type + * @enum {string} + */ + type: "dataset" | "collection" | "tool_request"; + /** Visible */ + visible?: boolean | null; + }; /** * GroupCreatePayload * @description Payload schema for creating a group. @@ -15376,6 +15433,14 @@ export interface components { */ username_and_slug?: string | null; }; + /** HistoryGraphResponse */ + HistoryGraphResponse: { + /** Edges */ + edges: components["schemas"]["GraphEdge"][]; + /** Nodes */ + nodes: components["schemas"]["GraphNode"][]; + truncated: components["schemas"]["TruncationInfo"]; + }; /** * HistorySummary * @description History summary information. @@ -23730,6 +23795,22 @@ export interface components { */ title?: string | null; }; + /** TruncationInfo */ + TruncationInfo: { + /** + * Item Count Capped + * @default false + */ + item_count_capped: boolean; + /** + * Scope Type + * @default recent + * @enum {string} + */ + scope_type: "recent" | "seed_centered"; + /** Seed In Scope */ + seed_in_scope?: boolean | null; + }; /** UndeleteHistoriesPayload */ UndeleteHistoriesPayload: { /** @@ -39149,6 +39230,63 @@ export interface operations { }; }; }; + graph_api_histories__history_id__graph_get: { + parameters: { + query?: { + /** @description Maximum number of nodes. Applied at history scope. */ + limit?: number; + /** @description Include deleted datasets and collections. */ + include_deleted?: boolean; + /** @description Optional: focus on subgraph reachable from this node (e.g. d). */ + seed?: string | null; + /** @description Direction for seed-based subgraph extraction. */ + direction?: "backward" | "forward" | "both"; + /** @description Max depth for seed-based subgraph extraction. */ + depth?: number; + /** @description Center the selection window on this item. Format: d{encoded_id} or c{encoded_id}. */ + seed_scope?: string | null; + }; + header?: { + /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ + "run-as"?: string | null; + }; + path: { + /** @description The encoded database identifier of the History. */ + history_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HistoryGraphResponse"]; + }; + }; + /** @description Request Error */ + "4XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + /** @description Server Error */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + }; + }; index_jobs_summary_api_histories__history_id__jobs_summary_get: { parameters: { query?: { diff --git a/client/src/components/Graph/GraphEdges.vue b/client/src/components/Graph/GraphEdges.vue new file mode 100644 index 000000000000..e3480b3559da --- /dev/null +++ b/client/src/components/Graph/GraphEdges.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/client/src/components/Graph/GraphNode.vue b/client/src/components/Graph/GraphNode.vue new file mode 100644 index 000000000000..111bd2d76700 --- /dev/null +++ b/client/src/components/Graph/GraphNode.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/client/src/components/Graph/GraphView.vue b/client/src/components/Graph/GraphView.vue new file mode 100644 index 000000000000..8d23b3649a08 --- /dev/null +++ b/client/src/components/Graph/GraphView.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/client/src/components/Graph/ZoomControl.vue b/client/src/components/Graph/ZoomControl.vue new file mode 100644 index 000000000000..4dc9669f9d6c --- /dev/null +++ b/client/src/components/Graph/ZoomControl.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/client/src/components/Graph/types.ts b/client/src/components/Graph/types.ts new file mode 100644 index 000000000000..e35af12cc180 --- /dev/null +++ b/client/src/components/Graph/types.ts @@ -0,0 +1,48 @@ +import type { IconDefinition } from "@fortawesome/free-solid-svg-icons"; + +/** A labeled port on a graph node (input or output connector) */ +export interface GraphNodePort { + name: string; + label: string; +} + +/** A positioned node after layout */ +export interface GraphNode { + id: string; + x: number; + y: number; + width: number; + height: number; + label: string; + icon: IconDefinition; + badge?: string | null; + cssClass?: string; + /** Input ports displayed in the node body */ + inputs?: GraphNodePort[]; + /** Output ports displayed in the node body */ + outputs?: GraphNodePort[]; + /** Arbitrary domain data attached by the mapper */ + data?: Record; +} + +/** A positioned edge after layout, with routed points */ +export interface GraphEdge { + id: string; + source: string; + target: string; + cssClass?: string; + /** Render as a collection ribbon (multiple parallel lines) */ + isCollection?: boolean; + points: { x: number; y: number }[]; +} + +/** Edge rendering style */ +export type EdgeStyle = "orthogonal" | "curved"; + +/** Complete layout result ready for rendering */ +export interface GraphLayout { + nodes: GraphNode[]; + edges: GraphEdge[]; + width: number; + height: number; +} diff --git a/client/src/components/History/Content/model/states.ts b/client/src/components/History/Content/model/states.ts index 39d1750a619c..0dfaf272f398 100644 --- a/client/src/components/History/Content/model/states.ts +++ b/client/src/components/History/Content/model/states.ts @@ -40,7 +40,7 @@ export type State = | "new_populated_state" | "inaccessible"; -interface StateRepresentation { +export interface StateRepresentation { status: "success" | "warning" | "info" | "danger" | "secondary"; text?: string; displayName?: string; diff --git a/client/src/components/History/Graph/HistoryGraphDetails.vue b/client/src/components/History/Graph/HistoryGraphDetails.vue new file mode 100644 index 000000000000..381bac02a7c2 --- /dev/null +++ b/client/src/components/History/Graph/HistoryGraphDetails.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/client/src/components/History/Graph/HistoryGraphMinimap.vue b/client/src/components/History/Graph/HistoryGraphMinimap.vue new file mode 100644 index 000000000000..38a487f9a3c1 --- /dev/null +++ b/client/src/components/History/Graph/HistoryGraphMinimap.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/client/src/components/History/Graph/HistoryGraphView.vue b/client/src/components/History/Graph/HistoryGraphView.vue new file mode 100644 index 000000000000..27bc0e58e80c --- /dev/null +++ b/client/src/components/History/Graph/HistoryGraphView.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/client/src/components/History/Graph/historyGraphMapper.ts b/client/src/components/History/Graph/historyGraphMapper.ts new file mode 100644 index 000000000000..e830da59e29a --- /dev/null +++ b/client/src/components/History/Graph/historyGraphMapper.ts @@ -0,0 +1,208 @@ +import { faFile, faLayerGroup, faWrench } from "@fortawesome/free-solid-svg-icons"; + +import type { components } from "@/api/schema"; +import type { GraphEdge, GraphNode, GraphNodePort } from "@/components/Graph/types"; +import { type StateRepresentation, STATES } from "@/components/History/Content/model/states"; + +type ApiGraphNode = components["schemas"]["GraphNode"]; +type ApiGraphEdge = components["schemas"]["GraphEdge"]; +export type HistoryGraphResponse = components["schemas"]["HistoryGraphResponse"]; + +/** Node width — uniform across all types */ +const NODE_WIDTH = 200; + +/** Compute node height based on content: header + ports + padding */ +const HEADER_LINE_HEIGHT = 20; +const HEADER_PADDING = 10; +const HEADER_ICON_CHARS = 3; +const CHARS_PER_LINE = 22; +const PORT_ROW_HEIGHT = 18; +const RULE_HEIGHT = 5; +const BODY_PADDING = 8; +const MIN_NODE_HEIGHT = 32; + +function estimateHeaderLines(label: string): number { + const effectiveLength = label.length + HEADER_ICON_CHARS; + return Math.max(1, Math.ceil(effectiveLength / CHARS_PER_LINE)); +} + +function computeNodeHeight( + label: string, + inputCount: number, + outputCount: number, + hasBadgeBody: boolean = false, +): number { + const headerLines = estimateHeaderLines(label); + const headerHeight = headerLines * HEADER_LINE_HEIGHT + HEADER_PADDING; + if (inputCount === 0 && outputCount === 0 && !hasBadgeBody) { + return Math.max(MIN_NODE_HEIGHT, headerHeight); + } + const portRows = inputCount + outputCount; + const rule = inputCount > 0 && outputCount > 0 ? RULE_HEIGHT : 0; + const badgeRow = hasBadgeBody ? PORT_ROW_HEIGHT + BODY_PADDING : 0; + return headerHeight + portRows * PORT_ROW_HEIGHT + rule + BODY_PADDING + badgeRow; +} + +/** User-facing type labels */ +export const NODE_TYPE_LABELS: Record = { + dataset: "Dataset", + collection: "Collection", + tool_request: "Tool Execution", +}; + +const NODE_ICONS: Record = { + dataset: faFile, + collection: faLayerGroup, + tool_request: faWrench, +}; + +const NODE_CSS_CLASS: Record = { + dataset: "node-dataset", + collection: "node-collection", + tool_request: "node-tool-request", +}; + +// ── Label resolution ── + +function resolveNodeLabel(node: ApiGraphNode): string { + const hid = node.hid ? `${node.hid}: ` : ""; + switch (node.type) { + case "dataset": + return `${hid}${node.name ?? node.extension ?? "Dataset"}`; + case "collection": + return `${hid}${node.name ?? node.collection_type ?? "Collection"}`; + case "tool_request": + return node.tool_name ?? shortenToolId(node.tool_id); + } +} + +function resolveNodeBadge(node: ApiGraphNode): string | null { + switch (node.type) { + case "dataset": + return node.extension ?? null; + case "collection": + return node.collection_type ?? null; + case "tool_request": + return null; + } +} + +function shortenToolId(toolId: string | null | undefined): string { + if (!toolId) { + return "Tool"; + } + // Strip toolshed prefix: + // "toolshed.g2.bx.psu.edu/repos/iuc/bwa_mem/bwa_mem/1.0" → "bwa_mem" + const parts = toolId.split("/"); + if (parts.length >= 2) { + return parts[parts.length - 2] ?? toolId; + } + return toolId; +} + +function isCollectionEdge(edge: ApiGraphEdge): boolean { + return edge.type === "collection_input" || edge.type === "collection_output"; +} + +// ── Public API ── + +/** + * Map API graph nodes to generic GraphNode[] for the renderer. + * Returns nodes with dimensions, labels, icons, badges, and domain data. + */ +export function mapNodes(apiNodes: ApiGraphNode[], apiEdges: ApiGraphEdge[]): GraphNode[] { + // Build a lookup for node labels by ID + const nodeLabels = new Map(); + for (const node of apiNodes) { + nodeLabels.set(node.id, resolveNodeLabel(node)); + } + + // Build input/output port lists per node from edges + const inputPorts = new Map(); + const outputPorts = new Map(); + for (const edge of apiEdges) { + const sourceLabel = nodeLabels.get(edge.source) ?? edge.source; + const targetLabel = nodeLabels.get(edge.target) ?? edge.target; + + // Target node gets an input port labeled by the source + if (!inputPorts.has(edge.target)) { + inputPorts.set(edge.target, []); + } + inputPorts.get(edge.target)!.push({ name: edge.source, label: sourceLabel }); + + // Source node gets an output port labeled by the target + if (!outputPorts.has(edge.source)) { + outputPorts.set(edge.source, []); + } + outputPorts.get(edge.source)!.push({ name: edge.target, label: targetLabel }); + } + + return apiNodes.map((node) => { + // Only tool_request nodes show input/output port lists. + // Dataset and collection nodes show state + badge in the body. + const isToolRequest = node.type === "tool_request"; + const inputs = isToolRequest ? (inputPorts.get(node.id) ?? []) : []; + const outputs = isToolRequest ? (outputPorts.get(node.id) ?? []) : []; + const inputCount = inputPorts.get(node.id)?.length ?? 0; + const outputCount = outputPorts.get(node.id)?.length ?? 0; + const badge = resolveNodeBadge(node); + + // For datasets/collections, use state to determine icon. + // State-based coloring is handled by data-state attribute on the node element, + // which triggers the global $galaxy-state-bg / $galaxy-state-border rules from base.scss. + // Collections return populated_state ("ok"/"new"/"failed") — map "failed" → "error" + // to align with the dataset state vocabulary used by the STATES map and CSS rules. + const rawState = node.state; + const displayState = rawState === "failed" ? "error" : rawState; + const stateKey = displayState as keyof typeof STATES | undefined; + const stateRep: StateRepresentation | null = + !isToolRequest && stateKey && stateKey in STATES ? STATES[stateKey] : null; + const icon = stateRep?.icon ?? NODE_ICONS[node.type] ?? faFile; + const cssClass = NODE_CSS_CLASS[node.type]; + + // Data nodes always have a body (badge + state text) + const hasBody = !isToolRequest; + const label = nodeLabels.get(node.id)!; + return { + id: node.id, + x: 0, + y: 0, + width: NODE_WIDTH, + height: computeNodeHeight(label, inputs.length, outputs.length, hasBody), + label, + icon, + badge, + cssClass, + inputs, + outputs, + data: { + type: node.type, + typeLabel: NODE_TYPE_LABELS[node.type] ?? node.type, + /** Encoded ID without the type prefix (d/c/r) */ + encodedId: node.id.slice(1), + toolId: isToolRequest ? node.tool_id : null, + inputCount, + outputCount, + state: displayState, + stateText: stateRep?.text ?? null, + stateDisplayName: stateRep?.displayName ?? null, + stateSpin: stateRep?.spin ?? false, + }, + }; + }); +} + +/** + * Map API graph edges to generic GraphEdge[] for the renderer. + * Points are set to empty — the layout composable fills them in. + */ +export function mapEdges(apiEdges: ApiGraphEdge[]): GraphEdge[] { + return apiEdges.map((edge, idx) => ({ + id: `e${idx}`, + source: edge.source, + target: edge.target, + cssClass: isCollectionEdge(edge) ? "edge-collection" : "edge-dataset", + isCollection: isCollectionEdge(edge), + points: [], + })); +} diff --git a/client/src/components/History/Graph/useHistoryGraphData.ts b/client/src/components/History/Graph/useHistoryGraphData.ts new file mode 100644 index 000000000000..b787a4b48039 --- /dev/null +++ b/client/src/components/History/Graph/useHistoryGraphData.ts @@ -0,0 +1,55 @@ +import { type Ref, ref, watch } from "vue"; + +import { GalaxyApi } from "@/api"; + +import type { HistoryGraphResponse } from "./historyGraphMapper"; + +/** + * Fetch the history-scoped graph from the API. + * + * The default call returns the full history graph (within bounds). + * An optional seed parameter requests a focused subgraph. + */ +export function useHistoryGraphData(historyId: Ref, limit: Ref, seed?: Ref) { + const graphData = ref(null); + const loading = ref(false); + const error = ref(null); + + async function fetchGraph() { + loading.value = true; + error.value = null; + + try { + const query: Record = { + limit: limit.value, + }; + if (seed?.value) { + query.seed = seed.value; + } + + const { data, error: apiError } = await GalaxyApi().GET("/api/histories/{history_id}/graph", { + params: { + path: { history_id: historyId.value }, + query: query as any, + }, + }); + + if (apiError) { + error.value = apiError.err_msg || "Failed to load graph"; + graphData.value = null; + } else { + graphData.value = data; + } + } catch (e) { + error.value = e instanceof Error ? e.message : "Failed to load graph"; + graphData.value = null; + } finally { + loading.value = false; + } + } + + const watchSources = seed ? [historyId, limit, seed] : [historyId, limit]; + watch(watchSources, () => fetchGraph(), { immediate: true }); + + return { graphData, loading, error }; +} diff --git a/client/src/components/History/Graph/useHistoryGraphLayout.ts b/client/src/components/History/Graph/useHistoryGraphLayout.ts new file mode 100644 index 000000000000..12ead29cdff7 --- /dev/null +++ b/client/src/components/History/Graph/useHistoryGraphLayout.ts @@ -0,0 +1,158 @@ +import ELK, { type ElkExtendedEdge, type ElkNode } from "elkjs/lib/elk.bundled"; +import { type Ref, ref, watch } from "vue"; + +import type { EdgeStyle, GraphEdge, GraphLayout, GraphNode } from "@/components/Graph/types"; +import { computeControlPoints } from "@/utils/connectionPath"; + +import { type HistoryGraphResponse, mapEdges, mapNodes } from "./historyGraphMapper"; + +const elk = new ELK(); + +/** + * Composable that takes API graph data and produces a positioned layout using ELK.js. + * + * Uses the history graph mapper to convert API types to generic graph types, + * then runs ELK layered layout and extracts positioned nodes and edge paths. + * + * edgeStyle controls how edge points are computed: + * - "orthogonal": uses ELK's routed sections (straight segments with bends) + * - "curved": computes bezier control points between node ports (workflow editor style) + */ +export function useHistoryGraphLayout( + graphData: Ref, + edgeStyle: Ref = ref("orthogonal") as Ref, +) { + const layout = ref(null); + const layoutLoading = ref(false); + + watch( + [graphData, edgeStyle], + async ([data, style]) => { + if (!data || data.nodes.length === 0) { + layout.value = null; + return; + } + + layoutLoading.value = true; + try { + layout.value = await computeLayout(data, style); + } catch (e) { + console.error("History graph layout failed:", e); + layout.value = null; + } finally { + layoutLoading.value = false; + } + }, + { immediate: true }, + ); + + return { layout, layoutLoading }; +} + +async function computeLayout(data: HistoryGraphResponse, style: EdgeStyle): Promise { + // Map API types to generic graph types via the history mapper + const graphNodes = mapNodes(data.nodes, data.edges); + const graphEdges = mapEdges(data.edges); + + // Build ELK graph + const elkChildren: ElkNode[] = graphNodes.map((node) => ({ + id: node.id, + width: node.width, + height: node.height, + })); + + const elkEdges: ElkExtendedEdge[] = graphEdges.map((edge) => ({ + id: edge.id, + sources: [edge.source], + targets: [edge.target], + })); + + const elkGraph: ElkNode = { + id: "root", + layoutOptions: { + "elk.algorithm": "layered", + "elk.direction": "RIGHT", + "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", + "elk.layered.spacing.baseValue": "80", + "elk.spacing.nodeNode": "40", + "elk.layered.spacing.nodeNodeBetweenLayers": "80", + "elk.edgeRouting": "ORTHOGONAL", + }, + children: elkChildren, + edges: elkEdges, + }; + + const result = await elk.layout(elkGraph); + + // Apply ELK positions to graph nodes + const nodeById = new Map(graphNodes.map((n) => [n.id, n])); + const layoutNodes: GraphNode[] = (result.children ?? []).map((elkNode) => { + const gn = nodeById.get(elkNode.id)!; + return { + ...gn, + x: elkNode.x ?? 0, + y: elkNode.y ?? 0, + width: elkNode.width ?? gn.width, + height: elkNode.height ?? gn.height, + }; + }); + + // Build node position lookup for edge routing + const nodePositions = new Map(); + for (const n of layoutNodes) { + nodePositions.set(n.id, { x: n.x, y: n.y, w: n.width, h: n.height }); + } + + let layoutEdges: GraphEdge[]; + + if (style === "curved") { + // Compute bezier control points between node ports (workflow editor style) + layoutEdges = graphEdges.map((ge) => { + const src = nodePositions.get(ge.source); + const tgt = nodePositions.get(ge.target); + let points: { x: number; y: number }[] = []; + if (src && tgt) { + const controlPoints = computeControlPoints(src.x + src.w, src.y + src.h / 2, tgt.x, tgt.y + tgt.h / 2); + points = controlPoints.map(([x, y]) => ({ x, y })); + } + return { ...ge, points }; + }); + } else { + // Extract edge paths from ELK's orthogonal routing sections + const edgeById = new Map(graphEdges.map((e) => [e.id, e])); + layoutEdges = (result.edges ?? []).map((elkEdge) => { + const ge = edgeById.get(elkEdge.id)!; + const points: { x: number; y: number }[] = []; + + const sections = (elkEdge as ElkExtendedEdge).sections ?? []; + for (const section of sections) { + points.push({ x: section.startPoint.x, y: section.startPoint.y }); + if (section.bendPoints) { + for (const bp of section.bendPoints) { + points.push({ x: bp.x, y: bp.y }); + } + } + points.push({ x: section.endPoint.x, y: section.endPoint.y }); + } + + // Fallback: straight line between node edges + if (points.length === 0) { + const src = nodePositions.get(ge.source); + const tgt = nodePositions.get(ge.target); + if (src && tgt) { + points.push({ x: src.x + src.w, y: src.y + src.h / 2 }); + points.push({ x: tgt.x, y: tgt.y + tgt.h / 2 }); + } + } + + return { ...ge, points }; + }); + } + + return { + nodes: layoutNodes, + edges: layoutEdges, + width: result.width ?? 0, + height: result.height ?? 0, + }; +} diff --git a/client/src/components/History/HistoryOptions.test.ts b/client/src/components/History/HistoryOptions.test.ts index a6a36a777829..aae57772d51d 100644 --- a/client/src/components/History/HistoryOptions.test.ts +++ b/client/src/components/History/HistoryOptions.test.ts @@ -25,11 +25,18 @@ const expectedOptions = [ "Archive History", "Extract Workflow", "Show Invocations", + "Show History Graph", "Share & Manage Access", ]; // options enabled for logged-out users -const anonymousOptions = ["Resume Paused Jobs", "Delete History", "Export Tool References", "Export History to File"]; +const anonymousOptions = [ + "Resume Paused Jobs", + "Delete History", + "Export Tool References", + "Export History to File", + "Show History Graph", +]; // options disabled for logged-out users const anonymousDisabledOptions = expectedOptions.filter((option) => !anonymousOptions.includes(option)); diff --git a/client/src/components/History/HistoryOptions.vue b/client/src/components/History/HistoryOptions.vue index bb02389d1bc4..8a188ab830ad 100644 --- a/client/src/components/History/HistoryOptions.vue +++ b/client/src/components/History/HistoryOptions.vue @@ -2,6 +2,7 @@ import { faArchive, faBars, + faBezierCurve, faBurn, faClone, faColumns, @@ -231,6 +232,11 @@ watch( Show Invocations + + + Show History Graph + + import { computed, ref } from "vue"; -import type { Rectangle } from "@/components/Workflow/Editor/modules/geometry"; +import type { Rectangle } from "@/utils/geometry"; import { wait } from "@/utils/utils"; const bounds = ref({ x: 0, y: 0, width: 0, height: 0 }); diff --git a/client/src/components/Workflow/Editor/Comments/FrameComment.vue b/client/src/components/Workflow/Editor/Comments/FrameComment.vue index 37360dd97dbd..2b3d38d4a1ac 100644 --- a/client/src/components/Workflow/Editor/Comments/FrameComment.vue +++ b/client/src/components/Workflow/Editor/Comments/FrameComment.vue @@ -7,10 +7,10 @@ import { BButton, BButtonGroup } from "bootstrap-vue"; import purify from "dompurify"; import { computed, onMounted, reactive, ref, watch } from "vue"; -import { AxisAlignedBoundingBox, type Rectangle } from "@/components/Workflow/Editor/modules/geometry"; import { useWorkflowStores } from "@/composables/workflowStores"; import type { FrameWorkflowComment, WorkflowComment, WorkflowCommentColor } from "@/stores/workflowEditorCommentStore"; import type { Step } from "@/stores/workflowStepStore"; +import { AxisAlignedBoundingBox, type Rectangle } from "@/utils/geometry"; import { LazyMoveMultipleAction } from "../Actions/workflowActions"; import { brighterColors, darkenedColors } from "./colors"; diff --git a/client/src/components/Workflow/Editor/Comments/FreehandComment.vue b/client/src/components/Workflow/Editor/Comments/FreehandComment.vue index 45e44a9dd581..7de1de018071 100644 --- a/client/src/components/Workflow/Editor/Comments/FreehandComment.vue +++ b/client/src/components/Workflow/Editor/Comments/FreehandComment.vue @@ -4,8 +4,8 @@ import { computed } from "vue"; import { useWorkflowStores } from "@/composables/workflowStores"; import type { FreehandWorkflowComment } from "@/stores/workflowEditorCommentStore"; +import { vecSubtract } from "@/utils/geometry"; -import { vecSubtract } from "../modules/geometry"; import { colors } from "./colors"; const props = defineProps<{ diff --git a/client/src/components/Workflow/Editor/Comments/useResizable.ts b/client/src/components/Workflow/Editor/Comments/useResizable.ts index 61515abdd2d0..e1303c5ec51f 100644 --- a/client/src/components/Workflow/Editor/Comments/useResizable.ts +++ b/client/src/components/Workflow/Editor/Comments/useResizable.ts @@ -2,8 +2,7 @@ import { useEventListener } from "@vueuse/core"; import { type Ref, watch } from "vue"; import { useWorkflowStores } from "@/composables/workflowStores"; - -import { vecSnap } from "../modules/geometry"; +import { vecSnap } from "@/utils/geometry"; /** * Common functionality required for handling a user resizable element. diff --git a/client/src/components/Workflow/Editor/SVGConnection.vue b/client/src/components/Workflow/Editor/SVGConnection.vue index 547ad7163128..195516f1edd3 100644 --- a/client/src/components/Workflow/Editor/SVGConnection.vue +++ b/client/src/components/Workflow/Editor/SVGConnection.vue @@ -1,11 +1,11 @@