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