mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
Feat/click connector mode (#106)
* fix: eliminate re-render loop causing export failures - Remove onModelUpdated callback that triggered infinite re-render cycles - Replace debounce hack with controlled useEffect-based export timing - Ensure DOM stability before export execution to prevent null containerRef - Add isExporting guard to prevent concurrent export operations Resolves issue where image export functionality was completely broken due to infinite re-rendering caused by onModelUpdated callback. Fixes #84 * Update packages/fossflow-lib/src/components/ExportImageDialog/ExportImageDialog.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixed connectors --------- Co-authored-by: Manfred Michaelis <mm@michm.de> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Stan <stanleylsmith@pm.me>
This commit is contained in:
@@ -25,8 +25,13 @@ root.render(
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
|
||||
// Register service worker for PWA functionality
|
||||
serviceWorkerRegistration.register({
|
||||
onSuccess: () => console.log('Service worker registered successfully'),
|
||||
onUpdate: () => console.log('Service worker update available')
|
||||
});
|
||||
// Service worker registration - only in production for PWA functionality
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
serviceWorkerRegistration.register({
|
||||
onSuccess: () => console.log('Service worker registered successfully'),
|
||||
onUpdate: () => console.log('Service worker update available')
|
||||
});
|
||||
} else {
|
||||
// Disable service worker in development to avoid cache issues
|
||||
serviceWorkerRegistration.unregister();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Box, Paper, Typography, Fade } from '@mui/material';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
export const ConnectorEmptySpaceTooltip = () => {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
|
||||
const mode = useUiStateStore((state) => state.mode);
|
||||
const mouse = useUiStateStore((state) => state.mouse);
|
||||
const { connectors } = useScene();
|
||||
const previousModeRef = useRef(mode);
|
||||
const shownForConnectorRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const previousMode = previousModeRef.current;
|
||||
|
||||
// Detect when we transition from isConnecting to not isConnecting (connection completed)
|
||||
if (
|
||||
mode.type === 'CONNECTOR' &&
|
||||
previousMode.type === 'CONNECTOR' &&
|
||||
previousMode.isConnecting &&
|
||||
!mode.isConnecting &&
|
||||
!mode.id // After connection is complete, id is set to null
|
||||
) {
|
||||
// Find the most recently created connector
|
||||
const latestConnector = connectors[connectors.length - 1];
|
||||
|
||||
if (latestConnector && latestConnector.id !== shownForConnectorRef.current) {
|
||||
// Check if either end is connected to empty space (tile reference)
|
||||
const hasEmptySpaceConnection = latestConnector.anchors.some(
|
||||
anchor => anchor.ref.tile && !anchor.ref.item
|
||||
);
|
||||
|
||||
if (hasEmptySpaceConnection) {
|
||||
// Show tooltip near the mouse position
|
||||
setTooltipPosition({
|
||||
x: mouse.position.screen.x,
|
||||
y: mouse.position.screen.y
|
||||
});
|
||||
setShowTooltip(true);
|
||||
shownForConnectorRef.current = latestConnector.id;
|
||||
|
||||
// Auto-hide after 12 seconds
|
||||
const timer = setTimeout(() => {
|
||||
setShowTooltip(false);
|
||||
}, 12000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide tooltip when switching away from connector mode
|
||||
if (mode.type !== 'CONNECTOR') {
|
||||
setShowTooltip(false);
|
||||
}
|
||||
|
||||
previousModeRef.current = mode;
|
||||
}, [mode, connectors, mouse.position.screen]);
|
||||
|
||||
// Remove the click handler - tooltip should persist
|
||||
// It will only hide after timeout or mode change
|
||||
|
||||
if (!showTooltip) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fade in={showTooltip} timeout={300}>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
left: Math.min(tooltipPosition.x + 20, window.innerWidth - 350),
|
||||
top: Math.min(tooltipPosition.y - 60, window.innerHeight - 100),
|
||||
zIndex: 1400, // Above most UI elements
|
||||
pointerEvents: 'none' // Don't block interactions
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={4}
|
||||
sx={{
|
||||
p: 2,
|
||||
maxWidth: 320,
|
||||
backgroundColor: 'background.paper',
|
||||
borderLeft: '4px solid',
|
||||
borderLeftColor: 'info.main'
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
To connect this connector to a node, <strong>left-click on the end of the connector</strong> and drag it to the desired node.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Fade>
|
||||
);
|
||||
};
|
||||
@@ -107,6 +107,18 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
|
||||
return () => clearTimeout(timer);
|
||||
}, [showGrid, backgroundColor]);
|
||||
|
||||
const downloadFile = useCallback(() => {
|
||||
if (!imageData) return;
|
||||
|
||||
const data = base64ToBlob(
|
||||
imageData.replace('data:image/png;base64,', ''),
|
||||
'image/png;charset=utf-8'
|
||||
);
|
||||
|
||||
downloadFileUtil(data, generateGenericFilename('png'));
|
||||
}, [imageData]);
|
||||
>>>>>>> 10145c9 (fix: eliminate re-render loop causing export failures)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
exportImage();
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ExportImageDialog } from '../ExportImageDialog/ExportImageDialog';
|
||||
import { HelpDialog } from '../HelpDialog/HelpDialog';
|
||||
import { SettingsDialog } from '../SettingsDialog/SettingsDialog';
|
||||
import { ConnectorHintTooltip } from '../ConnectorHintTooltip/ConnectorHintTooltip';
|
||||
import { ConnectorEmptySpaceTooltip } from '../ConnectorEmptySpaceTooltip/ConnectorEmptySpaceTooltip';
|
||||
|
||||
const ToolsEnum = {
|
||||
MAIN_MENU: 'MAIN_MENU',
|
||||
@@ -245,6 +246,7 @@ export const UiOverlay = () => {
|
||||
|
||||
{/* Show connector hint tooltip only in editable mode */}
|
||||
{editorMode === EditorModeEnum.EDITABLE && <ConnectorHintTooltip toolMenuRef={toolMenuRef} />}
|
||||
{editorMode === EditorModeEnum.EDITABLE && <ConnectorEmptySpaceTooltip />}
|
||||
|
||||
<SceneLayer>
|
||||
<Box ref={contextMenuAnchorRef} />
|
||||
|
||||
@@ -23,12 +23,20 @@ export const Connector: ModeActions = {
|
||||
)
|
||||
return;
|
||||
|
||||
// TypeScript type guard - we know mode is CONNECTOR type here
|
||||
const connectorMode = uiState.mode;
|
||||
|
||||
// Only update connector position in drag mode or when connecting in click mode
|
||||
if (uiState.connectorInteractionMode === 'drag' || uiState.mode.isConnecting) {
|
||||
const connector = getItemByIdOrThrow(
|
||||
scene.currentView.connectors ?? [],
|
||||
uiState.mode.id
|
||||
if (uiState.connectorInteractionMode === 'drag' || connectorMode.isConnecting) {
|
||||
// Try to find the connector - it might not exist yet
|
||||
const connectorItem = (scene.currentView.connectors ?? []).find(
|
||||
c => c.id === connectorMode.id
|
||||
);
|
||||
|
||||
// If connector doesn't exist yet, return early
|
||||
if (!connectorItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemAtTile = getItemAtTile({
|
||||
tile: uiState.mouse.position.tile,
|
||||
@@ -36,20 +44,20 @@ export const Connector: ModeActions = {
|
||||
});
|
||||
|
||||
if (itemAtTile?.type === 'ITEM') {
|
||||
const newConnector = produce(connector.value, (draft) => {
|
||||
const newConnector = produce(connectorItem, (draft) => {
|
||||
draft.anchors[1] = { id: generateId(), ref: { item: itemAtTile.id } };
|
||||
});
|
||||
|
||||
scene.updateConnector(uiState.mode.id, newConnector);
|
||||
scene.updateConnector(connectorMode.id!, newConnector);
|
||||
} else {
|
||||
const newConnector = produce(connector.value, (draft) => {
|
||||
const newConnector = produce(connectorItem, (draft) => {
|
||||
draft.anchors[1] = {
|
||||
id: generateId(),
|
||||
ref: { tile: uiState.mouse.position.tile }
|
||||
};
|
||||
});
|
||||
|
||||
scene.updateConnector(uiState.mode.id, newConnector);
|
||||
scene.updateConnector(connectorMode.id!, newConnector);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -99,11 +107,28 @@ export const Connector: ModeActions = {
|
||||
});
|
||||
} else {
|
||||
// Second click: complete the connection
|
||||
if (uiState.mode.id) {
|
||||
const connector = getItemByIdOrThrow(scene.connectors, uiState.mode.id);
|
||||
// We already checked mode.type === 'CONNECTOR' above
|
||||
const currentMode = uiState.mode;
|
||||
if (currentMode.id) {
|
||||
// Try to find the connector - it might not exist
|
||||
const connector = (scene.currentView.connectors ?? []).find(
|
||||
c => c.id === currentMode.id
|
||||
);
|
||||
|
||||
// If connector doesn't exist, reset mode and return
|
||||
if (!connector) {
|
||||
uiState.actions.setMode({
|
||||
type: 'CONNECTOR',
|
||||
showCursor: true,
|
||||
id: null,
|
||||
startAnchor: undefined,
|
||||
isConnecting: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the second anchor to the click position
|
||||
const newConnector = produce(connector.value, (draft) => {
|
||||
const newConnector = produce(connector, (draft) => {
|
||||
if (itemAtTile?.type === 'ITEM') {
|
||||
draft.anchors[1] = { id: generateId(), ref: { item: itemAtTile.id } };
|
||||
} else {
|
||||
@@ -114,18 +139,10 @@ export const Connector: ModeActions = {
|
||||
}
|
||||
});
|
||||
|
||||
scene.updateConnector(uiState.mode.id, newConnector);
|
||||
scene.updateConnector(currentMode.id, newConnector);
|
||||
|
||||
// Validate the connection
|
||||
const firstAnchor = newConnector.anchors[0];
|
||||
const lastAnchor = newConnector.anchors[newConnector.anchors.length - 1];
|
||||
|
||||
if (
|
||||
connector.value.path.tiles.length < 2 ||
|
||||
!(firstAnchor.ref.item && lastAnchor.ref.item)
|
||||
) {
|
||||
scene.deleteConnector(uiState.mode.id);
|
||||
}
|
||||
// Don't delete connectors to empty space - they're valid
|
||||
// Only validate minimum path length will be handled by the update
|
||||
|
||||
// Reset for next connection
|
||||
uiState.actions.setMode({
|
||||
@@ -171,17 +188,8 @@ export const Connector: ModeActions = {
|
||||
|
||||
// Only handle mouseup for drag mode
|
||||
if (uiState.connectorInteractionMode === 'drag') {
|
||||
const connector = getItemByIdOrThrow(scene.connectors, uiState.mode.id);
|
||||
const firstAnchor = connector.value.anchors[0];
|
||||
const lastAnchor =
|
||||
connector.value.anchors[connector.value.anchors.length - 1];
|
||||
|
||||
if (
|
||||
connector.value.path.tiles.length < 2 ||
|
||||
!(firstAnchor.ref.item && lastAnchor.ref.item)
|
||||
) {
|
||||
scene.deleteConnector(uiState.mode.id);
|
||||
}
|
||||
// Don't delete connectors to empty space - they're valid
|
||||
// Validation is handled in the reducer layer
|
||||
|
||||
uiState.actions.setMode({
|
||||
type: 'CONNECTOR',
|
||||
|
||||
Reference in New Issue
Block a user