mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-11-04 05:53:15 +00:00 
			
		
		
		
	feat(canvas): highlight the Show-All button when No-Tables are visible in the canvas (#612)
* feat(canvas): highlight the Show-All button when No-Tables are visible in the canvas * fix --------- Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
This commit is contained in:
		
							
								
								
									
										47
									
								
								src/hooks/use-debounce-v2.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/hooks/use-debounce-v2.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
import { useEffect, useRef, useCallback } from 'react';
 | 
			
		||||
import { debounce as utilsDebounce } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
interface DebouncedFunction {
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
    (...args: any[]): void;
 | 
			
		||||
    cancel?: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A hook that returns a debounced version of the provided function.
 | 
			
		||||
 * The debounced function will only be called after the specified delay
 | 
			
		||||
 * has passed without the function being called again.
 | 
			
		||||
 *
 | 
			
		||||
 * @param callback The function to debounce
 | 
			
		||||
 * @param delay The delay in milliseconds
 | 
			
		||||
 * @returns A debounced version of the callback
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
export function useDebounce<T extends (...args: any[]) => any>(
 | 
			
		||||
    callback: T,
 | 
			
		||||
    delay: number
 | 
			
		||||
): (...args: Parameters<T>) => void {
 | 
			
		||||
    // Use a ref to store the debounced function
 | 
			
		||||
    const debouncedFnRef = useRef<DebouncedFunction>();
 | 
			
		||||
 | 
			
		||||
    // Update the debounced function when dependencies change
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        // Create the debounced function
 | 
			
		||||
        debouncedFnRef.current = utilsDebounce(callback, delay);
 | 
			
		||||
 | 
			
		||||
        // Clean up when component unmounts or dependencies change
 | 
			
		||||
        return () => {
 | 
			
		||||
            if (debouncedFnRef.current?.cancel) {
 | 
			
		||||
                debouncedFnRef.current.cancel();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }, [callback, delay]);
 | 
			
		||||
 | 
			
		||||
    // Create a stable callback that uses the ref
 | 
			
		||||
    const debouncedCallback = useCallback((...args: Parameters<T>) => {
 | 
			
		||||
        debouncedFnRef.current?.(...args);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return debouncedCallback;
 | 
			
		||||
}
 | 
			
		||||
@@ -682,7 +682,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <CanvasContextMenu>
 | 
			
		||||
            <div className="relative flex h-full">
 | 
			
		||||
            <div className="relative flex h-full" id="canvas">
 | 
			
		||||
                <ReactFlow
 | 
			
		||||
                    colorMode={effectiveTheme}
 | 
			
		||||
                    className="canvas-cursor-default nodes-animated"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										86
									
								
								src/pages/editor-page/canvas/hooks/use-is-lost-in-canvas.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/pages/editor-page/canvas/hooks/use-is-lost-in-canvas.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
import { useCallback, useState } from 'react';
 | 
			
		||||
import { getTableDimensions } from '../canvas-utils';
 | 
			
		||||
import type { TableNodeType } from '../table-node/table-node';
 | 
			
		||||
import { useOnViewportChange, useReactFlow } from '@xyflow/react';
 | 
			
		||||
import { useDebounce } from '@/hooks/use-debounce-v2';
 | 
			
		||||
 | 
			
		||||
export const useIsLostInCanvas = () => {
 | 
			
		||||
    const { getNodes, getViewport } = useReactFlow();
 | 
			
		||||
    const [noTablesVisible, setNoTablesVisible] = useState<boolean>(false);
 | 
			
		||||
 | 
			
		||||
    // Check if any tables are visible in the current viewport
 | 
			
		||||
    const checkVisibleTables = useCallback(() => {
 | 
			
		||||
        const nodes = getNodes();
 | 
			
		||||
        const viewport = getViewport();
 | 
			
		||||
 | 
			
		||||
        // If there are no nodes at all, don't highlight the button
 | 
			
		||||
        if (nodes.length === 0) {
 | 
			
		||||
            setNoTablesVisible(false);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Count visible (not hidden) nodes
 | 
			
		||||
        const visibleNodes = nodes.filter((node) => !node.hidden);
 | 
			
		||||
 | 
			
		||||
        // If there are no visible nodes at all, don't highlight the button
 | 
			
		||||
        if (visibleNodes.length === 0) {
 | 
			
		||||
            setNoTablesVisible(false);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Calculate viewport boundaries
 | 
			
		||||
        const viewportLeft = -viewport.x / viewport.zoom;
 | 
			
		||||
        const viewportTop = -viewport.y / viewport.zoom;
 | 
			
		||||
 | 
			
		||||
        const width =
 | 
			
		||||
            document.getElementById('canvas')?.clientWidth || window.innerWidth;
 | 
			
		||||
        const height =
 | 
			
		||||
            document.getElementById('canvas')?.clientHeight ||
 | 
			
		||||
            window.innerHeight;
 | 
			
		||||
 | 
			
		||||
        const viewportRight = viewportLeft + width / viewport.zoom;
 | 
			
		||||
        const viewportBottom = viewportTop + height / viewport.zoom;
 | 
			
		||||
 | 
			
		||||
        // Check if any node is visible in the viewport
 | 
			
		||||
        const anyNodeVisible = visibleNodes.some((node) => {
 | 
			
		||||
            let nodeWidth = node.width || 0;
 | 
			
		||||
            let nodeHeight = node.height || 0;
 | 
			
		||||
 | 
			
		||||
            if (node.type === 'table' && node.data?.table) {
 | 
			
		||||
                const tableNodeType = node as TableNodeType;
 | 
			
		||||
                const dimensions = getTableDimensions(tableNodeType.data.table);
 | 
			
		||||
                nodeWidth = dimensions.width;
 | 
			
		||||
                nodeHeight = dimensions.height;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Node boundaries
 | 
			
		||||
            const nodeLeft = node.position.x;
 | 
			
		||||
            const nodeTop = node.position.y;
 | 
			
		||||
            const nodeRight = nodeLeft + nodeWidth;
 | 
			
		||||
            const nodeBottom = nodeTop + nodeHeight;
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                nodeRight >= viewportLeft &&
 | 
			
		||||
                nodeLeft <= viewportRight &&
 | 
			
		||||
                nodeBottom >= viewportTop &&
 | 
			
		||||
                nodeTop <= viewportBottom
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Only set to true if there are tables but none are visible
 | 
			
		||||
        setNoTablesVisible(!anyNodeVisible);
 | 
			
		||||
    }, [getNodes, getViewport]);
 | 
			
		||||
 | 
			
		||||
    // Create a debounced version of checkVisibleTables
 | 
			
		||||
    const debouncedCheckVisibleTables = useDebounce(checkVisibleTables, 1000);
 | 
			
		||||
 | 
			
		||||
    useOnViewportChange({
 | 
			
		||||
        onEnd: () => {
 | 
			
		||||
            debouncedCheckVisibleTables();
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        isLostInCanvas: noTablesVisible,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
@@ -1,17 +1,22 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import type { ButtonProps } from '@/components/button/button';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
export const ToolbarButton = React.forwardRef<
 | 
			
		||||
    React.ElementRef<typeof Button>,
 | 
			
		||||
    ButtonProps
 | 
			
		||||
>((props, ref) => {
 | 
			
		||||
    const { className, ...rest } = props;
 | 
			
		||||
    return (
 | 
			
		||||
        <Button
 | 
			
		||||
            ref={ref}
 | 
			
		||||
            variant="ghost"
 | 
			
		||||
            className={'w-[36px] p-2 hover:bg-primary-foreground'}
 | 
			
		||||
            {...props}
 | 
			
		||||
            className={cn(
 | 
			
		||||
                'w-[36px] p-2 hover:bg-primary-foreground',
 | 
			
		||||
                className
 | 
			
		||||
            )}
 | 
			
		||||
            {...rest}
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { keyboardShortcutsForOS } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
 | 
			
		||||
import { KeyboardShortcutAction } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
 | 
			
		||||
import { useIsLostInCanvas } from '../hooks/use-is-lost-in-canvas';
 | 
			
		||||
 | 
			
		||||
const convertToPercentage = (value: number) => `${Math.round(value * 100)}%`;
 | 
			
		||||
 | 
			
		||||
@@ -28,6 +29,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
 | 
			
		||||
    const { redo, undo, hasRedo, hasUndo } = useHistory();
 | 
			
		||||
    const { getZoom, zoomIn, zoomOut, fitView } = useReactFlow();
 | 
			
		||||
    const [zoom, setZoom] = useState<string>(convertToPercentage(getZoom()));
 | 
			
		||||
    const { isLostInCanvas } = useIsLostInCanvas();
 | 
			
		||||
 | 
			
		||||
    useOnViewportChange({
 | 
			
		||||
        onChange: ({ zoom }) => {
 | 
			
		||||
            setZoom(convertToPercentage(zoom));
 | 
			
		||||
@@ -93,7 +96,14 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
 | 
			
		||||
                    <Tooltip>
 | 
			
		||||
                        <TooltipTrigger asChild>
 | 
			
		||||
                            <span>
 | 
			
		||||
                                <ToolbarButton onClick={showAll}>
 | 
			
		||||
                                <ToolbarButton
 | 
			
		||||
                                    onClick={showAll}
 | 
			
		||||
                                    className={
 | 
			
		||||
                                        isLostInCanvas
 | 
			
		||||
                                            ? 'bg-pink-500 text-white hover:bg-pink-600 hover:text-white'
 | 
			
		||||
                                            : ''
 | 
			
		||||
                                    }
 | 
			
		||||
                                >
 | 
			
		||||
                                    <Scan />
 | 
			
		||||
                                </ToolbarButton>
 | 
			
		||||
                            </span>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user