From cd47c5d4932a0a6f0ec0d8aabfdc19a88353f680 Mon Sep 17 00:00:00 2001 From: Chloepeg Date: Mon, 16 Feb 2026 14:29:26 +0000 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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",