From 0e1982f5ea8ba7271686891611bc7f6cbed84f6a Mon Sep 17 00:00:00 2001 From: Ross Leitch Date: Tue, 10 Oct 2023 16:23:08 +1300 Subject: [PATCH] Bug fixes, start of schematic templates --- .../src/schema/schematics/index.ts | 54 +++++ .../app/hivecommand-frontend/package.json | 1 + .../src/views/schematic-editor/index.tsx | 72 +++++++ .../command-electrical-nodes/src/node.tsx | 3 +- .../src/nodes/index.tsx | 3 +- .../src/nodes/page-node.tsx | 6 +- .../src/nodes/wire-node.tsx | 147 ++++++++++++++ packages/core-ui/editor-canvas/src/index.tsx | 105 ++++++++-- .../migration.sql | 18 ++ .../core/command-data/prisma/schema.prisma | 19 ++ packages/editors/electrical/package.json | 2 + .../src/components/surface/index.tsx | 81 +++++--- packages/editors/electrical/src/index.tsx | 59 +++++- .../electrical/src/panes/pages/index.tsx | 83 ++++++-- .../electrical/src/panes/pages/modal.tsx | 30 ++- .../panes/pages/type-selector/components.tsx | 191 ++++++++++++++++++ .../src/panes/pages/type-selector/index.tsx | 71 +++++++ .../electrical/src/panes/symbols/index.tsx | 4 +- yarn.lock | 3 + 19 files changed, 878 insertions(+), 74 deletions(-) create mode 100644 packages/core-ui/command-electrical-nodes/src/nodes/wire-node.tsx create mode 100644 packages/core/command-data/prisma/migrations/20231010001217_add_schematic_templates/migration.sql create mode 100644 packages/editors/electrical/src/panes/pages/type-selector/components.tsx create mode 100644 packages/editors/electrical/src/panes/pages/type-selector/index.tsx diff --git a/packages/app/hivecommand-backend/src/schema/schematics/index.ts b/packages/app/hivecommand-backend/src/schema/schematics/index.ts index d33e13256..9f4052329 100644 --- a/packages/app/hivecommand-backend/src/schema/schematics/index.ts +++ b/packages/app/hivecommand-backend/src/schema/schematics/index.ts @@ -30,6 +30,7 @@ export default (prisma: PrismaClient) => { where: { ...filter, organisation: context.jwt.organisation }, include: { pages: true, + templates: true, versions: true } }); @@ -176,6 +177,7 @@ export default (prisma: PrismaClient) => { nodes: args.input.nodes || [], edges: args.input.edges || [], rank: newRank.toString(), + templateId: args.input.templateId, schematic: { connect: { id: args.schematic } } @@ -192,6 +194,7 @@ export default (prisma: PrismaClient) => { name: args.input.name, nodes: args.input.nodes, edges: args.input.edges, + templateId: args.input.templateId, schematic: { connect: { id: args.schematic } } @@ -254,6 +257,36 @@ export default (prisma: PrismaClient) => { }) return result.rank == newRank.toString(); + }, + createCommandSchematicPageTemplate: async (root: any, args: { schematic: string, input: any }, context: any) => { + return await prisma.electricalSchematicPageTemplate.create({ + data: { + ...args.input, + id: nanoid(), + schematic: { + connect: { id: args.schematic } + } + } + }); + }, + updateCommandSchematicPageTemplate: async (root: any, args: { schematic: string, id: string, input: any }, context: any) => { + return await prisma.electricalSchematicPageTemplate.update({ + where: { + id: args.id, + }, + data: { + ...args.input + } + }); + + }, + deleteCommandSchematicPageTemplate: async (root: any, args: { schematic: string, id: string }, context: any) => { + const res = await prisma.electricalSchematicPageTemplate.delete({ + where: { + id: args.id, + } + }); + return res != null; } } }, @@ -278,6 +311,10 @@ export default (prisma: PrismaClient) => { deleteCommandSchematicPage(schematic: ID, id: ID): Boolean! + createCommandSchematicPageTemplate(schematic: ID, input: CommandSchematicPageTemplateInput): CommandSchematicPageTemplate + updateCommandSchematicPageTemplate(schematic: ID, id: ID, input: CommandSchematicPageTemplateInput): CommandSchematicPageTemplate + deleteCommandSchematicPageTemplate(schematic: ID, id: ID): Boolean! + updateCommandSchematicPageOrder(schematic: ID, id: String, above: String, below: String): Boolean } @@ -299,6 +336,7 @@ export default (prisma: PrismaClient) => { versions: [CommandSchematicVersion] pages: [CommandSchematicPage] + templates: [CommandSchematicPageTemplate] createdAt: DateTime @@ -330,6 +368,8 @@ export default (prisma: PrismaClient) => { nodes: JSON edges: JSON + + templateId: String } type CommandSchematicPage { @@ -338,10 +378,24 @@ export default (prisma: PrismaClient) => { rank: String + template: CommandSchematicPageTemplate + nodes: JSON edges: JSON } + input CommandSchematicPageTemplateInput { + name: String + nodes: JSON + } + + type CommandSchematicPageTemplate { + id: ID! + name: String + + nodes: JSON + } + ` return { diff --git a/packages/app/hivecommand-frontend/package.json b/packages/app/hivecommand-frontend/package.json index 5d1c51820..91f184839 100644 --- a/packages/app/hivecommand-frontend/package.json +++ b/packages/app/hivecommand-frontend/package.json @@ -53,6 +53,7 @@ "webpack-config-single-spa-react": "^3.0.0", "webpack-config-single-spa-react-ts": "^3.0.0", "webpack-config-single-spa-ts": "^3.0.0", + "webpack-dev-middleware": "^6.1.1", "webpack-dev-server": "4.15.1", "webpack-merge": "^5.8.0" }, diff --git a/packages/app/hivecommand-frontend/src/views/schematic-editor/index.tsx b/packages/app/hivecommand-frontend/src/views/schematic-editor/index.tsx index 3c973bba8..5ff261864 100644 --- a/packages/app/hivecommand-frontend/src/views/schematic-editor/index.tsx +++ b/packages/app/hivecommand-frontend/src/views/schematic-editor/index.tsx @@ -32,6 +32,11 @@ export const SchematicEditor = () => { } } + templates { + id + name + } + pages { id name @@ -81,6 +86,39 @@ export const SchematicEditor = () => { awaitRefetchQueries: true }) + + const [createPageTemplate] = useMutation(gql` + mutation CreatePageTemplate($schematic: ID, $name: String) { + createCommandSchematicPageTemplate(schematic: $schematic, input: {name: $name}){ + id + } + } + `, { + refetchQueries: ['GetSchematic'], + awaitRefetchQueries: true + }) + + const [updatePageTemplate] = useMutation(gql` + mutation UpdatePageTemplate($schematic: ID, $id: ID, $input: CommandSchematicPageTemplateInput) { + updateCommandSchematicPageTemplate(schematic: $schematic, id: $id, input: $input){ + id + } + } + `, { + + // refetchQueries: ['GetSchematic'], + // awaitRefetchQueries: true + }) + + const [deletePageTemplate] = useMutation(gql` + mutation DeletePageTemplate($schematic: ID, $id: ID) { + deleteCommandSchematicPageTemplate(schematic: $schematic, id: $id) + } + `, { + refetchQueries: ['GetSchematic'], + awaitRefetchQueries: true + }) + const [ updatePageOrder ] = useMutation(gql` mutation UpdatePageOrder($schematic: ID, $id: String, $above: String, $below: String){ updateCommandSchematicPageOrder(schematic: $schematic, id: $id, below: $below, above: $above) @@ -150,6 +188,35 @@ export const SchematicEditor = () => { }) } + const onCreateTemplate = (page: any) => { + createPageTemplate({ + variables: { + schematic: id, + name: page.name + } + }) + } + + + const onUpdateTemplate = (page: any) => { + updatePageTemplate({ + variables: { + schematic: id, + id: page.id, + input: page + } + }) + } + + const onDeleteTemplate = (page: any) => { + deletePageTemplate({ + variables: { + schematic: id, + id: page.id + } + }) + } + const onUpdatePageOrder = (id: string, above: any, below: any) => { updatePageOrder({ variables: { @@ -190,9 +257,14 @@ export const SchematicEditor = () => { title={schematic?.name} versions={schematic?.versions || []} pages={pages || []} + templates={schematic?.templates || []} onCreatePage={onCreatePage} onUpdatePage={onUpdatePage} onDeletePage={onDeletePage} + onCreateTemplate={onCreateTemplate} + onUpdateTemplate={onUpdateTemplate} + onDeleteTemplate={onDeleteTemplate} + onUpdatePageOrder={onUpdatePageOrder} onExport={() => { openExportModal(true) diff --git a/packages/core-ui/command-electrical-nodes/src/node.tsx b/packages/core-ui/command-electrical-nodes/src/node.tsx index 5572add96..2d5f822d1 100644 --- a/packages/core-ui/command-electrical-nodes/src/node.tsx +++ b/packages/core-ui/command-electrical-nodes/src/node.tsx @@ -1,4 +1,4 @@ -import { CanvasNode, BoxNode, TextNode, ElectricalSymbol, PageNode } from './nodes'; +import { CanvasNode, BoxNode, TextNode, ElectricalSymbol, PageNode, WireNode } from './nodes'; export const nodeTypes = { electricalSymbol: ElectricalSymbol, @@ -6,5 +6,6 @@ export const nodeTypes = { canvasNode: CanvasNode, box: BoxNode, text: TextNode, + wire: WireNode } diff --git a/packages/core-ui/command-electrical-nodes/src/nodes/index.tsx b/packages/core-ui/command-electrical-nodes/src/nodes/index.tsx index 3de852739..bd7f24005 100644 --- a/packages/core-ui/command-electrical-nodes/src/nodes/index.tsx +++ b/packages/core-ui/command-electrical-nodes/src/nodes/index.tsx @@ -2,4 +2,5 @@ export * from './canvas-node' export * from './electrical-symbol' export * from './text-node' export * from './box-node' -export * from './page-node' \ No newline at end of file +export * from './page-node' +export * from './wire-node'; \ No newline at end of file diff --git a/packages/core-ui/command-electrical-nodes/src/nodes/page-node.tsx b/packages/core-ui/command-electrical-nodes/src/nodes/page-node.tsx index f3d3a7ceb..7d732a462 100644 --- a/packages/core-ui/command-electrical-nodes/src/nodes/page-node.tsx +++ b/packages/core-ui/command-electrical-nodes/src/nodes/page-node.tsx @@ -7,7 +7,11 @@ export const PageNode = (props: NodeProps) => { const ratio = 210 / 297 return ( - + {/* */} diff --git a/packages/core-ui/command-electrical-nodes/src/nodes/wire-node.tsx b/packages/core-ui/command-electrical-nodes/src/nodes/wire-node.tsx new file mode 100644 index 000000000..64f3fa7f9 --- /dev/null +++ b/packages/core-ui/command-electrical-nodes/src/nodes/wire-node.tsx @@ -0,0 +1,147 @@ +import React, { MouseEventHandler, useState, useMemo, useEffect } from 'react'; +import { BaseEdge, EdgeProps, NodeProps, getBezierPath, useReactFlow } from 'reactflow'; +import { useElectricalNodeContext } from '../context'; + + +export const WireNode = ({ + id, + // style = {}, + data +}: NodeProps) => { + + const { project } = useReactFlow() + + const [ points, setPoints ] = useState(data?.points || []); + + const { onEdgePointCreated, onEdgePointChanged, printMode } = useElectricalNodeContext(); + + const directPath = useMemo(() => `M ${points?.map((x: any, ix: any) => `${x.x} ${x.y} ${ix < points?.length - 1 ? 'L' : ''}`).join(' ')}`, [points]); + + const [draggingPoint, setDraggingPoint] = useState(null) + const [deltaPoint, setDeltaPoint] = useState<{ x: number, y: number } | null>(null); + + useEffect(() => { + setPoints(data?.points); + }, [data?.points]) + + return ( + + {/* */} + {points?.map((point: any, ix: number) => points?.[ix + 1] && ( + { + if (e.metaKey || e.ctrlKey) { + + onEdgePointCreated?.(id, ix, { x: e.clientX, y: e.clientY }) + } + }} + className="react-flow__edge-path" + style={{ + ...data?.style + }} + d={`M ${point.x} ${point.y} L ${points?.[ix + 1].x} ${points?.[ix + 1].y}`} /> + + ))} + {/* { + if(e.metaKey || e.ctrlKey){ + alert("CTRL") + } + }} + // markerEnd={markerEnd} + + style={{ + // fill: 'none', + // stroke: style.stroke || 'black', + ...style + }} /> */} + {points?.map((point: any, ix: number) => !printMode && ( + { + + (e.currentTarget as any).setPointerCapture((e as any).pointerId) + + setDraggingPoint(ix); + setDeltaPoint(project({ x: e.clientX, y: e.clientY })) + }} + onPointerMove={(e) => { + + console.log("MOVING", draggingPoint, points) + let nextPoint = project({ x: e.clientX, y: e.clientY }); + + if(deltaPoint && draggingPoint != null){ + setPoints((points: any[]) => { + let p = points.slice(); + + p[ix] = { + x: p[ix].x + (nextPoint.x - deltaPoint.x), //nextPoint.x, + y: p[ix].y + (nextPoint.y - deltaPoint.y) //nextPoint.y + } + return p; + }) + } + + // if (deltaPoint && draggingPoint != null) { + // let delta = { x: nextPoint.x - deltaPoint.x, y: nextPoint.y - deltaPoint.y }; + + // // let e = (page?.edges || []).slice(); + + // // let edgeIx = (page?.edges || []).findIndex((a: any) => a.id == id) + + // // const points = (e[edgeIx].data.points || []).slice(); + + // // points[ix] = { + // // ...points[ix], + // // x: e[edgeIx].data.points[draggingPoint].x + delta.x, + // // y: e[edgeIx].data.points[draggingPoint].y + delta.y + // // } + + // // e[edgeIx] = { + // // ...e[edgeIx], + // // data: { + // // ...e[edgeIx].data, + // // points + // // } + // // } + + // // onUpdatePage?.({ + // // ...page, + // // edges: e + // // }, "mouseMove") + + // onEdgePointChanged?.(id, ix, { + // x: delta.x, + // y: delta.y + // }) + // } + + if (draggingPoint != null) { + setDeltaPoint(nextPoint) + } + }} + onPointerUp={(e) => { + (e.currentTarget as any).releasePointerCapture((e as any).pointerId) + + // let nextPoint = project({ x: e.clientX, y: e.clientY }); + + + onEdgePointChanged?.(id, ix, { + x: points[ix].x, //- (deltaPoint?.x || 0), + y: points[ix].y // - (deltaPoint?.y || 0) + }) + + setDraggingPoint(null) + setDeltaPoint(null); + }} + style={{ cursor: 'pointer', pointerEvents: 'all' }} + r={2} + cx={point.x} + cy={point.y} + fill='black' /> + ))} + + // + ) +} diff --git a/packages/core-ui/editor-canvas/src/index.tsx b/packages/core-ui/editor-canvas/src/index.tsx index d1c7e787f..406a25824 100644 --- a/packages/core-ui/editor-canvas/src/index.tsx +++ b/packages/core-ui/editor-canvas/src/index.tsx @@ -1,55 +1,134 @@ -import React, {MouseEvent, useState} from 'react'; +import React, {MouseEvent, useEffect, useState} from 'react'; import { Box } from '@mui/material'; -import { ReactFlow, MiniMap, Controls, Background, Node, Edge, NodeTypes, EdgeTypes } from 'reactflow'; +import { ReactFlow, MiniMap, Controls, Background, Node, Edge, NodeTypes, EdgeTypes, useOnSelectionChange, useNodesState, useEdgesState, CoordinateExtent, ConnectionMode, useViewport, useReactFlow } from 'reactflow'; + +export interface EditorCanvasSelection { + nodes?: Node[]; + edges?: Edge[]; +} export interface EditorCanvasProps { nodes?: Node[], edges?: Edge[], + nodeTypes?: NodeTypes, edgeTypes?: EdgeTypes fitView?: boolean; - + translateExtent?: CoordinateExtent + + selection?: EditorCanvasSelection; + onSelect?: (selection: EditorCanvasSelection) => void; onNodeDoubleClick?: (node: Node) => void; + + wrapper?: any; } export const EditorCanvas : React.FC = (props) => { + const [ nodes, setNodes, onNodesChange ] = useNodesState(props.nodes || []) + const [ edges, setEdges, onEdgesChange ] = useEdgesState(props.edges || []) + + const [ selection, setSelection ] = useState({}); + const [ selectionZone, setSelectionZone ] = useState<{start?: {x: number, y: number}} | null>(null); + const { project } = useReactFlow(); + + const { x, y, zoom } = useViewport() + const onSelectionStart = (e: MouseEvent) => { + let bounds = props.wrapper?.getBoundingClientRect(); + setSelectionZone({ - start: { - x: e.clientX, - y: e.clientY - } + start: project({ + x: e.clientX - (bounds.x || 0), + y: e.clientY - (bounds.y || 0) + }) }) } const onSelectionEnd = (e: MouseEvent) => { + let bounds = props.wrapper?.getBoundingClientRect(); let zone = { ...selectionZone, - end: { - x: e.clientX, - y: e.clientY - } + end: project({ + x: e.clientX - (bounds.x || 0), + y: e.clientY - (bounds.y || 0) + }) } + + let zoneOrientationX = (zone?.start?.x || 0) < (zone.end?.x || 0) + let zoneOrientationY = (zone?.start?.y || 0) < (zone.end?.y || 0) + + let currentSelection = Object.assign({}, selection); + + let selectEdges = props.edges?.filter((edge) => { + if((currentSelection?.edges || []).findIndex((a) => a.id == edge.id) > -1){ + return false; + } + return edge.data?.points?.filter((point: {x: number, y: number}) => { + + let xIn = zoneOrientationX ? point.x > (zone.start?.x || 0) && point.x < (zone.end?.x || 0) : point.x > (zone.end?.x || 0) && point.x < (zone.start?.x || 0) + let yIn = zoneOrientationY ? point.y > (zone.start?.y || 0) && point.y < (zone.end?.y || 0) : point.y > (zone.end?.y || 0) && point.y < (zone.start?.y || 0) + + return xIn && yIn + })?.length > 0; + }) + + currentSelection.edges = currentSelection.edges?.concat(selectEdges || []) + + setSelection(currentSelection); + + props.onSelect?.(currentSelection); setSelectionZone(null); } + useOnSelectionChange({ + onChange: (params) => { + setSelection(params); + props.onSelect?.(params) + } + }) + + useEffect(() => { + setNodes( + (props.nodes || []).map((e) => ({ + ...e, + selected: selection?.nodes?.find((a) => a.id == e.id) != null + })) + ) + }, [props.nodes, selection]) + + useEffect(() => { + setEdges( + (props.edges || []).map((e) => ({ + ...e, + selected: selection?.edges?.find((a) => a.id == e.id) != null + })) + ) + }, [props.edges, selection]) + + return ( props.onNodeDoubleClick?.(node)} + connectionMode={ConnectionMode.Loose} > diff --git a/packages/core/command-data/prisma/migrations/20231010001217_add_schematic_templates/migration.sql b/packages/core/command-data/prisma/migrations/20231010001217_add_schematic_templates/migration.sql new file mode 100644 index 000000000..10051d918 --- /dev/null +++ b/packages/core/command-data/prisma/migrations/20231010001217_add_schematic_templates/migration.sql @@ -0,0 +1,18 @@ +-- AlterTable +ALTER TABLE "ElectricalSchematicPage" ADD COLUMN "templateId" TEXT; + +-- CreateTable +CREATE TABLE "ElectricalSchematicPageTemplate" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "nodes" JSONB, + "schematicId" TEXT NOT NULL, + + CONSTRAINT "ElectricalSchematicPageTemplate_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "ElectricalSchematicPage" ADD CONSTRAINT "ElectricalSchematicPage_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "ElectricalSchematicPageTemplate"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ElectricalSchematicPageTemplate" ADD CONSTRAINT "ElectricalSchematicPageTemplate_schematicId_fkey" FOREIGN KEY ("schematicId") REFERENCES "ElectricalSchematic"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/core/command-data/prisma/schema.prisma b/packages/core/command-data/prisma/schema.prisma index 8bef0b525..4f0137369 100644 --- a/packages/core/command-data/prisma/schema.prisma +++ b/packages/core/command-data/prisma/schema.prisma @@ -273,6 +273,8 @@ model ElectricalSchematic { pages ElectricalSchematicPage[] @relation(name: "hasPages") + templates ElectricalSchematicPageTemplate[] @relation(name: "hasPageTemplates") + organisation String createdAt DateTime @default(now()) @@ -300,6 +302,9 @@ model ElectricalSchematicPage { name String + template ElectricalSchematicPageTemplate? @relation(name: "usesPageTemplate", references: [id], fields: [templateId]) + templateId String? + nodes Json? edges Json? @@ -311,6 +316,20 @@ model ElectricalSchematicPage { createdAt DateTime @default(now()) } + +model ElectricalSchematicPageTemplate { + id String @id + + name String + + nodes Json? + + pages ElectricalSchematicPage[] @relation(name: "usesPageTemplate") + + schematic ElectricalSchematic @relation(name: "hasPageTemplates", fields: [schematicId], references: [id]) + schematicId String +} + model Program { id String @id diff --git a/packages/editors/electrical/package.json b/packages/editors/electrical/package.json index 301fb3a40..4f6717324 100644 --- a/packages/editors/electrical/package.json +++ b/packages/editors/electrical/package.json @@ -5,6 +5,7 @@ "packageManager": "yarn@3.6.0", "main": "dist/index.js", "devDependencies": { + "@mui/base": "^5.0.0-beta.18", "typescript": "^5.2.2" }, "publishConfig": { @@ -19,6 +20,7 @@ "@hive-command/editor-toolbar": "workspace:^" }, "peerDependencies": { + "@mui/base": "*", "@mui/material": "*" } } diff --git a/packages/editors/electrical/src/components/surface/index.tsx b/packages/editors/electrical/src/components/surface/index.tsx index cfed72429..97c4bc26c 100644 --- a/packages/editors/electrical/src/components/surface/index.tsx +++ b/packages/editors/electrical/src/components/surface/index.tsx @@ -1,4 +1,4 @@ -import { EditorCanvas } from "@hive-command/editor-canvas"; +import { EditorCanvas, EditorCanvasSelection } from "@hive-command/editor-canvas"; import { Box } from "@mui/material"; import React, {KeyboardEvent, useEffect, useMemo, useRef, useState} from "react"; import { EditorOverlay } from "./overlay"; @@ -20,11 +20,15 @@ export interface ElectricalSurfaceProps { onUpdate?: (page?: ElectricalPage) => void; + selection?: EditorCanvasSelection; + onSelect?: (selection: EditorCanvasSelection) => void; + clipboard?: any; activeTool?: ActiveTool; } export const ElectricalSurface : React.FC = (props) => { + const surfaceRef = React.useRef(null); const canvasRef = React.useRef(null); @@ -41,6 +45,7 @@ export const ElectricalSurface : React.FC = (props) => { } }, [cursorPosition, surfaceRef.current]) + const [editNode, setEditNode] = useState(null); const onKeyDown = (e: KeyboardEvent) => { @@ -72,10 +77,14 @@ export const ElectricalSurface : React.FC = (props) => { } const mouseDown = (ev: MouseEvent) => { - ev.stopPropagation(); + // ev.stopPropagation(); + + // console.log("PointerDown"); - surfaceRef?.current?.setPointerCapture((ev as any).pointerId) + // surfaceRef?.current?.setPointerCapture((ev as any).pointerId) + + //--- // console.log("CURRENT TARGET", ev.currentTarget) // // console.log("MouseDown", ev, surfaceRef.current, canvasRef.current); // if(!(ev as any).fake && ev.target && canvasRef.current?.contains(ev.target as any)){ @@ -118,7 +127,7 @@ export const ElectricalSurface : React.FC = (props) => { } const mouseUp = (ev: MouseEvent) => { - surfaceRef?.current?.releasePointerCapture((ev as any).pointerId) + // surfaceRef?.current?.releasePointerCapture((ev as any).pointerId) } surfaceRef?.current?.addEventListener('pointerdown', mouseDown, true) @@ -134,6 +143,38 @@ export const ElectricalSurface : React.FC = (props) => { } }, []) + const canvas = useMemo(() => { + const ratio = 210/297 //A4; + + const width = 1080; + const height = 1080 * ratio; + + return ( + { + const { props }: any = ToolProps.find((a) => a.id == node.type) || {} + + if (props) { + setEditNode({ + ...node, + props: Object.keys(props).map((key) => ({ key, type: props[key], value: node?.data?.[key] })) + }) + } + }} /> + ) + }, [props.page]) return ( = (props) => { let b = surfaceRef?.current?.getBoundingClientRect(); - console.log(e.clientX, e.clientY); - e.clientX = e.clientX - (b?.x || 0); e.clientY = e.clientY - (b?.y || 0); - console.log(e.clientX, e.clientY); toolRef?.current?.onClick(e); }} @@ -178,7 +216,14 @@ export const ElectricalSurface : React.FC = (props) => { // // event.clientY = e.clientY; // canvasRef.current?.dispatchEvent(event) }}> - + = (props) => { } } - console.log("new text") - - // setApplying(true) props.onUpdate?.({ ...props.page, @@ -218,22 +260,7 @@ export const ElectricalSurface : React.FC = (props) => { onClose={() => { setEditNode(null); }} /> - { - const { props }: any = ToolProps.find((a) => a.id == node.type) || {} - - if (props) { - setEditNode({ - ...node, - props: Object.keys(props).map((key) => ({ key, type: props[key], value: node?.data?.[key] })) - }) - } - }} /> + {canvas} void; @@ -23,6 +25,12 @@ export interface ElectricalEditorProps { onUpdatePage?: (page: any) => void; onDeletePage?: (page: any) => void; + + onCreateTemplate?: (page: any) => void; + onUpdateTemplate?: (page: any) => void; + onDeleteTemplate?: (page: any) => void; + + onUpdatePageOrder?: (id: string, above: any, below: any) => void; } @@ -38,16 +46,39 @@ export const ElectricalEditor : React.FC = (props) => { const activePage = props.pages?.find((a) => a.id == selectedPage?.id); + const activeTemplate = props.templates?.find((a) => a.id == selectedPage?.id); + const [ activeTool, setActiveTool ] = useState() - console.log(ToolMenu, Tools) + const [ selection, setSelection ] = useState({}); const tools = useMemo(() => ToolMenu(), []) + + const onDelete = useCallback(() => { + // alert("Deleting " + selection?.length) + props.onUpdatePage?.({ + ...activePage, + nodes: activePage?.nodes?.filter((a) => { + return (selection.nodes || []).findIndex((b) => b.id == a.id) < 0; + }), + edges: activePage?.edges?.filter((a) => { + return (selection.edges || []).findIndex((b) => b.id == a.id) < 0; + }) + }) + setSelection({}); + }, [selection, activePage]) + useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { - if(e.key == 'Escape'){ - setActiveTool(undefined) + switch(e.key){ + case 'Escape': + setActiveTool(undefined); + break; + case 'Delete': + case 'Backspace': + onDelete(); + break; } } @@ -56,11 +87,12 @@ export const ElectricalEditor : React.FC = (props) => { return () => { document.removeEventListener('keydown', onKeyDown) } - }, []) + }, [onDelete]) + return ( - + = (props) => { { setSelectedPage(pageId) }} /> - + { - console.log("UPDATE", page); props.onUpdatePage?.(page) }} + selection={selection} + onSelect={(selection) => { + console.log(selection); + setSelection(selection) + }} clipboard={{}} activeTool={activeTool} /> @@ -102,7 +144,6 @@ export const ElectricalEditor : React.FC = (props) => { { - console.log(symbol) setActiveTool({type: 'symbol', data: {symbol: elements?.find((a) => a.name == symbol)}}) }} /> diff --git a/packages/editors/electrical/src/panes/pages/index.tsx b/packages/editors/electrical/src/panes/pages/index.tsx index 782edd38b..09e45450e 100644 --- a/packages/editors/electrical/src/panes/pages/index.tsx +++ b/packages/editors/electrical/src/panes/pages/index.tsx @@ -22,9 +22,13 @@ import { } from '@dnd-kit/sortable'; import { PageItem } from './item'; import { BasePane } from '@hive-command/editor-panes'; +import { StyledOption } from './type-selector/components'; +import { TypeSelector } from './type-selector'; export interface PagePaneProps { pages?: any[]; + templates?: any[]; + onReorderPage?: (id: string, above: any, below: any) => void, selectedPage?: any; @@ -32,10 +36,16 @@ export interface PagePaneProps { onCreatePage?: (page: any) => void; onUpdatePage?: (page: any) => void; onDeletePage?: (page: any) => void; + + onCreateTemplate?: (page: any) => void; + onUpdateTemplate?: (page: any) => void; + onDeleteTemplate?: (page: any) => void; } export const PagesPane : React.FC = (props) => { + const [ view, setView ] = useState<'templates' | 'pages'>('pages'); + const [ search, setSearch ] = useState(null); const [modalOpen, openModal] = useState(false); @@ -43,10 +53,12 @@ export const PagesPane : React.FC = (props) => { const [activeId, setActiveId] = useState(null); - const { pages, onReorderPage, selectedPage } = props; //useEditorContext(); + const { pages, templates, onReorderPage, selectedPage } = props; //useEditorContext(); const sortedPages = useMemo(() => pages?.slice()?.sort((a: any, b: any) => (a.rank || '').localeCompare(b.rank || '')), [pages]); + const sortedTemplates = useMemo(() => templates?.slice()?.sort((a: any, b: any) => (a.rank || '').localeCompare(b.rank || '')), [templates]) + const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { @@ -99,6 +111,30 @@ export const PagesPane : React.FC = (props) => { } + const items = useMemo(() => { + + if(view == 'pages'){ + return sortedPages?.map((page: any) => ( + onEdit(page)} + onSelectPage={props.onSelectPage} + page={page} /> + + )); + }else{ + return sortedTemplates?.map((template: any) => ( + onEdit(template)} + onSelectPage={props.onSelectPage} + page={template} /> + )) + } + }, [view, sortedPages, sortedTemplates, selectedPage]) + return ( = (props) => { - Pages - - + { + setView(value as any) + }} /> + + + openModal(true)} size="small"> @@ -140,9 +185,15 @@ export const PagesPane : React.FC = (props) => { { - props.onDeletePage?.(selected) + if(view == 'pages'){ + props.onDeletePage?.(selected) + }else{ + props.onDeleteTemplate?.(selected) + } openModal(false) setSelected(null); @@ -153,9 +204,17 @@ export const PagesPane : React.FC = (props) => { }} onSubmit={(page: any) => { if (selected) { - props.onUpdatePage?.(page); + if(view == 'pages'){ + props.onUpdatePage?.(page); + }else{ + props.onUpdateTemplate?.(page) + } } else { - props.onCreatePage?.(page); + if(view == 'pages'){ + props.onCreatePage?.(page); + }else{ + props.onCreateTemplate?.(page); + } } openModal(false); }} @@ -163,15 +222,7 @@ export const PagesPane : React.FC = (props) => { - {sortedPages?.map((page: any) => ( - onEdit(page)} - onSelectPage={props.onSelectPage} - page={page} /> - - ))} + {items} diff --git a/packages/editors/electrical/src/panes/pages/modal.tsx b/packages/editors/electrical/src/panes/pages/modal.tsx index 3940ba6fb..738bb9f32 100644 --- a/packages/editors/electrical/src/panes/pages/modal.tsx +++ b/packages/editors/electrical/src/panes/pages/modal.tsx @@ -1,9 +1,13 @@ -import { DialogActions, Button, DialogContent, DialogTitle, Dialog, TextField, Box } from "@mui/material"; +import { DialogActions, Button, DialogContent, DialogTitle, Dialog, TextField, Box, Select, MenuItem, FormControl, InputLabel } from "@mui/material"; import React, { useEffect, useState } from "react"; export interface PageModalProps { open: boolean; selected?: any; + view?: string; + + templates?: {id: string, name: string}[] + onClose?: () => void; onSubmit?: (page: any) => void; onDelete?: () => void; @@ -19,13 +23,31 @@ export const PageModal : React.FC = (props) => { }) }, [props.selected]) + const title = props.view == 'pages' ? 'Page' : 'Template'; + return ( - {props.selected ? "Update" : "Create"} Page + {props.selected ? "Update" : "Create"} {title} - setPage({...page, name: e.target.value})} label="Page name" size="small" fullWidth /> - + setPage({...page, name: e.target.value})} + label={`${title} name`} + size="small" + fullWidth /> + + {props.view == 'pages' && ( + + Template + + + + )} diff --git a/packages/editors/electrical/src/panes/pages/type-selector/components.tsx b/packages/editors/electrical/src/panes/pages/type-selector/components.tsx new file mode 100644 index 000000000..61c4c4b25 --- /dev/null +++ b/packages/editors/electrical/src/panes/pages/type-selector/components.tsx @@ -0,0 +1,191 @@ +import { styled } from "@mui/system"; +import { Popper } from '@mui/base/Popper'; +import { selectClasses } from '@mui/base/Select' +import { optionClasses, Option } from '@mui/base/Option' +import { useOption } from '@mui/base/useOption' +import clsx from 'clsx'; + +const blue = { + 100: '#DAECFF', + 200: '#99CCF3', + 400: '#3399FF', + 500: '#007FFF', + 600: '#0072E5', + 900: '#003A75', + }; + + const grey = { + 50: '#f6f8fa', + 100: '#eaeef2', + 200: '#d0d7de', + 300: '#afb8c1', + 400: '#8c959f', + 500: '#6e7781', + 600: '#57606a', + 700: '#424a53', + 800: '#32383f', + 900: '#24292f', + }; + + export const StyledButton = styled('button')( + ({ theme }) => ` + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + box-sizing: border-box; + min-width: 100px; + // padding: 8px 12px; + border-radius: 8px; + text-align: left; + line-height: 1.5; + cursor: pointer; + background: transparent; // ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; + border: 0px; //1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + /*box-shadow: 0px 2px 6px ${ + theme.palette.mode === 'dark' ? 'rgba(0,0,0, 0.50)' : 'rgba(0,0,0, 0.05)' + };*/ + + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 120ms; + + &:hover { + // background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]}; + // border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]}; + } + + &.${selectClasses.focusVisible} { + border-color: ${blue[400]}; + outline: 3px solid ${theme.palette.mode === 'dark' ? blue[500] : blue[200]}; + } + + // &.${selectClasses.expanded} { + // &::after { + // content: '▴'; + // } + // } + + // &::after { + // content: '▾'; + // float: right; + // } + `, + ); + + export const StyledListbox = styled('ul')( + ({ theme }) => ` + font-family: IBM Plex Sans, sans-serif; + font-size: 0.875rem; + box-sizing: border-box; + min-height: calc(1.5em + 22px); + // min-width: 320px; + padding: 12px; + border-radius: 12px; + text-align: left; + line-height: 1.5; + background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + padding: 5px; + margin: 5px 0 0 0; + position: absolute; + height: auto; + width: 100%; + overflow: auto; + z-index: 1; + outline: 0px; + list-style: none; + box-shadow: 0px 2px 6px ${ + theme.palette.mode === 'dark' ? 'rgba(0,0,0, 0.50)' : 'rgba(0,0,0, 0.05)' + }; + + &.hidden { + opacity: 0; + visibility: hidden; + transition: opacity 0.4s ease, visibility 0.4s step-end; + } + `, + ); + + export interface OptionProps { + children?: React.ReactNode; + className?: string; + value: string; + disabled?: boolean; + } + + export const CustomOption = (props: OptionProps) => { + + const { children, value, className, disabled = false } = props; + + const { getRootProps, highlighted } = useOption({ + value, + disabled, + label: children, + }); + + + return ( + + {children} + + ); + } + + export const StyledOption = styled(Option)( + ({ theme }) => ` + list-style: none; + padding: 6px; + margin-bottom: 3px; + border-radius: 8px; + cursor: default; + + &:last-of-type { + border-bottom: none; + } + + &.${optionClasses.selected} { + background-color: ${theme.palette.mode === 'dark' ? blue[900] : blue[100]}; + color: ${theme.palette.mode === 'dark' ? blue[100] : blue[900]}; + } + + &.${optionClasses.highlighted} { + background-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + } + + &.${optionClasses.highlighted}.${optionClasses.selected} { + background-color: ${theme.palette.mode === 'dark' ? blue[900] : blue[100]}; + color: ${theme.palette.mode === 'dark' ? blue[100] : blue[900]}; + } + + &.${optionClasses.disabled} { + color: ${theme.palette.mode === 'dark' ? grey[700] : grey[400]}; + } + + &:hover:not(.${optionClasses.disabled}) { + background-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + } + `, + ); + + export const StyledPopper = styled(Popper)` + z-index: 1; + `; + + export const Label = styled('label')( + ({ theme }) => ` + font-family: IBM Plex Sans, sans-serif; + font-size: 0.85rem; + display: block; + margin-bottom: 4px; + font-weight: 400; + color: ${theme.palette.mode === 'dark' ? grey[400] : grey[700]}; + `, + ); \ No newline at end of file diff --git a/packages/editors/electrical/src/panes/pages/type-selector/index.tsx b/packages/editors/electrical/src/panes/pages/type-selector/index.tsx new file mode 100644 index 000000000..f23f0fcd5 --- /dev/null +++ b/packages/editors/electrical/src/panes/pages/type-selector/index.tsx @@ -0,0 +1,71 @@ +import { Select, SelectProps, SelectProvider, useSelect } from "@mui/base"; +import React from "react"; +import { CustomOption, StyledButton, StyledListbox, StyledOption, StyledPopper } from "./components"; +import { ExpandMore, ExpandLess } from '@mui/icons-material' + +export interface TypeSelectorProps { + options?: {id: string, label: string}[]; + value?: string; + onChange?: (ev: any, value: any) => void; +} + +export const TypeSelector = React.forwardRef(function CustomSelect< + TValue extends {}, + Multiple extends boolean, + >({options, value, onChange}: TypeSelectorProps, ref: React.ForwardedRef) { + + const listboxRef = React.useRef(null); + const [listboxVisible, setListboxVisible] = React.useState(false); + + const { getButtonProps, getListboxProps, contextValue, value: selectValue } = useSelect< + string, + false + >({ + listboxRef, + onOpenChange: setListboxVisible, + open: listboxVisible, + onChange, + value + }); + + console.log(selectValue) + + React.useEffect(() => { + if (listboxVisible) { + listboxRef.current?.focus(); + } + }, [listboxVisible]); + + return ( +
+ + {options?.find((a) => a.id == selectValue)?.label || ( + {' '} + )} + {listboxVisible ? () : } + + + + {options?.map((option) => { + return ( + + {option.label} + + ); + })} + + + {/* + {(props.className || '').indexOf('Mui-expanded') > -1 ? ( + + ) : } */} +
+ + ); +}) \ No newline at end of file diff --git a/packages/editors/electrical/src/panes/symbols/index.tsx b/packages/editors/electrical/src/panes/symbols/index.tsx index 9584c4d20..5cf86928e 100644 --- a/packages/editors/electrical/src/panes/symbols/index.tsx +++ b/packages/editors/electrical/src/panes/symbols/index.tsx @@ -31,8 +31,8 @@ export const SymbolsPane : React.FC = (props) => { {elements?.filter(filterSearch)?.map((item) => ( - props.onSelectSymbol?.(item?.name)} sx={{ display: 'flex', diff --git a/yarn.lock b/yarn.lock index 4eb4e1363..5aeda5a33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4783,8 +4783,10 @@ __metadata: "@hive-command/editor-canvas": "workspace:^" "@hive-command/editor-panes": "workspace:^" "@hive-command/editor-toolbar": "workspace:^" + "@mui/base": ^5.0.0-beta.18 typescript: ^5.2.2 peerDependencies: + "@mui/base": "*" "@mui/material": "*" languageName: unknown linkType: soft @@ -4978,6 +4980,7 @@ __metadata: webpack-config-single-spa-react: ^3.0.0 webpack-config-single-spa-react-ts: ^3.0.0 webpack-config-single-spa-ts: ^3.0.0 + webpack-dev-middleware: ^6.1.1 webpack-dev-server: 4.15.1 webpack-merge: ^5.8.0 languageName: unknown