From cd47c5d4932a0a6f0ec0d8aabfdc19a88353f680 Mon Sep 17 00:00:00 2001 From: Chloepeg Date: Mon, 16 Feb 2026 14:29:26 +0000 Subject: [PATCH 01/12] Add template files support to New dropdown - Enhance CustomDropdown component to support divider and header option types - Add template options to Sidebar "New" dropdown with visual grouping - Add template data structure in home.ts assets - Update dropdown styling for divider and header elements - Add TypeScript types for dropdown option kinds (item/divider/header) - Update package dependencies --- package-lock.json | 17 +++++++- src/assets/home.ts | 19 +++++++++ src/components/Recent/GraphTable.tsx | 1 - .../Sidebar/CustomDropDown.module.css | 26 +++++++++++++ src/components/Sidebar/CustomDropDown.tsx | 32 ++++++++++++--- src/components/Sidebar/Sidebar.tsx | 39 +++++++++++++++---- types/index.d.ts | 1 + 7 files changed, 121 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec37af7..ff9d2fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -3450,6 +3451,7 @@ "version": "20.14.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -3812,6 +3814,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4507,6 +4510,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", "dev": true, + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -5839,6 +5843,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7953,6 +7958,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -10808,6 +10814,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -11102,6 +11109,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11113,6 +11121,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -11604,6 +11613,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", @@ -12620,6 +12630,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12661,7 +12672,8 @@ "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "peer": true }, "node_modules/type-check": { "version": "0.4.0", @@ -12787,6 +12799,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "devOptional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13024,6 +13037,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "dev": true, + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -13136,6 +13150,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", diff --git a/src/assets/home.ts b/src/assets/home.ts index 2bc9767..3cde38a 100644 --- a/src/assets/home.ts +++ b/src/assets/home.ts @@ -58,3 +58,22 @@ export const graphs = [ Thumbnail: img } ]; + +export const templates = [ + { + id: '1', + date: 'Date modified', + Caption: 'Template 1', + ContextData: 'C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe', + Thumbnail: img + }, + { + id: '2', + date: 'Date modified', + Caption: 'Template 2', + ContextData: 'C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe', + Thumbnail: img, + Author: 'Dynamo Team', + Description: 'description' + } +]; diff --git a/src/components/Recent/GraphTable.tsx b/src/components/Recent/GraphTable.tsx index bff0b17..5a8293d 100644 --- a/src/components/Recent/GraphTable.tsx +++ b/src/components/Recent/GraphTable.tsx @@ -41,7 +41,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/Sidebar/CustomDropDown.module.css b/src/components/Sidebar/CustomDropDown.module.css index 9321d37..8201f7b 100644 --- a/src/components/Sidebar/CustomDropDown.module.css +++ b/src/components/Sidebar/CustomDropDown.module.css @@ -104,6 +104,32 @@ background-color: #434343; } +.dropdown-option-static { + cursor: default; +} + +.dropdown-option-static:hover { + background-color: transparent; +} + +.dropdown-option-divider { + padding: 6px 0; +} + +.dropdown-option-header { + padding: 6px 10px 4px; + color: #c9c9c9; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.6px; +} + +.dropdown-divider { + height: 1px; + background-color: #6a6a6a; + margin: 0 10px; +} + .vertical-line { position: absolute; right: 37px; diff --git a/src/components/Sidebar/CustomDropDown.tsx b/src/components/Sidebar/CustomDropDown.tsx index 5511ee5..a641a01 100644 --- a/src/components/Sidebar/CustomDropDown.tsx +++ b/src/components/Sidebar/CustomDropDown.tsx @@ -26,6 +26,9 @@ export const CustomDropdown = ({ /** Peforms the selected action type when used as a Drop-down */ const handleOptionSelect = (option: option) => { + if (option.kind === 'divider' || option.kind === 'header') { + return; + } setIsOpen(false); if (onSelectionChange) { onSelectionChange(option.value); @@ -84,11 +87,30 @@ export const CustomDropdown = ({
- {options.map((option, index) => ( -
handleOptionSelect(option)}> - {option.label} -
- ))} + {options.map((option, index) => { + const isSelectable = option.kind !== 'divider' && option.kind !== 'header'; + const optionClassName = ` + ${styles['dropdown-option']} + ${option.kind === 'divider' ? styles['dropdown-option-divider'] : ''} + ${option.kind === 'header' ? styles['dropdown-option-header'] : ''} + ${!isSelectable ? styles['dropdown-option-static'] : ''} + `; + + return ( +
handleOptionSelect(option) : undefined} + > + {option.kind === 'divider' ? ( +
+ ) : ( + option.label + )} +
+ ); + })}
); diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index e5308a8..c1bafd3 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -1,15 +1,43 @@ import { CustomDropdown } from './CustomDropDown'; import { FormattedMessage } from 'react-intl'; import { Tooltip } from '../Common/Tooltip'; -import { sideBarCommand } from '../../functions/utility'; +import { openFile, sideBarCommand } from '../../functions/utility'; +import { templates } from '../../assets/home'; import styles from './Sidebar.module.css'; export const Sidebar = ({ onItemSelect, selectedSidebarItem }: Sidebar) => { const isSelected = (item: string) => selectedSidebarItem === item; + const templateOptions: option[] = templates.map((template) => ({ + label: template.Caption, + value: template.ContextData, + kind: 'item' as const + })); + const newDropdownOptions: option[] = [ + { label: , value: 'workspace', kind: 'item' as const }, + { label: , value: 'custom-node', kind: 'item' as const }, + ...(templateOptions.length + ? [ + { label: '', value: 'divider-templates', kind: 'divider' as const }, + { label: 'Templates', value: 'templates-header', kind: 'header' as const }, + ...templateOptions + ] + : []) + ]; /**Trigger the backend command based on the drop-down value */ - const setSelectedValue = (value: SidebarCommand) => { - sideBarCommand(value); + const setSelectedValue = (value: string) => { + if ( + value === 'open-file' || + value === 'open-template' || + value === 'open-backup-locations' || + value === 'workspace' || + value === 'custom-node' + ) { + sideBarCommand(value); + return; + } + + openFile(value); }; return ( @@ -35,10 +63,7 @@ export const Sidebar = ({ onItemSelect, selectedSidebarItem }: Sidebar) => { id="newDropdown" placeholder={} onSelectionChange={setSelectedValue} - options={[ - { label: , value: 'workspace' }, - { label: , value: 'custom-node' } - ]} + options={newDropdownOptions} />
diff --git a/types/index.d.ts b/types/index.d.ts index f1fa9c9..8df9006 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -58,6 +58,7 @@ type CardItem = { type option = { label: JSX.Element | null | string; value: string; + kind?: 'item' | 'divider' | 'header'; } type Dropdown = { From b7b87fa3fb89d4a5e024c6891c7b1928aa0cd166 Mon Sep 17 00:00:00 2001 From: Chloepeg Date: Mon, 16 Feb 2026 15:30:28 +0000 Subject: [PATCH 02/12] Add Templates section to home page - Add Templates section above Recent section on home page - Implement templates data loading from home.ts (dev) and backend (prod) - Add independent grid/list view toggle for Templates section - Add templatesPageViewMode setting for view preference persistence - Map template data structure (date -> DateModified) for component compatibility - Add receiveTemplatesDataFromDotNet window function for backend integration - Add "Templates" translation key to locale files - Reuse existing GraphGridItem and GraphTable components for templates display --- src/components/Recent/PageRecent.tsx | 105 ++++++++++++++++++++++++++- src/locales/en.json | 1 + types/index.d.ts | 2 + 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/src/components/Recent/PageRecent.tsx b/src/components/Recent/PageRecent.tsx index 6711191..f007ebe 100644 --- a/src/components/Recent/PageRecent.tsx +++ b/src/components/Recent/PageRecent.tsx @@ -14,7 +14,9 @@ import { useSettings } from '../SettingsContext'; export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => { const { settings, updateSettings } = useSettings(); 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 let initialGraphs = []; @@ -24,7 +26,17 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => initialGraphs = require('../../assets/home').graphs; } - const [graphs, setGraphs] = useState(initialGraphs); + const [graphs, setGraphs] = useState(initialGraphs); + + // 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); // A method exposed to the backend used to set the graph data coming from Dynamo const receiveGraphDataFromDotNet = (jsonData) => { @@ -37,16 +49,29 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => } }; + // A method exposed to the backend used to set the templates data coming from Dynamo + const receiveTemplatesDataFromDotNet = (jsonData) => { + 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); + } + }; + useEffect(() => { // If we are under production, we will override the graphs with the actual data sent from Dynamo if (process.env.NODE_ENV !== 'development') { window.receiveGraphDataFromDotNet = receiveGraphDataFromDotNet; + window.receiveTemplatesDataFromDotNet = receiveTemplatesDataFromDotNet; } // Cleanup function (optional) return () => { if (process.env.NODE_ENV !== 'development') { delete window.receiveGraphDataFromDotNet; + delete window.receiveTemplatesDataFromDotNet; } }; }, []); @@ -54,7 +79,14 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => 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) { @@ -66,6 +98,16 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => } }, [viewMode]); + useEffect(() => { + if (templatesInitialized || (settings?.templatesPageViewMode && settings.templatesPageViewMode !== templatesViewMode)) { + setTemplatesInitialized(true); + updateSettings({ templatesPageViewMode: templatesViewMode }); + + // Send settings to Dynamo to save + saveHomePageSettings({ ...settings, templatesPageViewMode: templatesViewMode }); + } + }, [templatesViewMode]); + // This variable defins the table structure displaying the graphs const columns: Column[] = React.useMemo(() => [ { @@ -103,8 +145,67 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => 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(
+ {/* Templates Section */} +
+

+
+
+ + +
+
+ {templatesViewMode === 'list' && ( + + )} + {templatesViewMode === 'grid' && ( +
+ {templates.map(template => ( + + ))} +
+ )} +
+ + {/* Recent Section */}

diff --git a/src/locales/en.json b/src/locales/en.json index ad14596..9f21a54 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -7,6 +7,7 @@ "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", diff --git a/types/index.d.ts b/types/index.d.ts index 8df9006..13545c1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -9,6 +9,7 @@ type ShowSamplesCommand = 'open-graphs' | 'open-datasets'; type HomePageSetting = { recentPageViewMode: 'grid' | 'list' | undefined; samplesViewMode: 'grid' | 'list' | undefined; + templatesPageViewMode: 'grid' | 'list' | undefined; sideBarWidth: string | undefined; }; interface Window { @@ -19,6 +20,7 @@ interface Window { receiveGraphDataFromDotNet: (jsonData: any) => void; receiveSamplesDataFromDotNet: (jsonData: any) => void; receiveTrainingVideoDataFromDotNet: (jsonData: any) => void; + receiveTemplatesDataFromDotNet?: (jsonData: any) => void; chrome?: { webview?: any; }; From 8a871bf4332a6961dcca4f5b22faa7ac1e075f60 Mon Sep 17 00:00:00 2001 From: Chloepeg Date: Tue, 24 Feb 2026 18:19:00 +0000 Subject: [PATCH 03/12] Open templates in a new editable workspace from template sidebar dropdown - Add newWorkspaceWithTemplate utility function - Implement listener pattern for template data sharing between Sidebar and PageRecent - Map hardcoded dropdown options to real template files from backend - Update TypeScript types for listener pattern --- src/components/Recent/PageRecent.tsx | 36 ++++++++++--- src/components/Sidebar/Sidebar.tsx | 79 +++++++++++++++++++++++++--- src/functions/utility.ts | 10 ++++ types/index.d.ts | 1 + 4 files changed, 111 insertions(+), 15 deletions(-) diff --git a/src/components/Recent/PageRecent.tsx b/src/components/Recent/PageRecent.tsx index f007ebe..e2aacb3 100644 --- a/src/components/Recent/PageRecent.tsx +++ b/src/components/Recent/PageRecent.tsx @@ -60,20 +60,36 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => } }; + // Ensure templates fan-out handler exists (shared with Sidebar) + const ensureTemplatesFanout = () => { + if (!window.__templatesListeners) { + window.__templatesListeners = []; + } + if (!window.receiveTemplatesDataFromDotNet) { + window.receiveTemplatesDataFromDotNet = (jsonData: any) => { + const data = jsonData || []; + window.__templatesListeners?.forEach(fn => fn(data)); + }; + } + }; + useEffect(() => { // If we are under production, we will override the graphs with the actual data sent from Dynamo if (process.env.NODE_ENV !== 'development') { window.receiveGraphDataFromDotNet = receiveGraphDataFromDotNet; - window.receiveTemplatesDataFromDotNet = receiveTemplatesDataFromDotNet; - } + + // Use listener pattern for templates (shared with Sidebar) + ensureTemplatesFanout(); + const listener = (data: any) => receiveTemplatesDataFromDotNet(data); + window.__templatesListeners!.push(listener); - // Cleanup function (optional) - return () => { - if (process.env.NODE_ENV !== 'development') { + return () => { delete window.receiveGraphDataFromDotNet; - delete window.receiveTemplatesDataFromDotNet; - } - }; + if (window.__templatesListeners) { + window.__templatesListeners = window.__templatesListeners.filter(l => l !== listener); + } + }; + } }, []); useEffect(() => { @@ -98,6 +114,10 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => } }, [viewMode]); + useEffect(() => { + + }, []); + useEffect(() => { if (templatesInitialized || (settings?.templatesPageViewMode && settings.templatesPageViewMode !== templatesViewMode)) { setTemplatesInitialized(true); diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index c1bafd3..613d578 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -1,17 +1,54 @@ import { CustomDropdown } from './CustomDropDown'; import { FormattedMessage } from 'react-intl'; import { Tooltip } from '../Common/Tooltip'; -import { openFile, sideBarCommand } from '../../functions/utility'; -import { templates } from '../../assets/home'; +import { openFile, sideBarCommand, newWorkspaceWithTemplate } from '../../functions/utility'; import styles from './Sidebar.module.css'; +import { useState, useEffect } from 'react'; + +// Ensure the fan-out handler exists (shared by Sidebar and Homepage) +function ensureTemplatesFanout() { + if (!window.__templatesListeners) { + window.__templatesListeners = []; + } + + if (!window.receiveTemplatesDataFromDotNet) { + window.receiveTemplatesDataFromDotNet = (jsonData: any) => { + const data = jsonData || []; + window.__templatesListeners?.forEach(fn => fn(data)); + }; + } +} export const Sidebar = ({ onItemSelect, selectedSidebarItem }: Sidebar) => { const isSelected = (item: string) => selectedSidebarItem === item; - const templateOptions: option[] = templates.map((template) => ({ - label: template.Caption, - value: template.ContextData, - kind: 'item' as const - })); + + // Store real templates from backend + const [realTemplates, setRealTemplates] = useState([]); + + // Set up listener pattern + useEffect(() => { + if (process.env.NODE_ENV !== 'development') { + ensureTemplatesFanout(); + + const listener = (data: any) => { + setRealTemplates(data || []); + }; + + window.__templatesListeners!.push(listener); + + return () => { + if (window.__templatesListeners) { + window.__templatesListeners = window.__templatesListeners.filter(l => l !== listener); + } + }; + } + }, []); + + // hardcoded template options (just for UI display) + const templateOptions: option[] = [ + { label: 'Template 1', value: 'sidebar-template-1', kind: 'item' as const }, + { label: 'Template 2', value: 'sidebar-template-2', kind: 'item' as const }, + ]; const newDropdownOptions: option[] = [ { label: , value: 'workspace', kind: 'item' as const }, { label: , value: 'custom-node', kind: 'item' as const }, @@ -37,6 +74,34 @@ export const Sidebar = ({ onItemSelect, selectedSidebarItem }: Sidebar) => { return; } + // Handle template selections by filename pattern (order-independent) + if (value === 'sidebar-template-1') { + // Template 1 = Template_00_HowToCreateADynamoGraph.dyn + const template00 = realTemplates.find(t => + (t?.ContextData || '').includes('Template_00_') + ); + if (template00?.ContextData) { + newWorkspaceWithTemplate(template00.ContextData); + } else { + console.error('Template_00_ not found. Templates not loaded yet or missing.'); + } + return; + } + + if (value === 'sidebar-template-2') { + // Template 2 = Template_01_DynamoWorkflowImportExport.dyn + const template01 = realTemplates.find(t => + (t?.ContextData || '').includes('Template_01_') + ); + if (template01?.ContextData) { + newWorkspaceWithTemplate(template01.ContextData); + } else { + console.error('Template_01_ not found. Templates not loaded yet or missing.'); + } + return; + } + + // Not a template, try opening as file openFile(value); }; diff --git a/src/functions/utility.ts b/src/functions/utility.ts index 0008a31..92295e2 100644 --- a/src/functions/utility.ts +++ b/src/functions/utility.ts @@ -8,6 +8,16 @@ export function openFile(path:string) { } } + /** + * A call to a backend function requesting to create a new workspace and open a template file + * @param {string} path - the location of the template file on the system + */ +export function newWorkspaceWithTemplate(path: string) { + if (window.chrome?.webview !== undefined) { + window.chrome.webview.hostObjects.scriptObject.NewWorkspaceWithTemplate(path); + } +} + /** * A call to a backend function requesting the start of a guided tour * @param {string} guidedTour - the type of guided tour to be started diff --git a/types/index.d.ts b/types/index.d.ts index 13545c1..4738511 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -21,6 +21,7 @@ interface Window { receiveSamplesDataFromDotNet: (jsonData: any) => void; receiveTrainingVideoDataFromDotNet: (jsonData: any) => void; receiveTemplatesDataFromDotNet?: (jsonData: any) => void; + __templatesListeners?: Array<(data: any) => void>; chrome?: { webview?: any; }; From 362f23a0923cc35628da8d37576d13776f508401 Mon Sep 17 00:00:00 2001 From: Chloepeg Date: Mon, 2 Mar 2026 11:49:09 +0000 Subject: [PATCH 04/12] template data to use React Context - Add TemplatesContext to manage template state centrally - Update Sidebar and PageRecent to use useTemplates() hook - Remove duplicate global handlers to prevent race conditions - Aligns with existing React patterns --- src/App.tsx | 5 ++- src/components/Recent/PageRecent.tsx | 53 +++++----------------------- src/components/Sidebar/Sidebar.tsx | 41 +++------------------ src/components/TemplatesContext.tsx | 52 +++++++++++++++++++++++++++ types/index.d.ts | 3 +- 5 files changed, 70 insertions(+), 84 deletions(-) create mode 100644 src/components/TemplatesContext.tsx diff --git a/src/App.tsx b/src/App.tsx index 05d7a56..1caab83 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/components/Recent/PageRecent.tsx b/src/components/Recent/PageRecent.tsx index e2aacb3..5d5aa1d 100644 --- a/src/components/Recent/PageRecent.tsx +++ b/src/components/Recent/PageRecent.tsx @@ -10,6 +10,7 @@ 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) => { const { settings, updateSettings } = useSettings(); @@ -28,16 +29,6 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => const [graphs, setGraphs] = useState(initialGraphs); - // 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); - // A method exposed to the backend used to set the graph data coming from Dynamo const receiveGraphDataFromDotNet = (jsonData) => { try { @@ -49,47 +40,21 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => } }; - // A method exposed to the backend used to set the templates data coming from Dynamo - const receiveTemplatesDataFromDotNet = (jsonData) => { - 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); - } - }; - - // Ensure templates fan-out handler exists (shared with Sidebar) - const ensureTemplatesFanout = () => { - if (!window.__templatesListeners) { - window.__templatesListeners = []; - } - if (!window.receiveTemplatesDataFromDotNet) { - window.receiveTemplatesDataFromDotNet = (jsonData: any) => { - const data = jsonData || []; - window.__templatesListeners?.forEach(fn => fn(data)); - }; - } - }; + // Get templates from context + const templates = useTemplates(); useEffect(() => { // If we are under production, we will override the graphs with the actual data sent from Dynamo if (process.env.NODE_ENV !== 'development') { window.receiveGraphDataFromDotNet = receiveGraphDataFromDotNet; - - // Use listener pattern for templates (shared with Sidebar) - ensureTemplatesFanout(); - const listener = (data: any) => receiveTemplatesDataFromDotNet(data); - window.__templatesListeners!.push(listener); + } - return () => { + // Cleanup function (optional) + return () => { + if (process.env.NODE_ENV !== 'development') { delete window.receiveGraphDataFromDotNet; - if (window.__templatesListeners) { - window.__templatesListeners = window.__templatesListeners.filter(l => l !== listener); - } - }; - } + } + }; }, []); useEffect(() => { diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index 613d578..bc5f280 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -2,49 +2,16 @@ import { CustomDropdown } from './CustomDropDown'; import { FormattedMessage } from 'react-intl'; import { Tooltip } from '../Common/Tooltip'; import { openFile, sideBarCommand, newWorkspaceWithTemplate } from '../../functions/utility'; +import { useTemplates } from '../TemplatesContext'; import styles from './Sidebar.module.css'; -import { useState, useEffect } from 'react'; - -// Ensure the fan-out handler exists (shared by Sidebar and Homepage) -function ensureTemplatesFanout() { - if (!window.__templatesListeners) { - window.__templatesListeners = []; - } - - if (!window.receiveTemplatesDataFromDotNet) { - window.receiveTemplatesDataFromDotNet = (jsonData: any) => { - const data = jsonData || []; - window.__templatesListeners?.forEach(fn => fn(data)); - }; - } -} export const Sidebar = ({ onItemSelect, selectedSidebarItem }: Sidebar) => { const isSelected = (item: string) => selectedSidebarItem === item; - // Store real templates from backend - const [realTemplates, setRealTemplates] = useState([]); - - // Set up listener pattern - useEffect(() => { - if (process.env.NODE_ENV !== 'development') { - ensureTemplatesFanout(); - - const listener = (data: any) => { - setRealTemplates(data || []); - }; - - window.__templatesListeners!.push(listener); - - return () => { - if (window.__templatesListeners) { - window.__templatesListeners = window.__templatesListeners.filter(l => l !== listener); - } - }; - } - }, []); + // Get templates from context + const realTemplates = useTemplates(); - // hardcoded template options (just for UI display) + // Hardcoded template options (just for UI display) const templateOptions: option[] = [ { label: 'Template 1', value: 'sidebar-template-1', kind: 'item' as const }, { label: 'Template 2', value: 'sidebar-template-2', kind: 'item' as const }, diff --git a/src/components/TemplatesContext.tsx b/src/components/TemplatesContext.tsx new file mode 100644 index 0000000..06cc382 --- /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/types/index.d.ts b/types/index.d.ts index 4738511..5926287 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -20,8 +20,7 @@ interface Window { receiveGraphDataFromDotNet: (jsonData: any) => void; receiveSamplesDataFromDotNet: (jsonData: any) => void; receiveTrainingVideoDataFromDotNet: (jsonData: any) => void; - receiveTemplatesDataFromDotNet?: (jsonData: any) => void; - __templatesListeners?: Array<(data: any) => void>; + receiveTemplatesDataFromDotNet: (jsonData: any) => void; chrome?: { webview?: any; }; From 249dce705193b1a19f9081e801f039291d85baa0 Mon Sep 17 00:00:00 2001 From: Chloepeg Date: Mon, 2 Mar 2026 15:32:37 +0000 Subject: [PATCH 05/12] Update sidebar to match renamed template files Update template matching logic to use new filenames: - Template 1: matches 'Create a Graph.dyn' (was Template_00_) - Template 2: matches 'Import & Export Workflow.dyn' (was Template_01_) --- src/components/Sidebar/Sidebar.tsx | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index bc5f280..8f2e037 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -43,27 +43,29 @@ export const Sidebar = ({ onItemSelect, selectedSidebarItem }: Sidebar) => { // Handle template selections by filename pattern (order-independent) if (value === 'sidebar-template-1') { - // Template 1 = Template_00_HowToCreateADynamoGraph.dyn - const template00 = realTemplates.find(t => - (t?.ContextData || '').includes('Template_00_') - ); - if (template00?.ContextData) { - newWorkspaceWithTemplate(template00.ContextData); + // Template 1 = Create a Graph.dyn + const template1 = realTemplates.find(t => { + const path = (t?.ContextData || '').toLowerCase(); + return path.includes('create a graph.dyn'); + }); + if (template1?.ContextData) { + newWorkspaceWithTemplate(template1.ContextData); } else { - console.error('Template_00_ not found. Templates not loaded yet or missing.'); + console.error('Template 1 (Create a Graph) not found. Templates not loaded yet or missing.'); } return; } if (value === 'sidebar-template-2') { - // Template 2 = Template_01_DynamoWorkflowImportExport.dyn - const template01 = realTemplates.find(t => - (t?.ContextData || '').includes('Template_01_') - ); - if (template01?.ContextData) { - newWorkspaceWithTemplate(template01.ContextData); + // Template 2 = Import & Export Workflow.dyn + const template2 = realTemplates.find(t => { + const path = (t?.ContextData || '').toLowerCase(); + return path.includes('import & export workflow.dyn') || path.includes('import and export workflow.dyn'); + }); + if (template2?.ContextData) { + newWorkspaceWithTemplate(template2.ContextData); } else { - console.error('Template_01_ not found. Templates not loaded yet or missing.'); + console.error('Template 2 (Import & Export Workflow) not found. Templates not loaded yet or missing.'); } return; } From 1cf3efb88cb5e1d7bdc17c7a28600cd5b8fee6f0 Mon Sep 17 00:00:00 2001 From: Chloepeg Date: Tue, 3 Mar 2026 16:22:25 +0000 Subject: [PATCH 06/12] Drop down menu on side panel removed Drop down menu on side panel removed, will be added back later - Remove Template 1 and Template 2 options from Sidebar dropdown - Remove newWorkspaceWithTemplate function from utility.ts - Remove divider/header support from CustomDropdown component - Remove template data from home.ts assets - Update type definitions to remove template-related types --- package-lock.json | 17 +---- src/assets/home.ts | 19 ----- src/components/Recent/PageRecent.tsx | 4 - .../Sidebar/CustomDropDown.module.css | 26 ------- src/components/Sidebar/CustomDropDown.tsx | 32 ++------ src/components/Sidebar/Sidebar.tsx | 75 ++----------------- src/functions/utility.ts | 10 --- types/index.d.ts | 1 - 8 files changed, 14 insertions(+), 170 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff9d2fd..ec37af7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,7 +86,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -3451,7 +3450,6 @@ "version": "20.14.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -3814,7 +3812,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4510,7 +4507,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", "dev": true, - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -5843,7 +5839,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7958,7 +7953,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -10814,7 +10808,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -11109,7 +11102,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11121,7 +11113,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -11613,7 +11604,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", @@ -12630,7 +12620,6 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12672,8 +12661,7 @@ "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "peer": true + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/type-check": { "version": "0.4.0", @@ -12799,7 +12787,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "devOptional": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13037,7 +13024,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "dev": true, - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -13150,7 +13136,6 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", diff --git a/src/assets/home.ts b/src/assets/home.ts index 3cde38a..2bc9767 100644 --- a/src/assets/home.ts +++ b/src/assets/home.ts @@ -58,22 +58,3 @@ export const graphs = [ Thumbnail: img } ]; - -export const templates = [ - { - id: '1', - date: 'Date modified', - Caption: 'Template 1', - ContextData: 'C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe', - Thumbnail: img - }, - { - id: '2', - date: 'Date modified', - Caption: 'Template 2', - ContextData: 'C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe', - Thumbnail: img, - Author: 'Dynamo Team', - Description: 'description' - } -]; diff --git a/src/components/Recent/PageRecent.tsx b/src/components/Recent/PageRecent.tsx index 5d5aa1d..05eb05e 100644 --- a/src/components/Recent/PageRecent.tsx +++ b/src/components/Recent/PageRecent.tsx @@ -79,10 +79,6 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => } }, [viewMode]); - useEffect(() => { - - }, []); - useEffect(() => { if (templatesInitialized || (settings?.templatesPageViewMode && settings.templatesPageViewMode !== templatesViewMode)) { setTemplatesInitialized(true); diff --git a/src/components/Sidebar/CustomDropDown.module.css b/src/components/Sidebar/CustomDropDown.module.css index 8201f7b..9321d37 100644 --- a/src/components/Sidebar/CustomDropDown.module.css +++ b/src/components/Sidebar/CustomDropDown.module.css @@ -104,32 +104,6 @@ background-color: #434343; } -.dropdown-option-static { - cursor: default; -} - -.dropdown-option-static:hover { - background-color: transparent; -} - -.dropdown-option-divider { - padding: 6px 0; -} - -.dropdown-option-header { - padding: 6px 10px 4px; - color: #c9c9c9; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.6px; -} - -.dropdown-divider { - height: 1px; - background-color: #6a6a6a; - margin: 0 10px; -} - .vertical-line { position: absolute; right: 37px; diff --git a/src/components/Sidebar/CustomDropDown.tsx b/src/components/Sidebar/CustomDropDown.tsx index a641a01..5511ee5 100644 --- a/src/components/Sidebar/CustomDropDown.tsx +++ b/src/components/Sidebar/CustomDropDown.tsx @@ -26,9 +26,6 @@ export const CustomDropdown = ({ /** Peforms the selected action type when used as a Drop-down */ const handleOptionSelect = (option: option) => { - if (option.kind === 'divider' || option.kind === 'header') { - return; - } setIsOpen(false); if (onSelectionChange) { onSelectionChange(option.value); @@ -87,30 +84,11 @@ export const CustomDropdown = ({
- {options.map((option, index) => { - const isSelectable = option.kind !== 'divider' && option.kind !== 'header'; - const optionClassName = ` - ${styles['dropdown-option']} - ${option.kind === 'divider' ? styles['dropdown-option-divider'] : ''} - ${option.kind === 'header' ? styles['dropdown-option-header'] : ''} - ${!isSelectable ? styles['dropdown-option-static'] : ''} - `; - - return ( -
handleOptionSelect(option) : undefined} - > - {option.kind === 'divider' ? ( -
- ) : ( - option.label - )} -
- ); - })} + {options.map((option, index) => ( +
handleOptionSelect(option)}> + {option.label} +
+ ))}
); diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index 8f2e037..5ec9280 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -1,77 +1,15 @@ import { CustomDropdown } from './CustomDropDown'; import { FormattedMessage } from 'react-intl'; import { Tooltip } from '../Common/Tooltip'; -import { openFile, sideBarCommand, newWorkspaceWithTemplate } from '../../functions/utility'; -import { useTemplates } from '../TemplatesContext'; +import { sideBarCommand } from '../../functions/utility'; import styles from './Sidebar.module.css'; export const Sidebar = ({ onItemSelect, selectedSidebarItem }: Sidebar) => { const isSelected = (item: string) => selectedSidebarItem === item; - - // Get templates from context - const realTemplates = useTemplates(); - - // Hardcoded template options (just for UI display) - const templateOptions: option[] = [ - { label: 'Template 1', value: 'sidebar-template-1', kind: 'item' as const }, - { label: 'Template 2', value: 'sidebar-template-2', kind: 'item' as const }, - ]; - const newDropdownOptions: option[] = [ - { label: , value: 'workspace', kind: 'item' as const }, - { label: , value: 'custom-node', kind: 'item' as const }, - ...(templateOptions.length - ? [ - { label: '', value: 'divider-templates', kind: 'divider' as const }, - { label: 'Templates', value: 'templates-header', kind: 'header' as const }, - ...templateOptions - ] - : []) - ]; /**Trigger the backend command based on the drop-down value */ - const setSelectedValue = (value: string) => { - if ( - value === 'open-file' || - value === 'open-template' || - value === 'open-backup-locations' || - value === 'workspace' || - value === 'custom-node' - ) { - sideBarCommand(value); - return; - } - - // Handle template selections by filename pattern (order-independent) - if (value === 'sidebar-template-1') { - // Template 1 = Create a Graph.dyn - const template1 = realTemplates.find(t => { - const path = (t?.ContextData || '').toLowerCase(); - return path.includes('create a graph.dyn'); - }); - if (template1?.ContextData) { - newWorkspaceWithTemplate(template1.ContextData); - } else { - console.error('Template 1 (Create a Graph) not found. Templates not loaded yet or missing.'); - } - return; - } - - if (value === 'sidebar-template-2') { - // Template 2 = Import & Export Workflow.dyn - const template2 = realTemplates.find(t => { - const path = (t?.ContextData || '').toLowerCase(); - return path.includes('import & export workflow.dyn') || path.includes('import and export workflow.dyn'); - }); - if (template2?.ContextData) { - newWorkspaceWithTemplate(template2.ContextData); - } else { - console.error('Template 2 (Import & Export Workflow) not found. Templates not loaded yet or missing.'); - } - return; - } - - // Not a template, try opening as file - openFile(value); + const setSelectedValue = (value: SidebarCommand) => { + sideBarCommand(value); }; return ( @@ -97,7 +35,10 @@ export const Sidebar = ({ onItemSelect, selectedSidebarItem }: Sidebar) => { id="newDropdown" placeholder={} onSelectionChange={setSelectedValue} - options={newDropdownOptions} + options={[ + { label: , value: 'workspace' }, + { label: , value: 'custom-node' } + ]} />
@@ -137,4 +78,4 @@ export const Sidebar = ({ onItemSelect, selectedSidebarItem }: Sidebar) => {
) -} \ No newline at end of file +} diff --git a/src/functions/utility.ts b/src/functions/utility.ts index 92295e2..0008a31 100644 --- a/src/functions/utility.ts +++ b/src/functions/utility.ts @@ -8,16 +8,6 @@ export function openFile(path:string) { } } - /** - * A call to a backend function requesting to create a new workspace and open a template file - * @param {string} path - the location of the template file on the system - */ -export function newWorkspaceWithTemplate(path: string) { - if (window.chrome?.webview !== undefined) { - window.chrome.webview.hostObjects.scriptObject.NewWorkspaceWithTemplate(path); - } -} - /** * A call to a backend function requesting the start of a guided tour * @param {string} guidedTour - the type of guided tour to be started diff --git a/types/index.d.ts b/types/index.d.ts index 5926287..2c58a4c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -60,7 +60,6 @@ type CardItem = { type option = { label: JSX.Element | null | string; value: string; - kind?: 'item' | 'divider' | 'header'; } type Dropdown = { From 20f21b0747b1dfce922d06808367e046849bf3d6 Mon Sep 17 00:00:00 2001 From: Chloepeg Date: Tue, 10 Mar 2026 17:23:43 +0000 Subject: [PATCH 07/12] fix development build TemplatesContext requires templates export in development mode --- src/assets/home.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/assets/home.ts b/src/assets/home.ts index 2bc9767..12ec2f1 100644 --- a/src/assets/home.ts +++ b/src/assets/home.ts @@ -58,3 +58,5 @@ export const graphs = [ Thumbnail: img } ]; + +export const templates = graphs; \ No newline at end of file From 9f777aba226050e7b9d8a36cb0dd9b6adb150314 Mon Sep 17 00:00:00 2001 From: Chloepeg Date: Tue, 10 Mar 2026 19:48:29 +0000 Subject: [PATCH 08/12] Fix build errors Fix module path in TemplatesContext.tsx and restore package-lock.json metadata. --- package-lock.json | 17 ++++++++++++++++- src/components/TemplatesContext.tsx | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec37af7..ff9d2fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -3450,6 +3451,7 @@ "version": "20.14.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -3812,6 +3814,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4507,6 +4510,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", "dev": true, + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -5839,6 +5843,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7953,6 +7958,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -10808,6 +10814,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -11102,6 +11109,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11113,6 +11121,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -11604,6 +11613,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", @@ -12620,6 +12630,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12661,7 +12672,8 @@ "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "peer": true }, "node_modules/type-check": { "version": "0.4.0", @@ -12787,6 +12799,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "devOptional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13024,6 +13037,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "dev": true, + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -13136,6 +13150,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", diff --git a/src/components/TemplatesContext.tsx b/src/components/TemplatesContext.tsx index 06cc382..7c216c9 100644 --- a/src/components/TemplatesContext.tsx +++ b/src/components/TemplatesContext.tsx @@ -10,7 +10,7 @@ export const TemplatesProvider = ({ children }) => { // 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; + initialTemplates = require('../assets/home').templates; } const [templates, setTemplates] = useState(initialTemplates); From f3b6fa4ad0e9a2c2388d8e985d96afb7fc92cac7 Mon Sep 17 00:00:00 2001 From: Chloepeg Date: Mon, 2 Mar 2026 13:12:25 +0000 Subject: [PATCH 09/12] Swap Recent and Templates order on home page Display Recent files section above Templates section on the home page. Visual reordering only, there is no functional changes to either section --- src/components/Recent/PageRecent.tsx | 68 ++++++++++++++-------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/components/Recent/PageRecent.tsx b/src/components/Recent/PageRecent.tsx index 05eb05e..67898a0 100644 --- a/src/components/Recent/PageRecent.tsx +++ b/src/components/Recent/PageRecent.tsx @@ -145,77 +145,77 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => return(
- {/* Templates Section */} + {/* Recent Section */}
-

+

- {templatesViewMode === 'list' && ( - + {viewMode === 'list' && ( + )} - {templatesViewMode === 'grid' && ( -
- {templates.map(template => ( - + {viewMode === 'grid' && ( +
+ {graphs.map(graph => ( + ))}
)}
- {/* Recent Section */} + {/* Templates Section */}
-

+

- {viewMode === 'list' && ( - + {templatesViewMode === 'list' && ( + )} - {viewMode === 'grid' && ( -
- {graphs.map(graph => ( - + {templatesViewMode === 'grid' && ( +
+ {templates.map(template => ( + ))}
)} From 5d62553f21c388df8e5c5d00af503e22f21bcce0 Mon Sep 17 00:00:00 2001 From: Chloepeg Date: Tue, 10 Mar 2026 17:44:19 +0000 Subject: [PATCH 10/12] Add tooltip to Templates heading Add info icon with tooltip to the Templates section title that appears when hovering over the Template heading Changes: - Add tooltip component to Templates title in PageRecent.tsx - Add tooltip text to locale file (en.json) - Extend Tooltip component to support right-side positioning with arrow - Update tooltip styling for wider layout to fit space inbetween - Position tooltip to the right of the title with left-pointing arrow --- src/components/Common/Tooltip.tsx | 80 ++++++++++++++++++++-------- src/components/Recent/PageRecent.tsx | 6 ++- src/index.css | 35 ++++++++++-- src/locales/en.json | 1 + types/index.d.ts | 1 + 5 files changed, 94 insertions(+), 29 deletions(-) diff --git a/src/components/Common/Tooltip.tsx b/src/components/Common/Tooltip.tsx index 68aec0a..f337000 100644 --- a/src/components/Common/Tooltip.tsx +++ b/src/components/Common/Tooltip.tsx @@ -1,46 +1,80 @@ 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 }: Tooltip) => { const [show, setShow] = useState(false); const [position, setPosition] = useState({}); const tooltipRef = useRef(null); const contentRef = useRef(null); // Ref for the tooltip content + // Set arrow direction based on position prop - default to 'up' for tooltips below + const arrowDirection = positionProp === 'right' ? 'left' : 'up'; useEffect(() => { if (tooltipRef.current && contentRef.current && show) { - 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 - } - // 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 - } - - setPosition({ - top: top, - left: left, - position: 'absolute' + // Use requestAnimationFrame to ensure tooltip is rendered and measured correctly + requestAnimationFrame(() => { + if (tooltipRef.current && contentRef.current) { + const targetRect = tooltipRef.current.getBoundingClientRect(); + const tooltipRect = contentRef.current.getBoundingClientRect(); + + let left: number; + let top: number; + + // If position prop is 'right', position to the right of the element + if (positionProp === 'right') { + left = targetRect.right + window.scrollX + verticalOffset; + top = targetRect.top + window.scrollY + (targetRect.height / 2) - (tooltipRect.height / 2); + + // Check if the tooltip is going off the right side of the screen + if (left + tooltipRect.width > window.innerWidth + window.scrollX) { + // If it goes off right, position it to the left of the element instead + left = targetRect.left + window.scrollX - tooltipRect.width - verticalOffset; + } + } else { + // Default: position tooltip below the element (centered) + left = targetRect.left + window.scrollX + (targetRect.width / 2); + top = targetRect.bottom + window.scrollY + verticalOffset; + + // Check if the tooltip is going off the right side of the screen + if (left + tooltipRect.width / 2 > window.innerWidth + window.scrollX) { + left = window.innerWidth + window.scrollX - tooltipRect.width / 2 - 10; + } + // Check if the tooltip is going off the left side of the screen + if (left - tooltipRect.width / 2 < window.scrollX) { + left = window.scrollX + tooltipRect.width / 2 + 10; + } + } + + // Check if the tooltip is going off the top of the screen + if (top < window.scrollY) { + top = window.scrollY + 10; + } + // Check if the tooltip is going off the bottom of the screen + if (top + tooltipRect.height > window.innerHeight + window.scrollY) { + top = window.innerHeight + window.scrollY - tooltipRect.height - 10; + } + + setPosition({ + top: top, + left: left, + position: 'absolute', + // For default positioning (below), center using transform + transform: positionProp === 'right' ? 'none' : 'translateX(-50%)' + }); + } }); } - }, [show, content, verticalOffset]); // Added 'content' to dependencies array + }, [show, content, verticalOffset, positionProp]); return ( - setShow(true)} onMouseLeave={() => setShow(false)} ref={tooltipRef}> {children} {show && ( -
+
{content}
diff --git a/src/components/Recent/PageRecent.tsx b/src/components/Recent/PageRecent.tsx index 67898a0..3d43710 100644 --- a/src/components/Recent/PageRecent.tsx +++ b/src/components/Recent/PageRecent.tsx @@ -182,7 +182,11 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => {/* Templates Section */}
-

+ } position="right"> +

+ +

+
{/* Templates Section */} -
+
+

+ +

} position="right"> -

- -

+
diff --git a/src/locales/en.json b/src/locales/en.json index 20fe593..97e1b20 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -13,7 +13,7 @@ "tooltip.text.recent": "View recent files", "tooltip.text.samples": "View sample graphs", "tooltip.text.learning": "View learning content", - "tooltip.text.templates": "A collection of templates demonstrating how to organise Dynamo graphs.\nTemplates are graphs designed to help you start and organise workflows more efficiently. Opening a template creates a new editable graph that can be customised to match your project or office standards.", + "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", From 1de53e89e672f7bff1fa9c881d7b55b09b6f6dff Mon Sep 17 00:00:00 2001 From: Chloepeg Date: Thu, 11 Jun 2026 14:34:06 +0100 Subject: [PATCH 12/12] Pr feedback changes PageRecent cleanup: Removed duplicate handleTemplateRowClick. Removed inline template normalization. Uses new TemplatesSection. Both view-mode save effects now use one merged settings payload. New Templates component: Added src/components/Recent/TemplatesSection.tsx. Added empty co-located TemplatesSection.module.css for repo convention. TemplatesContext: Replaced any[] with TemplateInput / Template types. Typed children. Normalizes template data once in the context. Tooltip/CSS: Added optional tooltipClassName. Restored global tooltip width to 300px. Scoped Templates tooltip width to 500px. Removed unused arrow-right tooltip CSS. Localization: Added title.text.templates and recent.templates.tooltip across all locale files. Renamed old tooltip.text.templates usage to recent.templates.tooltip. Types: Updated Graph shape to use DateModified. Added optional Author / Description. Added tooltipClassName to Tooltip. --- src/assets/home.ts | 2 +- src/components/Common/Tooltip.tsx | 4 +- src/components/Recent/PageRecent.tsx | 85 +++----------- .../Recent/TemplatesSection.module.css | 1 + src/components/Recent/TemplatesSection.tsx | 80 +++++++++++++ src/components/TemplatesContext.tsx | 109 +++++++++++------- src/index.css | 17 +-- src/locales/cs.json | 2 + src/locales/de.json | 2 + src/locales/en-GB.json | 2 + src/locales/en.json | 2 +- src/locales/es.json | 2 + src/locales/fr.json | 2 + src/locales/it.json | 2 + src/locales/ja.json | 2 + src/locales/ko.json | 2 + src/locales/pl.json | 2 + src/locales/pt-BR.json | 2 + src/locales/ru.json | 2 + src/locales/zh-Hans.json | 2 + src/locales/zh-Hant.json | 2 + types/index.d.ts | 5 +- 22 files changed, 205 insertions(+), 126 deletions(-) create mode 100644 src/components/Recent/TemplatesSection.module.css create mode 100644 src/components/Recent/TemplatesSection.tsx diff --git a/src/assets/home.ts b/src/assets/home.ts index e3a8423..cee2920 100644 --- a/src/assets/home.ts +++ b/src/assets/home.ts @@ -62,4 +62,4 @@ export const graphs = [ } ]; -export const templates = graphs; \ No newline at end of file +export const templates = graphs; diff --git a/src/components/Common/Tooltip.tsx b/src/components/Common/Tooltip.tsx index c4716e3..8de0593 100644 --- a/src/components/Common/Tooltip.tsx +++ b/src/components/Common/Tooltip.tsx @@ -1,7 +1,7 @@ import { useState, useRef, useEffect, CSSProperties } from 'react'; import Portal from './Portal'; // Import your Portal component -export const Tooltip = ({ children, content, verticalOffset = 12, position: positionProp = 'below' }: Tooltip) => { +export const Tooltip = ({ children, content, verticalOffset = 12, position: positionProp = 'below', tooltipClassName = '' }: Tooltip) => { const [show, setShow] = useState(false); const [position, setPosition] = useState({}); const tooltipRef = useRef(null); @@ -75,7 +75,7 @@ export const Tooltip = ({ children, content, verticalOffset = 12, position: posi {children} {show && ( -
+
{content}
diff --git a/src/components/Recent/PageRecent.tsx b/src/components/Recent/PageRecent.tsx index b9219c2..1451d39 100644 --- a/src/components/Recent/PageRecent.tsx +++ b/src/components/Recent/PageRecent.tsx @@ -5,7 +5,8 @@ import { CustomNameCellRenderer } from './CustomNameCellRenderer'; import { CustomLocationCellRenderer } from './CustomLocationCellRenderer'; import { CustomAuthorCellRenderer } from './CustomAuthorCellRenderer'; import { GraphTable } from './GraphTable'; -import { GridViewIcon, ListViewIcon, QuestionMarkIcon } from '../Common/CustomIcons'; +import { TemplatesSection } from './TemplatesSection'; +import { GridViewIcon, ListViewIcon } from '../Common/CustomIcons'; import { openFile, saveHomePageSettings } from '../../functions/utility'; import { FormattedMessage } from 'react-intl'; import { Tooltip } from '../Common/Tooltip'; @@ -71,10 +72,11 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => useEffect(() => { if (initialized || recentPageViewMode !== viewMode) { setInitialized(true); - updateSettings({ recentPageViewMode: viewMode }); + const mergedSettings = { ...settings, recentPageViewMode: viewMode }; + updateSettings(mergedSettings); // Send settings to Dynamo to save - saveHomePageSettings({ ...settings, recentPageViewMode: viewMode }); + saveHomePageSettings(mergedSettings); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [viewMode]); @@ -82,10 +84,11 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => useEffect(() => { if (templatesInitialized || (settings?.templatesPageViewMode && settings.templatesPageViewMode !== templatesViewMode)) { setTemplatesInitialized(true); - updateSettings({ templatesPageViewMode: templatesViewMode }); + const mergedSettings = { ...settings, templatesPageViewMode: templatesViewMode }; + updateSettings(mergedSettings); // Send settings to Dynamo to save - saveHomePageSettings({ ...settings, templatesPageViewMode: templatesViewMode }); + saveHomePageSettings(mergedSettings); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [templatesViewMode]); @@ -127,23 +130,6 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => 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 */} @@ -183,53 +169,14 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => )}
- {/* Templates Section */} -
-

- -

- } position="right"> - - -
-
- - -
-
- {templatesViewMode === 'list' && ( - - )} - {templatesViewMode === 'grid' && ( -
- {templates.map(template => ( - - ))} -
- )} -
+
); }; diff --git a/src/components/Recent/TemplatesSection.module.css b/src/components/Recent/TemplatesSection.module.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/components/Recent/TemplatesSection.module.css @@ -0,0 +1 @@ + diff --git a/src/components/Recent/TemplatesSection.tsx b/src/components/Recent/TemplatesSection.tsx new file mode 100644 index 0000000..b70d320 --- /dev/null +++ b/src/components/Recent/TemplatesSection.tsx @@ -0,0 +1,80 @@ +import { FormattedMessage } from 'react-intl'; +import { GraphGridItem } from './GraphGridItem'; +import { GraphTable } from './GraphTable'; +import { GridViewIcon, ListViewIcon, QuestionMarkIcon } from '../Common/CustomIcons'; +import { Tooltip } from '../Common/Tooltip'; + +type TemplateItem = Graph & { + Author: string; + Description: string; +}; + +interface TemplatesSectionProps { + columns: Column[]; + templates: TemplateItem[]; + templatesViewMode: string; + setTemplatesViewMode: (viewMode: string) => void; + onRowClick: (row: Row) => void; + setIsDisabled: (disable: boolean) => void; +} + +export const TemplatesSection = ({ + columns, + templates, + templatesViewMode, + setTemplatesViewMode, + onRowClick, + setIsDisabled, +}: TemplatesSectionProps) => { + return ( + <> +
+

+ +

+ } + position="right" + tooltipClassName="template-info-tooltip"> + + +
+
+ + +
+
+ {templatesViewMode === 'list' && ( + + )} + {templatesViewMode === 'grid' && ( +
+ {templates.map(template => ( + + ))} +
+ )} +
+ + ); +}; diff --git a/src/components/TemplatesContext.tsx b/src/components/TemplatesContext.tsx index 7c216c9..170dac5 100644 --- a/src/components/TemplatesContext.tsx +++ b/src/components/TemplatesContext.tsx @@ -1,52 +1,81 @@ import { createContext, useContext, useState, useEffect } from 'react'; +import type { ReactNode } from 'react'; + +type TemplateInput = Omit & { + date?: string; + Author?: string; + DateModified?: string; + Description?: string; +}; + +type Template = Graph & { + Author: string; + Description: string; +}; + +const normalizeTemplate = (template: TemplateInput): Template => { + const { date, DateModified, Author, Description, ...templateData } = template; + + return { + ...templateData, + DateModified: DateModified || date || '', + Author: Author || '', + Description: Description || '', + }; +}; // Create the context -const TemplatesContext = createContext([]); +const TemplatesContext = createContext([]); + +type TemplatesProviderProps = { + children: ReactNode; +}; // 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; - } +export const TemplatesProvider = ({ children }: TemplatesProviderProps) => { + // Set a placeholder for the templates which will be used differently during dev and prod + let initialTemplates: Template[] = []; - 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); - } - }; + // If we are under development, we will load the templates from the local asset folder + if (process.env.NODE_ENV === 'development') { + // eslint-disable-next-line @typescript-eslint/no-require-imports + initialTemplates = require('../assets/home').templates.map(normalizeTemplate); + } + + 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: TemplateInput[] | null) => { + try { + // jsonData is already an object, so no need to parse it + const data = (jsonData || []).map(normalizeTemplate); + 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} - - ); -} + // Cleanup function + return () => { + if (process.env.NODE_ENV !== 'development') { + delete window.receiveTemplatesDataFromDotNet; + } + }; + }, []); + + return ( + + {children} + + ); +}; // Use templates hook export function useTemplates() { - return useContext(TemplatesContext); + return useContext(TemplatesContext); } diff --git a/src/index.css b/src/index.css index ef5b329..5ecf971 100644 --- a/src/index.css +++ b/src/index.css @@ -199,7 +199,7 @@ select { font-size: small; border-radius: 2px; z-index: 1000; - max-width: 500px; /* Increased width to make it wider */ + max-width: 300px; white-space: normal; overflow-wrap: break-word; opacity: 0; /* Initially, set the opacity to 0 to hide the tooltip */ @@ -211,6 +211,10 @@ select { animation-play-state: running; /* Start the animation when the tooltip is shown */ } +.tooltip-box.template-info-tooltip { + max-width: 500px; +} + .tooltip-arrow { position: absolute; bottom: 100%; /* Align it at the bottom of the tooltip box */ @@ -234,17 +238,6 @@ select { 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/cs.json b/src/locales/cs.json index 1415641..4e39e23 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -7,11 +7,13 @@ "button.title.text.workspace": "Pracovní prostor", "button.title.text.custom.node": "Vlastní uzel", "title.text.recent": "Poslední", + "title.text.templates": "Templates", "title.text.samples": "Ukázky", "title.text.learning": "Výuka", "tooltip.text.recent": "Zobrazit poslední soubory", "tooltip.text.samples": "Zobrazení vzorových grafů", "tooltip.text.learning": "Zobrazit výukový obsah", + "recent.templates.tooltip": "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": "Zobrazení rastru", "tooltip.text.list.view.button": "Zobrazení seznamu", "learning.title.text.learning": "Výuka", diff --git a/src/locales/de.json b/src/locales/de.json index 1e941b2..52476db 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -7,11 +7,13 @@ "button.title.text.workspace": "Arbeitsbereich", "button.title.text.custom.node": "Benutzerdefinierter Block", "title.text.recent": "Zuletzt verwendet", + "title.text.templates": "Templates", "title.text.samples": "Beispiele", "title.text.learning": "Lerninhalte", "tooltip.text.recent": "Zuletzt verwendete Dateien anzeigen", "tooltip.text.samples": "Beispieldiagramme anzeigen", "tooltip.text.learning": "Lerninhalte anzeigen", + "recent.templates.tooltip": "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": "Rasteransicht", "tooltip.text.list.view.button": "Listenansicht", "learning.title.text.learning": "Lerninhalte", diff --git a/src/locales/en-GB.json b/src/locales/en-GB.json index d12d456..7fb33b4 100644 --- a/src/locales/en-GB.json +++ b/src/locales/en-GB.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", + "recent.templates.tooltip": "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/src/locales/en.json b/src/locales/en.json index 97e1b20..7e0b06b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -13,7 +13,7 @@ "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.", + "recent.templates.tooltip": "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/src/locales/es.json b/src/locales/es.json index 176ef81..15cac84 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -7,11 +7,13 @@ "button.title.text.workspace": "Espacio de trabajo", "button.title.text.custom.node": "Nodo personalizado", "title.text.recent": "Recientes", + "title.text.templates": "Templates", "title.text.samples": "Muestras", "title.text.learning": "Formación", "tooltip.text.recent": "Ver archivos recientes", "tooltip.text.samples": "Ver gráficos de muestra", "tooltip.text.learning": "Ver contenido de aprendizaje", + "recent.templates.tooltip": "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": "Vista de rejilla", "tooltip.text.list.view.button": "Vista de lista", "learning.title.text.learning": "Formación", diff --git a/src/locales/fr.json b/src/locales/fr.json index 2b1556a..06bb629 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -7,11 +7,13 @@ "button.title.text.workspace": "Espace de travail", "button.title.text.custom.node": "Noeud personnalisé", "title.text.recent": "Récent", + "title.text.templates": "Templates", "title.text.samples": "Exemples", "title.text.learning": "Formation", "tooltip.text.recent": "Afficher les fichiers récents", "tooltip.text.samples": "Afficher des exemples de graphiques", "tooltip.text.learning": "Afficher le contenu de formation", + "recent.templates.tooltip": "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": "Vue grille", "tooltip.text.list.view.button": "Vue liste", "learning.title.text.learning": "Formation", diff --git a/src/locales/it.json b/src/locales/it.json index 99614c2..4a4072c 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -7,11 +7,13 @@ "button.title.text.workspace": "Area di lavoro", "button.title.text.custom.node": "Nodo personalizzato", "title.text.recent": "Recenti", + "title.text.templates": "Templates", "title.text.samples": "Esempi", "title.text.learning": "Apprendimento", "tooltip.text.recent": "Visualizza file recenti", "tooltip.text.samples": "Visualizza grafici di esempio", "tooltip.text.learning": "Visualizza contenuto di formazione", + "recent.templates.tooltip": "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": "Vista griglia", "tooltip.text.list.view.button": "Vista elenco", "learning.title.text.learning": "Apprendimento", diff --git a/src/locales/ja.json b/src/locales/ja.json index 4539315..c6f224a 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -7,11 +7,13 @@ "button.title.text.workspace": "ワークスペース", "button.title.text.custom.node": "カスタム ノード", "title.text.recent": "最近使用したファイル", + "title.text.templates": "Templates", "title.text.samples": "サンプル", "title.text.learning": "学習", "tooltip.text.recent": "最近使用したファイルを表示", "tooltip.text.samples": "サンプル グラフを表示", "tooltip.text.learning": "学習コンテンツを表示", + "recent.templates.tooltip": "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": "グリッド ビュー", "tooltip.text.list.view.button": "リスト ビュー", "learning.title.text.learning": "学習", diff --git a/src/locales/ko.json b/src/locales/ko.json index 85b58c5..0a83171 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -7,11 +7,13 @@ "button.title.text.workspace": "작업공간", "button.title.text.custom.node": "사용자 지정 노드", "title.text.recent": "최근", + "title.text.templates": "Templates", "title.text.samples": "샘플", "title.text.learning": "학습", "tooltip.text.recent": "최근 파일 보기", "tooltip.text.samples": "샘플 그래프 보기", "tooltip.text.learning": "학습 컨텐츠 보기", + "recent.templates.tooltip": "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": "그리드 뷰", "tooltip.text.list.view.button": "리스트 뷰", "learning.title.text.learning": "학습", diff --git a/src/locales/pl.json b/src/locales/pl.json index 80815dc..fae274e 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -7,11 +7,13 @@ "button.title.text.workspace": "Obszar roboczy", "button.title.text.custom.node": "Węzeł niestandardowy", "title.text.recent": "Ostatnie", + "title.text.templates": "Templates", "title.text.samples": "Przykłady", "title.text.learning": "Nauka", "tooltip.text.recent": "Wyświetl ostatnio używane pliki", "tooltip.text.samples": "Wyświetl przykładowe wykresy", "tooltip.text.learning": "Wyświetl materiały szkoleniowe", + "recent.templates.tooltip": "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": "Widok siatki", "tooltip.text.list.view.button": "Widok listy", "learning.title.text.learning": "Nauka", diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index c0f690d..6cb8194 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -7,11 +7,13 @@ "button.title.text.workspace": "Espaço de trabalho", "button.title.text.custom.node": "Nó personalizado", "title.text.recent": "Recentes", + "title.text.templates": "Templates", "title.text.samples": "Amostras", "title.text.learning": "Aprendizagem", "tooltip.text.recent": "Ver arquivos recentes", "tooltip.text.samples": "Ver gráficos de amostra", "tooltip.text.learning": "Ver conteúdo educativo", + "recent.templates.tooltip": "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": "Vista de grade", "tooltip.text.list.view.button": "Vista de lista", "learning.title.text.learning": "Aprendizagem", diff --git a/src/locales/ru.json b/src/locales/ru.json index 19823d5..669c73c 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -7,11 +7,13 @@ "button.title.text.workspace": "Рабочее пространство", "button.title.text.custom.node": "Пользовательский узел", "title.text.recent": "Последние", + "title.text.templates": "Templates", "title.text.samples": "Образцы", "title.text.learning": "Обучение", "tooltip.text.recent": "Просмотр последних файлов", "tooltip.text.samples": "Просмотр образцов графиков", "tooltip.text.learning": "Просмотр обучающих материалов", + "recent.templates.tooltip": "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": "Сетка", "tooltip.text.list.view.button": "Список", "learning.title.text.learning": "Обучение", diff --git a/src/locales/zh-Hans.json b/src/locales/zh-Hans.json index c6c4ffd..75f0d36 100644 --- a/src/locales/zh-Hans.json +++ b/src/locales/zh-Hans.json @@ -7,11 +7,13 @@ "button.title.text.workspace": "工作空间", "button.title.text.custom.node": "自定义节点", "title.text.recent": "最近使用的项目", + "title.text.templates": "Templates", "title.text.samples": "样例", "title.text.learning": "学习", "tooltip.text.recent": "查看最近使用的文件", "tooltip.text.samples": "查看样例图", "tooltip.text.learning": "查看学习内容", + "recent.templates.tooltip": "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": "网格视图", "tooltip.text.list.view.button": "列表视图", "learning.title.text.learning": "学习", diff --git a/src/locales/zh-Hant.json b/src/locales/zh-Hant.json index d7f4ed3..6c50cc9 100644 --- a/src/locales/zh-Hant.json +++ b/src/locales/zh-Hant.json @@ -7,11 +7,13 @@ "button.title.text.workspace": "工作區", "button.title.text.custom.node": "自訂節點", "title.text.recent": "最近", + "title.text.templates": "Templates", "title.text.samples": "範例", "title.text.learning": "學習", "tooltip.text.recent": "檢視最近使用的檔案", "tooltip.text.samples": "檢視範例圖表", "tooltip.text.learning": "檢視學習內容", + "recent.templates.tooltip": "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": "網格檢視", "tooltip.text.list.view.button": "清單檢視", "learning.title.text.learning": "學習", diff --git a/types/index.d.ts b/types/index.d.ts index cb29041..5a19931 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -84,10 +84,12 @@ type GraphItem = { type Graph = { id: string; - date: string; + DateModified: string; Caption: string; ContextData: string; Thumbnail: string; + Author?: string; + Description?: string; } type GraphTable = { @@ -197,4 +199,5 @@ type Tooltip = { content?: JSX.Element | null | string; verticalOffset?: number; position?: 'right' | 'below'; + tooltipClassName?: string; } \ No newline at end of file