mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-11-03 21:43:23 +00:00 
			
		
		
		
	zoom in, out, percentage, save & width nodes
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -22,3 +22,5 @@ dist-ssr
 | 
			
		||||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.sw?
 | 
			
		||||
 | 
			
		||||
.env
 | 
			
		||||
							
								
								
									
										21
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										21
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -40,7 +40,8 @@
 | 
			
		||||
        "react-router-dom": "^6.26.0",
 | 
			
		||||
        "react-use": "^17.5.1",
 | 
			
		||||
        "tailwind-merge": "^2.4.0",
 | 
			
		||||
        "tailwindcss-animate": "^1.0.7"
 | 
			
		||||
        "tailwindcss-animate": "^1.0.7",
 | 
			
		||||
        "timeago-react": "^3.0.6"
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@types/node": "^22.1.0",
 | 
			
		||||
@@ -8436,6 +8437,24 @@
 | 
			
		||||
        "node": ">=10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/timeago-react": {
 | 
			
		||||
      "version": "3.0.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/timeago-react/-/timeago-react-3.0.6.tgz",
 | 
			
		||||
      "integrity": "sha512-4ywnCX3iFjdp84WPK7gt8s4n0FxXbYM+xv8hYL73p83dpcMxzmO+0W4xJuxflnkWNvum5aEaqTe6LZ3lUIudjQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "timeago.js": "^4.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/timeago.js": {
 | 
			
		||||
      "version": "4.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/to-fast-properties": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,8 @@
 | 
			
		||||
    "react-router-dom": "^6.26.0",
 | 
			
		||||
    "react-use": "^17.5.1",
 | 
			
		||||
    "tailwind-merge": "^2.4.0",
 | 
			
		||||
    "tailwindcss-animate": "^1.0.7"
 | 
			
		||||
    "tailwindcss-animate": "^1.0.7",
 | 
			
		||||
    "timeago-react": "^3.0.6"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/node": "^22.1.0",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								src/components/badge/badge-variants.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/components/badge/badge-variants.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
import { cva } from 'class-variance-authority';
 | 
			
		||||
 | 
			
		||||
export const badgeVariants = cva(
 | 
			
		||||
    'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
 | 
			
		||||
    {
 | 
			
		||||
        variants: {
 | 
			
		||||
            variant: {
 | 
			
		||||
                default:
 | 
			
		||||
                    'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
 | 
			
		||||
                secondary:
 | 
			
		||||
                    'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
 | 
			
		||||
                destructive:
 | 
			
		||||
                    'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
 | 
			
		||||
                outline: 'text-foreground',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        defaultVariants: {
 | 
			
		||||
            variant: 'default',
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										17
									
								
								src/components/badge/badge.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/components/badge/badge.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { type VariantProps } from 'class-variance-authority';
 | 
			
		||||
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { badgeVariants } from './badge-variants';
 | 
			
		||||
 | 
			
		||||
export interface BadgeProps
 | 
			
		||||
    extends React.HTMLAttributes<HTMLDivElement>,
 | 
			
		||||
        VariantProps<typeof badgeVariants> {}
 | 
			
		||||
 | 
			
		||||
function Badge({ className, variant, ...props }: BadgeProps) {
 | 
			
		||||
    return (
 | 
			
		||||
        <div className={cn(badgeVariants({ variant }), className)} {...props} />
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Badge };
 | 
			
		||||
@@ -22,6 +22,7 @@ export interface ChartDBContext {
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
    loadDiagram: (diagramId: string) => Promise<Diagram | undefined>;
 | 
			
		||||
    updateDiagramUpdatedAt: () => Promise<void>;
 | 
			
		||||
 | 
			
		||||
    // Database type operations
 | 
			
		||||
    updateDatabaseType: (databaseType: DatabaseType) => Promise<void>;
 | 
			
		||||
@@ -135,6 +136,7 @@ export const chartDBContext = createContext<ChartDBContext>({
 | 
			
		||||
    // General operations
 | 
			
		||||
    updateDiagramId: emptyFn,
 | 
			
		||||
    updateDiagramName: emptyFn,
 | 
			
		||||
    updateDiagramUpdatedAt: emptyFn,
 | 
			
		||||
    loadDiagram: emptyFn,
 | 
			
		||||
 | 
			
		||||
    // Database type operations
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,16 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const updateDiagramUpdatedAt: ChartDBContext['updateDiagramUpdatedAt'] =
 | 
			
		||||
        useCallback(async () => {
 | 
			
		||||
            const updatedAt = new Date();
 | 
			
		||||
            setDiagramUpdatedAt(updatedAt);
 | 
			
		||||
            await db.updateDiagram({
 | 
			
		||||
                id: diagramId,
 | 
			
		||||
                attributes: { updatedAt },
 | 
			
		||||
            });
 | 
			
		||||
        }, [db, diagramId, setDiagramUpdatedAt]);
 | 
			
		||||
 | 
			
		||||
    const updateDatabaseType: ChartDBContext['updateDatabaseType'] =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            async (databaseType) => {
 | 
			
		||||
@@ -906,12 +916,12 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                updateDiagramName,
 | 
			
		||||
                loadDiagram,
 | 
			
		||||
                updateDatabaseType,
 | 
			
		||||
                updateDiagramUpdatedAt,
 | 
			
		||||
                createTable,
 | 
			
		||||
                addTable,
 | 
			
		||||
                getTable,
 | 
			
		||||
                removeTable,
 | 
			
		||||
                updateTable,
 | 
			
		||||
                // updateTables,
 | 
			
		||||
                updateTablesState,
 | 
			
		||||
                updateField,
 | 
			
		||||
                removeField,
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    db.version(1).stores({
 | 
			
		||||
        diagrams: '++id, name, databaseType, createdAt, updatedAt',
 | 
			
		||||
        db_tables:
 | 
			
		||||
            '++id, diagramId, name, x, y, fields, indexes, color, createdAt',
 | 
			
		||||
            '++id, diagramId, name, x, y, fields, indexes, color, createdAt, width',
 | 
			
		||||
        db_relationships:
 | 
			
		||||
            '++id, diagramId, name, sourceTableId, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
 | 
			
		||||
        config: '++id, defaultDiagramId',
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ export interface DBTable {
 | 
			
		||||
    color: string;
 | 
			
		||||
    isView: boolean;
 | 
			
		||||
    createdAt: number;
 | 
			
		||||
    width?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const createTablesFromMetadata = ({
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import {
 | 
			
		||||
    NodePositionChange,
 | 
			
		||||
    NodeRemoveChange,
 | 
			
		||||
    useReactFlow,
 | 
			
		||||
    NodeDimensionChange,
 | 
			
		||||
} from '@xyflow/react';
 | 
			
		||||
import '@xyflow/react/dist/style.css';
 | 
			
		||||
import { TableNode, TableNodeType } from './table-node';
 | 
			
		||||
@@ -75,6 +76,7 @@ export const Canvas: React.FC<CanvasProps> = () => {
 | 
			
		||||
                data: {
 | 
			
		||||
                    table,
 | 
			
		||||
                },
 | 
			
		||||
                width: table.width ?? 224,
 | 
			
		||||
            }))
 | 
			
		||||
        );
 | 
			
		||||
    }, [tables, setNodes]);
 | 
			
		||||
@@ -112,9 +114,15 @@ export const Canvas: React.FC<CanvasProps> = () => {
 | 
			
		||||
                        (change) => change.type === 'remove'
 | 
			
		||||
                    ) as NodeRemoveChange[];
 | 
			
		||||
 | 
			
		||||
                    const sizeChanges: NodeDimensionChange[] = changes.filter(
 | 
			
		||||
                        (change) =>
 | 
			
		||||
                            change.type === 'dimensions' && change.resizing
 | 
			
		||||
                    ) as NodeDimensionChange[];
 | 
			
		||||
 | 
			
		||||
                    if (
 | 
			
		||||
                        positionChanges.length > 0 ||
 | 
			
		||||
                        removeChanges.length > 0
 | 
			
		||||
                        removeChanges.length > 0 ||
 | 
			
		||||
                        sizeChanges.length > 0
 | 
			
		||||
                    ) {
 | 
			
		||||
                        updateTablesState((currentTables) =>
 | 
			
		||||
                            currentTables
 | 
			
		||||
@@ -123,11 +131,29 @@ export const Canvas: React.FC<CanvasProps> = () => {
 | 
			
		||||
                                        (change) =>
 | 
			
		||||
                                            change.id === currentTable.id
 | 
			
		||||
                                    );
 | 
			
		||||
                                    if (positionChange) {
 | 
			
		||||
                                    const sizeChange = sizeChanges.find(
 | 
			
		||||
                                        (change) =>
 | 
			
		||||
                                            change.id === currentTable.id
 | 
			
		||||
                                    );
 | 
			
		||||
                                    if (positionChange || sizeChange) {
 | 
			
		||||
                                        return {
 | 
			
		||||
                                            id: currentTable.id,
 | 
			
		||||
                                            x: positionChange.position?.x,
 | 
			
		||||
                                            y: positionChange.position?.y,
 | 
			
		||||
                                            ...(positionChange
 | 
			
		||||
                                                ? {
 | 
			
		||||
                                                      x: positionChange.position
 | 
			
		||||
                                                          ?.x,
 | 
			
		||||
                                                      y: positionChange.position
 | 
			
		||||
                                                          ?.y,
 | 
			
		||||
                                                  }
 | 
			
		||||
                                                : {}),
 | 
			
		||||
                                            ...(sizeChange
 | 
			
		||||
                                                ? {
 | 
			
		||||
                                                      width:
 | 
			
		||||
                                                          sizeChange.dimensions
 | 
			
		||||
                                                              ?.width ??
 | 
			
		||||
                                                          currentTable.width,
 | 
			
		||||
                                                  }
 | 
			
		||||
                                                : {}),
 | 
			
		||||
                                        };
 | 
			
		||||
                                    }
 | 
			
		||||
                                    return currentTable;
 | 
			
		||||
@@ -180,11 +206,13 @@ export const Canvas: React.FC<CanvasProps> = () => {
 | 
			
		||||
 | 
			
		||||
                    return onEdgesChange(changes);
 | 
			
		||||
                }}
 | 
			
		||||
                maxZoom={5}
 | 
			
		||||
                minZoom={0.1}
 | 
			
		||||
                onConnect={onConnect}
 | 
			
		||||
                proOptions={{
 | 
			
		||||
                    hideAttribution: true,
 | 
			
		||||
                }}
 | 
			
		||||
                fitView={false} // todo think about it
 | 
			
		||||
                fitView={false}
 | 
			
		||||
                nodeTypes={nodeTypes}
 | 
			
		||||
                edgeTypes={edgeTypes}
 | 
			
		||||
                defaultEdgeOptions={{
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { NodeProps, Node } from '@xyflow/react';
 | 
			
		||||
import { NodeProps, Node, NodeResizer } from '@xyflow/react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Pencil, Table2 } from 'lucide-react';
 | 
			
		||||
import { Label } from '@/components/label/label';
 | 
			
		||||
@@ -30,8 +30,16 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = ({
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div
 | 
			
		||||
            className={`flex flex-col w-56 bg-background border ${selected ? 'border-slate-400' : ''} rounded-lg shadow-sm`}
 | 
			
		||||
            className={`flex flex-col w-full bg-background border ${selected ? 'border-slate-400' : ''} rounded-lg shadow-sm`}
 | 
			
		||||
        >
 | 
			
		||||
            <NodeResizer
 | 
			
		||||
                isVisible={focused}
 | 
			
		||||
                lineClassName="!border-none !w-2"
 | 
			
		||||
                minWidth={224}
 | 
			
		||||
                maxWidth={600}
 | 
			
		||||
                shouldResize={(event) => event.dy === 0}
 | 
			
		||||
                handleClassName="!hidden"
 | 
			
		||||
            />
 | 
			
		||||
            <div
 | 
			
		||||
                className="h-2 rounded-t-lg"
 | 
			
		||||
                style={{ backgroundColor: table.color }}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +1,66 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import { Card, CardContent } from '@/components/card/card';
 | 
			
		||||
import { ZoomIn, ZoomOut, Save, Redo, Undo } from 'lucide-react';
 | 
			
		||||
import { ZoomIn, ZoomOut, Save, Redo, Undo, Scan } from 'lucide-react';
 | 
			
		||||
import { Separator } from '@/components/separator/separator';
 | 
			
		||||
import { ToolbarButton } from './toolbar-button';
 | 
			
		||||
import { useHistory } from '@/hooks/use-history';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useOnViewportChange, useReactFlow } from '@xyflow/react';
 | 
			
		||||
 | 
			
		||||
const convertToPercentage = (value: number) => `${Math.round(value * 100)}%`;
 | 
			
		||||
 | 
			
		||||
export interface ToolbarProps {}
 | 
			
		||||
 | 
			
		||||
export const Toolbar: React.FC<ToolbarProps> = () => {
 | 
			
		||||
    const { updateDiagramUpdatedAt } = useChartDB();
 | 
			
		||||
    const { redo, undo, hasRedo, hasUndo } = useHistory();
 | 
			
		||||
    const { getZoom, zoomIn, zoomOut, fitView } = useReactFlow();
 | 
			
		||||
    const [zoom, setZoom] = useState<string>(convertToPercentage(getZoom()));
 | 
			
		||||
    useOnViewportChange({
 | 
			
		||||
        onChange: ({ zoom }) => {
 | 
			
		||||
            setZoom(convertToPercentage(zoom));
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const zoomDuration = 200;
 | 
			
		||||
    const zoomInHandler = () => {
 | 
			
		||||
        zoomIn({ duration: zoomDuration });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const zoomOutHandler = () => {
 | 
			
		||||
        zoomOut({ duration: zoomDuration });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const resetZoom = () => {
 | 
			
		||||
        fitView({
 | 
			
		||||
            minZoom: 1,
 | 
			
		||||
            maxZoom: 1,
 | 
			
		||||
            duration: zoomDuration,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const showAll = () => {
 | 
			
		||||
        fitView({ duration: zoomDuration });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="px-1">
 | 
			
		||||
            <Card className="shadow-none p-0 bg-secondary h-[44px]">
 | 
			
		||||
                <CardContent className="p-1 flex flex-row h-full items-center">
 | 
			
		||||
                    <ToolbarButton>
 | 
			
		||||
                        <ZoomIn />
 | 
			
		||||
                    </ToolbarButton>
 | 
			
		||||
                    <ToolbarButton>
 | 
			
		||||
                        <ZoomOut />
 | 
			
		||||
                    <ToolbarButton onClick={updateDiagramUpdatedAt}>
 | 
			
		||||
                        <Save />
 | 
			
		||||
                    </ToolbarButton>
 | 
			
		||||
                    <Separator orientation="vertical" />
 | 
			
		||||
                    <ToolbarButton>
 | 
			
		||||
                        <Save />
 | 
			
		||||
                    <ToolbarButton onClick={showAll}>
 | 
			
		||||
                        <Scan />
 | 
			
		||||
                    </ToolbarButton>
 | 
			
		||||
                    <Separator orientation="vertical" />
 | 
			
		||||
                    <ToolbarButton onClick={zoomOutHandler}>
 | 
			
		||||
                        <ZoomOut />
 | 
			
		||||
                    </ToolbarButton>
 | 
			
		||||
                    <ToolbarButton onClick={resetZoom}>{zoom}</ToolbarButton>
 | 
			
		||||
                    <ToolbarButton onClick={zoomInHandler}>
 | 
			
		||||
                        <ZoomIn />
 | 
			
		||||
                    </ToolbarButton>
 | 
			
		||||
                    <Separator orientation="vertical" />
 | 
			
		||||
                    <ToolbarButton onClick={undo} disabled={!hasUndo}>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import React, { useCallback, useEffect, useState } from 'react';
 | 
			
		||||
import TimeAgo from 'timeago-react';
 | 
			
		||||
import {
 | 
			
		||||
    Menubar,
 | 
			
		||||
    MenubarContent,
 | 
			
		||||
@@ -13,12 +14,13 @@ import {
 | 
			
		||||
} from '@/components/menubar/menubar';
 | 
			
		||||
import { Label } from '@/components/label/label';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Check, Pencil } from 'lucide-react';
 | 
			
		||||
import { Check, Pencil, Save } from 'lucide-react';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useClickAway, useKeyPressEvent } from 'react-use';
 | 
			
		||||
import ChartDBLogo from '@/assets/logo.png';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { Badge } from '@/components/badge/badge';
 | 
			
		||||
 | 
			
		||||
export interface TopNavbarProps {}
 | 
			
		||||
 | 
			
		||||
@@ -173,7 +175,18 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
 | 
			
		||||
                    )}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="hidden flex-1 justify-end sm:flex"></div>
 | 
			
		||||
            <div className="hidden flex-1 justify-end sm:flex">
 | 
			
		||||
                <Badge variant="secondary" className="flex gap-1">
 | 
			
		||||
                    <Save className="h-4" />
 | 
			
		||||
                    Last saved
 | 
			
		||||
                    <TimeAgo
 | 
			
		||||
                        datetime={currentDiagram.updatedAt}
 | 
			
		||||
                        opts={{
 | 
			
		||||
                            minInterval: 60,
 | 
			
		||||
                        }}
 | 
			
		||||
                    />
 | 
			
		||||
                </Badge>
 | 
			
		||||
            </div>
 | 
			
		||||
        </nav>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user