zoom in, out, percentage, save & width nodes

This commit is contained in:
Guy Ben-Aharon
2024-08-22 18:36:10 +03:00
parent 05bd822627
commit e4ee415e6d
13 changed files with 183 additions and 22 deletions

2
.gitignore vendored
View File

@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.env

21
package-lock.json generated
View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ export interface DBTable {
color: string;
isView: boolean;
createdAt: number;
width?: number;
}
export const createTablesFromMetadata = ({

View File

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

View File

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

View File

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

View File

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