fix(filter): show visible/total count badge for schema and area groups (#1045)

This commit is contained in:
Guy Ben-Aharon
2026-01-03 13:10:43 +02:00
committed by GitHub
parent ded5b14a62
commit 84e55f5df6
3 changed files with 94 additions and 152 deletions

View File

@@ -354,13 +354,27 @@ function TreeNode<Type extends string, Context extends Record<Type, unknown>>({
<span
{...node.labelProps}
className={cn(
'text-xs truncate min-w-0 flex-1 w-0',
'text-xs truncate min-w-0 flex-1 w-0 flex items-center gap-1.5',
isSelected && 'font-medium text-primary text-white',
node.labelProps?.className
)}
{...(isSelected ? { 'data-selected': true } : {})}
>
{node.empty ? '' : node.name}
<span className="truncate">
{node.empty ? '' : node.name}
</span>
{node.suffix && (
<span
className={cn(
'flex-shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium leading-none',
isSelected
? 'bg-sky-400/50 text-white'
: 'bg-gray-200/50 text-muted-foreground dark:bg-gray-700/50'
)}
>
{node.suffix}
</span>
)}
</span>
{renderActionsComponent && renderActionsComponent(node)}
{isHovered && renderHoverComponent

View File

@@ -12,6 +12,7 @@ export interface TreeNode<
icon?: LucideIcon;
iconProps?: React.ComponentProps<LucideIcon>;
labelProps?: React.ComponentProps<'span'>;
suffix?: string;
type: Type;
unselectable?: boolean;
tooltip?: string;

View File

@@ -13,6 +13,43 @@ import { Box, Database, Layers, Table } from 'lucide-react';
import { filterTable } from '@/lib/domain/diagram-filter/filter';
import { defaultSchemas } from '@/lib/data/default-schemas';
type TableWithVisibility = RelevantTableData & { visible: boolean };
const computeTableVisibility = (
tables: RelevantTableData[],
filter: DiagramFilter | undefined,
databaseType: DatabaseType
): TableWithVisibility[] =>
tables.map((table) => ({
...table,
visible: filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
}),
}));
const createTableChildren = (
tablesWithVisibility: TableWithVisibility[]
): TreeNode<NodeType, NodeContext>[] =>
tablesWithVisibility.map((table) => ({
id: table.id,
name: table.name,
type: 'table' as const,
isFolder: false,
icon: Table,
context: {
tableSchema: table.schema,
visible: table.visible,
} satisfies TableContext,
className: !table.visible ? 'opacity-50' : '',
}));
export const generateTreeDataByAreas = ({
areas,
databaseType,
@@ -50,27 +87,23 @@ export const generateTreeDataByAreas = ({
// Create nodes for areas
areas.forEach((area) => {
const areaTables = tablesByArea.get(area.id) || [];
if (areaTables.length === 0) return;
// 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
);
// Pre-compute visibility for all tables in this area (single pass)
const tablesWithVisibility = computeTableVisibility(
areaTables,
filter,
databaseType
);
const visibleCount = tablesWithVisibility.filter(
(t) => t.visible
).length;
const areaVisible = visibleCount === areaTables.length;
const areaNode: TreeNode<NodeType, NodeContext> = {
id: `area-${area.id}`,
name: `${area.name} (${areaTables.length})`,
name: area.name,
suffix: `${visibleCount}/${areaTables.length}`,
type: 'area',
isFolder: true,
icon: Box,
@@ -81,59 +114,28 @@ export const generateTreeDataByAreas = ({
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' : '',
};
}
),
children: createTableChildren(tablesWithVisibility),
};
if (areaTables.length > 0) {
nodes.push(areaNode);
}
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 tablesWithVisibility = computeTableVisibility(
tablesWithoutArea,
filter,
databaseType
);
const visibleCount = tablesWithVisibility.filter(
(t) => t.visible
).length;
const ungroupedVisible = visibleCount === tablesWithoutArea.length;
const ungroupedNode: TreeNode<NodeType, NodeContext> = {
id: 'ungrouped',
name: `Ungrouped (${tablesWithoutArea.length})`,
name: 'Ungrouped',
suffix: `${visibleCount}/${tablesWithoutArea.length}`,
type: 'area',
isFolder: true,
icon: Layers,
@@ -144,33 +146,7 @@ export const generateTreeDataByAreas = ({
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' : '',
};
}
),
children: createTableChildren(tablesWithVisibility),
};
nodes.push(ungroupedNode);
}
@@ -191,7 +167,7 @@ export const generateTreeDataBySchemas = ({
}): TreeNode<NodeType, NodeContext>[] => {
const nodes: TreeNode<NodeType, NodeContext>[] = [];
// Group tables by schema (existing logic)
// Group tables by schema
const tablesBySchema = new Map<string, RelevantTableData[]>();
relevantTableData.forEach((table) => {
@@ -211,42 +187,21 @@ export const generateTreeDataBySchemas = ({
});
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
);
}
// Pre-compute visibility for all tables in this schema (single pass)
const tablesWithVisibility = computeTableVisibility(
schemaTables,
filter,
databaseType
);
const visibleCount = tablesWithVisibility.filter(
(t) => t.visible
).length;
const schemaVisible = visibleCount === schemaTables.length;
const schemaNode: TreeNode<NodeType, NodeContext> = {
id: `schema-${schemaName}`,
name: `${schemaName} (${schemaTables.length})`,
name: schemaName,
suffix: `${visibleCount}/${schemaTables.length}`,
type: 'schema',
isFolder: true,
icon: Database,
@@ -255,35 +210,7 @@ export const generateTreeDataBySchemas = ({
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' : '',
};
}
),
children: createTableChildren(tablesWithVisibility),
};
nodes.push(schemaNode);
});