{
}}
ref={(el) => {
if (el) {
- // trigger remeasurement when content changes and load
- requestAnimationFrame(() => {
- handleSectionResize(virtualItem.index);
- });
+ rowVirtualizer.measureElement(el);
}
}}
>
@@ -269,7 +286,7 @@ export const List = ({ setCurrentIndex }: IListProps) => {
- handleCopyGroupLink(item.group.Name)}
>
@@ -283,9 +300,6 @@ export const List = ({ setCurrentIndex }: IListProps) => {
{
-
- }}
search={search}
/>
diff --git a/Website/components/datamodelview/Relationships.tsx b/Website/components/datamodelview/Relationships.tsx
index daa4c6b..243626d 100644
--- a/Website/components/datamodelview/Relationships.tsx
+++ b/Website/components/datamodelview/Relationships.tsx
@@ -2,7 +2,7 @@
import { EntityType } from "@/lib/Types"
import { CascadeConfiguration } from "./entity/CascadeConfiguration"
-import { useState } from "react"
+import { useState, useEffect } from "react"
import { useDatamodelView, useDatamodelViewDispatch } from "@/contexts/DatamodelViewContext"
import React from "react"
import { highlightMatch } from "../datamodelview/List";
@@ -14,15 +14,16 @@ type SortColumn = 'name' | 'tableSchema' | 'lookupField' | 'type' | 'behavior' |
interface IRelationshipsProps {
entity: EntityType;
+ search?: string;
onVisibleCountChange?: (count: number) => void;
}
-export const Relationships = ({ entity, onVisibleCountChange, search = "" }: IRelationshipsProps & { search?: string }) => {
+export const Relationships = ({ entity, search = "", onVisibleCountChange }: IRelationshipsProps) => {
const [sortColumn, setSortColumn] = useState("name")
const [sortDirection, setSortDirection] = useState("asc")
const [typeFilter, setTypeFilter] = useState("all")
const [searchQuery, setSearchQuery] = useState("")
-
+
const theme = useTheme();
const dispatch = useDatamodelViewDispatch();
@@ -130,11 +131,10 @@ export const Relationships = ({ entity, onVisibleCountChange, search = "" }: IRe
const sortedRelationships = getSortedRelationships();
const highlightTerm = searchQuery || search; // Use internal search or parent search for highlighting
- React.useEffect(() => {
- if (onVisibleCountChange) {
- onVisibleCountChange(sortedRelationships.length);
- }
- }, [onVisibleCountChange, sortedRelationships.length]);
+ // Notify parent of visible count changes
+ useEffect(() => {
+ onVisibleCountChange?.(sortedRelationships.length);
+ }, [sortedRelationships.length, onVisibleCountChange]);
return <>
Filter by type
-
-
+
)
},
// Custom comparison function to prevent unnecessary re-renders
(prevProps, nextProps) => {
// Only re-render if entity, search or group changes
- return prevProps.entity.SchemaName === nextProps.entity.SchemaName &&
- prevProps.search === nextProps.search &&
- prevProps.group.Name === nextProps.group.Name;
+ return prevProps.entity.SchemaName === nextProps.entity.SchemaName &&
+ prevProps.search === nextProps.search &&
+ prevProps.group.Name === nextProps.group.Name;
}
);
diff --git a/Website/components/datamodelview/TimeSlicedSearch.tsx b/Website/components/datamodelview/TimeSlicedSearch.tsx
index 633ebc0..f3e421c 100644
--- a/Website/components/datamodelview/TimeSlicedSearch.tsx
+++ b/Website/components/datamodelview/TimeSlicedSearch.tsx
@@ -229,6 +229,13 @@ export const TimeSlicedSearch = ({
setAnchorEl(null);
};
+ // Close menu when focus moves back to search input or elsewhere
+ const handleSearchFocus = () => {
+ if (open) {
+ setAnchorEl(null);
+ }
+ };
+
const searchInput = (
@@ -245,6 +252,7 @@ export const TimeSlicedSearch = ({
aria-label="Search attributes in tables"
value={localValue}
onChange={handleChange}
+ onFocus={handleSearchFocus}
spellCheck={false}
autoComplete="off"
autoCapitalize="off"
@@ -254,11 +262,15 @@ export const TimeSlicedSearch = ({
{isTyping && localValue.length >= 3 ? (
- ) : localValue ? (
-
- {currentIndex}
- {totalResults}
-
+ ) : localValue && totalResults !== undefined && totalResults > 0 ? (
+
+ {currentIndex}/{totalResults}
+
) : null}
diff --git a/Website/components/datamodelview/searchWorker.ts b/Website/components/datamodelview/searchWorker.ts
index 433eef4..54fe808 100644
--- a/Website/components/datamodelview/searchWorker.ts
+++ b/Website/components/datamodelview/searchWorker.ts
@@ -6,9 +6,15 @@ interface InitMessage {
groups: GroupType[];
}
+interface EntityFilterState {
+ hideStandardFields: boolean;
+ typeFilter: string;
+}
+
interface SearchMessage {
type: 'search';
data: string;
+ entityFilters?: Record;
}
type WorkerMessage = InitMessage | SearchMessage | string;
@@ -18,6 +24,7 @@ interface ResultsMessage {
data: Array<
| { type: 'group'; group: GroupType }
| { type: 'entity'; group: GroupType; entity: EntityType }
+ | { type: 'attribute'; group: GroupType; entity: EntityType; attribute: AttributeType }
>;
complete: boolean;
progress?: number;
@@ -32,17 +39,24 @@ type WorkerResponse = ResultsMessage | StartedMessage;
let groups: GroupType[] | null = null;
const CHUNK_SIZE = 20; // Process results in chunks
-self.onmessage = async function(e: MessageEvent) {
+self.onmessage = async function (e: MessageEvent) {
// Handle initialization
if (e.data && typeof e.data === 'object' && 'type' in e.data && e.data.type === 'init') {
groups = e.data.groups;
return;
}
-
+
+ if (!groups) {
+ const response: WorkerResponse = { type: 'results', data: [], complete: true };
+ self.postMessage(response);
+ return;
+ }
+
// Handle search
const search = (typeof e.data === 'string' ? e.data : e.data?.data || '').trim().toLowerCase();
-
- if (!groups) {
+ const entityFilters: Record = (typeof e.data === 'object' && 'entityFilters' in e.data) ? e.data.entityFilters || {} : {};
+
+ if (!search) {
const response: WorkerResponse = { type: 'results', data: [], complete: true };
self.postMessage(response);
return;
@@ -51,61 +65,82 @@ self.onmessage = async function(e: MessageEvent) {
// First quickly send back a "started" message
const startedMessage: WorkerResponse = { type: 'started' };
self.postMessage(startedMessage);
-
+
const allItems: Array<
| { type: 'group'; group: GroupType }
| { type: 'entity'; group: GroupType; entity: EntityType }
+ | { type: 'attribute'; group: GroupType; entity: EntityType; attribute: AttributeType }
> = [];
-
- // Find all matches
+
+ ////////////////////////////////////////////////
+ // Finding matches part
+ ////////////////////////////////////////////////
for (const group of groups) {
- const filteredEntities = group.Entities.filter((entity: EntityType) => {
- if (!search) return true;
-
- // Match entity schema or display name
- const entityMatch = entity.SchemaName.toLowerCase().includes(search) ||
- (entity.DisplayName && entity.DisplayName.toLowerCase().includes(search));
-
- // Match any attribute schema, display name, description, or option names
- const attrMatch = entity.Attributes.some((attr: AttributeType) => {
+ let groupUsed = false;
+ for (const entity of group.Entities) {
+ // Get entity-specific filters (default to showing all if not set)
+ const entityFilter = entityFilters[entity.SchemaName] || { hideStandardFields: true, typeFilter: 'all' };
+
+ // Find all matching attributes
+ const matchingAttributes = entity.Attributes.filter((attr: AttributeType) => {
+ // Apply hideStandardFields filter
+ if (entityFilter.hideStandardFields) {
+ const isStandardFieldHidden = !attr.IsCustomAttribute && !attr.IsStandardFieldModified;
+ if (isStandardFieldHidden) return false;
+ }
+
+ // Apply type filter
+ if (entityFilter.typeFilter && entityFilter.typeFilter !== 'all') {
+ // Special case: ChoiceAttribute filter also includes StatusAttribute
+ if (entityFilter.typeFilter === 'ChoiceAttribute') {
+ if (attr.AttributeType !== 'ChoiceAttribute' && attr.AttributeType !== 'StatusAttribute') {
+ return false;
+ }
+ } else {
+ if (attr.AttributeType !== entityFilter.typeFilter) {
+ return false;
+ }
+ }
+ }
+
+ // Apply search matching
const basicMatch = attr.SchemaName.toLowerCase().includes(search) ||
(attr.DisplayName && attr.DisplayName.toLowerCase().includes(search)) ||
(attr.Description && attr.Description.toLowerCase().includes(search));
-
- // Check options for ChoiceAttribute and StatusAttribute
let optionsMatch = false;
if (attr.AttributeType === 'ChoiceAttribute' || attr.AttributeType === 'StatusAttribute') {
optionsMatch = attr.Options.some(option => option.Name.toLowerCase().includes(search));
}
-
+
return basicMatch || optionsMatch;
});
-
- return entityMatch || attrMatch;
- });
-
- if (filteredEntities.length > 0) {
- allItems.push({ type: 'group', group });
- for (const entity of filteredEntities) {
+
+ // If we have matching attributes, add the entity first (for sidebar) then the attributes
+ if (matchingAttributes.length > 0) {
+ if (!groupUsed) allItems.push({ type: 'group', group });
+ groupUsed = true;
allItems.push({ type: 'entity', group, entity });
+ for (const attr of matchingAttributes) {
+ allItems.push({ type: 'attribute', group, entity, attribute: attr });
+ }
}
}
}
-
+
// Send results in chunks to prevent UI blocking
for (let i = 0; i < allItems.length; i += CHUNK_SIZE) {
const chunk = allItems.slice(i, i + CHUNK_SIZE);
const isLastChunk = i + CHUNK_SIZE >= allItems.length;
-
+
const response: WorkerResponse = {
type: 'results',
data: chunk,
complete: isLastChunk,
progress: Math.min(100, Math.round((i + CHUNK_SIZE) / allItems.length * 100))
};
-
+
self.postMessage(response);
-
+
// Small delay between chunks to let the UI breathe
if (!isLastChunk) {
// Use a proper yielding mechanism to let the UI breathe
diff --git a/Website/contexts/DatamodelDataContext.tsx b/Website/contexts/DatamodelDataContext.tsx
index 5fa747e..1ea1ed8 100644
--- a/Website/contexts/DatamodelDataContext.tsx
+++ b/Website/contexts/DatamodelDataContext.tsx
@@ -1,7 +1,7 @@
'use client'
import React, { createContext, useContext, useReducer, ReactNode } from "react";
-import { EntityType, GroupType, RelationshipType, SolutionType, SolutionWarningType } from "@/lib/Types";
+import { AttributeType, EntityType, GroupType, SolutionType, SolutionWarningType } from "@/lib/Types";
import { useSearchParams } from "next/navigation";
interface DataModelAction {
@@ -17,6 +17,7 @@ interface DatamodelDataState extends DataModelAction {
filtered: Array<
| { type: 'group'; group: GroupType }
| { type: 'entity'; group: GroupType; entity: EntityType }
+ | { type: 'attribute'; group: GroupType; entity: EntityType; attribute: AttributeType }
>;
}
diff --git a/Website/contexts/DatamodelViewContext.tsx b/Website/contexts/DatamodelViewContext.tsx
index 97c540f..744b5c4 100644
--- a/Website/contexts/DatamodelViewContext.tsx
+++ b/Website/contexts/DatamodelViewContext.tsx
@@ -9,6 +9,7 @@ export interface DatamodelViewState {
currentSection: string | null;
scrollToSection: (sectionId: string) => void;
scrollToGroup: (groupName: string) => void;
+ scrollToAttribute: (sectionId: string, attrSchema: string) => void;
loading: boolean;
loadingSection: string | null;
restoreSection: () => void;
@@ -19,19 +20,21 @@ const initialState: DatamodelViewState = {
currentSection: null,
scrollToSection: () => { throw new Error("scrollToSection not initialized yet!"); },
scrollToGroup: () => { throw new Error("scrollToGroup not initialized yet!"); },
+ scrollToAttribute: () => { throw new Error("scrollToAttribute not initialized yet!"); },
loading: true,
loadingSection: null,
restoreSection: () => { throw new Error("restoreSection not initialized yet!"); },
}
-type DatamodelViewAction =
+type DatamodelViewAction =
| { type: 'SET_CURRENT_GROUP', payload: string | null }
| { type: 'SET_CURRENT_SECTION', payload: string | null }
| { type: 'SET_SCROLL_TO_SECTION', payload: (sectionId: string) => void }
| { type: 'SET_SCROLL_TO_GROUP', payload: (groupName: string) => void }
| { type: 'SET_LOADING', payload: boolean }
| { type: 'SET_LOADING_SECTION', payload: string | null }
- | { type: 'SET_RESTORE_SECTION', payload: () => void };
+ | { type: 'SET_RESTORE_SECTION', payload: () => void }
+ | { type: 'SET_SCROLL_TO_ATTRIBUTE', payload: (sectionId: string, attrSchema: string) => void };
const datamodelViewReducer = (state: DatamodelViewState, action: DatamodelViewAction): DatamodelViewState => {
@@ -50,6 +53,8 @@ const datamodelViewReducer = (state: DatamodelViewState, action: DatamodelViewAc
return { ...state, loadingSection: action.payload }
case 'SET_RESTORE_SECTION':
return { ...state, restoreSection: action.payload }
+ case 'SET_SCROLL_TO_ATTRIBUTE':
+ return { ...state, scrollToAttribute: action.payload }
default:
return state;
}
@@ -70,7 +75,7 @@ export const DatamodelViewProvider = ({ children }: { children: ReactNode }) =>
try { datamodelViewState.scrollToSection(""); } catch { return; }
dispatch({ type: "SET_CURRENT_GROUP", payload: groupParam });
dispatch({ type: "SET_CURRENT_SECTION", payload: sectionParam });
- datamodelViewState.scrollToSection(sectionParam);
+ datamodelViewState.scrollToSection(sectionParam);
}, [datamodelViewState.scrollToSection])
return (
diff --git a/Website/contexts/EntityFiltersContext.tsx b/Website/contexts/EntityFiltersContext.tsx
new file mode 100644
index 0000000..fc03374
--- /dev/null
+++ b/Website/contexts/EntityFiltersContext.tsx
@@ -0,0 +1,55 @@
+'use client'
+
+import React, { createContext, useContext, useReducer, ReactNode, useCallback } from "react";
+
+export interface EntityFilterState {
+ hideStandardFields: boolean;
+ typeFilter: string;
+}
+
+interface EntityFiltersState {
+ filters: Map; // Map of entitySchemaName -> filter state
+}
+
+type EntityFiltersAction =
+ | { type: "SET_ENTITY_FILTERS"; entitySchemaName: string; filters: EntityFilterState }
+ | { type: "CLEAR_ENTITY_FILTERS"; entitySchemaName: string };
+
+const initialState: EntityFiltersState = {
+ filters: new Map(),
+};
+
+const EntityFiltersContext = createContext(initialState);
+const EntityFiltersDispatchContext = createContext>(() => { });
+
+const entityFiltersReducer = (state: EntityFiltersState, action: EntityFiltersAction): EntityFiltersState => {
+ switch (action.type) {
+ case "SET_ENTITY_FILTERS": {
+ const newFilters = new Map(state.filters);
+ newFilters.set(action.entitySchemaName, action.filters);
+ return { ...state, filters: newFilters };
+ }
+ case "CLEAR_ENTITY_FILTERS": {
+ const newFilters = new Map(state.filters);
+ newFilters.delete(action.entitySchemaName);
+ return { ...state, filters: newFilters };
+ }
+ default:
+ return state;
+ }
+};
+
+export const EntityFiltersProvider = ({ children }: { children: ReactNode }) => {
+ const [state, dispatch] = useReducer(entityFiltersReducer, initialState);
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export const useEntityFilters = () => useContext(EntityFiltersContext);
+export const useEntityFiltersDispatch = () => useContext(EntityFiltersDispatchContext);
diff --git a/Website/contexts/SearchPerformanceContext.tsx b/Website/contexts/SearchPerformanceContext.tsx
deleted file mode 100644
index 4825503..0000000
--- a/Website/contexts/SearchPerformanceContext.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-'use client'
-
-import React, { createContext, useContext, useRef, useCallback, ReactNode } from 'react';
-
-interface SearchPerformanceContextType {
- scheduleImmediateUpdate: (callback: () => void) => void;
- scheduleBackgroundUpdate: (callback: () => void) => void;
- cancelScheduledUpdate: (id: number) => void;
-}
-
-const SearchPerformanceContext = createContext(null);
-
-export const SearchPerformanceProvider = ({ children }: { children: ReactNode }) => {
- const immediateUpdatesRef = useRef void>>(new Set());
- const backgroundUpdatesRef = useRef void>>(new Set());
-
- const immediateUpdateMap = useRef