mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-02 04:53:27 +00:00
Compare commits
3 Commits
jf/fix_con
...
jf/prevent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e90887e9ee | ||
|
|
ad1e59bdd2 | ||
|
|
6282a555bb |
@@ -1,6 +1,10 @@
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { Area } from '@/lib/domain/area';
|
||||
import { calcTableHeight } from '@/lib/domain/db-table';
|
||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
import { filterTable } from '@/lib/domain/diagram-filter/filter';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import { DatabaseType } from '@/lib/domain/database-type';
|
||||
|
||||
/**
|
||||
* Check if a table is inside an area based on their positions and dimensions
|
||||
@@ -30,16 +34,54 @@ const isTableInsideArea = (table: DBTable, area: Area): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an area is visible based on its tables
|
||||
*/
|
||||
const isAreaVisible = (
|
||||
area: Area,
|
||||
tables: DBTable[],
|
||||
filter?: DiagramFilter,
|
||||
databaseType?: DatabaseType
|
||||
): boolean => {
|
||||
const tablesInArea = tables.filter((t) => t.parentAreaId === area.id);
|
||||
|
||||
// If area has no tables, consider it visible
|
||||
if (tablesInArea.length === 0) return true;
|
||||
|
||||
// Area is visible if at least one table in it is visible
|
||||
return tablesInArea.some((table) =>
|
||||
filterTable({
|
||||
table: { id: table.id, schema: table.schema },
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[databaseType || DatabaseType.GENERIC],
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find which area contains a table
|
||||
*/
|
||||
const findContainingArea = (table: DBTable, areas: Area[]): Area | null => {
|
||||
const findContainingArea = (
|
||||
table: DBTable,
|
||||
areas: Area[],
|
||||
tables: DBTable[],
|
||||
filter?: DiagramFilter,
|
||||
databaseType?: DatabaseType
|
||||
): Area | null => {
|
||||
// Sort areas by order (if available) to prioritize top-most areas
|
||||
const sortedAreas = [...areas].sort(
|
||||
(a, b) => (b.order ?? 0) - (a.order ?? 0)
|
||||
);
|
||||
|
||||
for (const area of sortedAreas) {
|
||||
// Skip hidden areas - they shouldn't capture tables
|
||||
if (!isAreaVisible(area, tables, filter, databaseType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isTableInsideArea(table, area)) {
|
||||
return area;
|
||||
}
|
||||
@@ -53,10 +95,33 @@ const findContainingArea = (table: DBTable, areas: Area[]): Area | null => {
|
||||
*/
|
||||
export const updateTablesParentAreas = (
|
||||
tables: DBTable[],
|
||||
areas: Area[]
|
||||
areas: Area[],
|
||||
filter?: DiagramFilter,
|
||||
databaseType?: DatabaseType
|
||||
): DBTable[] => {
|
||||
return tables.map((table) => {
|
||||
const containingArea = findContainingArea(table, areas);
|
||||
// Skip hidden tables - they shouldn't be assigned to areas
|
||||
const isTableVisible = filterTable({
|
||||
table: { id: table.id, schema: table.schema },
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[databaseType || DatabaseType.GENERIC],
|
||||
},
|
||||
});
|
||||
|
||||
if (!isTableVisible) {
|
||||
// Hidden tables keep their current parent area (don't change)
|
||||
return table;
|
||||
}
|
||||
|
||||
const containingArea = findContainingArea(
|
||||
table,
|
||||
areas,
|
||||
tables,
|
||||
filter,
|
||||
databaseType
|
||||
);
|
||||
const newParentAreaId = containingArea?.id || null;
|
||||
|
||||
// Only update if parentAreaId has changed
|
||||
@@ -80,3 +145,26 @@ export const getTablesInArea = (
|
||||
): DBTable[] => {
|
||||
return tables.filter((table) => table.parentAreaId === areaId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get visible tables that are inside a specific area
|
||||
*/
|
||||
export const getVisibleTablesInArea = (
|
||||
areaId: string,
|
||||
tables: DBTable[],
|
||||
filter?: DiagramFilter,
|
||||
databaseType?: DatabaseType
|
||||
): DBTable[] => {
|
||||
return tables.filter((table) => {
|
||||
if (table.parentAreaId !== areaId) return false;
|
||||
|
||||
return filterTable({
|
||||
table: { id: table.id, schema: table.schema },
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[databaseType || DatabaseType.GENERIC],
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,8 +5,19 @@ import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { X, Search, Eye, EyeOff, Database, Table, Funnel } from 'lucide-react';
|
||||
import {
|
||||
X,
|
||||
Search,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Database,
|
||||
Table,
|
||||
Funnel,
|
||||
Layers,
|
||||
Box,
|
||||
} from 'lucide-react';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/button/button';
|
||||
import { Input } from '@/components/input/input';
|
||||
@@ -18,14 +29,17 @@ import type { TreeNode } from '@/components/tree-view/tree';
|
||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
||||
import { filterSchema, filterTable } from '@/lib/domain/diagram-filter/filter';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
|
||||
|
||||
export interface CanvasFilterProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type NodeType = 'schema' | 'table';
|
||||
type NodeType = 'schema' | 'area' | 'table';
|
||||
type GroupingMode = 'schema' | 'area';
|
||||
|
||||
type SchemaContext = { name: string; visible: boolean };
|
||||
type AreaContext = { id: string; name: string; visible: boolean };
|
||||
type TableContext = {
|
||||
tableSchema?: string | null;
|
||||
visible: boolean;
|
||||
@@ -33,6 +47,7 @@ type TableContext = {
|
||||
|
||||
type NodeContext = {
|
||||
schema: SchemaContext;
|
||||
area: AreaContext;
|
||||
table: TableContext;
|
||||
};
|
||||
|
||||
@@ -44,7 +59,7 @@ type RelevantTableData = {
|
||||
|
||||
export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
const { tables, databaseType } = useChartDB();
|
||||
const { tables, databaseType, areas } = useChartDB();
|
||||
const {
|
||||
filter,
|
||||
toggleSchemaFilter,
|
||||
@@ -56,6 +71,7 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
||||
const [groupingMode, setGroupingMode] = useState<GroupingMode>('schema');
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Extract only the properties needed for tree data
|
||||
@@ -76,40 +92,99 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
|
||||
// Convert tables to tree nodes
|
||||
const treeData = useMemo(() => {
|
||||
// Group tables by schema
|
||||
const tablesBySchema = new Map<string, RelevantTableData[]>();
|
||||
|
||||
relevantTableData.forEach((table) => {
|
||||
const schema = !databaseWithSchemas
|
||||
? 'All Tables'
|
||||
: (table.schema ?? defaultSchemas[databaseType] ?? 'default');
|
||||
|
||||
if (!tablesBySchema.has(schema)) {
|
||||
tablesBySchema.set(schema, []);
|
||||
}
|
||||
tablesBySchema.get(schema)!.push(table);
|
||||
});
|
||||
|
||||
// Sort tables within each schema
|
||||
tablesBySchema.forEach((tables) => {
|
||||
tables.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
// Convert to tree nodes
|
||||
const nodes: TreeNode<NodeType, NodeContext>[] = [];
|
||||
|
||||
tablesBySchema.forEach((schemaTables, schemaName) => {
|
||||
let schemaVisible;
|
||||
if (groupingMode === 'area') {
|
||||
// Group tables by area
|
||||
const tablesByArea = new Map<string | null, DBTable[]>();
|
||||
const tablesWithoutArea: DBTable[] = [];
|
||||
|
||||
if (databaseWithSchemas) {
|
||||
const schemaId = schemaNameToSchemaId(schemaName);
|
||||
schemaVisible = filterSchema({
|
||||
schemaId,
|
||||
schemaIdsFilter: filter?.schemaIds,
|
||||
});
|
||||
} else {
|
||||
// if at least one table is visible, the schema is considered visible
|
||||
schemaVisible = schemaTables.some((table) =>
|
||||
tables.forEach((table) => {
|
||||
if (table.parentAreaId) {
|
||||
if (!tablesByArea.has(table.parentAreaId)) {
|
||||
tablesByArea.set(table.parentAreaId, []);
|
||||
}
|
||||
tablesByArea.get(table.parentAreaId)!.push(table);
|
||||
} else {
|
||||
tablesWithoutArea.push(table);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort tables within each area
|
||||
tablesByArea.forEach((areaTables) => {
|
||||
areaTables.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
tablesWithoutArea.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Create nodes for areas
|
||||
areas.forEach((area) => {
|
||||
const areaTables = tablesByArea.get(area.id) || [];
|
||||
|
||||
// Check if at least one table in the area is visible
|
||||
const areaVisible =
|
||||
areaTables.length === 0 ||
|
||||
areaTables.some((table) =>
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const areaNode: TreeNode<NodeType, NodeContext> = {
|
||||
id: `area-${area.id}`,
|
||||
name: `${area.name} (${areaTables.length})`,
|
||||
type: 'area',
|
||||
isFolder: true,
|
||||
icon: Box,
|
||||
context: {
|
||||
id: area.id,
|
||||
name: area.name,
|
||||
visible: areaVisible,
|
||||
} as AreaContext,
|
||||
className: !areaVisible ? 'opacity-50' : '',
|
||||
children: areaTables.map(
|
||||
(table): TreeNode<NodeType, NodeContext> => {
|
||||
const tableVisible = filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
type: 'table',
|
||||
isFolder: false,
|
||||
icon: Table,
|
||||
context: {
|
||||
tableSchema: table.schema,
|
||||
visible: tableVisible,
|
||||
} as TableContext,
|
||||
className: !tableVisible ? 'opacity-50' : '',
|
||||
};
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
if (areaTables.length > 0) {
|
||||
nodes.push(areaNode);
|
||||
}
|
||||
});
|
||||
|
||||
// Add ungrouped tables
|
||||
if (tablesWithoutArea.length > 0) {
|
||||
const ungroupedVisible = tablesWithoutArea.some((table) =>
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
@@ -121,19 +196,84 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const schemaNode: TreeNode<NodeType, NodeContext> = {
|
||||
id: `schema-${schemaName}`,
|
||||
name: `${schemaName} (${schemaTables.length})`,
|
||||
type: 'schema',
|
||||
isFolder: true,
|
||||
icon: Database,
|
||||
context: { name: schemaName, visible: schemaVisible },
|
||||
className: !schemaVisible ? 'opacity-50' : '',
|
||||
children: schemaTables.map(
|
||||
(table): TreeNode<NodeType, NodeContext> => {
|
||||
const tableVisible = filterTable({
|
||||
const ungroupedNode: TreeNode<NodeType, NodeContext> = {
|
||||
id: 'ungrouped',
|
||||
name: `Ungrouped (${tablesWithoutArea.length})`,
|
||||
type: 'area',
|
||||
isFolder: true,
|
||||
icon: Layers,
|
||||
context: {
|
||||
id: 'ungrouped',
|
||||
name: 'Ungrouped',
|
||||
visible: ungroupedVisible,
|
||||
} as AreaContext,
|
||||
className: !ungroupedVisible ? 'opacity-50' : '',
|
||||
children: tablesWithoutArea.map(
|
||||
(table): TreeNode<NodeType, NodeContext> => {
|
||||
const tableVisible = filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
type: 'table',
|
||||
isFolder: false,
|
||||
icon: Table,
|
||||
context: {
|
||||
tableSchema: table.schema,
|
||||
visible: tableVisible,
|
||||
} as TableContext,
|
||||
className: !tableVisible ? 'opacity-50' : '',
|
||||
};
|
||||
}
|
||||
),
|
||||
};
|
||||
nodes.push(ungroupedNode);
|
||||
}
|
||||
} else {
|
||||
// Group tables by schema (existing logic)
|
||||
const tablesBySchema = new Map<string, RelevantTableData[]>();
|
||||
|
||||
relevantTableData.forEach((table) => {
|
||||
const schema = !databaseWithSchemas
|
||||
? 'All Tables'
|
||||
: (table.schema ??
|
||||
defaultSchemas[databaseType] ??
|
||||
'default');
|
||||
|
||||
if (!tablesBySchema.has(schema)) {
|
||||
tablesBySchema.set(schema, []);
|
||||
}
|
||||
tablesBySchema.get(schema)!.push(table);
|
||||
});
|
||||
|
||||
// Sort tables within each schema
|
||||
tablesBySchema.forEach((tables) => {
|
||||
tables.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
tablesBySchema.forEach((schemaTables, schemaName) => {
|
||||
let schemaVisible;
|
||||
|
||||
if (databaseWithSchemas) {
|
||||
const schemaId = schemaNameToSchemaId(schemaName);
|
||||
schemaVisible = filterSchema({
|
||||
schemaId,
|
||||
schemaIdsFilter: filter?.schemaIds,
|
||||
});
|
||||
} else {
|
||||
// if at least one table is visible, the schema is considered visible
|
||||
schemaVisible = schemaTables.some((table) =>
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
@@ -142,30 +282,65 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const hidden = !tableVisible;
|
||||
const schemaNode: TreeNode<NodeType, NodeContext> = {
|
||||
id: `schema-${schemaName}`,
|
||||
name: `${schemaName} (${schemaTables.length})`,
|
||||
type: 'schema',
|
||||
isFolder: true,
|
||||
icon: Database,
|
||||
context: {
|
||||
name: schemaName,
|
||||
visible: schemaVisible,
|
||||
} as SchemaContext,
|
||||
className: !schemaVisible ? 'opacity-50' : '',
|
||||
children: schemaTables.map(
|
||||
(table): TreeNode<NodeType, NodeContext> => {
|
||||
const tableVisible = filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
type: 'table',
|
||||
isFolder: false,
|
||||
icon: Table,
|
||||
context: {
|
||||
tableSchema: table.schema,
|
||||
visible: tableVisible,
|
||||
},
|
||||
className: hidden ? 'opacity-50' : '',
|
||||
};
|
||||
}
|
||||
),
|
||||
};
|
||||
nodes.push(schemaNode);
|
||||
});
|
||||
const hidden = !tableVisible;
|
||||
|
||||
return {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
type: 'table',
|
||||
isFolder: false,
|
||||
icon: Table,
|
||||
context: {
|
||||
tableSchema: table.schema,
|
||||
visible: tableVisible,
|
||||
} as TableContext,
|
||||
className: hidden ? 'opacity-50' : '',
|
||||
};
|
||||
}
|
||||
),
|
||||
};
|
||||
nodes.push(schemaNode);
|
||||
});
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}, [relevantTableData, databaseType, filter, databaseWithSchemas]);
|
||||
}, [
|
||||
relevantTableData,
|
||||
tables,
|
||||
databaseType,
|
||||
filter,
|
||||
databaseWithSchemas,
|
||||
groupingMode,
|
||||
areas,
|
||||
]);
|
||||
|
||||
// Initialize expanded state with all schemas expanded
|
||||
useMemo(() => {
|
||||
@@ -240,9 +415,12 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
const renderActions = useCallback(
|
||||
(node: TreeNode<NodeType, NodeContext>) => {
|
||||
if (node.type === 'schema') {
|
||||
const schemaContext = node.context as SchemaContext;
|
||||
const schemaId = schemaNameToSchemaId(schemaContext.name);
|
||||
const schemaVisible = node.context.visible;
|
||||
const context = node.context as SchemaContext;
|
||||
const schemaVisible = context.visible;
|
||||
const schemaName = context.name;
|
||||
if (!schemaName) return null;
|
||||
|
||||
const schemaId = schemaNameToSchemaId(schemaName);
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -256,7 +434,7 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
toggleSchemaFilter(schemaId);
|
||||
} else {
|
||||
// Toggle visibility of all tables in this schema
|
||||
if (node.context.visible) {
|
||||
if (schemaVisible) {
|
||||
setTableIdsFilterEmpty();
|
||||
} else {
|
||||
clearTableIdsFilter();
|
||||
@@ -273,10 +451,83 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === 'area') {
|
||||
const context = node.context as AreaContext;
|
||||
const areaVisible = context.visible;
|
||||
const areaId = context.id;
|
||||
if (!areaId) return null;
|
||||
|
||||
// Get all tables in this area
|
||||
const areaTables =
|
||||
areaId === 'ungrouped'
|
||||
? tables.filter((t) => !t.parentAreaId)
|
||||
: tables.filter((t) => t.parentAreaId === areaId);
|
||||
const tableIds = areaTables.map((t) => t.id);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="size-7 h-fit p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// Toggle all tables in this area
|
||||
if (areaVisible) {
|
||||
// Hide all tables in this area
|
||||
tableIds.forEach((id) => {
|
||||
const isVisible = filterTable({
|
||||
table: {
|
||||
id,
|
||||
schema: tables.find(
|
||||
(t) => t.id === id
|
||||
)?.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
if (isVisible) {
|
||||
toggleTableFilter(id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Show all tables in this area
|
||||
tableIds.forEach((id) => {
|
||||
const isVisible = filterTable({
|
||||
table: {
|
||||
id,
|
||||
schema: tables.find(
|
||||
(t) => t.id === id
|
||||
)?.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
if (!isVisible) {
|
||||
toggleTableFilter(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!areaVisible ? (
|
||||
<EyeOff className="size-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Eye className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === 'table') {
|
||||
const tableId = node.id;
|
||||
const tableContext = node.context as TableContext;
|
||||
const tableVisible = tableContext.visible;
|
||||
const context = node.context as TableContext;
|
||||
const tableVisible = context.visible;
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -305,6 +556,9 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
clearTableIdsFilter,
|
||||
setTableIdsFilterEmpty,
|
||||
databaseWithSchemas,
|
||||
tables,
|
||||
filter,
|
||||
databaseType,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -312,10 +566,10 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
const handleNodeClick = useCallback(
|
||||
(node: TreeNode<NodeType, NodeContext>) => {
|
||||
if (node.type === 'table') {
|
||||
const tableContext = node.context as TableContext;
|
||||
const isTableVisible = tableContext.visible;
|
||||
const context = node.context as TableContext;
|
||||
const isTableVisible = context.visible;
|
||||
|
||||
// Only focus if neither table is hidden nor filtered by schema
|
||||
// Only focus if table is visible
|
||||
if (isTableVisible) {
|
||||
focusOnTable(node.id);
|
||||
}
|
||||
@@ -376,13 +630,34 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grouping Toggle */}
|
||||
<div className="border-b p-2">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={groupingMode}
|
||||
onValueChange={(value) => {
|
||||
if (value) setGroupingMode(value as GroupingMode);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<ToggleGroupItem value="schema" className="flex-1 text-xs">
|
||||
<Database className="mr-1.5 size-3.5" />
|
||||
{t('canvas_filter.group_by_schema', 'Group by Schema')}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="area" className="flex-1 text-xs">
|
||||
<Box className="mr-1.5 size-3.5" />
|
||||
{t('canvas_filter.group_by_area', 'Group by Area')}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
{/* Table Tree */}
|
||||
<ScrollArea className="flex-1 rounded-b-lg" type="auto">
|
||||
<TreeView
|
||||
data={filteredTreeData}
|
||||
onNodeClick={handleNodeClick}
|
||||
renderActionsComponent={renderActions}
|
||||
defaultFolderIcon={Database}
|
||||
defaultFolderIcon={groupingMode === 'area' ? Box : Database}
|
||||
defaultIcon={Table}
|
||||
expanded={expanded}
|
||||
setExpanded={setExpanded}
|
||||
|
||||
@@ -86,7 +86,7 @@ import { useCanvas } from '@/hooks/use-canvas';
|
||||
import type { AreaNodeType } from './area-node/area-node';
|
||||
import { AreaNode } from './area-node/area-node';
|
||||
import type { Area } from '@/lib/domain/area';
|
||||
import { updateTablesParentAreas, getTablesInArea } from './area-utils';
|
||||
import { updateTablesParentAreas, getVisibleTablesInArea } from './area-utils';
|
||||
import { CanvasFilter } from './canvas-filter/canvas-filter';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { ShowAllButton } from './show-all-button';
|
||||
@@ -142,15 +142,40 @@ const tableToTableNode = (
|
||||
};
|
||||
};
|
||||
|
||||
const areaToAreaNode = (area: Area): AreaNodeType => ({
|
||||
id: area.id,
|
||||
type: 'area',
|
||||
position: { x: area.x, y: area.y },
|
||||
data: { area },
|
||||
width: area.width,
|
||||
height: area.height,
|
||||
zIndex: -10,
|
||||
});
|
||||
const areaToAreaNode = (
|
||||
area: Area,
|
||||
tables: DBTable[],
|
||||
filter?: DiagramFilter,
|
||||
databaseType?: DatabaseType
|
||||
): AreaNodeType => {
|
||||
// Get all tables in this area
|
||||
const tablesInArea = tables.filter((t) => t.parentAreaId === area.id);
|
||||
|
||||
// Check if at least one table in the area is visible
|
||||
const hasVisibleTable =
|
||||
tablesInArea.length === 0 ||
|
||||
tablesInArea.some((table) =>
|
||||
filterTable({
|
||||
table: { id: table.id, schema: table.schema },
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[databaseType || DatabaseType.GENERIC],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
id: area.id,
|
||||
type: 'area',
|
||||
position: { x: area.x, y: area.y },
|
||||
data: { area },
|
||||
width: area.width,
|
||||
height: area.height,
|
||||
zIndex: -10,
|
||||
hidden: !hasVisibleTable,
|
||||
};
|
||||
};
|
||||
|
||||
export interface CanvasProps {
|
||||
initialTables: DBTable[];
|
||||
@@ -409,7 +434,9 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
},
|
||||
};
|
||||
}),
|
||||
...areas.map(areaToAreaNode),
|
||||
...areas.map((area) =>
|
||||
areaToAreaNode(area, tables, filter, databaseType)
|
||||
),
|
||||
];
|
||||
|
||||
// Check if nodes actually changed
|
||||
@@ -468,7 +495,12 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const checkParentAreas = debounce(() => {
|
||||
const updatedTables = updateTablesParentAreas(tables, areas);
|
||||
const updatedTables = updateTablesParentAreas(
|
||||
tables,
|
||||
areas,
|
||||
filter,
|
||||
databaseType
|
||||
);
|
||||
const needsUpdate: Array<{
|
||||
id: string;
|
||||
parentAreaId: string | null;
|
||||
@@ -509,7 +541,14 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
}, 300);
|
||||
|
||||
checkParentAreas();
|
||||
}, [tablePositions, areas, updateTablesState, tables]);
|
||||
}, [
|
||||
tablePositions,
|
||||
areas,
|
||||
updateTablesState,
|
||||
tables,
|
||||
filter,
|
||||
databaseType,
|
||||
]);
|
||||
|
||||
const onConnectHandler = useCallback(
|
||||
async (params: AddEdgeParams) => {
|
||||
@@ -888,16 +927,37 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
const deltaX = change.position.x - currentArea.x;
|
||||
const deltaY = change.position.y - currentArea.y;
|
||||
|
||||
const childTables = getTablesInArea(
|
||||
// Only move visible child tables
|
||||
const childTables = getVisibleTablesInArea(
|
||||
change.id,
|
||||
tables
|
||||
tables,
|
||||
filter,
|
||||
databaseType
|
||||
);
|
||||
|
||||
// Update child table positions in storage
|
||||
if (childTables.length > 0) {
|
||||
updateTablesState((currentTables) =>
|
||||
currentTables.map((table) => {
|
||||
if (table.parentAreaId === change.id) {
|
||||
// Only move visible tables that are in this area
|
||||
const isVisible = filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[
|
||||
databaseType
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
table.parentAreaId === change.id &&
|
||||
isVisible
|
||||
) {
|
||||
return {
|
||||
id: table.id,
|
||||
x: table.x + deltaX,
|
||||
@@ -961,6 +1021,8 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
tables,
|
||||
areas,
|
||||
getNode,
|
||||
databaseType,
|
||||
filter,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user