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:
Jonathan Fishner
2025-03-10 15:51:33 +02:00
committed by GitHub
parent 09b1275475
commit 62beb68fa1
5 changed files with 152 additions and 4 deletions

View 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;
}

View File

@@ -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"

View 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,
};
};

View File

@@ -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}
/>
);
});

View File

@@ -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>