diff --git a/Website/components/datamodelview/Attributes.tsx b/Website/components/datamodelview/Attributes.tsx index 92fef61..ea1e7c6 100644 --- a/Website/components/datamodelview/Attributes.tsx +++ b/Website/components/datamodelview/Attributes.tsx @@ -7,7 +7,7 @@ import { useState } from "react" import { ArrowUpDown, ArrowUp, ArrowDown, EyeOff, Eye, Search, X } from "lucide-react" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./../ui/select" import { Input } from "../ui/input" -import { AttributeDetails } from "./../entity/AttributeDetails" +import { AttributeDetails } from "./entity/AttributeDetails" import BooleanAttribute from "./../attributes/BooleanAttribute" import ChoiceAttribute from "./../attributes/ChoiceAttribute" import DateTimeAttribute from "./../attributes/DateTimeAttribute" diff --git a/Website/components/datamodelview/DatamodelView.tsx b/Website/components/datamodelview/DatamodelView.tsx index 3115c48..b921aee 100644 --- a/Website/components/datamodelview/DatamodelView.tsx +++ b/Website/components/datamodelview/DatamodelView.tsx @@ -10,6 +10,8 @@ import { List } from "./List"; import { TimeSlicedSearch } from "./TimeSlicedSearch"; import React, { useState, useEffect, useRef, useCallback } from "react"; import { useDatamodelData, useDatamodelDataDispatch } from "@/contexts/DatamodelDataContext"; +import { updateURL } from "@/lib/url-utils"; +import { useSearchParams } from "next/navigation"; export function DatamodelView() { const dispatch = useSidebarDispatch(); @@ -38,6 +40,7 @@ function DatamodelViewContent() { // Calculate total search results const totalResults = filtered.length > 0 ? filtered.filter(item => item.type === 'entity').length : 0; + const initialLocalValue = useSearchParams().get('globalsearch') || ""; // Isolated search handlers - these don't depend on component state const handleSearch = useCallback((searchValue: string) => { @@ -49,6 +52,7 @@ function DatamodelViewContent() { datamodelDataDispatch({ type: "SET_FILTERED", payload: [] }); } } + updateURL({ query: { globalsearch: searchValue.length >= 3 ? searchValue : "" } }) datamodelDataDispatch({ type: "SET_SEARCH", payload: searchValue.length >= 3 ? searchValue : "" }); setCurrentSearchIndex(searchValue.length >= 3 ? 1 : 0); // Reset to first result when searching, 0 when cleared }, [groups, datamodelDataDispatch]); @@ -207,6 +211,7 @@ function DatamodelViewContent() { onLoadingChange={handleLoadingChange} onNavigateNext={handleNavigateNext} onNavigatePrevious={handleNavigatePrevious} + initialLocalValue={initialLocalValue} currentIndex={currentSearchIndex} totalResults={totalResults} /> diff --git a/Website/components/datamodelview/List.tsx b/Website/components/datamodelview/List.tsx index 8a66f44..8df6a5c 100644 --- a/Website/components/datamodelview/List.tsx +++ b/Website/components/datamodelview/List.tsx @@ -5,6 +5,7 @@ import { useVirtualizer } from '@tanstack/react-virtual'; import { Section } from "./Section"; import { useDatamodelData } from "@/contexts/DatamodelDataContext"; import { AttributeType, EntityType, GroupType } from "@/lib/Types"; +import { updateURL } from "@/lib/url-utils"; interface IListProps { } @@ -84,7 +85,7 @@ export const List = ({ }: IListProps) => { return item.type === 'group' ? 92 : 300; }, // Override scroll behavior to prevent jumping during tab switches - scrollToFn: (offset, options) => { + scrollToFn: (offset) => { // When switching tabs during search, don't change scroll position if (isTabSwitching.current && !isIntentionalScroll.current) { return; @@ -98,7 +99,6 @@ export const List = ({ }: IListProps) => { // Default scroll behavior for other cases const scrollElement = parentRef.current; if (scrollElement) { - console.log("setting scroll to: " + offset + " - " + options.adjustments) scrollElement.scrollTop = offset; } }, @@ -235,6 +235,7 @@ export const List = ({ }: IListProps) => { const item = flatItems[firstVisibleItem.index]; if (item?.type === 'entity') { if (item.entity.SchemaName !== datamodelView.currentSection) { + updateURL({ query: { group: item.group.Name, section: item.entity.SchemaName } }); dispatch({ type: "SET_CURRENT_GROUP", payload: item.group.Name }); dispatch({ type: "SET_CURRENT_SECTION", payload: item.entity.SchemaName }); } diff --git a/Website/components/datamodelview/Relationships.tsx b/Website/components/datamodelview/Relationships.tsx index b8782a1..887dd90 100644 --- a/Website/components/datamodelview/Relationships.tsx +++ b/Website/components/datamodelview/Relationships.tsx @@ -3,7 +3,7 @@ import { EntityType } from "@/lib/Types" import { TableHeader, TableRow, TableHead, TableBody, TableCell, Table } from "../ui/table" import { Button } from "../ui/button" -import { CascadeConfiguration } from "../entity/CascadeConfiguration" +import { CascadeConfiguration } from "./entity/CascadeConfiguration" import { useState } from "react" import { ArrowUpDown, ArrowUp, ArrowDown, Search, X } from "lucide-react" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select" diff --git a/Website/components/datamodelview/Section.tsx b/Website/components/datamodelview/Section.tsx index 56d2cbb..a7c274e 100644 --- a/Website/components/datamodelview/Section.tsx +++ b/Website/components/datamodelview/Section.tsx @@ -2,8 +2,8 @@ import { EntityType, GroupType } from "@/lib/Types" import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs" -import { EntityHeader } from "../entity/EntityHeader" -import { SecurityRoles } from "../entity/SecurityRoles" +import { EntityHeader } from "./entity/EntityHeader" +import { SecurityRoles } from "./entity/SecurityRoles" import Keys from "./Keys" import { KeyRound, Tags, Unplug } from "lucide-react" import { Attributes } from "./Attributes" @@ -28,7 +28,7 @@ export const Section = React.memo( // Handle tab changes to notify parent component const handleTabChange = React.useCallback((value: string) => { if (onTabChange) { - onTabChange(true); // Signal that tab switching is starting + onTabChange(true); } setTab(value); }, [onTabChange]); diff --git a/Website/components/datamodelview/TimeSlicedSearch.tsx b/Website/components/datamodelview/TimeSlicedSearch.tsx index 009a6d6..28b8b48 100644 --- a/Website/components/datamodelview/TimeSlicedSearch.tsx +++ b/Website/components/datamodelview/TimeSlicedSearch.tsx @@ -12,6 +12,7 @@ interface TimeSlicedSearchProps { onLoadingChange: (loading: boolean) => void; onNavigateNext?: () => void; onNavigatePrevious?: () => void; + initialLocalValue: string; currentIndex?: number; totalResults?: number; placeholder?: string; @@ -23,11 +24,12 @@ export const TimeSlicedSearch = ({ onLoadingChange, onNavigateNext, onNavigatePrevious, + initialLocalValue, currentIndex = 0, totalResults = 0, placeholder = "Search attributes...", }: TimeSlicedSearchProps) => { - const [localValue, setLocalValue] = useState(''); + const [localValue, setLocalValue] = useState(initialLocalValue); const [isTyping, setIsTyping] = useState(false); const [portalRoot, setPortalRoot] = useState(null); const [lastValidSearch, setLastValidSearch] = useState(''); diff --git a/Website/components/entity/AttributeDetails.tsx b/Website/components/datamodelview/entity/AttributeDetails.tsx similarity index 98% rename from Website/components/entity/AttributeDetails.tsx rename to Website/components/datamodelview/entity/AttributeDetails.tsx index 945d74f..cb282d7 100644 --- a/Website/components/entity/AttributeDetails.tsx +++ b/Website/components/datamodelview/entity/AttributeDetails.tsx @@ -2,7 +2,7 @@ import { AttributeType, CalculationMethods, RequiredLevel } from "@/lib/Types"; import { Calculator, CircleAlert, CirclePlus, Eye, Lock, Sigma, Zap } from "lucide-react"; -import { HybridTooltip, HybridTooltipContent, HybridTooltipTrigger } from "../ui/hybridtooltop"; +import { HybridTooltip, HybridTooltipContent, HybridTooltipTrigger } from "../../ui/hybridtooltop"; export function AttributeDetails({ attribute }: { attribute: AttributeType }) { const details = []; diff --git a/Website/components/entity/CascadeConfiguration.tsx b/Website/components/datamodelview/entity/CascadeConfiguration.tsx similarity index 100% rename from Website/components/entity/CascadeConfiguration.tsx rename to Website/components/datamodelview/entity/CascadeConfiguration.tsx diff --git a/Website/components/entity/EntityDetails.tsx b/Website/components/datamodelview/entity/EntityDetails.tsx similarity index 98% rename from Website/components/entity/EntityDetails.tsx rename to Website/components/datamodelview/entity/EntityDetails.tsx index fe7559c..e7a0237 100644 --- a/Website/components/entity/EntityDetails.tsx +++ b/Website/components/datamodelview/entity/EntityDetails.tsx @@ -2,7 +2,7 @@ import { EntityType, OwnershipType } from "@/lib/Types"; import { Eye, ClipboardList, Paperclip, Building, Users } from "lucide-react"; -import { HybridTooltip, HybridTooltipContent, HybridTooltipTrigger } from "../ui/hybridtooltop"; +import { HybridTooltip, HybridTooltipContent, HybridTooltipTrigger } from "../../ui/hybridtooltop"; type EntityDetailType = { icon: JSX.Element; diff --git a/Website/components/entity/EntityHeader.tsx b/Website/components/datamodelview/entity/EntityHeader.tsx similarity index 100% rename from Website/components/entity/EntityHeader.tsx rename to Website/components/datamodelview/entity/EntityHeader.tsx diff --git a/Website/components/entity/SecurityRoles.tsx b/Website/components/datamodelview/entity/SecurityRoles.tsx similarity index 98% rename from Website/components/entity/SecurityRoles.tsx rename to Website/components/datamodelview/entity/SecurityRoles.tsx index df1fc7f..ba1e98f 100644 --- a/Website/components/entity/SecurityRoles.tsx +++ b/Website/components/datamodelview/entity/SecurityRoles.tsx @@ -2,7 +2,7 @@ import { SecurityRole, PrivilegeDepth } from "@/lib/Types"; import { Ban, User, Users, Boxes, Building2, Minus } from "lucide-react"; -import { HybridTooltip, HybridTooltipContent, HybridTooltipTrigger } from "../ui/hybridtooltop"; +import { HybridTooltip, HybridTooltipContent, HybridTooltipTrigger } from "../../ui/hybridtooltop"; export function SecurityRoles({ roles }: { roles: SecurityRole[] }) { return ( diff --git a/Website/contexts/DatamodelDataContext.tsx b/Website/contexts/DatamodelDataContext.tsx index 708f8e8..744788d 100644 --- a/Website/contexts/DatamodelDataContext.tsx +++ b/Website/contexts/DatamodelDataContext.tsx @@ -2,6 +2,7 @@ import React, { createContext, useContext, useReducer, ReactNode } from "react"; import { GroupType } from "@/lib/Types"; +import { useSearchParams } from "next/navigation"; interface DatamodelDataState { groups: GroupType[]; @@ -36,7 +37,13 @@ const datamodelDataReducer = (state: DatamodelDataState, action: any): Datamodel export const DatamodelDataProvider = ({ children }: { children: ReactNode }) => { const [state, dispatch] = useReducer(datamodelDataReducer, initialState); + const searchParams = useSearchParams(); + const globalsearchParam = searchParams.get('globalsearch'); + React.useEffect(() => { + + dispatch({ type: "SET_SEARCH", payload: globalsearchParam || "" }); + const worker = new Worker(new URL("../components/datamodelview/dataLoaderWorker.js", import.meta.url)); worker.onmessage = (e) => { dispatch({ type: "SET_GROUPS", payload: e.data }); diff --git a/Website/contexts/DatamodelViewContext.tsx b/Website/contexts/DatamodelViewContext.tsx index 3d9b756..597ed2e 100644 --- a/Website/contexts/DatamodelViewContext.tsx +++ b/Website/contexts/DatamodelViewContext.tsx @@ -46,11 +46,17 @@ export const DatamodelViewProvider = ({ children }: { children: ReactNode }) => const [datamodelViewState, dispatch] = useReducer(datamodelViewReducer, initialState); const searchParams = useSearchParams(); - const entityParam = searchParams.get('section'); + const sectionParam = searchParams.get('section'); + const groupParam = searchParams.get('group'); + // on initial load set data from query params useEffect(() => { - dispatch({ type: "SET_CURRENT_GROUP", payload: entityParam }); - }, [entityParam]) + if (!sectionParam) return; + try { datamodelViewState.scrollToSection(""); } catch { return; } + dispatch({ type: "SET_CURRENT_GROUP", payload: groupParam }); + dispatch({ type: "SET_CURRENT_SECTION", payload: sectionParam }); + datamodelViewState.scrollToSection(sectionParam); + }, [datamodelViewState.scrollToSection]) return ( diff --git a/Website/lib/url-utils.ts b/Website/lib/url-utils.ts new file mode 100644 index 0000000..49457e5 --- /dev/null +++ b/Website/lib/url-utils.ts @@ -0,0 +1,40 @@ +type UpdateMode = "push" | "replace"; + +interface URLParts { + /** e.g. "/inbox/42" (must be same-origin; you can't change domain/protocol/port) */ + path?: string; + /** set value to null/undefined to remove a param */ + query?: Record; + /** e.g. "section-2" or "#section-2"; pass "" to clear; omit to leave unchanged */ + hash?: string; + /** optional state object you want back on popstate */ + state?: unknown; + /** optional document title (mostly ignored by browsers) */ + title?: string; +} + +export function updateURL( + { path, query, hash, state, title }: URLParts, + mode: UpdateMode = "push" +): void { + const url = new URL(window.location.href); + + if (typeof path === "string") { + url.pathname = path.startsWith("/") ? path : `/${path}`; + } + + if (query) { + for (const [k, v] of Object.entries(query)) { + if (v === null || v === undefined) url.searchParams.delete(k); + else url.searchParams.set(k, String(v)); + } + } + + if (hash !== undefined) { + url.hash = hash ? (hash.startsWith("#") ? hash : `#${hash}`) : ""; + } + + const method = mode === "push" ? "pushState" : "replaceState"; + window.history[method](state ?? {}, title ?? "", url); + if (title) document.title = title; // cross-browser title update +}