From b4f82d9129a13386f32bffcb3deda8151a000ae2 Mon Sep 17 00:00:00 2001 From: Andreas Schultz Date: Mon, 16 Dec 2024 15:07:35 +0100 Subject: [PATCH 001/260] Add support for react-flow v10: wrapper with scrolling, mini map etc. --- .../react-flow/ReactFlow/ReactFlowV10.tsx | 115 +++++++ src/cmem/react-flow/configuration/graph.ts | 4 +- src/cmem/react-flow/configuration/linking.ts | 4 +- src/cmem/react-flow/configuration/workflow.ts | 4 +- .../extensions/scrollOnDragHookV10.ts | 291 ++++++++++++++++++ src/cmem/react-flow/index.ts | 1 + src/extensions/react-flow/index.ts | 1 + .../react-flow/minimap/MiniMapV10.tsx | 154 +++++++++ 8 files changed, 571 insertions(+), 3 deletions(-) create mode 100644 src/cmem/react-flow/ReactFlow/ReactFlowV10.tsx create mode 100644 src/cmem/react-flow/extensions/scrollOnDragHookV10.ts create mode 100644 src/extensions/react-flow/minimap/MiniMapV10.tsx diff --git a/src/cmem/react-flow/ReactFlow/ReactFlowV10.tsx b/src/cmem/react-flow/ReactFlow/ReactFlowV10.tsx new file mode 100644 index 000000000..60675dc41 --- /dev/null +++ b/src/cmem/react-flow/ReactFlow/ReactFlowV10.tsx @@ -0,0 +1,115 @@ +import React from "react"; +import { default as ReactFlowOriginal, ReactFlowProps as ReactFlowOriginalProps, NodeTypes, EdgeTypes } from "react-flow-renderer-lts"; + +import { CLASSPREFIX as eccgui } from "../../../configuration/constants"; +import { ReactFlowMarkers } from "../../../extensions/react-flow/markers/ReactFlowMarkers"; +import { ReactFlowHotkeyContext } from "../extensions/ReactFlowHotkeyContext"; +import { + useReactFlowScrollOnDragV10, +} from "../extensions/scrollOnDragHookV10"; + +import * as graphConfig from "./../configuration/graph"; +import * as linkingConfig from "./../configuration/linking"; +import * as unspecifiedConfig from "./../configuration/unspecified"; +import * as workflowConfig from "./../configuration/workflow"; + +export interface ReactFlowPropsV10 extends ReactFlowOriginalProps { + /** + * Load `ReactFlow` component with pre-configured values for `nodeTypes` and `edgeTypes` + */ + configuration?: "unspecified" | "graph" | "workflow" | "linking"; + + /** + * Types data transfers that can be dragged in and dropped on the canvas. + */ + dropzoneFor?: string[]; + + /** If defined the canvas scrolls on all drag operations (node, selection, edge connect) + * when the mouse pointer comes near the canvas borders or goes beyond them. + * The `id` property of the ReactFlow component must be set in order for this to work. + * + * NOTE: If scrollOnDrag is defined, a ReactFlowProvider must be wrapped around this component (or a parent). */ + scrollOnDrag?: { + /** Time in milliseconds to wait before the canvas scrolls the next step. */ + scrollInterval: number; + /** + * The size of each scroll step. + * This should be a number between 0.0 - 1.0. + * E.g. a value of 0.25 will lead to a scroll step size of a quarter of the visible canvas. */ + scrollStepSize: number; + }; +} + +const configReactFlow: Record = { + unspecified: unspecifiedConfig, + graph: graphConfig, + workflow: workflowConfig, + linking: linkingConfig, +}; + +/** + * `ReactFlow` v10 container extension that includes pre-configured nodes and edges for + * Corporate Memory tools. + */ +export const ReactFlowV10 = React.forwardRef( + ({ configuration = "unspecified", scrollOnDrag, dropzoneFor, children, className, ...originalProps }, outerRef) => { + const innerRef = React.useRef(null); + React.useImperativeHandle(outerRef, () => innerRef.current!, []); + + React.useEffect(() => { + const reactflowContainer = innerRef?.current; + + if (reactflowContainer && dropzoneFor) { + const addDragover = (event: DragEvent) => { + reactflowContainer.classList.add(`${eccgui}-graphviz__canvas--draghover`); + event.preventDefault(); + }; + + const removeDragover = (event: DragEvent) => { + if (reactflowContainer === event.target) { + reactflowContainer.classList.remove(`${eccgui}-graphviz__canvas--draghover`); + } + }; + + reactflowContainer.addEventListener("dragover", addDragover); + reactflowContainer.addEventListener("dragleave", removeDragover); + reactflowContainer.addEventListener("drop", removeDragover); + return () => { + reactflowContainer.removeEventListener("dragover", addDragover); + reactflowContainer.removeEventListener("dragleave", removeDragover); + reactflowContainer.removeEventListener("drop", removeDragover); + }; + } + return; + }, [innerRef, dropzoneFor]); + + /** If the hot keys should be disabled. By default, they are always disabled. */ + const { hotKeysDisabled } = React.useContext(ReactFlowHotkeyContext); + + const scrollOnDragFunctions = useReactFlowScrollOnDragV10({ + reactFlowProps: originalProps, + scrollOnDrag, + }); + + const { selectionKeyCode, multiSelectionKeyCode, deleteKeyCode, zoomActivationKeyCode } = originalProps; + + return ( + + {children} + + + ); + } +); diff --git a/src/cmem/react-flow/configuration/graph.ts b/src/cmem/react-flow/configuration/graph.ts index 47c5a9b02..b6c639c24 100644 --- a/src/cmem/react-flow/configuration/graph.ts +++ b/src/cmem/react-flow/configuration/graph.ts @@ -1,6 +1,8 @@ import { EdgeDefault } from "./../../../extensions/react-flow/edges/EdgeDefault"; import { NodeDefault } from "./../../../extensions/react-flow/nodes/NodeDefault"; import { GRAPH_NODE_TYPES } from "./typing"; +import {ComponentType} from "react"; +import {NodeProps} from "react-flow-renderer-lts"; const edgeTypes = { default: EdgeDefault, @@ -14,7 +16,7 @@ const edgeTypes = { danger: EdgeDefault, }; -const nodeTypes: Record = { +const nodeTypes: Record> = { default: NodeDefault, graph: NodeDefault, class: NodeDefault, diff --git a/src/cmem/react-flow/configuration/linking.ts b/src/cmem/react-flow/configuration/linking.ts index 0f29571b8..036e475be 100644 --- a/src/cmem/react-flow/configuration/linking.ts +++ b/src/cmem/react-flow/configuration/linking.ts @@ -2,6 +2,8 @@ import { EdgeStep } from "./../../../extensions/react-flow/edges/EdgeStep"; import { NodeDefault } from "./../../../extensions/react-flow/nodes/NodeDefault"; import { StickyNoteNode } from "./../nodes/StickyNoteNode"; import { LINKING_NODE_TYPES } from "./typing"; +import {ComponentType} from "react"; +import {NodeProps} from "react-flow-renderer-lts"; const edgeTypes = { default: EdgeStep, @@ -12,7 +14,7 @@ const edgeTypes = { danger: EdgeStep, }; -const nodeTypes: Record = { +const nodeTypes: Record> = { default: NodeDefault, sourcepath: NodeDefault, targetpath: NodeDefault, diff --git a/src/cmem/react-flow/configuration/workflow.ts b/src/cmem/react-flow/configuration/workflow.ts index 946376290..14ff19a51 100644 --- a/src/cmem/react-flow/configuration/workflow.ts +++ b/src/cmem/react-flow/configuration/workflow.ts @@ -2,6 +2,8 @@ import { EdgeStep } from "./../../../extensions/react-flow/edges/EdgeStep"; import { NodeDefault } from "./../../../extensions/react-flow/nodes/NodeDefault"; import { StickyNoteNode } from "./../nodes/StickyNoteNode"; import { WORKFLOW_NODE_TYPES } from "./typing"; +import {ComponentType} from "react"; +import {NodeProps} from "react-flow-renderer-lts"; const edgeTypes = { default: EdgeStep, @@ -10,7 +12,7 @@ const edgeTypes = { danger: EdgeStep, }; -const nodeTypes: Record = { +const nodeTypes: Record> = { default: NodeDefault, dataset: NodeDefault, linking: NodeDefault, diff --git a/src/cmem/react-flow/extensions/scrollOnDragHookV10.ts b/src/cmem/react-flow/extensions/scrollOnDragHookV10.ts new file mode 100644 index 000000000..ebc3543d5 --- /dev/null +++ b/src/cmem/react-flow/extensions/scrollOnDragHookV10.ts @@ -0,0 +1,291 @@ +import React, { MouseEvent as ReactMouseEvent, useCallback } from "react"; +import { + Edge, + Node, + OnInit, + useStore, + OnConnectStart, + OnConnectStartParams, + OnConnectStop, + Transform, + ReactFlowInstance, +} from "react-flow-renderer-lts"; + +import { ReactFlowPropsV10 } from "../ReactFlow/ReactFlowV10"; +import {HandleType} from "react-flow-renderer-lts/dist/esm/types/handles"; + +interface IProps { + /** The original react-flow props. */ + reactFlowProps: ReactFlowPropsV10; + /** If defined the canvas scrolls on all drag operations (node, selection, edge connect) + * when the mouse pointer comes near the canvas borders or goes beyond them. + * The `id` property of the ReactFlow component must be set in order for this to work. */ + scrollOnDrag?: { + /** Time in milliseconds to wait before the canvas scrolls the next step. */ + scrollInterval: number; + + /** + * The size of each scroll step. + * This should be a number between 0.0 - 1.0. + * E.g. a value of 0.25 will lead to a scroll step size of a quarter of the visible canvas. */ + scrollStepSize: number; + }; +} + +interface ScrollState { + // The react-flow instance + reactFlowInstance?: ReactFlowInstance; + // The current x position of the react-flow view + currentX: number; + // The current y position of the react-flow view + currentY: number; + // The current Zoom level + currentZoom: number; + // The current scroll function callback, when scrolling is active + scrollTaskId?: NodeJS.Timeout; + // If a warning of the react-flow instance with the given ID has not been found + loggedWarning: boolean; + // If the x-axis is currently being scrolled + scrollX: boolean; + // If the y-axis is currently being scrolled + scrollY: boolean; + // Only if this is true the canvas will scroll when moving the mouse past it + draggingOperationActive: boolean; +} + +type ReturnType = Pick< + ReactFlowPropsV10, + | "onInit" + | "onNodeDragStart" + | "onNodeDragStop" + | "onConnectStart" + | "onConnectStop" + | "onSelectionDragStart" + | "onSelectionDragStop" + | "onEdgeUpdateStart" + | "onEdgeUpdateEnd" +>; + +/** Handles the scrolling of the react-flow canvas on all drag operations when the mouse pointer gets near or over the borders. + * The return value contains the wrapped react-flow callback functions that need to be handed over to the react-flow component. */ +export const useReactFlowScrollOnDragV10 = ({ reactFlowProps, scrollOnDrag }: IProps): ReturnType => { + /** Tracks the zoom on drag to border functionality. */ + const scrollState = React.useRef({ + reactFlowInstance: undefined, + currentX: 0, + currentY: 0, + currentZoom: 1, + loggedWarning: false, + scrollX: false, + scrollY: false, + draggingOperationActive: false, + }); + + const useStoreStateInternal = (): Transform => { + try { + return useStore((state) => state.transform); + } catch (ex) { + if (reactFlowProps.id && scrollOnDrag) { + console.warn("Scroll on drag is not correctly working. Reason: " + ex); + } + return [0, 0, 1]; + } + }; + + /** The current position and zoom factor of the view port. */ + const [currentX, currentY, currentZoom] = useStoreStateInternal(); + scrollState.current.currentX = currentX; + scrollState.current.currentY = currentY; + scrollState.current.currentZoom = currentZoom; + + const originalOnInit = reactFlowProps.onInit; + const originalOnNodeDragStart = reactFlowProps.onNodeDragStart; + const originalOnNodeDragStop = reactFlowProps.onNodeDragStop; + const originalOnConnectStart = reactFlowProps.onConnectStart; + const originalOnConnectStop = reactFlowProps.onConnectStop; + const originalOnSelectionDragStart = reactFlowProps.onSelectionDragStart; + const originalOnSelectionDragStop = reactFlowProps.onSelectionDragStop; + const originalOnEdgeUpdateStart = reactFlowProps.onEdgeUpdateStart; + const originalOnEdgeUpdateEnd = reactFlowProps.onEdgeUpdateEnd; + + const scrollInterval = scrollOnDrag?.scrollInterval; + const scrollStepSize = scrollOnDrag?.scrollStepSize; + + const reactFlowInstanceId = reactFlowProps.id; + + const clearIntervalIfExists = React.useCallback(() => { + if (scrollState.current.scrollTaskId) { + clearInterval(scrollState.current.scrollTaskId); + } + }, []); + + const setScrolling = React.useCallback( + (active: boolean) => { + scrollState.current.draggingOperationActive = active; + if (!active) { + clearIntervalIfExists(); + } + }, + [clearIntervalIfExists] + ); + + // Handle scrolling if any operation is active e.g. connecting or dragging a node + React.useEffect(() => { + if (scrollInterval && scrollStepSize && reactFlowInstanceId) { + const handleScrolling = (event: MouseEvent) => { + const state = scrollState.current; + if (!state.draggingOperationActive) { + clearIntervalIfExists(); + return; + } + // Check if mouse pointer is outside of the canvas + const canvasElement = document.getElementById(reactFlowInstanceId); + if (!canvasElement) { + if (!state.loggedWarning) { + console.warn("No element found with ID " + reactFlowInstanceId); + state.loggedWarning = true; + } + return; + } + const boundingRect = canvasElement.getBoundingClientRect(); + const xStepSize = boundingRect.width * scrollStepSize; + const yStepSize = boundingRect.height * scrollStepSize; + if ( + boundingRect.top > event.clientY || + boundingRect.bottom < event.clientY || + boundingRect.left > event.clientX || + boundingRect.right < event.clientX + ) { + const scrollX: number = + boundingRect.left > event.clientX + ? xStepSize + : boundingRect.right < event.clientX + ? -xStepSize + : 0; + const scrollY: number = + boundingRect.top > event.clientY + ? yStepSize + : boundingRect.bottom < event.clientY + ? -yStepSize + : 0; + if (state.scrollY === (scrollY !== 0) && state.scrollX === (scrollX !== 0)) { + // Nothing has changed, do not change interval function + return; + } + clearIntervalIfExists(); + state.scrollTaskId = setInterval(() => { + state.reactFlowInstance?.setViewport({ + x: state.currentX + scrollX, + y: state.currentY + scrollY, + zoom: state.currentZoom, + }); + }, scrollInterval); + } else { + clearIntervalIfExists(); + } + }; + const disableScrollingOnMouseUp = () => { + scrollState.current.draggingOperationActive = false; + clearIntervalIfExists(); + }; + document.addEventListener("mousemove", handleScrolling); + document.addEventListener("mouseup", disableScrollingOnMouseUp); + return () => { + document.removeEventListener("mousemove", handleScrolling); + document.removeEventListener("mouseup", disableScrollingOnMouseUp); + }; + } else { + return undefined; + } + }, [scrollInterval, scrollStepSize, reactFlowInstanceId, clearIntervalIfExists]); + + const onInit: OnInit = useCallback( + (rfi: ReactFlowInstance) => { + scrollState.current.reactFlowInstance = rfi; + originalOnInit?.(rfi); + }, + [originalOnInit] + ); + + /** Wrap original callbacks to turn scrolling on and off. */ + const onConnectStart: OnConnectStart = React.useCallback( + (event: ReactMouseEvent, params: OnConnectStartParams) => { + setScrolling(true); + originalOnConnectStart?.(event, params); + }, + [originalOnConnectStart, setScrolling] + ); + + const onConnectStop: OnConnectStop = React.useCallback( + (event: MouseEvent) => { + setScrolling(false); + originalOnConnectStop?.(event); + }, + [originalOnConnectStop, setScrolling] + ); + + const onNodeDragStart = React.useCallback( + (event: ReactMouseEvent, node: Node, nodes: Node[]) => { + setScrolling(true); + originalOnNodeDragStart?.(event, node, nodes); + }, + [originalOnNodeDragStart, setScrolling] + ); + + const onNodeDragStop = React.useCallback( + (event: ReactMouseEvent, node: Node, nodes: Node[]) => { + setScrolling(false); + originalOnNodeDragStop?.(event, node, nodes); + }, + [originalOnNodeDragStop, setScrolling] + ); + + const onSelectionDragStart = React.useCallback( + (event: ReactMouseEvent, nodes: Node[]) => { + setScrolling(true); + originalOnSelectionDragStart?.(event, nodes); + }, + [originalOnSelectionDragStart, setScrolling] + ); + + const onSelectionDragStop = React.useCallback( + (event: ReactMouseEvent, nodes: Node[]) => { + setScrolling(false); + originalOnSelectionDragStop?.(event, nodes); + }, + [originalOnSelectionDragStop, setScrolling] + ); + + const onEdgeUpdateStart = React.useCallback( + (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => { + setScrolling(true); + originalOnEdgeUpdateStart?.(event, edge, handleType); + }, + [originalOnEdgeUpdateStart, setScrolling] + ); + + const onEdgeUpdateEnd = React.useCallback( + (event: MouseEvent, edge: Edge, handleType: HandleType) => { + setScrolling(false); + originalOnEdgeUpdateEnd?.(event, edge, handleType); + }, + [originalOnEdgeUpdateEnd, setScrolling] + ); + + if (!reactFlowProps.id || !scrollOnDrag) { + // No instance ID or config available, return empty object that will not overwrite any react-flow config parameters + return {}; + } else { + return { + onInit, + onNodeDragStart, + onNodeDragStop, + onConnectStart, + onConnectStop, + onSelectionDragStart, + onSelectionDragStop, + onEdgeUpdateStart, + onEdgeUpdateEnd, + }; + } +}; diff --git a/src/cmem/react-flow/index.ts b/src/cmem/react-flow/index.ts index 21ebd12f6..cc35e0b9c 100644 --- a/src/cmem/react-flow/index.ts +++ b/src/cmem/react-flow/index.ts @@ -1,3 +1,4 @@ export * from "./ReactFlow/ReactFlow"; +export * from "./ReactFlow/ReactFlowV10"; export * from "./StickyNoteModal/StickyNoteModal"; export * from "./extensions/scrollOnDragHook"; diff --git a/src/extensions/react-flow/index.ts b/src/extensions/react-flow/index.ts index f2dd94494..71cdb047b 100644 --- a/src/extensions/react-flow/index.ts +++ b/src/extensions/react-flow/index.ts @@ -15,6 +15,7 @@ export * from "./edges/EdgeTools"; export * from "./edges/EdgeLabel"; export * from "./markers/ReactFlowMarkers"; export * from "./minimap/MiniMap"; +export * from "./minimap/MiniMapV10"; export * from "./minimap/utils"; // deprecated exports diff --git a/src/extensions/react-flow/minimap/MiniMapV10.tsx b/src/extensions/react-flow/minimap/MiniMapV10.tsx new file mode 100644 index 000000000..29ea6cf47 --- /dev/null +++ b/src/extensions/react-flow/minimap/MiniMapV10.tsx @@ -0,0 +1,154 @@ +import React, { memo, useEffect } from "react"; +import { MiniMap as ReactFlowMiniMap, MiniMapProps as ReactFlowMiniMapProps, ReactFlowInstance, XYPosition } from "react-flow-renderer-lts"; +import { FlowTransform } from "react-flow-renderer/dist/types"; + +import { miniMapUtils } from "../minimap/utils"; +import {Viewport} from "react-flow-renderer-lts/dist/esm/types/general"; + +export interface MiniMapV10Props extends ReactFlowMiniMapProps { + /** + * React-Flow instance + */ + flowInstance?: ReactFlowInstance; + /** + * Enable navigating the react-flow canvas by dragging and clicking on the mini-map. + */ + enableNavigation?: boolean; + /** + * Properties are forwarded to the HTML `div` element used as minimap wrapper. + * Data attributes for test ids could be included here. + */ + wrapperProps?: Omit< + React.HTMLAttributes, + "onMouseDown" | "onMouseUp" | "onMouseMove" | "onMouseLeave" + >; +} + +interface configParams { + // Key has been pressed down over the mini-map and navigation mode has thus started + navigationOn: boolean; + // The mini-map element + minimapElement: Element | null; + // The react-flow element + flowElement: Element | null; +} + +let minimapCalcConf: configParams = { + navigationOn: false, + minimapElement: null, + flowElement: null, +}; + +/** An improved mini-map for react-flow that supports navigation via the mini-map. */ +export const MiniMapV10 = memo( + ({ + flowInstance, + enableNavigation = false, + maskColor = "#ddddddbb", + nodeClassName = miniMapUtils.nodeClassName, + nodeColor = miniMapUtils.nodeColor, + nodeStrokeColor = miniMapUtils.borderColor, + wrapperProps, + ...minimapProps + }: MiniMapV10Props) => { + const minimapWrapper = React.useRef(null); + + useEffect(() => { + const minimapDiv: HTMLDivElement | null = minimapWrapper.current; + if (enableNavigation && flowInstance && minimapDiv) { + minimapCalcConf = { + navigationOn: false, + minimapElement: minimapDiv.querySelector(".react-flow__minimap"), + flowElement: minimapDiv.closest(".react-flow"), + }; + } + }, [flowInstance, enableNavigation]); + + /** Changes the viewport of the react-flow view as given by the mini-maps canvas bounds. */ + const moveViewPort = React.useCallback((canvasBounds: DOMRect, canvasPosition: XYPosition) => { + if(!flowInstance) { + return + } + const zoom: number = flowInstance.getZoom() + const canvasNewState: FlowTransform & Viewport = { + zoom, + x: canvasPosition.x * zoom + canvasBounds.width / 2, + y: canvasPosition.y * zoom + canvasBounds.height / 2, + }; + const instanceStateV10 = flowInstance as ReactFlowInstance + instanceStateV10.setViewport(canvasNewState) + }, [flowInstance]) + + // sets the visible area of the canvas based on mouse movement on the mini-map + const handleMiniMapMouseMove = (event: any) => { + const minimapConfig = minimapCalcConf.minimapElement?.getAttribute("viewBox")?.split(" "); + if ( + minimapCalcConf.navigationOn && + minimapCalcConf.minimapElement && + minimapCalcConf.flowElement && + flowInstance && + minimapConfig + ) { + const minimapBounds = minimapCalcConf.minimapElement.getBoundingClientRect(); + const canvasBounds: DOMRect = minimapCalcConf.flowElement.getBoundingClientRect(); + + const minimapCoordinates = { + x0: parseInt(minimapConfig[0]), + y0: parseInt(minimapConfig[1]), + x1: parseInt(minimapConfig[2]) + parseInt(minimapConfig[0]), + y1: parseInt(minimapConfig[3]) + parseInt(minimapConfig[1]), + }; + const minimapClick = { + x: event.clientX - minimapBounds.left, + y: event.clientY - minimapBounds.top, + }; + const canvasPosition = { + x: + ((minimapCoordinates.x1 - minimapCoordinates.x0) / minimapBounds.width) * minimapClick.x * -1 - + minimapCoordinates.x0, + y: + ((minimapCoordinates.y1 - minimapCoordinates.y0) / minimapBounds.height) * minimapClick.y * -1 - + minimapCoordinates.y0, + }; + moveViewPort(canvasBounds, canvasPosition) + } + }; + + // sets the view for the user when clicked and finish navigation + const handleMiniMapMouseUp = (event: any) => { + handleMiniMapMouseMove(event); + minimapCalcConf.navigationOn = false; + }; + + // enables the mini-map fake drag effect see "handleMiniMapMouseMove" above. + const handleMiniMapMouseDown = () => { + if (enableNavigation && flowInstance) { + minimapCalcConf.navigationOn = true; + } + }; + + return ( +
+ +
+ ); + } +); From bf7b99eaef5fea657e9394f639186c7dc78e23c7 Mon Sep 17 00:00:00 2001 From: Michael Haschke Date: Wed, 8 Jan 2025 16:49:25 +0100 Subject: [PATCH 002/260] fix Tabs stories --- src/components/Tabs/stories/Tab.stories.tsx | 2 +- src/components/Tabs/stories/TabPanel.stories.tsx | 2 +- src/components/Tabs/stories/TabTitle.stories.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Tabs/stories/Tab.stories.tsx b/src/components/Tabs/stories/Tab.stories.tsx index d7a567748..44ec92bdc 100644 --- a/src/components/Tabs/stories/Tab.stories.tsx +++ b/src/components/Tabs/stories/Tab.stories.tsx @@ -4,7 +4,7 @@ import { Meta, StoryFn } from "@storybook/react"; import { Tab as TabDummyForStorybook, Tabs } from "./../../../../"; export default { - title: "Components/Tabs", + title: "Components/Tabs/Tab", component: TabDummyForStorybook, argTypes: { backgroundColor: { diff --git a/src/components/Tabs/stories/TabPanel.stories.tsx b/src/components/Tabs/stories/TabPanel.stories.tsx index 0ba72f783..270a513ed 100644 --- a/src/components/Tabs/stories/TabPanel.stories.tsx +++ b/src/components/Tabs/stories/TabPanel.stories.tsx @@ -5,7 +5,7 @@ import { Meta, StoryFn } from "@storybook/react"; import { TabPanel } from "./../../../"; export default { - title: "Components/Tabs", + title: "Components/Tabs/TabPanel", component: TabPanel, argTypes: {}, } as Meta; diff --git a/src/components/Tabs/stories/TabTitle.stories.tsx b/src/components/Tabs/stories/TabTitle.stories.tsx index 8bb53ba50..c4124754d 100644 --- a/src/components/Tabs/stories/TabTitle.stories.tsx +++ b/src/components/Tabs/stories/TabTitle.stories.tsx @@ -4,7 +4,7 @@ import { Meta, StoryFn } from "@storybook/react"; import { Tabs, TabTitle as TabTitleOrg } from "./../../../"; export default { - title: "Components/Tabs", + title: "Components/Tabs/TabTitle", component: TabTitleOrg, argTypes: {}, } as Meta; From 8f5b53660ad84763fdfffd17c7a36c174d019e90 Mon Sep 17 00:00:00 2001 From: Andreas Schultz Date: Wed, 8 Jan 2025 16:56:52 +0100 Subject: [PATCH 003/260] Add story for react-flow v10 node --- .../nodes/stories/NodeDefaultV10.stories.tsx | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/extensions/react-flow/nodes/stories/NodeDefaultV10.stories.tsx diff --git a/src/extensions/react-flow/nodes/stories/NodeDefaultV10.stories.tsx b/src/extensions/react-flow/nodes/stories/NodeDefaultV10.stories.tsx new file mode 100644 index 000000000..6bbe2299d --- /dev/null +++ b/src/extensions/react-flow/nodes/stories/NodeDefaultV10.stories.tsx @@ -0,0 +1,127 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { Meta, StoryFn } from "@storybook/react"; + +import { ReactFlowV10 } from "./../../../../cmem"; +import { NodeDefault } from "./../NodeDefault"; +import { Default as NodeContentExample } from "./NodeContent.stories"; +import { nodeTypes } from "./nodeTypes"; +import {Edge, Node} from "react-flow-renderer-lts"; + +export default { + title: "Extensions/React Flow V10/Node", + component: NodeDefault, + argTypes: { + id: { + control: "text", + description: "Internal node identifier.", + }, + position: { + type: { required: true }, + description: "Position on React-Flow canvas.", + table: { + type: { summary: "XYPosition" }, + }, + }, + type: { + control: "select", + description: "Key of the imported and connected `nodeTypes` to specify what node implementation is used.", + table: { + type: { summary: "string" }, + defaultValue: { summary: "default" }, + }, + options: Object.keys(nodeTypes), + mapping: Object.fromEntries(Object.keys(nodeTypes).map((type) => [type, type])), + }, + style: { + control: "object", + description: "css properties", + table: { + type: { summary: "React.CSSProperties" }, + }, + }, + className: { + control: "text", + description: "additional class name", + }, + targetPosition: { + description: "'left' | 'right' | 'top' | 'bottom' handle position", + table: { + type: { summary: "Position" }, + defaultValue: { summary: "Position.Left" }, + }, + }, + sourcePosition: { + description: "'left' | 'right' | 'top' | 'bottom' handle position", + table: { + type: { summary: "Position" }, + defaultValue: { summary: "Position.Right" }, + }, + }, + isHidden: { + control: "boolean", + description: "if true, the node will not be rendered", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + }, + }, + draggable: { + control: "boolean", + description: "if option is not set, the node is draggable (overwrites general nodesDraggable option)", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + }, + }, + connectable: { + control: "boolean", + description: "if option is not set, the node is connectable (overwrites general nodesConnectable option)", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + }, + }, + selectable: { + control: "boolean", + description: "if option is not set, the node is selectable (overwrites general elementsSelectable option)", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + }, + }, + dragHandle: { + control: "text", + description: "selector for specifying an element as a drag handle", + }, + }, +} as Meta; + +const NodeDefaultExample = (args: any) => { + const [nodes, setNodes] = useState([] as Node[]); + const [edges, setEdges] = useState([] as Edge[]); + //const [edgeTools, setEdgeTools] = useState(<>); + + useEffect(() => { + setNodes([args] as Node[]) + }, [args]); + + return ( + + ); +}; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + id: "1", + type: "default", + data: NodeContentExample.args, + position: { x: 50, y: 50 }, +}; From 0210d34c6e316ea09ac1dac6dd6dc66adf195ccc Mon Sep 17 00:00:00 2001 From: Michael Haschke Date: Thu, 9 Jan 2025 15:45:59 +0100 Subject: [PATCH 004/260] include color palette as custom properties --- src/components/Application/_colors.scss | 15 ++++ src/components/Application/application.scss | 1 + src/configuration/_palettes.scss | 91 +++++++++++++++++++++ src/configuration/_variables.scss | 2 + 4 files changed, 109 insertions(+) create mode 100644 src/components/Application/_colors.scss create mode 100644 src/configuration/_palettes.scss diff --git a/src/components/Application/_colors.scss b/src/components/Application/_colors.scss new file mode 100644 index 000000000..6c79ea7fe --- /dev/null +++ b/src/components/Application/_colors.scss @@ -0,0 +1,15 @@ +@use "sass:map"; +@use "sass:list"; + +:root { + @each $palette-group-name, $palette-group-tints in $eccgui-color-palette-light { + @each $palette-tint-name, $palette-tint-colors in $palette-group-tints { + @for $i from 1 through list.length($palette-tint-colors) { + #{eccgui-color-name($palette-group-name, $palette-tint-name, $i * 100)}: #{list.nth( + $palette-tint-colors, + $i + )}; + } + } + } +} diff --git a/src/components/Application/application.scss b/src/components/Application/application.scss index 78d1b5d45..ea0de1677 100644 --- a/src/components/Application/application.scss +++ b/src/components/Application/application.scss @@ -1,4 +1,5 @@ // @import 'config'; +@import "colors"; @import "header"; @import "toolbar"; diff --git a/src/configuration/_palettes.scss b/src/configuration/_palettes.scss new file mode 100644 index 000000000..f93c8c8de --- /dev/null +++ b/src/configuration/_palettes.scss @@ -0,0 +1,91 @@ +@use "sass:list"; +@use "sass:color"; +@use "sass:math"; + +/** + * Always provide a list of 9 color tints. + * If the list do not provide 9 colors then create them based on the first and last color in the list. + */ +@function eccgui-create-color-tints($colorset) { + $colorset-steps: 9; // number of tints + $count-colors: list.length($colorset); + + @if $count-colors == $colorset-steps { + @return $colorset; + } + + @if $count-colors < 1 { + @error "Need at least 1 color to create color tints."; + } + + @debug "Got only #{$count-colors} tints: #{$colorset}"; + + $color-tint-start: rgb( + color.red(list.nth($colorset, 1)), + color.green(list.nth($colorset, 1)), + color.blue(list.nth($colorset, 1)) + ); + $color-tint-end: rgb( + color.red(list.nth($colorset, -1)), + color.green(list.nth($colorset, -1)), + color.blue(list.nth($colorset, -1)) + ); + $colorset-fallback: $color-tint-start; + + @for $i from 2 to $colorset-steps { + $tint-step: color.mix($color-tint-end, $color-tint-start, 100% * math.div($i, $colorset-steps)); + $colorset-fallback: list.append( + $colorset-fallback, + rgb(color.red($tint-step), color.green($tint-step), color.blue($tint-step)) + ); + } + + $colorset-fallback: list.append($colorset-fallback, $color-tint-end); + + @debug "Create fallback with 9 tints: #{$colorset-fallback}"; + @return $colorset-fallback; +} + +/** + * Create name for custom property + */ +@function eccgui-color-name($group_or_name, $tint: null, $weight: null) { + @if $group_or_name && $tint && $weight { + @return "--#{$eccgui}-color-palette-#{$group_or_name}-#{$tint}-#{$weight}"; + } @else { + @return "--#{$eccgui}-color-#{$group_or_name}"; + } +} + +/** + * Base definition for colors. + * Can be overwritten if defined before this file is included. + * You need to define all or nothing, we currently support overwriting it only partly. + */ + +$eccgui-color-palette-light: ( + "identity": ( + "brand": eccgui-create-color-tints(#fae1cc rgb(254 143 1)), + "accent": eccgui-create-color-tints(#e5f4fb rgb(10 103 163)), + ), + "semantic": ( + "info": eccgui-create-color-tints(rgb(227 242 253) rgb(21 101 192)), + "success": eccgui-create-color-tints(rgb(232 245 233) #1b5e20), + "warning": eccgui-create-color-tints(rgb(255 243 224) #e65100), + "danger": eccgui-create-color-tints(rgb(255 235 238) #b71c1c), + ), + "layout": ( + "indigo": eccgui-create-color-tints(#ebcef2 #46247a), + "pink": eccgui-create-color-tints(#fadaec #b23a78), + "rajah": eccgui-create-color-tints(#fef0e1 #88592c), + "yellow": eccgui-create-color-tints(#fce7e7 #d4af37), + "lime": eccgui-create-color-tints(#cde0d6 #789f3f), + "teal": eccgui-create-color-tints(#9addd6 #1f7a68), + "cyan": eccgui-create-color-tints(#ccf9fe #00a3c4), + "brown": eccgui-create-color-tints(#da780d #632114), + "blue": eccgui-create-color-tints(#e5f4fb #0074b1), + "grey": eccgui-create-color-tints(#dcdde4 #101016), + "red": eccgui-create-color-tints(#f9ecf4 #c41e3a), + "green": eccgui-create-color-tints(#e8ede9 #008c30), + ), +) !default; diff --git a/src/configuration/_variables.scss b/src/configuration/_variables.scss index 6f980db67..43750488f 100644 --- a/src/configuration/_variables.scss +++ b/src/configuration/_variables.scss @@ -15,6 +15,8 @@ $eccgui: "eccgui" !default; +@import "palettes"; + // -- Configuration stack of colors -------------------------------------------- $eccgui-color-primary: rgb(254 143 1) !default; From 3799bac8194f35cd4ccea21a5a03df015282b50e Mon Sep 17 00:00:00 2001 From: Michael Haschke Date: Thu, 9 Jan 2025 15:53:36 +0100 Subject: [PATCH 005/260] allow filtering when fetching css custom properties --- src/common/utils/CssCustomProperties.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/common/utils/CssCustomProperties.ts b/src/common/utils/CssCustomProperties.ts index ec9495c1d..8522a83c0 100644 --- a/src/common/utils/CssCustomProperties.ts +++ b/src/common/utils/CssCustomProperties.ts @@ -13,6 +13,7 @@ interface getLocalCssStyleRulePropertiesProps extends getLocalCssStyleRulesProps propertyType?: "all" | "normal" | "custom"; } interface getCustomPropertiesProps extends getLocalCssStyleRulesProps { + filterName?: (name?: string) => boolean; removeDashPrefix?: boolean; returnObject?: boolean; } @@ -87,7 +88,7 @@ export default class CssCustomProperties { const { propertyType = "all", ...otherFilters } = filter; return CssCustomProperties.listLocalCssStyleRules(otherFilters) .map((cssrule) => { - return [...(cssrule as any).style].map((propertyname) => { + return [...(cssrule as AllowedCSSRule).style].map((propertyname) => { return [propertyname.trim(), (cssrule as CSSStyleRule).style.getPropertyValue(propertyname).trim()]; }); }) @@ -104,17 +105,21 @@ export default class CssCustomProperties { }; static listCustomProperties = (props: getCustomPropertiesProps = {}) => { - const { removeDashPrefix = true, returnObject = true, ...filterProps } = props; + const { removeDashPrefix = true, returnObject = true, filterName = () => true, ...filterProps } = props; const customProperties = CssCustomProperties.listLocalCssStyleRuleProperties({ ...filterProps, propertyType: "custom", - }).map((declaration) => { - if (removeDashPrefix) { - return [declaration[0].substr(2), declaration[1]]; - } - return declaration; - }); + }) + .filter((declaration) => { + return filterName(declaration[0]); + }) + .map((declaration) => { + if (removeDashPrefix) { + return [declaration[0].substr(2), declaration[1]]; + } + return declaration; + }); return returnObject ? Object.fromEntries(customProperties) : customProperties; }; From f702e2d7e5db13d78b74772bf5e64b2bd35f7e6a Mon Sep 17 00:00:00 2001 From: Michael Haschke Date: Thu, 9 Jan 2025 16:09:02 +0100 Subject: [PATCH 006/260] use story to include protype for color palette tool --- src/cmem/markdown/Markdown.stories.tsx | 1 - src/common/utils/CssCustomProperties.ts | 2 +- .../stories/Application.stories.tsx | 4 +- .../stories/ColorPalettes.stories.tsx | 220 ++++++++++++++++++ 4 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 src/components/Application/stories/ColorPalettes.stories.tsx diff --git a/src/cmem/markdown/Markdown.stories.tsx b/src/cmem/markdown/Markdown.stories.tsx index d067e7912..2754e20cf 100644 --- a/src/cmem/markdown/Markdown.stories.tsx +++ b/src/cmem/markdown/Markdown.stories.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Blockquote } from "@blueprintjs/core"; import { Meta, StoryFn } from "@storybook/react"; import { Markdown } from "./../../../index"; diff --git a/src/common/utils/CssCustomProperties.ts b/src/common/utils/CssCustomProperties.ts index 8522a83c0..a7166fcc9 100644 --- a/src/common/utils/CssCustomProperties.ts +++ b/src/common/utils/CssCustomProperties.ts @@ -13,7 +13,7 @@ interface getLocalCssStyleRulePropertiesProps extends getLocalCssStyleRulesProps propertyType?: "all" | "normal" | "custom"; } interface getCustomPropertiesProps extends getLocalCssStyleRulesProps { - filterName?: (name?: string) => boolean; + filterName?: (name: string) => boolean; removeDashPrefix?: boolean; returnObject?: boolean; } diff --git a/src/components/Application/stories/Application.stories.tsx b/src/components/Application/stories/Application.stories.tsx index e2feb5907..7e0aa6cf4 100644 --- a/src/components/Application/stories/Application.stories.tsx +++ b/src/components/Application/stories/Application.stories.tsx @@ -29,11 +29,11 @@ interface ApplicationBasicExampleProps { } function ApplicationBasicExample(args: ApplicationBasicExampleProps) { - return <>; + return args ? <> : <>; } export default { - title: "Components/Application", + title: "Components/Application/Elements", component: ApplicationBasicExample, subcomponents: { ApplicationContainer, diff --git a/src/components/Application/stories/ColorPalettes.stories.tsx b/src/components/Application/stories/ColorPalettes.stories.tsx new file mode 100644 index 000000000..432f24ca4 --- /dev/null +++ b/src/components/Application/stories/ColorPalettes.stories.tsx @@ -0,0 +1,220 @@ +import React from "react"; +import { Meta, StoryFn } from "@storybook/react"; +import Color from "color"; + +import CssCustomProperties from "./../../../common/utils/CssCustomProperties"; +import { + Button, + CLASSPREFIX as eccgui, + FieldItem, + FieldItemRow, + Section, + SectionHeader, + Spacing, + Tabs, + TextArea, + TextField, + TitleSubsection, +} from "./../../../index"; + +interface ColorPaletteConfiguratorProps { + customColorProperties?: string; +} + +const ColorPaletteConfigurator = ({ customColorProperties }: ColorPaletteConfiguratorProps) => { + const palettePrefix = `--${eccgui}-color-palette-`; + + const createPaletteData = (csscustomprops: string | undefined) => { + const colors = ( + csscustomprops + ? csscustomprops.split(";").map((rule: string) => { + return rule.split(":").map((rulepart: string) => { + return rulepart.trim(); + }); + }) + : new CssCustomProperties({ + selectorText: `:root`, + filterName: (name: string) => { + return name.includes(palettePrefix); + }, + removeDashPrefix: false, + returnObject: false, + }).customProperties() + ) + .filter((colorconfig: object) => { + if (!Array.isArray(colorconfig)) { + return false; + } + if (colorconfig.length !== 2) { + return false; + } + return true; + }) + .map((colorconfig: object) => { + return [colorconfig[0].replace(palettePrefix, ""), Color(colorconfig[1]).rgb()]; + }); + + const data = new Object(); + + for (const [key, value] of colors) { + const hierarchy = key.split("-"); + if (!data[hierarchy[0]]) { + data[hierarchy[0]] = new Object(); + } + if (!data[hierarchy[0]][hierarchy[1]]) { + data[hierarchy[0]][hierarchy[1]] = new Object(); + } + if (!data[hierarchy[0]][hierarchy[1]][hierarchy[2]]) { + data[hierarchy[0]][hierarchy[1]][hierarchy[2]] = value; + } + } + + return data; + }; + + const createCustomPropsSerialization = (data: object) => { + let serialization = ""; + for (const [group, tints] of Object.entries(data)) { + for (const [tint, weights] of Object.entries(tints as object)) { + for (const [weight, value] of Object.entries(weights)) { + serialization = + serialization + + `--${eccgui}-color-palette-${group}-${tint}-${weight}: ${(value as Color).hex()};\n`; + } + } + } + return serialization.trim(); + }; + + const createSassSerialization = (data: object) => { + const createTintData = (tint: string, weights: object) => { + return `\t\t"${tint}": eccgui-create-color-tints(${Object.values(weights) + .map((color) => color.hex()) + .join(" ")}),\n`; + }; + + const createGroupData = (group: string, tints: object) => { + let groupData = `\t"${group}": (\n`; + for (const [tint, weights] of Object.entries(tints)) { + groupData = groupData + createTintData(tint, weights); + } + return groupData + `\t),\n`; + }; + + let sassData = `$eccgui-color-palette-light: (\n`; + + for (const [group, tints] of Object.entries(data)) { + sassData = sassData + createGroupData(group, tints); + } + + return sassData + `) !default;`; + }; + + const [paletteData, setPaletteData] = React.useState(undefined); + const [userPaletteProps, setUserPaletteProps] = React.useState(undefined); + + React.useEffect(() => { + const paletteData = createPaletteData(customColorProperties); + //console.log("paletteData", paletteData); + setPaletteData(paletteData); + }, [customColorProperties]); + + const editorPanel = ( +
+ {paletteData && + Object.keys(paletteData).map((group, id) => { + return ( +
+ + {group} + + + {Object.keys(paletteData[group]).map((tint, id) => { + return ( + + {Object.keys(paletteData[group][tint]).map((weight, id) => { + return ( + + { + paletteData[group][tint][weight] = Color(newcolor).rgb(); + // console.log("new palette", paletteData); + setPaletteData({ ...paletteData }); + }} + /> + + ); + })} + + ); + })} + +
+ ); + })} +
+ ); + + return ( + {}} + tabs={[ + { + id: "editor", + panel: editorPanel, + title: "Editor", + }, + { + id: "css", + panel: ( +
+