mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-02 13:03:17 +00:00
Compare commits
3 Commits
jf/fix_add
...
jf/prevent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e90887e9ee | ||
|
|
ad1e59bdd2 | ||
|
|
6282a555bb |
@@ -11,40 +11,21 @@ const PopoverAnchor = PopoverPrimitive.Anchor;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
|
||||
container?: HTMLElement | null;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, align = 'center', sideOffset = 4, container, ...props },
|
||||
ref
|
||||
) => {
|
||||
const Content = (
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
// If container is explicitly null, don't use Portal
|
||||
if (container === null) {
|
||||
return Content;
|
||||
}
|
||||
|
||||
// Otherwise, use Portal (default behavior)
|
||||
return (
|
||||
<PopoverPrimitive.Portal container={container}>
|
||||
{Content}
|
||||
</PopoverPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
);
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||
|
||||
@@ -94,10 +94,6 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
setOpen?.(isOpen);
|
||||
setIsOpen(isOpen);
|
||||
|
||||
if (isOpen) {
|
||||
setSearchTerm('');
|
||||
}
|
||||
|
||||
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
|
||||
},
|
||||
[setOpen]
|
||||
@@ -229,7 +225,6 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
<CommandItem
|
||||
className="flex items-center"
|
||||
key={option.value}
|
||||
value={option.label}
|
||||
keywords={option.regex ? [option.regex] : undefined}
|
||||
onSelect={() =>
|
||||
handleSelect(
|
||||
@@ -281,7 +276,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={onOpenChange}>
|
||||
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
|
||||
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -355,7 +350,6 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
popoverClassName
|
||||
)}
|
||||
align="center"
|
||||
container={null}
|
||||
>
|
||||
<Command
|
||||
filter={(value, search, keywords) => {
|
||||
@@ -423,30 +417,25 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
<CommandEmpty>
|
||||
{emptyPlaceholder ?? 'No results found.'}
|
||||
</CommandEmpty>
|
||||
|
||||
<ScrollArea>
|
||||
<div className="max-h-64">
|
||||
{hasGroups ? (
|
||||
Object.entries(groups).map(
|
||||
([groupName, groupOptions]) => (
|
||||
<CommandGroup
|
||||
key={groupName}
|
||||
heading={groupName}
|
||||
>
|
||||
<CommandList>
|
||||
{groupOptions.map(
|
||||
renderOption
|
||||
)}
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<CommandGroup>
|
||||
<CommandList>
|
||||
{options.map(renderOption)}
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
)}
|
||||
<div className="max-h-64 w-full">
|
||||
<CommandList className="max-h-fit w-full">
|
||||
{hasGroups
|
||||
? Object.entries(groups).map(
|
||||
([groupName, groupOptions]) => (
|
||||
<CommandGroup
|
||||
key={groupName}
|
||||
heading={groupName}
|
||||
>
|
||||
{groupOptions.map(
|
||||
renderOption
|
||||
)}
|
||||
</CommandGroup>
|
||||
)
|
||||
)
|
||||
: options.map(renderOption)}
|
||||
</CommandList>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { DBSchema } from '@/lib/domain';
|
||||
import type {
|
||||
DiagramFilter,
|
||||
FilterTableInfo,
|
||||
} from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
import { emptyFn } from '@/lib/utils';
|
||||
import { createContext } from 'react';
|
||||
|
||||
@@ -12,37 +9,40 @@ export interface DiagramFilterContext {
|
||||
hasActiveFilter: boolean;
|
||||
schemasDisplayed: DBSchema[];
|
||||
|
||||
// schemas
|
||||
schemaIdsFilter?: string[];
|
||||
addSchemaIdsFilter: (...ids: string[]) => void;
|
||||
removeSchemaIdsFilter: (...ids: string[]) => void;
|
||||
clearSchemaIdsFilter: () => void;
|
||||
clearTableIdsFilter: () => void;
|
||||
|
||||
// tables
|
||||
tableIdsFilter?: string[];
|
||||
addTableIdsFilter: (...ids: string[]) => void;
|
||||
removeTableIdsFilter: (...ids: string[]) => void;
|
||||
clearTableIdsFilter: () => void;
|
||||
setTableIdsFilterEmpty: () => void;
|
||||
|
||||
// reset
|
||||
resetFilter: () => void;
|
||||
|
||||
// smart filters
|
||||
toggleSchemaFilter: (schemaId: string) => void;
|
||||
toggleTableFilter: (tableId: string) => void;
|
||||
addSchemaToFilter: (schemaId: string) => void;
|
||||
addTablesToFilter: (attrs: {
|
||||
tableIds?: string[];
|
||||
filterCallback?: (table: FilterTableInfo) => boolean;
|
||||
}) => void;
|
||||
removeTablesFromFilter: (attrs: {
|
||||
tableIds?: string[];
|
||||
filterCallback?: (table: FilterTableInfo) => boolean;
|
||||
}) => void;
|
||||
addSchemaIfFiltered: (schemaId: string) => void;
|
||||
}
|
||||
|
||||
export const diagramFilterContext = createContext<DiagramFilterContext>({
|
||||
hasActiveFilter: false,
|
||||
addSchemaIdsFilter: emptyFn,
|
||||
addTableIdsFilter: emptyFn,
|
||||
clearSchemaIdsFilter: emptyFn,
|
||||
clearTableIdsFilter: emptyFn,
|
||||
setTableIdsFilterEmpty: emptyFn,
|
||||
removeSchemaIdsFilter: emptyFn,
|
||||
removeTableIdsFilter: emptyFn,
|
||||
resetFilter: emptyFn,
|
||||
toggleSchemaFilter: emptyFn,
|
||||
toggleTableFilter: emptyFn,
|
||||
addSchemaToFilter: emptyFn,
|
||||
addSchemaIfFiltered: emptyFn,
|
||||
schemasDisplayed: [],
|
||||
addTablesToFilter: emptyFn,
|
||||
removeTablesFromFilter: emptyFn,
|
||||
});
|
||||
|
||||
@@ -7,25 +7,18 @@ import React, {
|
||||
} from 'react';
|
||||
import type { DiagramFilterContext } from './diagram-filter-context';
|
||||
import { diagramFilterContext } from './diagram-filter-context';
|
||||
import type {
|
||||
DiagramFilter,
|
||||
FilterTableInfo,
|
||||
} from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
import {
|
||||
reduceFilter,
|
||||
spreadFilterTables,
|
||||
} from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
import { reduceFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
import { useStorage } from '@/hooks/use-storage';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { filterTable } from '@/lib/domain/diagram-filter/filter';
|
||||
import { filterSchema, filterTable } from '@/lib/domain/diagram-filter/filter';
|
||||
import { schemaNameToSchemaId } from '@/lib/domain';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import type { ChartDBEvent } from '../chartdb-context/chartdb-context';
|
||||
|
||||
export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { diagramId, tables, schemas, databaseType, events } = useChartDB();
|
||||
const { diagramId, tables, schemas, databaseType } = useChartDB();
|
||||
const { getDiagramFilter, updateDiagramFilter } = useStorage();
|
||||
const [filter, setFilter] = useState<DiagramFilter>({});
|
||||
|
||||
@@ -33,18 +26,14 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
||||
return schemas.map((schema) => schema.id);
|
||||
}, [schemas]);
|
||||
|
||||
const allTables: FilterTableInfo[] = useMemo(() => {
|
||||
return tables.map(
|
||||
(table) =>
|
||||
({
|
||||
id: table.id,
|
||||
schemaId: table.schema
|
||||
? schemaNameToSchemaId(table.schema)
|
||||
: defaultSchemas[databaseType],
|
||||
schema: table.schema ?? defaultSchemas[databaseType],
|
||||
areaId: table.parentAreaId ?? undefined,
|
||||
}) satisfies FilterTableInfo
|
||||
);
|
||||
const allTables = useMemo(() => {
|
||||
return tables.map((table) => ({
|
||||
id: table.id,
|
||||
schemaId: table.schema
|
||||
? schemaNameToSchemaId(table.schema)
|
||||
: defaultSchemas[databaseType],
|
||||
schema: table.schema,
|
||||
}));
|
||||
}, [tables, databaseType]);
|
||||
|
||||
const diagramIdOfLoadedFilter = useRef<string | null>(null);
|
||||
@@ -77,6 +66,33 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
||||
}
|
||||
}, [diagramId, getDiagramFilter]);
|
||||
|
||||
// Schema methods
|
||||
const addSchemaIds: DiagramFilterContext['addSchemaIdsFilter'] =
|
||||
useCallback((...ids: string[]) => {
|
||||
setFilter(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
schemaIds: [
|
||||
...new Set([...(prev.schemaIds || []), ...ids]),
|
||||
],
|
||||
}) satisfies DiagramFilter
|
||||
);
|
||||
}, []);
|
||||
|
||||
const removeSchemaIds: DiagramFilterContext['removeSchemaIdsFilter'] =
|
||||
useCallback((...ids: string[]) => {
|
||||
setFilter(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
schemaIds: prev.schemaIds?.filter(
|
||||
(id) => !ids.includes(id)
|
||||
),
|
||||
}) satisfies DiagramFilter
|
||||
);
|
||||
}, []);
|
||||
|
||||
const clearSchemaIds: DiagramFilterContext['clearSchemaIdsFilter'] =
|
||||
useCallback(() => {
|
||||
setFilter(
|
||||
@@ -88,6 +104,35 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Table methods
|
||||
const addTableIds: DiagramFilterContext['addTableIdsFilter'] = useCallback(
|
||||
(...ids: string[]) => {
|
||||
setFilter(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
tableIds: [
|
||||
...new Set([...(prev.tableIds || []), ...ids]),
|
||||
],
|
||||
}) satisfies DiagramFilter
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const removeTableIds: DiagramFilterContext['removeTableIdsFilter'] =
|
||||
useCallback((...ids: string[]) => {
|
||||
setFilter(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
tableIds: prev.tableIds?.filter(
|
||||
(id) => !ids.includes(id)
|
||||
),
|
||||
}) satisfies DiagramFilter
|
||||
);
|
||||
}, []);
|
||||
|
||||
const clearTableIds: DiagramFilterContext['clearTableIdsFilter'] =
|
||||
useCallback(() => {
|
||||
setFilter(
|
||||
@@ -122,20 +167,10 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const currentSchemaIds = prev.schemaIds;
|
||||
|
||||
// Check if schema is currently visible
|
||||
const isSchemaVisible = !allTables.some(
|
||||
(table) =>
|
||||
table.schemaId === schemaId &&
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter: prev,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
}) === false
|
||||
);
|
||||
const isSchemaVisible = filterSchema({
|
||||
schemaId,
|
||||
schemaIdsFilter: currentSchemaIds,
|
||||
});
|
||||
|
||||
let newSchemaIds: string[] | undefined;
|
||||
let newTableIds: string[] | undefined = prev.tableIds;
|
||||
@@ -189,11 +224,11 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
||||
schemaIds: newSchemaIds,
|
||||
tableIds: newTableIds,
|
||||
},
|
||||
allTables satisfies FilterTableInfo[]
|
||||
allTables
|
||||
);
|
||||
});
|
||||
},
|
||||
[allSchemasIds, allTables, databaseType]
|
||||
[allSchemasIds, allTables]
|
||||
);
|
||||
|
||||
const toggleTableFilterForNoSchema = useCallback(
|
||||
@@ -236,7 +271,7 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
||||
schemaIds: undefined,
|
||||
tableIds: newTableIds,
|
||||
},
|
||||
allTables satisfies FilterTableInfo[]
|
||||
allTables
|
||||
);
|
||||
});
|
||||
},
|
||||
@@ -324,14 +359,14 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
||||
schemaIds: newSchemaIds,
|
||||
tableIds: newTableIds,
|
||||
},
|
||||
allTables satisfies FilterTableInfo[]
|
||||
allTables
|
||||
);
|
||||
});
|
||||
},
|
||||
[allTables, databaseType, toggleTableFilterForNoSchema]
|
||||
);
|
||||
|
||||
const addSchemaToFilter: DiagramFilterContext['addSchemaToFilter'] =
|
||||
const addSchemaIfFiltered: DiagramFilterContext['addSchemaIfFiltered'] =
|
||||
useCallback(
|
||||
(schemaId: string) => {
|
||||
setFilter((prev) => {
|
||||
@@ -371,147 +406,32 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
||||
|
||||
const schemasDisplayed: DiagramFilterContext['schemasDisplayed'] =
|
||||
useMemo(() => {
|
||||
if (!hasActiveFilter) {
|
||||
if (!filter.schemaIds) {
|
||||
return schemas;
|
||||
}
|
||||
|
||||
const displayedSchemaIds = new Set<string>();
|
||||
for (const table of allTables) {
|
||||
if (
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
})
|
||||
) {
|
||||
if (table.schemaId) {
|
||||
displayedSchemaIds.add(table.schemaId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return schemas.filter((schema) =>
|
||||
displayedSchemaIds.has(schema.id)
|
||||
filter.schemaIds?.includes(schema.id)
|
||||
);
|
||||
}, [hasActiveFilter, schemas, allTables, filter, databaseType]);
|
||||
|
||||
const addTablesToFilter: DiagramFilterContext['addTablesToFilter'] =
|
||||
useCallback(
|
||||
({ tableIds, filterCallback }) => {
|
||||
setFilter((prev) => {
|
||||
let tableIdsToAdd: string[];
|
||||
|
||||
if (tableIds) {
|
||||
// If tableIds are provided, use them directly
|
||||
tableIdsToAdd = tableIds;
|
||||
} else if (filterCallback) {
|
||||
// If filterCallback is provided, filter tables based on it
|
||||
tableIdsToAdd = allTables
|
||||
.filter(filterCallback)
|
||||
.map((table) => table.id);
|
||||
} else {
|
||||
// If neither is provided, do nothing
|
||||
return prev;
|
||||
}
|
||||
|
||||
const filterByTableIds = spreadFilterTables(
|
||||
prev,
|
||||
allTables satisfies FilterTableInfo[]
|
||||
);
|
||||
|
||||
const currentTableIds = filterByTableIds.tableIds || [];
|
||||
const newTableIds = [
|
||||
...new Set([...currentTableIds, ...tableIdsToAdd]),
|
||||
];
|
||||
|
||||
return reduceFilter(
|
||||
{
|
||||
...filterByTableIds,
|
||||
tableIds: newTableIds,
|
||||
},
|
||||
allTables satisfies FilterTableInfo[]
|
||||
);
|
||||
});
|
||||
},
|
||||
[allTables]
|
||||
);
|
||||
|
||||
const removeTablesFromFilter: DiagramFilterContext['removeTablesFromFilter'] =
|
||||
useCallback(
|
||||
({ tableIds, filterCallback }) => {
|
||||
setFilter((prev) => {
|
||||
let tableIdsToRemovoe: string[];
|
||||
|
||||
if (tableIds) {
|
||||
// If tableIds are provided, use them directly
|
||||
tableIdsToRemovoe = tableIds;
|
||||
} else if (filterCallback) {
|
||||
// If filterCallback is provided, filter tables based on it
|
||||
tableIdsToRemovoe = allTables
|
||||
.filter(filterCallback)
|
||||
.map((table) => table.id);
|
||||
} else {
|
||||
// If neither is provided, do nothing
|
||||
return prev;
|
||||
}
|
||||
|
||||
const filterByTableIds = spreadFilterTables(
|
||||
prev,
|
||||
allTables satisfies FilterTableInfo[]
|
||||
);
|
||||
|
||||
const currentTableIds = filterByTableIds.tableIds || [];
|
||||
const newTableIds = currentTableIds.filter(
|
||||
(id) => !tableIdsToRemovoe.includes(id)
|
||||
);
|
||||
|
||||
return reduceFilter(
|
||||
{
|
||||
...filterByTableIds,
|
||||
tableIds: newTableIds,
|
||||
},
|
||||
allTables satisfies FilterTableInfo[]
|
||||
);
|
||||
});
|
||||
},
|
||||
[allTables]
|
||||
);
|
||||
|
||||
const eventConsumer = useCallback(
|
||||
(event: ChartDBEvent) => {
|
||||
if (!hasActiveFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.action === 'add_tables') {
|
||||
addTablesToFilter({
|
||||
tableIds: event.data.tables.map((table) => table.id),
|
||||
});
|
||||
}
|
||||
},
|
||||
[hasActiveFilter, addTablesToFilter]
|
||||
);
|
||||
|
||||
events.useSubscription(eventConsumer);
|
||||
}, [filter.schemaIds, schemas]);
|
||||
|
||||
const value: DiagramFilterContext = {
|
||||
filter,
|
||||
schemaIdsFilter: filter.schemaIds,
|
||||
addSchemaIdsFilter: addSchemaIds,
|
||||
removeSchemaIdsFilter: removeSchemaIds,
|
||||
clearSchemaIdsFilter: clearSchemaIds,
|
||||
setTableIdsFilterEmpty: setTableIdsEmpty,
|
||||
tableIdsFilter: filter.tableIds,
|
||||
addTableIdsFilter: addTableIds,
|
||||
removeTableIdsFilter: removeTableIds,
|
||||
clearTableIdsFilter: clearTableIds,
|
||||
resetFilter,
|
||||
toggleSchemaFilter,
|
||||
toggleTableFilter,
|
||||
addSchemaToFilter,
|
||||
addSchemaIfFiltered,
|
||||
hasActiveFilter,
|
||||
schemasDisplayed,
|
||||
addTablesToFilter,
|
||||
removeTablesFromFilter,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import { Label } from '@/components/label/label';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
|
||||
export interface TableSchemaDialogProps extends BaseDialogProps {
|
||||
table?: DBTable;
|
||||
@@ -45,6 +46,7 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { databaseType } = useChartDB();
|
||||
const { addSchemaIfFiltered } = useDiagramFilter();
|
||||
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
|
||||
table?.schema
|
||||
? schemaNameToSchemaId(table.schema)
|
||||
@@ -93,6 +95,7 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
const { closeTableSchemaDialog } = useDialog();
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
let createdSchemaId: string;
|
||||
if (isCreatingNew && newSchemaName.trim()) {
|
||||
const newSchema: DBSchema = {
|
||||
id: schemaNameToSchemaId(newSchemaName.trim()),
|
||||
@@ -100,14 +103,26 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
tableCount: 0,
|
||||
};
|
||||
|
||||
createdSchemaId = newSchema.id;
|
||||
|
||||
onConfirm({ schema: newSchema });
|
||||
} else {
|
||||
const schema = schemas.find((s) => s.id === selectedSchemaId);
|
||||
if (!schema) return;
|
||||
|
||||
createdSchemaId = schema.id;
|
||||
onConfirm({ schema });
|
||||
}
|
||||
}, [onConfirm, selectedSchemaId, schemas, isCreatingNew, newSchemaName]);
|
||||
|
||||
addSchemaIfFiltered(createdSchemaId);
|
||||
}, [
|
||||
onConfirm,
|
||||
selectedSchemaId,
|
||||
schemas,
|
||||
isCreatingNew,
|
||||
newSchemaName,
|
||||
addSchemaIfFiltered,
|
||||
]);
|
||||
|
||||
const schemaOptions: SelectBoxOption[] = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -786,6 +786,12 @@ export function convertToChartDBDiagram(
|
||||
);
|
||||
|
||||
if (!sourceField || !targetField) {
|
||||
console.log('Relationship refers to non-existent field:', {
|
||||
sourceTable: rel.sourceTable,
|
||||
sourceField: rel.sourceColumn,
|
||||
targetTable: rel.targetTable,
|
||||
targetField: rel.targetColumn,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -203,6 +203,11 @@ export function findTableWithSchemaSupport(
|
||||
// If still not found with schema, try any match on the table name
|
||||
if (!table) {
|
||||
table = tables.find((t) => t.name === tableName);
|
||||
if (table) {
|
||||
console.log(
|
||||
`Found table ${tableName} without schema match, source schema: ${effectiveSchema}, table schema: ${table.schema}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return table;
|
||||
@@ -230,7 +235,11 @@ export function getTableIdWithSchemaSupport(
|
||||
// If still not found with schema, try without schema
|
||||
if (!tableId) {
|
||||
tableId = tableMap[tableName];
|
||||
if (!tableId) {
|
||||
if (tableId) {
|
||||
console.log(
|
||||
`Found table ID for ${tableName} without schema match, source schema: ${effectiveSchema}`
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`No table ID found for ${tableName} with schema ${effectiveSchema}`
|
||||
);
|
||||
|
||||
@@ -4,11 +4,9 @@ export interface DiagramFilter {
|
||||
tableIds?: string[];
|
||||
}
|
||||
|
||||
export interface FilterTableInfo {
|
||||
export interface TableInfo {
|
||||
id: string;
|
||||
schemaId?: string;
|
||||
schema?: string;
|
||||
areaId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,7 +18,7 @@ export interface FilterTableInfo {
|
||||
*/
|
||||
export function reduceFilter(
|
||||
filter: DiagramFilter,
|
||||
tables: FilterTableInfo[]
|
||||
tables: TableInfo[]
|
||||
): DiagramFilter {
|
||||
let { schemaIds, tableIds } = filter;
|
||||
|
||||
@@ -29,10 +27,6 @@ export function reduceFilter(
|
||||
return { schemaIds: undefined, tableIds: undefined };
|
||||
}
|
||||
|
||||
if (!schemaIds && tableIds && tableIds.length === 0) {
|
||||
return { schemaIds: undefined, tableIds: [] };
|
||||
}
|
||||
|
||||
// Get all unique schema IDs from tables
|
||||
const allSchemaIds = [
|
||||
...new Set(tables.filter((t) => t.schemaId).map((t) => t.schemaId!)),
|
||||
@@ -151,50 +145,3 @@ export function reduceFilter(
|
||||
tableIds: reducedTableIds,
|
||||
};
|
||||
}
|
||||
|
||||
export const spreadFilterTables = (
|
||||
filter: DiagramFilter,
|
||||
tables: FilterTableInfo[]
|
||||
): DiagramFilter => {
|
||||
const { schemaIds, tableIds } = filter;
|
||||
|
||||
// If no filters are defined, everything is visible (return undefined)
|
||||
if (!schemaIds && !tableIds) {
|
||||
const allTablesIds = new Set<string>();
|
||||
tables.forEach((table) => {
|
||||
allTablesIds.add(table.id);
|
||||
});
|
||||
|
||||
return { tableIds: Array.from(allTablesIds) };
|
||||
}
|
||||
|
||||
// If only tableIds is defined, return it as is
|
||||
if (!schemaIds && tableIds) {
|
||||
return { tableIds };
|
||||
}
|
||||
|
||||
// Collect all table IDs that should be visible
|
||||
const visibleTableIds = new Set<string>();
|
||||
|
||||
// Add existing tableIds to the set
|
||||
if (tableIds) {
|
||||
tableIds.forEach((id) => visibleTableIds.add(id));
|
||||
}
|
||||
|
||||
// Add all tables from specified schemas
|
||||
if (schemaIds) {
|
||||
const schemaSet = new Set(schemaIds);
|
||||
tables.forEach((table) => {
|
||||
if (table.schemaId && schemaSet.has(table.schemaId)) {
|
||||
visibleTableIds.add(table.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If no tables are visible, return empty array
|
||||
if (visibleTableIds.size === 0) {
|
||||
return { tableIds: [] };
|
||||
}
|
||||
|
||||
return { tableIds: Array.from(visibleTableIds) };
|
||||
};
|
||||
|
||||
@@ -37,6 +37,48 @@ export const filterTable = ({
|
||||
return false;
|
||||
};
|
||||
|
||||
export const filterTableBySchema = ({
|
||||
table,
|
||||
schemaIdsFilter,
|
||||
options = { defaultSchema: undefined },
|
||||
}: {
|
||||
table: { id: string; schema?: string | null };
|
||||
schemaIdsFilter?: string[];
|
||||
options?: {
|
||||
defaultSchema?: string;
|
||||
};
|
||||
}): boolean => {
|
||||
if (!schemaIdsFilter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const tableSchemaId = table.schema ?? options.defaultSchema;
|
||||
|
||||
if (tableSchemaId) {
|
||||
return schemaIdsFilter.includes(schemaNameToSchemaId(tableSchemaId));
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const filterSchema = ({
|
||||
schemaId,
|
||||
schemaIdsFilter,
|
||||
}: {
|
||||
schemaId?: string;
|
||||
schemaIdsFilter?: string[];
|
||||
}): boolean => {
|
||||
if (!schemaIdsFilter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!schemaId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return schemaIdsFilter.includes(schemaId);
|
||||
};
|
||||
|
||||
export const filterRelationship = ({
|
||||
tableA: { id: tableAId, schema: tableASchema },
|
||||
tableB: { id: tableBId, schema: tableBSchema },
|
||||
|
||||
@@ -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,32 +5,58 @@ import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { X, Search, Database, Table, Funnel, Box } 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';
|
||||
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import { useReactFlow } from '@xyflow/react';
|
||||
import { TreeView } from '@/components/tree-view/tree-view';
|
||||
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';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import type {
|
||||
GroupingMode,
|
||||
NodeContext,
|
||||
NodeType,
|
||||
RelevantTableData,
|
||||
TableContext,
|
||||
} from './types';
|
||||
import { generateTreeDataByAreas, generateTreeDataBySchemas } from './utils';
|
||||
import { FilterItemActions } from './filter-item-actions';
|
||||
|
||||
export interface CanvasFilterProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
type NodeContext = {
|
||||
schema: SchemaContext;
|
||||
area: AreaContext;
|
||||
table: TableContext;
|
||||
};
|
||||
|
||||
type RelevantTableData = {
|
||||
id: string;
|
||||
name: string;
|
||||
schema?: string | null;
|
||||
};
|
||||
|
||||
export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
const { tables, databaseType, areas } = useChartDB();
|
||||
@@ -40,8 +66,6 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
toggleTableFilter,
|
||||
clearTableIdsFilter,
|
||||
setTableIdsFilterEmpty,
|
||||
addTablesToFilter,
|
||||
removeTablesFromFilter,
|
||||
} = useDiagramFilter();
|
||||
const { fitView, setNodes } = useReactFlow();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -57,7 +81,6 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
schema: table.schema,
|
||||
parentAreaId: table.parentAreaId,
|
||||
})),
|
||||
[tables]
|
||||
);
|
||||
@@ -69,23 +92,249 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
|
||||
// Convert tables to tree nodes
|
||||
const treeData = useMemo(() => {
|
||||
const nodes: TreeNode<NodeType, NodeContext>[] = [];
|
||||
|
||||
if (groupingMode === 'area') {
|
||||
return generateTreeDataByAreas({
|
||||
areas,
|
||||
databaseType,
|
||||
filter,
|
||||
relevantTableData,
|
||||
// Group tables by area
|
||||
const tablesByArea = new Map<string | null, DBTable[]>();
|
||||
const tablesWithoutArea: DBTable[] = [];
|
||||
|
||||
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,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
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 {
|
||||
return generateTreeDataBySchemas({
|
||||
relevantTableData,
|
||||
databaseWithSchemas,
|
||||
databaseType,
|
||||
filter,
|
||||
// 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,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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],
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
tables,
|
||||
databaseType,
|
||||
filter,
|
||||
databaseWithSchemas,
|
||||
@@ -127,31 +376,6 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
return result;
|
||||
}, [treeData, searchQuery]);
|
||||
|
||||
// Render actions with proper memoization for performance
|
||||
const renderActions = useCallback(
|
||||
(node: TreeNode<NodeType, NodeContext>) => (
|
||||
<FilterItemActions
|
||||
node={node}
|
||||
databaseWithSchemas={databaseWithSchemas}
|
||||
toggleSchemaFilter={toggleSchemaFilter}
|
||||
toggleTableFilter={toggleTableFilter}
|
||||
clearTableIdsFilter={clearTableIdsFilter}
|
||||
setTableIdsFilterEmpty={setTableIdsFilterEmpty}
|
||||
addTablesToFilter={addTablesToFilter}
|
||||
removeTablesFromFilter={removeTablesFromFilter}
|
||||
/>
|
||||
),
|
||||
[
|
||||
databaseWithSchemas,
|
||||
toggleSchemaFilter,
|
||||
toggleTableFilter,
|
||||
clearTableIdsFilter,
|
||||
setTableIdsFilterEmpty,
|
||||
addTablesToFilter,
|
||||
removeTablesFromFilter,
|
||||
]
|
||||
);
|
||||
|
||||
const focusOnTable = useCallback(
|
||||
(tableId: string) => {
|
||||
// Make sure the table is visible
|
||||
@@ -187,6 +411,157 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
[fitView, setNodes]
|
||||
);
|
||||
|
||||
// Render component that's always visible (eye indicator)
|
||||
const renderActions = useCallback(
|
||||
(node: TreeNode<NodeType, NodeContext>) => {
|
||||
if (node.type === 'schema') {
|
||||
const context = node.context as SchemaContext;
|
||||
const schemaVisible = context.visible;
|
||||
const schemaName = context.name;
|
||||
if (!schemaName) return null;
|
||||
|
||||
const schemaId = schemaNameToSchemaId(schemaName);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="size-7 h-fit p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (databaseWithSchemas) {
|
||||
toggleSchemaFilter(schemaId);
|
||||
} else {
|
||||
// Toggle visibility of all tables in this schema
|
||||
if (schemaVisible) {
|
||||
setTableIdsFilterEmpty();
|
||||
} else {
|
||||
clearTableIdsFilter();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!schemaVisible ? (
|
||||
<EyeOff className="size-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Eye className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
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 context = node.context as TableContext;
|
||||
const tableVisible = context.visible;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="size-7 h-fit p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleTableFilter(tableId);
|
||||
}}
|
||||
>
|
||||
{!tableVisible ? (
|
||||
<EyeOff className="size-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Eye className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[
|
||||
toggleSchemaFilter,
|
||||
toggleTableFilter,
|
||||
clearTableIdsFilter,
|
||||
setTableIdsFilterEmpty,
|
||||
databaseWithSchemas,
|
||||
tables,
|
||||
filter,
|
||||
databaseType,
|
||||
]
|
||||
);
|
||||
|
||||
// Handle node click
|
||||
const handleNodeClick = useCallback(
|
||||
(node: TreeNode<NodeType, NodeContext>) => {
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
import { Button } from '@/components/button/button';
|
||||
import type { TreeNode } from '@/components/tree-view/tree';
|
||||
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
|
||||
import type {
|
||||
AreaContext,
|
||||
NodeContext,
|
||||
NodeType,
|
||||
// RelevantTableData,
|
||||
SchemaContext,
|
||||
TableContext,
|
||||
} from './types';
|
||||
import type { FilterTableInfo } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
|
||||
interface FilterItemActionsProps {
|
||||
node: TreeNode<NodeType, NodeContext>;
|
||||
databaseWithSchemas: boolean;
|
||||
toggleSchemaFilter: (schemaId: string) => void;
|
||||
toggleTableFilter: (tableId: string) => void;
|
||||
clearTableIdsFilter: () => void;
|
||||
setTableIdsFilterEmpty: () => void;
|
||||
addTablesToFilter: (attrs: {
|
||||
tableIds?: string[];
|
||||
filterCallback?: (table: FilterTableInfo) => boolean;
|
||||
}) => void;
|
||||
removeTablesFromFilter: (attrs: {
|
||||
tableIds?: string[];
|
||||
filterCallback?: (table: FilterTableInfo) => boolean;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const FilterItemActions: React.FC<FilterItemActionsProps> = ({
|
||||
node,
|
||||
databaseWithSchemas,
|
||||
toggleSchemaFilter,
|
||||
toggleTableFilter,
|
||||
clearTableIdsFilter,
|
||||
setTableIdsFilterEmpty,
|
||||
addTablesToFilter,
|
||||
removeTablesFromFilter,
|
||||
}) => {
|
||||
if (node.type === 'schema') {
|
||||
const context = node.context as SchemaContext;
|
||||
const schemaVisible = context.visible;
|
||||
const schemaName = context.name;
|
||||
const schemaId = schemaNameToSchemaId(schemaName);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="size-7 h-fit p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (databaseWithSchemas) {
|
||||
toggleSchemaFilter(schemaId);
|
||||
} else {
|
||||
// Toggle visibility of all tables in this schema
|
||||
if (schemaVisible) {
|
||||
setTableIdsFilterEmpty();
|
||||
} else {
|
||||
clearTableIdsFilter();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!schemaVisible ? (
|
||||
<EyeOff className="size-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Eye className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === 'area') {
|
||||
const context = node.context as AreaContext;
|
||||
const areaVisible = context.visible;
|
||||
const isUngrouped = context.isUngrouped;
|
||||
const areaId = context.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
|
||||
removeTablesFromFilter({
|
||||
filterCallback: (table) =>
|
||||
(isUngrouped && !table.areaId) ||
|
||||
(!isUngrouped && table.areaId === areaId),
|
||||
});
|
||||
} else {
|
||||
// Show all tables in this area
|
||||
addTablesToFilter({
|
||||
filterCallback: (table) =>
|
||||
(isUngrouped && !table.areaId) ||
|
||||
(!isUngrouped && table.areaId === areaId),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!areaVisible ? (
|
||||
<EyeOff className="size-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Eye className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === 'table') {
|
||||
const tableId = node.id;
|
||||
const context = node.context as TableContext;
|
||||
const tableVisible = context.visible;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="size-7 h-fit p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleTableFilter(tableId);
|
||||
}}
|
||||
>
|
||||
{!tableVisible ? (
|
||||
<EyeOff className="size-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Eye className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
export type NodeType = 'schema' | 'area' | 'table';
|
||||
export type GroupingMode = 'schema' | 'area';
|
||||
|
||||
export type SchemaContext = { name: string; visible: boolean };
|
||||
export type AreaContext = {
|
||||
id: string;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
isUngrouped: boolean;
|
||||
};
|
||||
export type TableContext = {
|
||||
tableSchema?: string | null;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
export type NodeContext = {
|
||||
schema: SchemaContext;
|
||||
area: AreaContext;
|
||||
table: TableContext;
|
||||
};
|
||||
|
||||
export type RelevantTableData = {
|
||||
id: string;
|
||||
name: string;
|
||||
schema?: string | null;
|
||||
parentAreaId?: string | null;
|
||||
};
|
||||
@@ -1,292 +0,0 @@
|
||||
import type { Area, DatabaseType } from '@/lib/domain';
|
||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
import type {
|
||||
AreaContext,
|
||||
NodeContext,
|
||||
NodeType,
|
||||
RelevantTableData,
|
||||
SchemaContext,
|
||||
TableContext,
|
||||
} from './types';
|
||||
import type { TreeNode } from '@/components/tree-view/tree';
|
||||
import { Box, Database, Layers, Table } from 'lucide-react';
|
||||
import { filterTable } from '@/lib/domain/diagram-filter/filter';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
|
||||
export const generateTreeDataByAreas = ({
|
||||
areas,
|
||||
databaseType,
|
||||
filter,
|
||||
relevantTableData,
|
||||
}: {
|
||||
areas: Area[];
|
||||
databaseType: DatabaseType;
|
||||
filter?: DiagramFilter;
|
||||
relevantTableData: RelevantTableData[];
|
||||
}): TreeNode<NodeType, NodeContext>[] => {
|
||||
const nodes: TreeNode<NodeType, NodeContext>[] = [];
|
||||
|
||||
// Group tables by area
|
||||
const tablesByArea = new Map<string | null, RelevantTableData[]>();
|
||||
const tablesWithoutArea: RelevantTableData[] = [];
|
||||
|
||||
relevantTableData.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],
|
||||
},
|
||||
}) === false
|
||||
);
|
||||
|
||||
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,
|
||||
isUngrouped: false,
|
||||
} satisfies 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,
|
||||
} satisfies 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,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
}) == false
|
||||
);
|
||||
|
||||
const ungroupedNode: TreeNode<NodeType, NodeContext> = {
|
||||
id: 'ungrouped',
|
||||
name: `Ungrouped (${tablesWithoutArea.length})`,
|
||||
type: 'area',
|
||||
isFolder: true,
|
||||
icon: Layers,
|
||||
context: {
|
||||
id: 'ungrouped',
|
||||
name: 'Ungrouped',
|
||||
visible: ungroupedVisible,
|
||||
isUngrouped: true,
|
||||
} satisfies 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,
|
||||
} satisfies TableContext,
|
||||
className: !tableVisible ? 'opacity-50' : '',
|
||||
};
|
||||
}
|
||||
),
|
||||
};
|
||||
nodes.push(ungroupedNode);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
};
|
||||
|
||||
export const generateTreeDataBySchemas = ({
|
||||
relevantTableData,
|
||||
databaseWithSchemas,
|
||||
databaseType,
|
||||
filter,
|
||||
}: {
|
||||
relevantTableData: RelevantTableData[];
|
||||
databaseWithSchemas: boolean;
|
||||
databaseType: DatabaseType;
|
||||
filter?: DiagramFilter;
|
||||
}): TreeNode<NodeType, NodeContext>[] => {
|
||||
const nodes: TreeNode<NodeType, NodeContext>[] = [];
|
||||
|
||||
// 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) {
|
||||
schemaVisible = !schemaTables.some(
|
||||
(table) =>
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
}) === false
|
||||
);
|
||||
} 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,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
}) === false
|
||||
);
|
||||
}
|
||||
|
||||
const schemaNode: TreeNode<NodeType, NodeContext> = {
|
||||
id: `schema-${schemaName}`,
|
||||
name: `${schemaName} (${schemaTables.length})`,
|
||||
type: 'schema',
|
||||
isFolder: true,
|
||||
icon: Database,
|
||||
context: {
|
||||
name: schemaName,
|
||||
visible: schemaVisible,
|
||||
} satisfies 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],
|
||||
},
|
||||
});
|
||||
|
||||
const hidden = !tableVisible;
|
||||
|
||||
return {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
type: 'table',
|
||||
isFolder: false,
|
||||
icon: Table,
|
||||
context: {
|
||||
tableSchema: table.schema,
|
||||
visible: tableVisible,
|
||||
} satisfies TableContext,
|
||||
className: hidden ? 'opacity-50' : '',
|
||||
};
|
||||
}
|
||||
),
|
||||
};
|
||||
nodes.push(schemaNode);
|
||||
});
|
||||
|
||||
return nodes;
|
||||
};
|
||||
@@ -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';
|
||||
@@ -495,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;
|
||||
@@ -536,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) => {
|
||||
@@ -915,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,
|
||||
@@ -988,6 +1021,8 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
tables,
|
||||
areas,
|
||||
getNode,
|
||||
databaseType,
|
||||
filter,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
import { Badge } from '@/components/badge/badge';
|
||||
import { checkIfCustomTypeUsed } from '../utils';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
|
||||
export interface CustomTypeListItemHeaderProps {
|
||||
customType: DBCustomType;
|
||||
@@ -50,7 +49,6 @@ export const CustomTypeListItemHeader: React.FC<
|
||||
highlightedCustomType,
|
||||
highlightCustomTypeId,
|
||||
tables,
|
||||
databaseType,
|
||||
} = useChartDB();
|
||||
const { schemasDisplayed } = useDiagramFilter();
|
||||
const { t } = useTranslation();
|
||||
@@ -165,9 +163,9 @@ export const CustomTypeListItemHeader: React.FC<
|
||||
|
||||
const schemaToDisplay = useMemo(() => {
|
||||
if (schemasDisplayed.length > 1) {
|
||||
return customType.schema ?? defaultSchemas[databaseType];
|
||||
return customType.schema;
|
||||
}
|
||||
}, [customType.schema, schemasDisplayed.length, databaseType]);
|
||||
}, [customType.schema, schemasDisplayed.length]);
|
||||
|
||||
return (
|
||||
<div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden">
|
||||
|
||||
@@ -268,9 +268,9 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
|
||||
|
||||
const schemaToDisplay = useMemo(() => {
|
||||
if (schemasDisplayed.length > 1) {
|
||||
return table.schema ?? defaultSchemas[databaseType];
|
||||
return table.schema;
|
||||
}
|
||||
}, [table.schema, schemasDisplayed.length, databaseType]);
|
||||
}, [table.schema, schemasDisplayed.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (table.name.trim()) {
|
||||
|
||||
Reference in New Issue
Block a user