mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-02 04:53:27 +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