diff --git a/package-lock.json b/package-lock.json index 8ec49e6..523d45f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13632,7 +13632,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/src/App.tsx b/src/App.tsx index 0582fd5..0a409c7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { IntlProvider } from 'react-intl'; import { getMessagesForLocale } from './localization/localization'; import { LayoutContainer } from './components/LayoutContainer'; import { SettingsProvider } from './components/SettingsContext'; +import { TemplatesProvider } from './components/TemplatesContext'; const App = () => { const [locale, setLocale] = useState('en'); @@ -25,7 +26,9 @@ const App = () => { return ( - + + + ); diff --git a/src/assets/home.ts b/src/assets/home.ts index 7a6f831..e3a8423 100644 --- a/src/assets/home.ts +++ b/src/assets/home.ts @@ -61,3 +61,5 @@ export const graphs = [ Thumbnail: img } ]; + +export const templates = graphs; \ No newline at end of file diff --git a/src/components/Common/Tooltip.tsx b/src/components/Common/Tooltip.tsx index 63422a3..c4716e3 100644 --- a/src/components/Common/Tooltip.tsx +++ b/src/components/Common/Tooltip.tsx @@ -1,46 +1,81 @@ import { useState, useRef, useEffect, CSSProperties } from 'react'; import Portal from './Portal'; // Import your Portal component -export const Tooltip = ({ children, content, verticalOffset = 12 }: Tooltip) => { +export const Tooltip = ({ children, content, verticalOffset = 12, position: positionProp = 'below' }: Tooltip) => { const [show, setShow] = useState(false); const [position, setPosition] = useState({}); - const tooltipRef = useRef(null); - const contentRef = useRef(null); // Ref for the tooltip content + const tooltipRef = useRef(null); + const contentRef = useRef(null); useEffect(() => { - if (tooltipRef.current && contentRef.current && show) { + if (!tooltipRef.current || !contentRef.current || !show) { + return; + } + + const updatePosition = () => { + if (!tooltipRef.current || !contentRef.current) { + return; + } + const targetRect = tooltipRef.current.getBoundingClientRect(); const tooltipRect = contentRef.current.getBoundingClientRect(); - - let left = targetRect.left + window.scrollX + (targetRect.width / 2); // Center align - const top = targetRect.bottom + window.scrollY + verticalOffset; - // Check if the tooltip is going off the right side of the screen - if (left + tooltipRect.width > window.innerWidth) { - left = window.innerWidth - tooltipRect.width / 2 - 10; // Adjust to keep it on screen + let left: number; + let top: number; + + if (positionProp === 'right') { + left = targetRect.right + window.scrollX + verticalOffset; + top = targetRect.top + window.scrollY + (targetRect.height / 2) - (tooltipRect.height / 2); + + if (left + tooltipRect.width > window.innerWidth + window.scrollX) { + left = targetRect.left + window.scrollX - tooltipRect.width - verticalOffset; + } + } else { + left = targetRect.left + window.scrollX + (targetRect.width / 2); + top = targetRect.bottom + window.scrollY + verticalOffset; + + if (left + tooltipRect.width / 2 > window.innerWidth + window.scrollX) { + left = window.innerWidth + window.scrollX - tooltipRect.width / 2 - 10; + } + + if (left - tooltipRect.width / 2 < window.scrollX) { + left = window.scrollX + tooltipRect.width / 2 + 10; + } + } + + if (top < window.scrollY) { + top = window.scrollY + 10; } - // Check if the tooltip is going off the left side of the screen - if (left - tooltipRect.width / 2 < 0) { - left += 10; // Adjust to keep it on screen + + if (top + tooltipRect.height > window.innerHeight + window.scrollY) { + top = window.innerHeight + window.scrollY - tooltipRect.height - 10; } setPosition({ top: top, left: left, - position: 'absolute' + position: 'absolute', + transform: positionProp === 'right' ? 'none' : 'translateX(-50%)' }); + }; + + if (window.requestAnimationFrame) { + const animationFrame = window.requestAnimationFrame(updatePosition); + return () => window.cancelAnimationFrame?.(animationFrame); } - }, [show, content, verticalOffset]); // Added 'content' to dependencies array + + updatePosition(); + }, [show, content, verticalOffset, positionProp]); return ( - setShow(true)} + setShow(true)} onMouseLeave={() => setShow(false)} ref={tooltipRef}> {children} {show && ( -
+
{content}
diff --git a/src/components/Recent/GraphTable.tsx b/src/components/Recent/GraphTable.tsx index 5517996..840d28f 100644 --- a/src/components/Recent/GraphTable.tsx +++ b/src/components/Recent/GraphTable.tsx @@ -13,11 +13,11 @@ export const GraphTable = ({ columns, data, onRowClick }: GraphTable) => { }), [] ); - const { - getTableProps, - getTableBodyProps, - headerGroups, - rows, + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, prepareRow, resetResizing } = useTable( @@ -27,7 +27,7 @@ export const GraphTable = ({ columns, data, onRowClick }: GraphTable) => { defaultColumn }, useFlexLayout, - useResizeColumns + useResizeColumns ); const handleRowClick = (row:Row) => { @@ -42,7 +42,6 @@ export const GraphTable = ({ columns, data, onRowClick }: GraphTable) => {
- {console.log(headerGroups)} {headerGroups.map((headerGroup) => ( {headerGroup.headers.map((column: any, columnIndex: number) => ( diff --git a/src/components/Recent/PageRecent.tsx b/src/components/Recent/PageRecent.tsx index 57ef6a7..b9219c2 100644 --- a/src/components/Recent/PageRecent.tsx +++ b/src/components/Recent/PageRecent.tsx @@ -5,27 +5,31 @@ import { CustomNameCellRenderer } from './CustomNameCellRenderer'; import { CustomLocationCellRenderer } from './CustomLocationCellRenderer'; import { CustomAuthorCellRenderer } from './CustomAuthorCellRenderer'; import { GraphTable } from './GraphTable'; -import { GridViewIcon, ListViewIcon } from '../Common/CustomIcons'; +import { GridViewIcon, ListViewIcon, QuestionMarkIcon } from '../Common/CustomIcons'; import { openFile, saveHomePageSettings } from '../../functions/utility'; import { FormattedMessage } from 'react-intl'; import { Tooltip } from '../Common/Tooltip'; import { useSettings } from '../SettingsContext'; +import { useTemplates } from '../TemplatesContext'; -export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => { +export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => { const { settings, updateSettings } = useSettings(); - const [viewMode, setViewMode] = useState(recentPageViewMode); + const templates = useTemplates(); + const [viewMode, setViewMode] = useState(recentPageViewMode); + const [templatesViewMode, setTemplatesViewMode] = useState(settings?.templatesPageViewMode || 'grid'); const [initialized, setInitialized] = useState(false); + const [templatesInitialized, setTemplatesInitialized] = useState(false); - // Set a placeholder for the graphs which will be used differently during dev and prod + // Set a placeholder for the graphs which will be used differently during dev and prod let initialGraphs = []; - + // If we are under development, we will load the graphs from the local asset folder if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line @typescript-eslint/no-require-imports initialGraphs = require('../../assets/home').graphs; } - const [graphs, setGraphs] = useState(initialGraphs); + const [graphs, setGraphs] = useState(initialGraphs); // A method exposed to the backend used to set the graph data coming from Dynamo const receiveGraphDataFromDotNet = (jsonData) => { @@ -50,31 +54,49 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => delete window.receiveGraphDataFromDotNet; } }; - }, []); + }, []); useEffect(() => { // Set the viewMode based on the HomePage preferences setViewMode(recentPageViewMode); - }, [recentPageViewMode]); + }, [recentPageViewMode]); + + useEffect(() => { + // Set the templatesViewMode based on the HomePage preferences + if (settings?.templatesPageViewMode) { + setTemplatesViewMode(settings.templatesPageViewMode); + } + }, [settings?.templatesPageViewMode]); useEffect(() => { if (initialized || recentPageViewMode !== viewMode) { setInitialized(true); updateSettings({ recentPageViewMode: viewMode }); - + // Send settings to Dynamo to save saveHomePageSettings({ ...settings, recentPageViewMode: viewMode }); - } + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [viewMode]); + useEffect(() => { + if (templatesInitialized || (settings?.templatesPageViewMode && settings.templatesPageViewMode !== templatesViewMode)) { + setTemplatesInitialized(true); + updateSettings({ templatesPageViewMode: templatesViewMode }); + + // Send settings to Dynamo to save + saveHomePageSettings({ ...settings, templatesPageViewMode: templatesViewMode }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [templatesViewMode]); + // This variable defins the table structure displaying the graphs const columns: Column[] = React.useMemo(() => [ { Header: 'Title', accessor: 'Caption', width: 300, - resizable: true, + resizable: true, Cell: CustomNameCellRenderer, }, { @@ -98,15 +120,33 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => // Handles mouse click over each row const handleRowClick = (row: Row) => { - // freezes the UI - setIsDisabled(true); - - const contextData = row.original.ContextData; + // freezes the UI + setIsDisabled(true); + + const contextData = row.original.ContextData; + openFile(contextData); + }; + + // Handles mouse click over each template row + const handleTemplateRowClick = (row: Row) => { + // freezes the UI + setIsDisabled(true); + + const contextData = row.original.ContextData; openFile(contextData); }; + // Map templates to match Graph structure for table view (templates use 'date' instead of 'DateModified') + const templatesForTable = templates.map(template => ({ + ...template, + DateModified: template.date || template.DateModified || '', + Author: template.Author || '', + Description: template.Description || '' + })); + return(
+ {/* Recent Section */}

@@ -133,7 +173,7 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) =>
{viewMode === 'list' && ( - )} + )} {viewMode === 'grid' && (
{graphs.map(graph => ( @@ -142,6 +182,54 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) =>
)}
+ + {/* Templates Section */} +
+

+ +

+ } position="right"> + + +
+
+ + +
+
+ {templatesViewMode === 'list' && ( + + )} + {templatesViewMode === 'grid' && ( +
+ {templates.map(template => ( + + ))} +
+ )} +
); -}; \ No newline at end of file +}; diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index add6a4b..1a7c8ed 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -7,19 +7,19 @@ import styles from './Sidebar.module.css'; export const Sidebar = ({ onItemSelect, selectedSidebarItem }: Sidebar) => { const isSelected = (item: string) => selectedSidebarItem === item; - /**Trigger the backend command based on the drop-down value */ + /**Trigger the backend command based on the drop-down value */ const setSelectedValue = (value: SidebarCommand) => { sideBarCommand(value); }; return ( - +

Dynamo

{/* Files Dropdown */} - } onSelectionChange={setSelectedValue} @@ -31,7 +31,7 @@ export const Sidebar = ({ onItemSelect, selectedSidebarItem }: Sidebar) => { /> {/* New Dropdown */} - } onSelectionChange={setSelectedValue} @@ -43,21 +43,21 @@ export const Sidebar = ({ onItemSelect, selectedSidebarItem }: Sidebar) => {
onItemSelect('Recent')} data-testid="nav-recent"> - }> + }>
onItemSelect('Samples')} data-testid="nav-samples"> - }> + }>
onItemSelect('Learning')} data-testid="nav-learning"> - }> + }> @@ -78,4 +78,4 @@ export const Sidebar = ({ onItemSelect, selectedSidebarItem }: Sidebar) => {
); -}; \ No newline at end of file +}; diff --git a/src/components/TemplatesContext.tsx b/src/components/TemplatesContext.tsx new file mode 100644 index 0000000..7c216c9 --- /dev/null +++ b/src/components/TemplatesContext.tsx @@ -0,0 +1,52 @@ +import { createContext, useContext, useState, useEffect } from 'react'; + +// Create the context +const TemplatesContext = createContext([]); + +// Provider component that wraps the app components +export const TemplatesProvider = ({ children }) => { + // Set a placeholder for the templates which will be used differently during dev and prod + let initialTemplates = []; + + // If we are under development, we will load the templates from the local asset folder + if (process.env.NODE_ENV === 'development') { + initialTemplates = require('../assets/home').templates; + } + + const [templates, setTemplates] = useState(initialTemplates); + + // Set up the backend handler once in the provider + useEffect(() => { + // If we are under production, we will set up the handler for templates data from Dynamo + if (process.env.NODE_ENV !== 'development') { + // A method exposed to the backend used to set the templates data coming from Dynamo + window.receiveTemplatesDataFromDotNet = (jsonData: any) => { + try { + // jsonData is already an object, so no need to parse it + const data = jsonData || []; + setTemplates(data); + } catch (error) { + console.error('Error processing templates data:', error); + } + }; + } + + // Cleanup function + return () => { + if (process.env.NODE_ENV !== 'development') { + delete window.receiveTemplatesDataFromDotNet; + } + }; + }, []); + + return ( + + {children} + + ); +} + +// Use templates hook +export function useTemplates() { + return useContext(TemplatesContext); +} diff --git a/src/index.css b/src/index.css index e06d67b..ef5b329 100644 --- a/src/index.css +++ b/src/index.css @@ -187,6 +187,10 @@ select { display: inline-block; /* or 'inline' depending on layout needs */ } +.tooltip-wrapper { + display: inline; +} + .tooltip-box { position: absolute; background-color: #eeeeee; @@ -195,10 +199,7 @@ select { font-size: small; border-radius: 2px; z-index: 1000; - top: 100%; - left: 50%; - max-width: 300px; - transform: translateX(-50%); + max-width: 500px; /* Increased width to make it wider */ white-space: normal; overflow-wrap: break-word; opacity: 0; /* Initially, set the opacity to 0 to hide the tooltip */ @@ -214,12 +215,36 @@ select { position: absolute; bottom: 100%; /* Align it at the bottom of the tooltip box */ left: 50%; + width: 0; + height: 0; border-width: 5px; border-style: solid; - border-color: transparent transparent #eeeeee transparent; + border-color: transparent transparent #eeeeee transparent; /* Point upward */ transform: translateX(-50%); } +/* Arrow pointing left (for tooltips on the right side) */ +.tooltip-box.arrow-left .tooltip-arrow { + bottom: auto; + left: -10px; /* Position on the left side of the tooltip box, moved further left */ + top: 50%; + width: 0; + height: 0; + border-color: transparent #eeeeee transparent transparent; /* Point left (toward the title) */ + transform: translateY(-50%); +} + +/* Arrow pointing right (for tooltips on the left side) */ +.tooltip-box.arrow-right .tooltip-arrow { + bottom: auto; + right: -5px; /* Position on the right side of the tooltip box */ + top: 50%; + width: 0; + height: 0; + border-color: transparent transparent transparent #eeeeee; /* Point right (toward the title) */ + transform: translateY(-50%); +} + /* Remove background color of the scrollbar track */ ::-webkit-scrollbar { width: 14px; /* Adjust the width as needed */ diff --git a/src/locales/en.json b/src/locales/en.json index ad14596..97e1b20 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -7,11 +7,13 @@ "button.title.text.workspace": "Workspace", "button.title.text.custom.node": "Custom Node", "title.text.recent": "Recent", + "title.text.templates": "Templates", "title.text.samples": "Samples", "title.text.learning": "Learning", "tooltip.text.recent": "View recent files", "tooltip.text.samples": "View sample graphs", "tooltip.text.learning": "View learning content", + "tooltip.text.templates": "Templates are workspaces with pre-built graph elements that help you start and organize workflows more efficiently. Opening a template creates a new editable graph that includes templated graph elements.", "tooltip.text.grid.view.button": "Grid view", "tooltip.text.list.view.button": "List view", "learning.title.text.learning": "Learning", diff --git a/tsconfig.json b/tsconfig.json index f7f83bc..7788fff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,8 +31,8 @@ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": ["./src"], /* Allow multiple folders to be treated as one when resolving modules. */ - "typeRoots": ["./types", "./node_modules/@types"], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": ["react", "node", "jest"], /* Specify type package names to be included without being referenced in a source file. */ + // "typeRoots": ["./types", "./node_modules/@types"], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": ["react", "node", "jest", "@testing-library/jest-dom"], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ diff --git a/types/index.d.ts b/types/index.d.ts index fc4a792..cb29041 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -12,6 +12,7 @@ type ShowSamplesCommand = 'open-graphs' | 'open-datasets'; type HomePageSetting = { recentPageViewMode: 'grid' | 'list' | undefined; samplesViewMode: 'grid' | 'list' | undefined; + templatesPageViewMode?: 'grid' | 'list'; sideBarWidth: string | undefined; }; interface Window { @@ -22,6 +23,7 @@ interface Window { receiveGraphDataFromDotNet: (jsonData: any) => void; receiveSamplesDataFromDotNet: (jsonData: any) => void; receiveTrainingVideoDataFromDotNet: (jsonData: any) => void; + receiveTemplatesDataFromDotNet: (jsonData: any) => void; chrome?: { webview?: any; }; @@ -194,4 +196,5 @@ type Tooltip = { children: JSX.Element | null | string; content?: JSX.Element | null | string; verticalOffset?: number; + position?: 'right' | 'below'; } \ No newline at end of file