redo/undo fix

This commit is contained in:
Guy Ben-Aharon
2024-09-09 16:23:47 +03:00
committed by Jonathan Fishner
parent b66758c7ed
commit b89a63553f
11 changed files with 102 additions and 168 deletions

View File

@@ -29,14 +29,17 @@ const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-[80%] max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:w-full sm:rounded-lg',
className
)}
{...props}
/>
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;

View File

@@ -14,7 +14,7 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed bottom-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 right-0 sm:top-auto sm:flex-col max-w-[360px]',
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className
)}
{...props}

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useMemo, useState } from 'react';
import { DBTable } from '@/lib/domain/db-table';
import { generateId } from '@/lib/utils';
import { deepCopy, generateId } from '@/lib/utils';
import { randomColor } from '@/lib/colors';
import { ChartDBContext, chartDBContext } from './chartdb-context';
import { DatabaseType } from '@/lib/domain/database-type';
@@ -380,8 +380,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
);
};
const prevTables = [...tables];
// const updatedTablesAttrs = updateFn(tables);
const prevTables = deepCopy(tables);
const updatedTables = updateTables(tables);
setTables(updateTables);

View File

@@ -223,14 +223,6 @@ export const en = {
one_to_many: 'One to Many',
many_to_one: 'Many to One',
},
toast: {
reorder: {
title: 'Tables reordered',
description: 'Click undo to revert changes',
undo: 'Undo',
},
},
},
};

View File

@@ -1,4 +1,5 @@
import { ForeignKeyInfo } from '../data/import-metadata/metadata-types/foreign-key-info';
import { schemaNameToSchemaId } from './db-schema';
import { DBTable } from './db-table';
import { generateId } from '@/lib/utils';
@@ -17,6 +18,20 @@ export interface DBRelationship {
export type RelationshipType = 'one_to_one' | 'one_to_many' | 'many_to_one';
export const shouldShowRelationshipBySchemaFilter = (
relationship: DBRelationship,
filteredSchemas?: string[]
): boolean =>
!filteredSchemas ||
!relationship.sourceSchema ||
!relationship.targetSchema ||
(filteredSchemas.includes(
schemaNameToSchemaId(relationship.sourceSchema)
) &&
filteredSchemas.includes(
schemaNameToSchemaId(relationship.targetSchema)
));
export const createRelationshipsFromMetadata = ({
foreignKeys,
tables,

View File

@@ -7,7 +7,8 @@ import { greyColor, randomColor } from '@/lib/colors';
import { DBRelationship } from './db-relationship';
import { PrimaryKeyInfo } from '../data/import-metadata/metadata-types/primary-key-info';
import { ViewInfo } from '../data/import-metadata/metadata-types/view-info';
import { generateId } from '../utils';
import { deepCopy, generateId } from '../utils';
import { schemaNameToSchemaId } from './db-schema';
export interface DBTable {
id: string;
@@ -25,6 +26,14 @@ export interface DBTable {
hidden?: boolean;
}
export const shouldShowTablesBySchemaFilter = (
table: DBTable,
filteredSchemas?: string[]
): boolean =>
!filteredSchemas ||
!table.schema ||
filteredSchemas.includes(schemaNameToSchemaId(table.schema));
export const createTablesFromMetadata = ({
tableInfos,
columns,
@@ -139,25 +148,20 @@ export const createTablesFromMetadata = ({
};
export const adjustTablePositions = ({
relationships,
tables,
filteredSchemas,
relationships: inputRelationships,
tables: inputTables,
}: {
tables: DBTable[];
relationships: DBRelationship[];
filteredSchemas?: string[];
}): DBTable[] => {
const filteredTables = filteredSchemas
? tables.filter(
(table) => !table.schema || filteredSchemas.includes(table.schema)
)
: tables;
const tables = deepCopy(inputTables);
const relationships = deepCopy(inputRelationships);
// Filter relationships to only include those between filtered tables
const filteredRelationships = relationships.filter(
(rel) =>
filteredTables.some((t) => t.id === rel.sourceTableId) &&
filteredTables.some((t) => t.id === rel.targetTableId)
tables.some((t) => t.id === rel.sourceTableId) &&
tables.some((t) => t.id === rel.targetTableId)
);
const tableWidth = 200;
@@ -181,7 +185,7 @@ export const adjustTablePositions = ({
});
// Sort tables by number of connections
const sortedTables = [...filteredTables].sort(
const sortedTables = [...tables].sort(
(a, b) =>
(tableConnections.get(b.id)?.size || 0) -
(tableConnections.get(a.id)?.size || 0)
@@ -256,7 +260,7 @@ export const adjustTablePositions = ({
connectedTables.forEach((connectedTableId) => {
if (!positionedTables.has(connectedTableId)) {
const connectedTable = filteredTables.find(
const connectedTable = tables.find(
(t) => t.id === connectedTableId
);
if (connectedTable) {
@@ -281,7 +285,7 @@ export const adjustTablePositions = ({
});
// Apply positions to filtered tables
filteredTables.forEach((table) => {
tables.forEach((table) => {
const position = tablePositions.get(table.id);
if (position) {
table.x = position.x;

View File

@@ -24,3 +24,5 @@ export const getOperatingSystem = (): 'mac' | 'windows' | 'unknown' => {
}
return 'unknown';
};
export const deepCopy = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));

View File

@@ -30,9 +30,12 @@ import { useBreakpoint } from '@/hooks/use-breakpoint';
import { Badge } from '@/components/badge/badge';
import { useTheme } from '@/hooks/use-theme';
import { useTranslation } from 'react-i18next';
import { DBTable, adjustTablePositions } from '@/lib/domain/db-table';
import {
DBTable,
adjustTablePositions,
shouldShowTablesBySchemaFilter,
} from '@/lib/domain/db-table';
import { useLocalConfig } from '@/hooks/use-local-config';
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
import {
Tooltip,
TooltipTrigger,
@@ -55,10 +58,7 @@ const tableToTableNode = (
table,
},
width: table.width ?? MIN_TABLE_SIZE,
hidden:
!!table.schema &&
!!filteredSchemas &&
!filteredSchemas.includes(schemaNameToSchemaId(table.schema)),
hidden: !shouldShowTablesBySchemaFilter(table, filteredSchemas),
});
export interface CanvasProps {
@@ -285,80 +285,35 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const isLoadingDOM =
tables.length > 0 ? !getInternalNode(tables[0].id) : false;
const reorderTables = useCallback(() => {
const newTables = adjustTablePositions({
relationships,
tables: tables.filter((table) =>
shouldShowTablesBySchemaFilter(table, filteredSchemas)
),
});
updateTablesState((currentTables) =>
currentTables.map((table) => {
const newTable = newTables.find((t) => t.id === table.id);
return {
id: table.id,
x: newTable?.x ?? table.x,
y: newTable?.y ?? table.y,
};
})
);
}, [filteredSchemas, relationships, tables, updateTablesState]);
const showReorderConfirmation = useCallback(() => {
showAlert({
title: t('reorder_diagram_alert.title'),
description: t('reorder_diagram_alert.description'),
actionLabel: t('reorder_diagram_alert.reorder'),
closeLabel: t('reorder_diagram_alert.cancel'),
onAction: () => {
const originalTables = tables.map((table) => ({ ...table }));
const newTables = adjustTablePositions({
relationships,
tables,
filteredSchemas,
});
updateTablesState(() => newTables, {
updateHistory: false,
forceOverride: true,
});
setNodes(
newTables.map((table) =>
tableToTableNode(table, filteredSchemas)
)
);
fitView({ padding: 0.2, duration: 1000 });
const { dismiss } = toast({
title: t('toast.reorder.title'),
description: t('toast.reorder.description'),
duration: 15000,
action: (
<Button
variant="outline"
size="sm"
onClick={() => {
updateTablesState(() => originalTables, {
updateHistory: false,
forceOverride: true,
});
setNodes(
originalTables.map((table) =>
tableToTableNode(table, filteredSchemas)
)
);
setTimeout(
() =>
fitView({
padding: 0.2,
duration: 1000,
}),
0
);
dismiss();
}}
>
{t('toast.reorder.undo')}
</Button>
),
});
},
onAction: reorderTables,
});
}, [
showAlert,
t,
relationships,
tables,
updateTablesState,
setNodes,
fitView,
toast,
filteredSchemas,
]);
const reorderTables = useCallback(() => {
showReorderConfirmation();
}, [showReorderConfirmation]);
}, [t, showAlert, reorderTables]);
return (
<div className="flex h-full">
@@ -397,7 +352,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
<Button
variant="secondary"
className="size-8 p-1 shadow-none"
onClick={reorderTables}
onClick={showReorderConfirmation}
>
<LayoutGrid className="size-4" />
</Button>

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback } from 'react';
import { NodeProps, Node, NodeResizer, useStore } from '@xyflow/react';
import { Button } from '@/components/button/button';
import {
@@ -34,7 +34,6 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = ({
const { updateTable } = useChartDB();
const edges = useStore((store) => store.edges) as TableEdgeType[];
const { openTableFromSidebar, selectSidebarSection } = useLayout();
const [isExpanded, setIsExpanded] = useState(false);
const selectedEdges = edges.filter(
(edge) => (edge.source === id || edge.target === id) && edge.selected
@@ -62,20 +61,9 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = ({
});
}, [table.id, updateTable]);
const toggleExpand = () => {
setIsExpanded(!isExpanded);
};
const visibleFields = isExpanded ? table.fields : table.fields.slice(0, 10);
const hiddenFieldsCount = table.fields.length - 10;
return (
<div
className={`flex w-full flex-col border-2 bg-slate-50 dark:bg-slate-950 ${
selected
? 'border-pink-600'
: 'border-slate-500 dark:border-slate-700'
} rounded-lg shadow-sm`}
className={`flex w-full flex-col border-2 bg-slate-50 dark:bg-slate-950 ${selected ? 'border-pink-600' : 'border-slate-500 dark:border-slate-700'} rounded-lg shadow-sm`}
onClick={(e) => {
if (e.detail === 2) {
openTableInEditor();
@@ -126,41 +114,20 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = ({
</Button>
</div>
</div>
<div className="flex flex-col">
{visibleFields.map((field) => (
<TableNodeField
key={field.id}
tableNodeId={id}
field={field}
focused={focused}
highlighted={selectedEdges.some(
(edge) =>
edge.sourceHandle?.endsWith(field.id) ||
edge.targetHandle?.endsWith(field.id)
)}
/>
))}
{!isExpanded && hiddenFieldsCount > 0 && (
<div
className="flex h-8 cursor-pointer items-center justify-center bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
onClick={toggleExpand}
>
<span className="text-sm text-slate-600 dark:text-slate-400">
... {hiddenFieldsCount} more fields
</span>
</div>
)}
{isExpanded && (
<div
className="flex h-8 cursor-pointer items-center justify-center bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
onClick={toggleExpand}
>
<span className="text-sm text-slate-600 dark:text-slate-400">
Show less
</span>
</div>
)}
</div>
{table.fields.map((field) => (
<TableNodeField
key={field.id}
focused={focused}
tableNodeId={id}
field={field}
highlighted={selectedEdges.some(
(edge) =>
edge.data?.relationship.sourceFieldId ===
field.id ||
edge.data?.relationship.targetFieldId === field.id
)}
/>
))}
</div>
);
};

View File

@@ -4,7 +4,10 @@ import { ListCollapse } from 'lucide-react';
import { Input } from '@/components/input/input';
import { RelationshipList } from './relationship-list/relationship-list';
import { useChartDB } from '@/hooks/use-chartdb';
import { DBRelationship } from '@/lib/domain/db-relationship';
import {
DBRelationship,
shouldShowRelationshipBySchemaFilter,
} from '@/lib/domain/db-relationship';
import { useLayout } from '@/hooks/use-layout';
import { EmptyState } from '@/components/empty-state/empty-state';
import { ScrollArea } from '@/components/scroll-area/scroll-area';
@@ -33,11 +36,7 @@ export const RelationshipsSection: React.FC<RelationshipsSectionProps> = () => {
const filterSchema: (relationship: DBRelationship) => boolean = (
relationship
) =>
!filteredSchemas ||
!relationship.sourceSchema ||
!relationship.targetSchema ||
(filteredSchemas.includes(relationship.sourceSchema) &&
filteredSchemas.includes(relationship.targetSchema));
shouldShowRelationshipBySchemaFilter(relationship, filteredSchemas);
return relationships.filter(filterSchema).filter(filterName);
}, [relationships, filterText, filteredSchemas]);

View File

@@ -4,7 +4,7 @@ import { Button } from '@/components/button/button';
import { Table, ListCollapse } from 'lucide-react';
import { Input } from '@/components/input/input';
import { DBTable } from '@/lib/domain/db-table';
import { DBTable, shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
import { useChartDB } from '@/hooks/use-chartdb';
import { useLayout } from '@/hooks/use-layout';
import { EmptyState } from '@/components/empty-state/empty-state';
@@ -30,9 +30,7 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
table.name.toLowerCase().includes(filterText.toLowerCase());
const filterSchema: (table: DBTable) => boolean = (table) =>
!filteredSchemas ||
!table.schema ||
filteredSchemas.includes(table.schema);
shouldShowTablesBySchemaFilter(table, filteredSchemas);
return tables.filter(filterSchema).filter(filterTableName);
}, [tables, filterText, filteredSchemas]);