From 24a3a03c653c2bb9ff4d3758f530d1ecab2d89a4 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 19 Sep 2026 01:57:42 +0100 Subject: [PATCH] Enhance Dashboard Functionality and Icon Integration - Updated DashboardLayout to utilize Electron context for dynamic spacing adjustments. - Enhanced About and Settings components to include icon keys for improved navigation clarity. - Refined DashboardOverviewPage to generate dynamic icon keys based on page names. - Improved DashboardTabs to support sidebar icons, enhancing visual consistency. - Expanded NavigationTabsContext to manage icon keys for tabs, streamlining tab interactions. - Updated sidebarIconMap to utilize component references for better performance and maintainability. --- src/components/Dashboard/Layout.jsx | 9 +- src/components/Dashboard/Management/About.jsx | 2 +- .../Dashboard/Management/Settings.jsx | 2 +- .../common/DashboardOverviewPage.jsx | 12 +- .../Dashboard/common/DashboardTabs.jsx | 7 +- .../context/NavigationTabsContext.jsx | 215 +++++++++++++----- src/components/Icons/sidebarIconMap.jsx | 154 +++++++------ src/desktop/window.js | 1 + 8 files changed, 270 insertions(+), 132 deletions(-) diff --git a/src/components/Dashboard/Layout.jsx b/src/components/Dashboard/Layout.jsx index 521779a3..39ddddbe 100644 --- a/src/components/Dashboard/Layout.jsx +++ b/src/components/Dashboard/Layout.jsx @@ -1,6 +1,7 @@ // DashboardLayout.js import PropTypes from 'prop-types' import { Layout, Flex } from 'antd' +import { useContext } from 'react' import { useLocation } from 'react-router-dom' import ProductionSidebar from './Production/ProductionSidebar' import InventorySidebar from './Inventory/InventorySidebar' @@ -16,12 +17,14 @@ import { useThemeContext } from './context/ThemeContext' import { MessageProvider } from './context/MessageContext' import { useDashboardObjectToolsContext } from './context/DashboardObjectToolsContext' import { useMediaQuery } from 'react-responsive' +import { ElectronContext } from './context/ElectronContext' const { Content } = Layout const DashboardLayout = ({ children }) => { const location = useLocation() const isMobile = useMediaQuery({ maxWidth: 768 }) + const { isElectron } = useContext(ElectronContext) const isProduction = location.pathname.startsWith('/dashboard/production') const isInventory = location.pathname.startsWith('/dashboard/inventory') const isFinance = location.pathname.startsWith('/dashboard/finance') @@ -61,7 +64,11 @@ const DashboardLayout = ({ children }) => { className='main-content-layout' > - + {currentObjectTools} diff --git a/src/components/Dashboard/Management/About.jsx b/src/components/Dashboard/Management/About.jsx index da6c0d60..ca2ec4ee 100644 --- a/src/components/Dashboard/Management/About.jsx +++ b/src/components/Dashboard/Management/About.jsx @@ -26,7 +26,7 @@ import { useNavigationTabPage } from '../context/NavigationTabsContext' const { Title, Text } = Typography const About = () => { - useNavigationTabPage({ title: 'About' }) + useNavigationTabPage({ title: 'About', iconKey: 'infoCircle' }) const [collapseState, updateCollapseState] = useCollapseState('About', { updater: true }) diff --git a/src/components/Dashboard/Management/Settings.jsx b/src/components/Dashboard/Management/Settings.jsx index faa7e6bd..3f4c3196 100644 --- a/src/components/Dashboard/Management/Settings.jsx +++ b/src/components/Dashboard/Management/Settings.jsx @@ -36,7 +36,7 @@ const LEGACY_ELECTRON_USER_KEYS = [ ] const Settings = () => { - useNavigationTabPage({ title: 'Settings' }) + useNavigationTabPage({ title: 'Settings', iconKey: 'settings' }) const { isDarkMode, isCompact, diff --git a/src/components/Dashboard/common/DashboardOverviewPage.jsx b/src/components/Dashboard/common/DashboardOverviewPage.jsx index 06bcc80d..d81fa1bf 100644 --- a/src/components/Dashboard/common/DashboardOverviewPage.jsx +++ b/src/components/Dashboard/common/DashboardOverviewPage.jsx @@ -125,10 +125,16 @@ const DashboardOverviewPage = ({ onOpenActionsModal, sections = [] }) => { - const overviewTitle = pageName.endsWith('Overview') - ? `Overview - ${pageName.slice(0, -'Overview'.length)}` + const overviewSection = pageName.endsWith('Overview') + ? pageName.slice(0, -'Overview'.length) + : '' + const overviewTitle = overviewSection + ? `Overview - ${overviewSection}` : pageName - useNavigationTabPage({ title: overviewTitle }) + const overviewIconKey = overviewSection + ? `${overviewSection.charAt(0).toLowerCase()}${overviewSection.slice(1)}` + : undefined + useNavigationTabPage({ title: overviewTitle, iconKey: overviewIconKey }) const [savedCollapseState, updateCollapseState] = useCollapseState( pageName, collapseDefaults diff --git a/src/components/Dashboard/common/DashboardTabs.jsx b/src/components/Dashboard/common/DashboardTabs.jsx index 90f99d92..2c4c4a18 100644 --- a/src/components/Dashboard/common/DashboardTabs.jsx +++ b/src/components/Dashboard/common/DashboardTabs.jsx @@ -8,6 +8,7 @@ import PlusIcon from '../../Icons/PlusIcon' import XMarkIcon from '../../Icons/XMarkIcon' import HomeIcon from '../../Icons/HomeIcon' import { getModelByName } from '../../../database/ObjectModels' +import { getSidebarIconComponent } from '../../Icons/sidebarIconMap' import { useNavigationTabs, useTabPreview } from '../context/NavigationTabsContext' import { getDesktopWindowId } from '../../../electrobun-bridge.js' import { hasExternalTabDrag, writeTabDragData } from './tabDrag' @@ -88,7 +89,8 @@ const DashboardTabItem = ({ const [previewOpen, setPreviewOpen] = useState(false) const previewTargetRef = useRef(null) const model = tab.modelName ? getModelByName(tab.modelName) : null - const Icon = model?.icon || HomeIcon + const SidebarIcon = getSidebarIconComponent(tab.iconKey) + const Icon = model?.icon || SidebarIcon || HomeIcon const handlePreviewOpenChange = useCallback( (nextOpen) => { @@ -178,7 +180,8 @@ DashboardTabItem.propTypes = { tab: PropTypes.shape({ id: PropTypes.string.isRequired, title: PropTypes.string, - modelName: PropTypes.string + modelName: PropTypes.string, + iconKey: PropTypes.string }).isRequired, tabIndex: PropTypes.number.isRequired, isSelected: PropTypes.bool, diff --git a/src/components/Dashboard/context/NavigationTabsContext.jsx b/src/components/Dashboard/context/NavigationTabsContext.jsx index 7c1bd53b..bc93607d 100644 --- a/src/components/Dashboard/context/NavigationTabsContext.jsx +++ b/src/components/Dashboard/context/NavigationTabsContext.jsx @@ -72,6 +72,7 @@ const createTabFromLocation = (location, extras = {}) => ({ id: createTabId(), title: extras.title || 'Farm Control', modelName: extras.modelName || null, + iconKey: extras.iconKey || null, history: [locationToEntry(location)], historyIndex: 0 }) @@ -80,6 +81,7 @@ const cloneCurrentPageTab = (tab, location) => ({ id: createTabId(), title: tab?.title || 'Farm Control', modelName: tab?.modelName || null, + iconKey: tab?.iconKey || null, history: [locationToEntry(location)], historyIndex: 0 }) @@ -87,6 +89,73 @@ const cloneCurrentPageTab = (tab, location) => ({ const getTabCurrentEntry = (tab) => tab?.history?.[tab.historyIndex] || tab?.history?.[tab.history.length - 1] +const normalizeHistory = (history, fallbackEntry) => { + if (Array.isArray(history) && history.length > 0) { + return history.map((item) => locationToEntry(item)) + } + return [fallbackEntry || { pathname: '/', search: '', hash: '' }] +} + +const normalizeTab = (tab, fallbackLocation) => { + const fallbackEntry = locationToEntry(fallbackLocation) + const history = normalizeHistory(tab?.history, fallbackEntry) + const rawIndex = Number.isInteger(tab?.historyIndex) + ? tab.historyIndex + : history.length - 1 + return { + id: tab?.id || createTabId(), + title: tab?.title || 'Farm Control', + modelName: tab?.modelName || null, + iconKey: tab?.iconKey || null, + history, + historyIndex: Math.min(Math.max(rawIndex, 0), history.length - 1) + } +} + +const applyLocationToTab = (tab, entry) => { + if (!tab || !entry) return tab + const currentEntry = getTabCurrentEntry(tab) + if (entriesEqual(currentEntry, entry)) return tab + if (!tab.history?.length) { + return { ...tab, history: [entry], historyIndex: 0 } + } + const truncated = tab.history.slice(0, tab.historyIndex + 1) + return { + ...tab, + history: [...truncated, entry], + historyIndex: truncated.length + } +} + +const applyPageMetaToTab = (tab, { title, modelName, iconKey } = {}) => { + const nextModelName = + modelName === undefined + ? iconKey !== undefined + ? null + : tab.modelName + : modelName || null + const nextIconKey = + iconKey === undefined + ? modelName !== undefined + ? null + : tab.iconKey + : iconKey || null + const nextTitle = title || tab.title + if ( + nextTitle === tab.title && + nextModelName === tab.modelName && + nextIconKey === tab.iconKey + ) { + return tab + } + return { + ...tab, + title: nextTitle, + modelName: nextModelName, + iconKey: nextIconKey + } +} + export const NavigationTabsProvider = ({ children }) => { const navigate = useNavigate() const location = useLocation() @@ -113,20 +182,53 @@ export const NavigationTabsProvider = ({ children }) => { const [tabPreviewCapturing, setTabPreviewCapturing] = useState({}) const tabsRef = useRef(tabs) const activeTabIdRef = useRef(activeTabId) + const locationRef = useRef(location) const previousActiveTabIdRef = useRef(null) - const isRestoringRef = useRef(false) + const isRestoringRef = useRef(null) const tabPaneElsRef = useRef(new Map()) const captureQueueRef = useRef(Promise.resolve()) const captureInFlightRef = useRef(new Map()) const previewThemeRef = useRef(isDarkMode ? 'dark' : 'light') tabsRef.current = tabs activeTabIdRef.current = activeTabId + locationRef.current = location previewThemeRef.current = isDarkMode ? 'dark' : 'light' + const isRestoreLocation = useCallback((entry) => { + const restoring = isRestoringRef.current + if (!restoring) return false + if (restoring === true) return true + return ( + entriesEqual(restoring.target, entry) || + entriesEqual(restoring.from, entry) + ) + }, []) + + const shouldIgnoreRestoredLocation = useCallback((entry) => { + const restoring = isRestoringRef.current + if (!restoring) return false + if (restoring === true) { + isRestoringRef.current = null + return true + } + if (entriesEqual(restoring.target, entry)) { + isRestoringRef.current = null + return true + } + if (entriesEqual(restoring.from, entry)) { + return true + } + isRestoringRef.current = null + return false + }, []) + const restoreToEntry = useCallback( (entry) => { if (!entry) return - isRestoringRef.current = true + isRestoringRef.current = { + target: locationToEntry(entry), + from: locationToEntry(locationRef.current) + } navigate(entryToPath(entry), { replace: true }) }, [navigate] @@ -141,13 +243,13 @@ export const NavigationTabsProvider = ({ children }) => { const nextEntry = getTabCurrentEntry(nextTab) setActiveTabId(tabId) - if (entriesEqual(nextEntry, location)) { + if (entriesEqual(nextEntry, locationRef.current)) { return } restoreToEntry(nextEntry) }, - [location, restoreToEntry] + [restoreToEntry] ) const addTab = useCallback(() => { @@ -194,13 +296,13 @@ export const NavigationTabsProvider = ({ children }) => { if (nextActive) { setActiveTabId(nextActive.id) const nextEntry = getTabCurrentEntry(nextActive) - if (!entriesEqual(nextEntry, location)) { + if (!entriesEqual(nextEntry, locationRef.current)) { restoreToEntry(nextEntry) } } return true }, - [handleWindowControl, location, restoreToEntry] + [handleWindowControl, restoreToEntry] ) const closeTab = useCallback( @@ -213,29 +315,30 @@ export const NavigationTabsProvider = ({ children }) => { const acceptIncomingTab = useCallback( (incomingTab, targetId, insertBefore = false) => { if (!incomingTab?.id) return + const tab = normalizeTab(incomingTab, locationRef.current) setTabs((current) => { - if (current.some((tab) => tab.id === incomingTab.id)) { + if (current.some((item) => item.id === tab.id)) { return current } const next = [...current] - const targetIndex = next.findIndex((tab) => tab.id === targetId) + const targetIndex = next.findIndex((item) => item.id === targetId) if (targetIndex === -1) { - next.push(incomingTab) + next.push(tab) return next } - next.splice(insertBefore ? targetIndex : targetIndex + 1, 0, incomingTab) + next.splice(insertBefore ? targetIndex : targetIndex + 1, 0, tab) return next }) - setActiveTabId(incomingTab.id) - const nextEntry = getTabCurrentEntry(incomingTab) - if (!entriesEqual(nextEntry, location)) { + setActiveTabId(tab.id) + const nextEntry = getTabCurrentEntry(tab) + if (!entriesEqual(nextEntry, locationRef.current)) { restoreToEntry(nextEntry) } }, - [location, restoreToEntry] + [restoreToEntry] ) const handleTabDragStart = useCallback( @@ -378,22 +481,31 @@ export const NavigationTabsProvider = ({ children }) => { void captureTabPreview(previousId) }, [activeTabId, captureTabPreview, hydrated]) - const setTabPage = useCallback(({ title, modelName } = {}) => { - const activeId = activeTabIdRef.current - if (!activeId) return + const setTabPage = useCallback( + ({ title, modelName, iconKey, location: pageLocation } = {}) => { + const activeId = activeTabIdRef.current + if (!activeId) return - setTabs((current) => - current.map((tab) => { - if (tab.id !== activeId) return tab - return { - ...tab, - title: title || tab.title, - modelName: - modelName === undefined ? tab.modelName : modelName || null - } + const pageEntry = pageLocation ? locationToEntry(pageLocation) : null + + setTabs((current) => { + if (current.length === 0) return current + + let changed = false + const next = current.map((tab) => { + if (tab.id !== activeId) return tab + let updated = applyPageMetaToTab(tab, { title, modelName, iconKey }) + if (pageEntry && !isRestoreLocation(pageEntry)) { + updated = applyLocationToTab(updated, pageEntry) + } + if (updated !== tab) changed = true + return updated + }) + return changed ? next : current }) - ) - }, []) + }, + [isRestoreLocation] + ) const goBack = useCallback(() => { const currentTab = tabsRef.current.find( @@ -457,22 +569,24 @@ export const NavigationTabsProvider = ({ children }) => { if (cancelled) return if (session?.tabs?.length) { - isRestoringRef.current = true - setTabs(session.tabs) + const restoredTabs = session.tabs.map((tab) => + normalizeTab(tab, location) + ) + setTabs(restoredTabs) const nextActiveId = session.activeTabId && - session.tabs.some((tab) => tab.id === session.activeTabId) + restoredTabs.some((tab) => tab.id === session.activeTabId) ? session.activeTabId - : session.tabs[0].id + : restoredTabs[0].id setActiveTabId(nextActiveId) const activeTab = - session.tabs.find((tab) => tab.id === nextActiveId) || - session.tabs[0] + restoredTabs.find((tab) => tab.id === nextActiveId) || + restoredTabs[0] const entry = getTabCurrentEntry(activeTab) if (!entriesEqual(entry, location)) { restoreToEntry(entry) } else { - isRestoringRef.current = false + isRestoringRef.current = null } } else { const initialTab = createTabFromLocation(location) @@ -493,12 +607,10 @@ export const NavigationTabsProvider = ({ children }) => { useEffect(() => { if (!isElectron || !hydrated) return - if (isRestoringRef.current) { - isRestoringRef.current = false - return - } const entry = locationToEntry(location) + if (shouldIgnoreRestoredLocation(entry)) return + setTabs((current) => { if (current.length === 0) { const initialTab = createTabFromLocation(location) @@ -506,20 +618,16 @@ export const NavigationTabsProvider = ({ children }) => { return [initialTab] } - return current.map((tab) => { + let changed = false + const next = current.map((tab) => { if (tab.id !== activeTabIdRef.current) return tab - const currentEntry = getTabCurrentEntry(tab) - if (entriesEqual(currentEntry, entry)) return tab - - const truncated = tab.history.slice(0, tab.historyIndex + 1) - return { - ...tab, - history: [...truncated, entry], - historyIndex: truncated.length - } + const updated = applyLocationToTab(tab, entry) + if (updated !== tab) changed = true + return updated }) + return changed ? next : current }) - }, [hydrated, isElectron, location]) + }, [hydrated, isElectron, location, shouldIgnoreRestoredLocation]) useEffect(() => { if (!isElectron || !hydrated || !syncWindowTabs) return undefined @@ -670,21 +778,22 @@ export const useTabPreview = (tabId) => { } // eslint-disable-next-line react-refresh/only-export-components -export const useNavigationTabPage = ({ title, modelName } = {}) => { +export const useNavigationTabPage = ({ title, modelName, iconKey } = {}) => { const { setTabPage, isElectron } = useContext(NavigationTabMetaContext) const store = useContext(NavigationTabActiveContext) + const pageLocation = useLocation() useEffect(() => { if (!isElectron || !title) return undefined const sync = () => { if (!store.getSnapshot()) return - setTabPage({ title, modelName }) + setTabPage({ title, modelName, iconKey, location: pageLocation }) } sync() return store.subscribe(sync) - }, [isElectron, modelName, setTabPage, store, title]) + }, [iconKey, isElectron, modelName, pageLocation, setTabPage, store, title]) } // eslint-disable-next-line react-refresh/only-export-components diff --git a/src/components/Icons/sidebarIconMap.jsx b/src/components/Icons/sidebarIconMap.jsx index 30920053..c6c04a20 100644 --- a/src/components/Icons/sidebarIconMap.jsx +++ b/src/components/Icons/sidebarIconMap.jsx @@ -62,79 +62,91 @@ import MailIcon from './MailIcon' import EmailAccountIcon from './EmailAccountIcon' import EmailTemplateIcon from './EmailTemplateIcon' -const toEmoji = (emoji) => {emoji} +const makeEmojiIcon = (emoji) => { + const EmojiIcon = (props) => ( + + {emoji} + + ) + return EmojiIcon +} -const sidebarIconMap = { - production: , - printer: , - printerProfile: , - filamentProfile: , - job: , - subJob: , - gcodeFile: , - inventory: , - filamentStock: , - partStock: , - productStock: , - stockEvent: , - stockAudit: , - stockAuditLevel: , - purchaseOrder: , - orderItem: , - shipment: , - stockLocation: , - stockTransfer: , - sales: , - client: , - salesOrder: , - marketplace: , - listing: , - listingVarient: , - fulfillmentPolicy: , - returnPolicy: , - paymentPolicy: , - finance: , - invoice: , - payment: , - filament: , - filamentSku: , - part: , - partSku: , - product: , - productCategory: , - productSku: , - vendor: , - material: , - noteType: , - settings: , - auditLog: , - developer: , - person: , - userGroup: , - permissionSetting: , - host: , - documentPrinter: , - documentTemplate: , - document: , - documentSize: , - documentJob: , - file: , - courier: , - courierService: , - taxRate: , - taxRecord: , - appPassword: , - email: , - emailAccount: , - emailTemplate: , - emailMessage: , - infoCircle: , - sessionStorage: toEmoji('🗃️'), - authDebug: toEmoji('🔐'), - apiDebug: toEmoji('🌐') +const sidebarIconComponents = { + production: ProductionIcon, + printer: PrinterIcon, + printerProfile: PrinterProfileIcon, + filamentProfile: FilamentProfileIcon, + job: JobIcon, + subJob: SubJobIcon, + gcodeFile: GCodeFileIcon, + inventory: InventoryIcon, + filamentStock: FilamentStockIcon, + partStock: PartStockIcon, + productStock: ProductStockIcon, + stockEvent: StockEventIcon, + stockAudit: StockAuditIcon, + stockAuditLevel: StockAuditLevelIcon, + purchaseOrder: PurchaseOrderIcon, + orderItem: OrderItemIcon, + shipment: ShipmentIcon, + stockLocation: StockLocationIcon, + stockTransfer: StockTransferIcon, + sales: SalesIcon, + client: ClientIcon, + salesOrder: SalesOrderIcon, + marketplace: MarketplaceIcon, + listing: ListingIcon, + listingVarient: ListingVarientIcon, + fulfillmentPolicy: FulfillmentPolicyIcon, + returnPolicy: ReturnPolicyIcon, + paymentPolicy: PaymentPolicyIcon, + finance: FinanceIcon, + invoice: InvoiceIcon, + payment: PaymentIcon, + filament: FilamentIcon, + filamentSku: FilamentSkuIcon, + part: PartIcon, + partSku: PartSkuIcon, + product: ProductIcon, + productCategory: ProductCategoryIcon, + productSku: ProductSkuIcon, + vendor: VendorIcon, + material: MaterialIcon, + noteType: NoteTypeIcon, + settings: SettingsIcon, + auditLog: AuditLogIcon, + developer: DeveloperIcon, + person: PersonIcon, + userGroup: PersonGroupIcon, + permissionSetting: PermissionSettingIcon, + host: HostIcon, + documentPrinter: DocumentPrinterIcon, + documentTemplate: DocumentTemplateIcon, + document: DocumentIcon, + documentSize: DocumentSizeIcon, + documentJob: DocumentJobIcon, + file: FileIcon, + courier: CourierIcon, + courierService: CourierServiceIcon, + taxRate: TaxRateIcon, + taxRecord: TaxRecordIcon, + appPassword: AppPasswordIcon, + email: MailIcon, + emailAccount: EmailAccountIcon, + emailTemplate: EmailTemplateIcon, + emailMessage: MailIcon, + infoCircle: InfoCircleIcon, + sessionStorage: makeEmojiIcon('🗃️'), + authDebug: makeEmojiIcon('🔐'), + apiDebug: makeEmojiIcon('🌐') +} + +export const getSidebarIconComponent = (iconKey) => { + if (!iconKey) return null + return sidebarIconComponents[iconKey] || null } export const getSidebarIconNode = (iconKey) => { - if (!iconKey) return null - return sidebarIconMap[iconKey] || null + const Icon = getSidebarIconComponent(iconKey) + return Icon ? : null } diff --git a/src/desktop/window.js b/src/desktop/window.js index 55a06fb0..058b988a 100644 --- a/src/desktop/window.js +++ b/src/desktop/window.js @@ -471,6 +471,7 @@ function createDefaultTab() { id, title: 'Overview - Production', modelName: null, + iconKey: 'production', history: [ { pathname: DEFAULT_DASHBOARD_PATH,