diff --git a/.gitignore b/.gitignore index a547bf36..3b0b4037 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f23efeab..68d804f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 59e83fd9..fd38e8d5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/badge/badge-variants.tsx b/src/components/badge/badge-variants.tsx new file mode 100644 index 00000000..ddfbb3a9 --- /dev/null +++ b/src/components/badge/badge-variants.tsx @@ -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', + }, + } +); diff --git a/src/components/badge/badge.tsx b/src/components/badge/badge.tsx new file mode 100644 index 00000000..b34a5e72 --- /dev/null +++ b/src/components/badge/badge.tsx @@ -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, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge }; diff --git a/src/context/chartdb-context/chartdb-context.tsx b/src/context/chartdb-context/chartdb-context.tsx index 379522b3..dd358f8c 100644 --- a/src/context/chartdb-context/chartdb-context.tsx +++ b/src/context/chartdb-context/chartdb-context.tsx @@ -22,6 +22,7 @@ export interface ChartDBContext { options?: { updateHistory: boolean } ) => Promise; loadDiagram: (diagramId: string) => Promise; + updateDiagramUpdatedAt: () => Promise; // Database type operations updateDatabaseType: (databaseType: DatabaseType) => Promise; @@ -135,6 +136,7 @@ export const chartDBContext = createContext({ // General operations updateDiagramId: emptyFn, updateDiagramName: emptyFn, + updateDiagramUpdatedAt: emptyFn, loadDiagram: emptyFn, // Database type operations diff --git a/src/context/chartdb-context/chartdb-provider.tsx b/src/context/chartdb-context/chartdb-provider.tsx index ed84f833..f491daae 100644 --- a/src/context/chartdb-context/chartdb-provider.tsx +++ b/src/context/chartdb-context/chartdb-provider.tsx @@ -46,6 +46,16 @@ export const ChartDBProvider: React.FC = ({ ] ); + 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 = ({ updateDiagramName, loadDiagram, updateDatabaseType, + updateDiagramUpdatedAt, createTable, addTable, getTable, removeTable, updateTable, - // updateTables, updateTablesState, updateField, removeField, diff --git a/src/context/storage-context/storage-provider.tsx b/src/context/storage-context/storage-provider.tsx index f490b14a..e323f9d5 100644 --- a/src/context/storage-context/storage-provider.tsx +++ b/src/context/storage-context/storage-provider.tsx @@ -32,7 +32,7 @@ export const StorageProvider: React.FC = ({ 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', diff --git a/src/lib/domain/db-table.ts b/src/lib/domain/db-table.ts index d027d030..39fe98c4 100644 --- a/src/lib/domain/db-table.ts +++ b/src/lib/domain/db-table.ts @@ -18,6 +18,7 @@ export interface DBTable { color: string; isView: boolean; createdAt: number; + width?: number; } export const createTablesFromMetadata = ({ diff --git a/src/pages/editor-page/canvas/canvas.tsx b/src/pages/editor-page/canvas/canvas.tsx index 16d7a612..e9e37083 100644 --- a/src/pages/editor-page/canvas/canvas.tsx +++ b/src/pages/editor-page/canvas/canvas.tsx @@ -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 = () => { data: { table, }, + width: table.width ?? 224, })) ); }, [tables, setNodes]); @@ -112,9 +114,15 @@ export const Canvas: React.FC = () => { (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 = () => { (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 = () => { 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={{ diff --git a/src/pages/editor-page/canvas/table-node.tsx b/src/pages/editor-page/canvas/table-node.tsx index 280dbef3..1cfbce14 100644 --- a/src/pages/editor-page/canvas/table-node.tsx +++ b/src/pages/editor-page/canvas/table-node.tsx @@ -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> = ({ return (
+ event.dy === 0} + handleClassName="!hidden" + />
`${Math.round(value * 100)}%`; export interface ToolbarProps {} export const Toolbar: React.FC = () => { + const { updateDiagramUpdatedAt } = useChartDB(); const { redo, undo, hasRedo, hasUndo } = useHistory(); + const { getZoom, zoomIn, zoomOut, fitView } = useReactFlow(); + const [zoom, setZoom] = useState(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 (
- - - - - + + - - + + + + + + + + {zoom} + + diff --git a/src/pages/editor-page/top-navbar/top-navbar.tsx b/src/pages/editor-page/top-navbar/top-navbar.tsx index bf533394..1da41954 100644 --- a/src/pages/editor-page/top-navbar/top-navbar.tsx +++ b/src/pages/editor-page/top-navbar/top-navbar.tsx @@ -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 = () => { )}
-
+
+ + + Last saved + + +
); };