mirror of
https://github.com/chartdb/chartdb.git
synced 2026-01-08 21:00:02 -06:00
redo/undo fix
This commit is contained in:
committed by
Jonathan Fishner
parent
b66758c7ed
commit
b89a63553f
@@ -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;
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -24,3 +24,5 @@ export const getOperatingSystem = (): 'mac' | 'windows' | 'unknown' => {
|
||||
}
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
export const deepCopy = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user