diff --git a/Generator/DataverseService.cs b/Generator/DataverseService.cs index 3bf58bd..7b95dd3 100644 --- a/Generator/DataverseService.cs +++ b/Generator/DataverseService.cs @@ -64,7 +64,6 @@ public DataverseService(IConfiguration configuration, ILogger var entityRootBehaviour = solutionComponents.Where(x => x.ComponentType == 1).ToDictionary(x => x.ObjectId, x => x.RootComponentBehavior); var attributesInSolution = solutionComponents.Where(x => x.ComponentType == 2).Select(x => x.ObjectId).ToHashSet(); var rolesInSolution = solutionComponents.Where(x => x.ComponentType == 20).Select(x => x.ObjectId).ToList(); - var pluginStepsInSolution = solutionComponents.Where(x => x.ComponentType == 92).Select(x => x.ObjectId).ToList(); var entitiesInSolutionMetadata = await GetEntityMetadata(entitiesInSolution); @@ -140,7 +139,7 @@ public DataverseService(IConfiguration configuration, ILogger .ToList(), RelevantManyToMany = x.ManyToManyRelationships - .Where(r => entityLogicalNamesInSolution.Contains(r.IntersectEntityName.ToLower())) + .Where(r => entityLogicalNamesInSolution.Contains(r.Entity1LogicalName) && entityLogicalNamesInSolution.Contains(r.Entity2LogicalName)) .ToList(), }) .Where(x => x.EntityMetadata.DisplayName.UserLocalizedLabel?.Label != null) @@ -215,16 +214,24 @@ private static Record MakeRecord( var manyToMany = relevantManyToMany .Where(x => logicalToSchema.ContainsKey(x.Entity1LogicalName) && logicalToSchema[x.Entity1LogicalName].IsInSolution) - .Select(x => new DTO.Relationship( - x.IsCustomRelationship ?? false, - x.Entity1AssociatedMenuConfiguration.Behavior == AssociatedMenuBehavior.UseLabel - ? x.Entity1AssociatedMenuConfiguration.Label.UserLocalizedLabel?.Label ?? x.Entity1NavigationPropertyName - : x.Entity1NavigationPropertyName, - logicalToSchema[x.Entity1LogicalName].Name, - "-", - x.SchemaName, - IsManyToMany: true, - null)) + .Select(x => + { + var useEntity1 = x.Entity1LogicalName == entity.LogicalName; + + var label = !useEntity1 + ? x.Entity1AssociatedMenuConfiguration.Label.UserLocalizedLabel?.Label ?? x.Entity1NavigationPropertyName + : x.Entity2AssociatedMenuConfiguration.Label.UserLocalizedLabel?.Label ?? x.Entity2NavigationPropertyName; + + return new DTO.Relationship( + x.IsCustomRelationship ?? false, + label, + logicalToSchema[!useEntity1 ? x.Entity1LogicalName : x.Entity2LogicalName].Name, + "-", + x.SchemaName, + IsManyToMany: true, + null + ); + }) .ToList(); Dictionary tablegroups = []; // logicalname -> group diff --git a/Website/app/api/markdown/route.ts b/Website/app/api/markdown/route.ts index a0037a6..2b7cd4e 100644 --- a/Website/app/api/markdown/route.ts +++ b/Website/app/api/markdown/route.ts @@ -1,16 +1,20 @@ import { NextResponse } from 'next/server' -import { readFileSync } from "fs"; +import { existsSync, readFileSync } from "fs"; import { join } from "path"; export async function GET() { const generatedPath = join(process.cwd(), 'generated', 'Introduction.md'); - const stubsPath = join(process.cwd(), 'stubs', 'Introduction.md'); - let fileContent; + + if (!existsSync(generatedPath)) { + console.error(`File not found at path: ${generatedPath}`); + return NextResponse.json({ error: 'File not found' }, { status: 404 }); + } + + let fileContent: string; try { fileContent = readFileSync(generatedPath, 'utf-8'); - } catch (error) { - fileContent = readFileSync(stubsPath, 'utf-8'); - console.error('Error reading generated wiki file, falling back to stubs:', error); + } catch { + return NextResponse.json({ error: 'File not found' }, { status: 404 }); } return NextResponse.json({ fileContent }) } diff --git a/Website/components/aboutview/AboutView.tsx b/Website/components/aboutview/AboutView.tsx index 6f6e0e4..4f579df 100644 --- a/Website/components/aboutview/AboutView.tsx +++ b/Website/components/aboutview/AboutView.tsx @@ -25,12 +25,19 @@ export const AboutView = ({}: IAboutViewProps) => { {/* Logo */} - + + + + DATA MODEL + VIEWER + @ DELEGATE | CONTEXT& + + {/* What is DMV */} diff --git a/Website/components/datamodelview/Attributes.tsx b/Website/components/datamodelview/Attributes.tsx index cebe4f8..0136830 100644 --- a/Website/components/datamodelview/Attributes.tsx +++ b/Website/components/datamodelview/Attributes.tsx @@ -388,7 +388,7 @@ export const Attributes = ({ entity, onVisibleCountChange, search = "" }: IAttri {highlightMatch(attribute.SchemaName, highlightTerm)} {getAttributeComponent(entity, attribute, highlightMatch, highlightTerm)} - + {highlightMatch(attribute.Description ?? "", highlightTerm)} diff --git a/Website/components/datamodelview/List.tsx b/Website/components/datamodelview/List.tsx index 303ff3a..21d3ebc 100644 --- a/Website/components/datamodelview/List.tsx +++ b/Website/components/datamodelview/List.tsx @@ -8,7 +8,7 @@ import { AttributeType, EntityType, GroupType } from "@/lib/Types"; import { updateURL } from "@/lib/url-utils"; import { copyToClipboard, generateGroupLink } from "@/lib/clipboard-utils"; import { useSnackbar } from "@/contexts/SnackbarContext"; -import { debounce, Tooltip } from '@mui/material'; +import { Box, CircularProgress, debounce, Tooltip } from '@mui/material'; interface IListProps { setCurrentIndex: (index: number) => void; @@ -24,16 +24,13 @@ export function highlightMatch(text: string, search: string) { export const List = ({ setCurrentIndex }: IListProps) => { const dispatch = useDatamodelViewDispatch(); - const { currentSection } = useDatamodelView(); + const { currentSection, loadingSection } = useDatamodelView(); const { groups, filtered, search } = useDatamodelData(); const { showSnackbar } = useSnackbar(); const parentRef = useRef(null); // used to relocate section after search/filter const [sectionVirtualItem, setSectionVirtualItem] = useState(null); - - // Track position before search for restoration - const isTabSwitching = useRef(false); - + const handleCopyGroupLink = useCallback(async (groupName: string) => { const link = generateGroupLink(groupName); const success = await copyToClipboard(link); @@ -163,8 +160,7 @@ export const List = ({ setCurrentIndex }: IListProps) => { } rowVirtualizer.scrollToIndex(sectionIndex, { - align: 'start', - behavior: 'smooth' + align: 'start' }); }, [flatItems]); @@ -180,8 +176,7 @@ export const List = ({ setCurrentIndex }: IListProps) => { } rowVirtualizer.scrollToIndex(groupIndex, { - align: 'start', - behavior: 'smooth' + align: 'start' }); }, [flatItems]); @@ -208,85 +203,85 @@ export const List = ({ setCurrentIndex }: IListProps) => { }, [rowVirtualizer]); return ( -
+ <> + + + +
- {/* Show no results message when searching but no items found */} - {flatItems.length === 0 && search && search.length >= 3 && ( -
-
No tables found
-
- No attributes match your search for "{search}" + {/* Show no results message when searching but no items found */} + {flatItems.length === 0 && search && search.length >= 3 && ( +
+
No tables found
+
+ No attributes match your search for "{search}" +
-
- )} - - {/* Virtualized list */} -
- {rowVirtualizer.getVirtualItems().map((virtualItem) => { - const item = flatItems[virtualItem.index]; + )} + + {/* Virtualized list */} +
+ {rowVirtualizer.getVirtualItems().map((virtualItem) => { + const item = flatItems[virtualItem.index]; - return ( -
{ - if (el) { - // trigger remeasurement when content changes and load - requestAnimationFrame(() => { - handleSectionResize(virtualItem.index); - }); - } - }} - > - {item.type === 'group' ? ( -
-
- -
handleCopyGroupLink(item.group.Name)} - > - {item.group.Name} -
-
-
-
- ) : ( -
-
{ - isTabSwitching.current = isChanging; - if (isChanging) { - // Reset after a short delay to allow for the content change - setTimeout(() => { - isTabSwitching.current = false; - }, 100); - } - }} - search={search} - /> -
- )} -
- ); - })} + return ( +
{ + if (el) { + // trigger remeasurement when content changes and load + requestAnimationFrame(() => { + handleSectionResize(virtualItem.index); + }); + } + }} + > + {item.type === 'group' ? ( +
+
+ +
handleCopyGroupLink(item.group.Name)} + > + {item.group.Name} +
+
+
+
+ ) : ( +
+
{ + + }} + search={search} + /> +
+ )} +
+ ); + })} +
-
+ ); }; diff --git a/Website/components/datamodelview/Relationships.tsx b/Website/components/datamodelview/Relationships.tsx index 458b29d..daa4c6b 100644 --- a/Website/components/datamodelview/Relationships.tsx +++ b/Website/components/datamodelview/Relationships.tsx @@ -368,7 +368,8 @@ export const Relationships = ({ entity, onVisibleCountChange, search = "" }: IRe color: 'primary.main' }} onClick={() => { - dispatch({ type: "SET_CURRENT_SECTION", payload: relationship.TableSchema }) + dispatch({ type: 'SET_LOADING_SECTION', payload: relationship.TableSchema }); + dispatch({ type: "SET_CURRENT_SECTION", payload: relationship.TableSchema }); scrollToSection(relationship.TableSchema); }} > diff --git a/Website/components/datamodelview/attributes/ChoiceAttribute.tsx b/Website/components/datamodelview/attributes/ChoiceAttribute.tsx index 74881a8..4c8c47a 100644 --- a/Website/components/datamodelview/attributes/ChoiceAttribute.tsx +++ b/Website/components/datamodelview/attributes/ChoiceAttribute.tsx @@ -2,7 +2,7 @@ import { useIsMobile } from "@/hooks/use-mobile" import { ChoiceAttributeType } from "@/lib/Types" import { formatNumberSeperator } from "@/lib/utils" import { Box, Typography, Chip } from "@mui/material" -import { CheckBoxOutlineBlankRounded, CheckBoxRounded, CheckRounded } from "@mui/icons-material" +import { CheckBoxOutlineBlankRounded, CheckBoxRounded, CheckRounded, RadioButtonCheckedRounded, RadioButtonUncheckedRounded } from "@mui/icons-material" export default function ChoiceAttribute({ attribute, highlightMatch, highlightTerm }: { attribute: ChoiceAttributeType, highlightMatch: (text: string, term: string) => string | React.JSX.Element, highlightTerm: string }) { @@ -38,9 +38,9 @@ export default function ChoiceAttribute({ attribute, highlightMatch, highlightTe ) : ( // For single-select, show radio buttons option.Value === attribute.DefaultValue ? ( - + ) : ( - + ) )} {highlightMatch(option.Name, highlightTerm)} diff --git a/Website/components/datamodelview/attributes/LookupAttribute.tsx b/Website/components/datamodelview/attributes/LookupAttribute.tsx index 3cc24bf..0534271 100644 --- a/Website/components/datamodelview/attributes/LookupAttribute.tsx +++ b/Website/components/datamodelview/attributes/LookupAttribute.tsx @@ -1,11 +1,12 @@ import { LookupAttributeType } from "@/lib/Types" -import { useDatamodelView } from "@/contexts/DatamodelViewContext" +import { useDatamodelView, useDatamodelViewDispatch } from "@/contexts/DatamodelViewContext" import { Box, Typography, Button, Chip } from "@mui/material" import { ContentPasteOffRounded, ContentPasteSearchRounded } from "@mui/icons-material"; export default function LookupAttribute({ attribute }: { attribute: LookupAttributeType }) { const { scrollToSection } = useDatamodelView(); + const dispatch = useDatamodelViewDispatch(); return ( @@ -18,7 +19,11 @@ export default function LookupAttribute({ attribute }: { attribute: LookupAttrib variant="outlined" size="small" startIcon={} - onClick={() => scrollToSection(target.Name)} + onClick={() => { + dispatch({ type: 'SET_LOADING_SECTION', payload: target.Name }); + dispatch({ type: "SET_CURRENT_SECTION", payload: target.Name }); + scrollToSection(target.Name); + }} sx={{ fontSize: { xs: '0.625rem', md: '0.875rem' }, height: { xs: '16px', md: '24px' }, diff --git a/Website/components/datamodelview/entity/AttributeDetails.tsx b/Website/components/datamodelview/entity/AttributeDetails.tsx index c3b1969..1bbde84 100644 --- a/Website/components/datamodelview/entity/AttributeDetails.tsx +++ b/Website/components/datamodelview/entity/AttributeDetails.tsx @@ -1,10 +1,10 @@ 'use client' -import { AttributeType, CalculationMethods, ComponentType, RequiredLevel } from "@/lib/Types"; -import { AccountTreeRounded, AddCircleOutlineRounded, CalculateRounded, ElectricBoltRounded, ErrorRounded, FunctionsRounded, JavascriptRounded, LockRounded, VisibilityRounded } from "@mui/icons-material"; -import { Tooltip } from "@mui/material"; +import { AttributeType, CalculationMethods, RequiredLevel } from "@/lib/Types"; +import { AddCircleOutlineRounded, CalculateRounded, ElectricBoltRounded, ErrorRounded, FunctionsRounded, LockRounded, VisibilityRounded } from "@mui/icons-material"; +import { Link, Tooltip } from "@mui/material"; -export function AttributeDetails({ attribute }: { attribute: AttributeType }) { +export function AttributeDetails({ entityName, attribute }: { entityName: string, attribute: AttributeType }) { const details = []; switch (attribute.RequiredLevel) { @@ -34,21 +34,15 @@ export function AttributeDetails({ attribute }: { attribute: AttributeType }) { details.push({ icon: , tooltip: "Field Security" }); } - if (attribute.AttributeUsages.some(a => a.ComponentType == ComponentType.Plugin)) { - const tooltip = `Plugins ${attribute.AttributeUsages.filter(au => au.ComponentType == ComponentType.Plugin).map(au => au.Name).join(", ")}`; - details.push({ icon: , tooltip }); + if (attribute.AttributeUsages.length > 0) { + const tooltip = Processes ${attribute.AttributeUsages.map(au => au.Name).join(", ")}.
Click to see more details.
; + details.push({ icon: ( + + + + ), tooltip }); } - - if (attribute.AttributeUsages.some(a => a.ComponentType == ComponentType.PowerAutomateFlow)) { - const tooltip = `Power Automate Flows ${attribute.AttributeUsages.filter(au => au.ComponentType == ComponentType.PowerAutomateFlow).map(au => au.Name).join(", ")}`; - details.push({ icon: , tooltip }); - } - - if (attribute.AttributeUsages.some(a => a.ComponentType == ComponentType.WebResource)) { - const tooltip = `Web Resources ${attribute.AttributeUsages.filter(au => au.ComponentType == ComponentType.WebResource).map(au => au.Name).join(", ")}`; - details.push({ icon: , tooltip }); - } - + return (
{details.map((detail, index) => ( diff --git a/Website/components/datamodelview/entity/EntityHeader.tsx b/Website/components/datamodelview/entity/EntityHeader.tsx index 289d9e3..907176a 100644 --- a/Website/components/datamodelview/entity/EntityHeader.tsx +++ b/Website/components/datamodelview/entity/EntityHeader.tsx @@ -30,9 +30,10 @@ export function EntityHeader({ entity }: { entity: EntityType }) { pr: { xl: 3, xs: 0 } }} > - + - + { const router = useRouter(); - const [wikipage, setWikipage] = useState(''); + const [wikipage, setWikipage] = useState(''); const [currentCarouselIndex, setCurrentCarouselIndex] = useState(0); const [slideDirection, setSlideDirection] = useState<'left' | 'right' | null>(null); @@ -65,12 +65,24 @@ export const HomeView = ({ }: IHomeViewProps) => { setElement(null); close(); fetch('/api/markdown') - .then(res => res.json()) - .then(data => setWikipage(data.fileContent.replace(/\\n/g, '\n'))); + .then(res => { + return res.json(); + }) + .then(data => { + if (data.fileContent) { + setWikipage(data.fileContent.replace(/\\n/g, '\n')); + } else { + setWikipage(undefined); + } + }) + .catch(error => { + console.error('Error fetching wiki page:', error); + setWikipage(undefined); + }); }, []); return ( - + { - - {wikipage ? ( -

, - h2: ({ ...props }) =>

, - h3: ({ ...props }) =>

, - h4: ({ ...props }) =>

, - p: ({ ...props }) =>

, - a: ({ ...props }) => , - li: ({ ...props }) =>

  • , - span: ({ ...props }) => , - img: ({ ...props }) => , - }}>{wikipage} - ) : ( -
    Loading wiki...
    - )} - + { + wikipage !== undefined && ( + + {wikipage.length > 0 ? ( +

    , + h2: ({ ...props }) =>

    , + h3: ({ ...props }) =>

    , + h4: ({ ...props }) =>

    , + p: ({ ...props }) =>

    , + a: ({ ...props }) => , + li: ({ ...props }) =>

  • , + span: ({ ...props }) => , + img: ({ ...props }) => , + }}>{wikipage} + ) : ( + Loading wiki page... + )} + + ) + } diff --git a/Website/components/loginview/LoginView.tsx b/Website/components/loginview/LoginView.tsx index 9694574..f52fed5 100644 --- a/Website/components/loginview/LoginView.tsx +++ b/Website/components/loginview/LoginView.tsx @@ -120,7 +120,14 @@ const LoginView = ({ }: LoginViewProps) => { )}
    - + Password { const [searchTerm, setSearchTerm] = useState('') const [isSearching, setIsSearching] = useState(false) const [selectedAttribute, setSelectedAttribute] = useState(null) + const initialAttribute = useSearchParams().get('attr') || ""; + const initialEntity = useSearchParams().get('ent') || ""; useEffect(() => { setElement(null); close(); - }, [setElement, close]) + + if (initialAttribute && initialEntity && groups.length > 0) { + const d = groups.flatMap(group => + group.Entities.flatMap(entity => + entity.Attributes.map(attribute => ({ + attribute, + entity, + group: group.Name, + })) + ) + ); + const foundAttribute = d.find(result => + result.attribute.SchemaName === initialAttribute + && result.entity.SchemaName === initialEntity); + if (foundAttribute) { + setSelectedAttribute(foundAttribute); + } + } + }, [groups]) const typeDistribution = useMemo(() => { return groups.reduce((acc, group) => { @@ -111,13 +132,13 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { // Simulate search delay for UX useEffect(() => { if (searchTerm.trim() && searchTerm.length >= 2) { - setIsSearching(true) - const timer = setTimeout(() => { - setIsSearching(false) - }, 300) - return () => clearTimeout(timer) + setIsSearching(true) + const timer = setTimeout(() => { + setIsSearching(false) + }, 300) + return () => clearTimeout(timer) } else { - setIsSearching(false) + setIsSearching(false) } }, [searchTerm]) diff --git a/Website/components/shared/Header.tsx b/Website/components/shared/Header.tsx index c8449d6..75c512e 100644 --- a/Website/components/shared/Header.tsx +++ b/Website/components/shared/Header.tsx @@ -2,7 +2,7 @@ import { useLoading } from '@/hooks/useLoading'; import { useAuth } from '@/contexts/AuthContext'; import { useSettings } from '@/contexts/SettingsContext'; import { useRouter } from 'next/navigation'; -import { AppBar, Toolbar, Box, LinearProgress, Button, Badge, Stack } from '@mui/material'; +import { AppBar, Toolbar, Box, LinearProgress, Button, Stack } from '@mui/material'; import SettingsPane from './elements/SettingsPane'; import { useIsMobile } from '@/hooks/use-mobile'; import { useSidebar } from '@/contexts/SidebarContext'; @@ -87,12 +87,10 @@ const Header = ({ }: HeaderProps) => { className='flex flex-col items-center p-1.5 rounded-lg text-gray-400 min-w-0' onClick={handleSettingsClick} > - - - - - - + + + + {isAuthenticated && ( <> diff --git a/Website/components/shared/Sidebar.tsx b/Website/components/shared/Sidebar.tsx index a09c7ad..e29e631 100644 --- a/Website/components/shared/Sidebar.tsx +++ b/Website/components/shared/Sidebar.tsx @@ -34,7 +34,6 @@ const Sidebar = ({ }: SidebarProps) => { href: '/', icon: , active: pathname === '/', - new: true, }, { label: 'Insights', @@ -60,7 +59,6 @@ const Sidebar = ({ }: SidebarProps) => { href: '/processes', icon: , active: pathname === '/processes', - new: true, } ]; diff --git a/Website/public/DMVLOGOHORZ.svg b/Website/public/DMVLOGOHORZ.svg deleted file mode 100644 index 109a38e..0000000 --- a/Website/public/DMVLOGOHORZ.svg +++ /dev/null @@ -1 +0,0 @@ -@Delegate \ No newline at end of file