Integrate TableStateProvider into App and Dashboard components for enhanced state management

- Added TableStateProvider to the App component, encapsulating routing logic to manage table states effectively.
- Removed redundant TableStateProvider from the DashboardLayout component, streamlining the component structure.
- Updated ControlPrinter and ObjectActions components to improve action visibility and navigation button rendering based on slicer integration state.
- Enhanced action filtering in ObjectActions to eliminate unnecessary dividers, improving menu item clarity.
This commit is contained in:
Tom Butcher 2026-08-01 15:21:58 +01:00
parent 12cae32e33
commit f3d6bebc38
4 changed files with 205 additions and 156 deletions

View File

@ -34,6 +34,7 @@ import AuthCallback from './components/App/AuthCallback.jsx'
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx' import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx' import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx'
import AuthLaunch from './components/App/AppLaunch.jsx' import AuthLaunch from './components/App/AppLaunch.jsx'
import { TableStateProvider } from './components/Dashboard/context/TableStateContext.jsx'
const SlicerIntegration = lazy( const SlicerIntegration = lazy(
() => () =>
import('./components/Dashboard/Production/Printers/SlicerIntegration.jsx') import('./components/Dashboard/Production/Printers/SlicerIntegration.jsx')
@ -86,76 +87,78 @@ const AppContent = () => {
<NotificationProvider> <NotificationProvider>
<SpotlightProvider> <SpotlightProvider>
<ActionsModalProvider> <ActionsModalProvider>
<Routes> <TableStateProvider>
<Route <Routes>
path='/applaunch' <Route
element={<AuthLaunch />} path='/applaunch'
/> element={<AuthLaunch />}
<Route />
path='/dashboard/electron/spotlightcontent' <Route
element={ path='/dashboard/electron/spotlightcontent'
<PrivateRoute element={
component={() => ( <PrivateRoute
<ElectronSpotlightContentPage /> component={() => (
)} <ElectronSpotlightContentPage />
/> )}
} />
/> }
<Route />
path='/' <Route
element={ path='/'
<PrivateRoute element={
component={() => ( <PrivateRoute
<Navigate component={() => (
to='/dashboard/production/overview' <Navigate
replace to='/dashboard/production/overview'
/> replace
)} />
/> )}
} />
/> }
<Route />
path='/auth/callback' <Route
element={<AuthCallback />} path='/auth/callback'
/> element={<AuthCallback />}
<Route />
path='/auth/marketplace/callback' <Route
element={<MarketplaceAuthCallback />} path='/auth/marketplace/callback'
/> element={<MarketplaceAuthCallback />}
<Route />
path='/email/notification' <Route
element={<EmailNotificationTemplate />} path='/email/notification'
/> element={<EmailNotificationTemplate />}
<Route />
path='/slicer' <Route
element={<SlicerIntegration />} path='/slicer'
/> element={<SlicerIntegration />}
/>
<Route <Route
path='/dashboard' path='/dashboard'
element={ element={
<PrivateRoute <PrivateRoute
component={() => <Dashboard />} component={() => <Dashboard />}
/> />
} }
> >
{ProductionRoutes} {ProductionRoutes}
{InventoryRoutes} {InventoryRoutes}
{FinanceRoutes} {FinanceRoutes}
{SalesRoutes} {SalesRoutes}
{ManagementRoutes} {ManagementRoutes}
{DeveloperRoutes} {DeveloperRoutes}
</Route> </Route>
<Route <Route
path='*' path='*'
element={ element={
<AppError <AppError
message='Error 404. Page not found.' message='Error 404. Page not found.'
showRefresh={false} showRefresh={false}
/> />
} }
/> />
</Routes> </Routes>
</TableStateProvider>
</ActionsModalProvider> </ActionsModalProvider>
</SpotlightProvider> </SpotlightProvider>
</NotificationProvider> </NotificationProvider>

View File

@ -12,7 +12,6 @@ import DashboardBreadcrumb from './common/DashboardBreadcrumb'
import DeveloperSidebar from './Developer/DeveloperSidebar' import DeveloperSidebar from './Developer/DeveloperSidebar'
import { useThemeContext } from './context/ThemeContext' import { useThemeContext } from './context/ThemeContext'
import { MessageProvider } from './context/MessageContext' import { MessageProvider } from './context/MessageContext'
import { TableStateProvider } from './context/TableStateContext'
const { Content } = Layout const { Content } = Layout
@ -29,42 +28,40 @@ const DashboardLayout = ({ children }) => {
return ( return (
<MessageProvider> <MessageProvider>
<TableStateProvider> <Layout
<Layout style={{ height: 'var(--unit-100vh)' }}
style={{ height: 'var(--unit-100vh)' }} className={isDarkMode ? 'dark-mode' : 'light-mode'}
className={isDarkMode ? 'dark-mode' : 'light-mode'} >
> <DashboardNavigation />
<DashboardNavigation /> <Layout>
<Layout> {isProduction ? (
{isProduction ? ( <ProductionSidebar />
<ProductionSidebar /> ) : isInventory ? (
) : isInventory ? ( <InventorySidebar />
<InventorySidebar /> ) : isFinance ? (
) : isFinance ? ( <FinanceSidebar />
<FinanceSidebar /> ) : isSales ? (
) : isSales ? ( <SalesSidebar />
<SalesSidebar /> ) : isManagement ? (
) : isManagement ? ( <ManagementSidebar />
<ManagementSidebar /> ) : isDeveloper ? (
) : isDeveloper ? ( <DeveloperSidebar />
<DeveloperSidebar /> ) : (
) : ( <ProductionSidebar /> // Default to production sidebar
<ProductionSidebar /> // Default to production sidebar )}
)} <Layout style={{ padding: '24px' }}>
<Layout style={{ padding: '24px' }}> <Content>
<Content> <Flex vertical style={{ height: '100%' }} gap='20px'>
<Flex vertical style={{ height: '100%' }} gap='20px'> <Flex justify='space-between'>
<Flex justify='space-between'> <DashboardBreadcrumb style={{ margin: '16px 0' }} />
<DashboardBreadcrumb style={{ margin: '16px 0' }} />
</Flex>
{children}
</Flex> </Flex>
</Content>
</Layout> {children}
</Flex>
</Content>
</Layout> </Layout>
</Layout> </Layout>
</TableStateProvider> </Layout>
</MessageProvider> </MessageProvider>
) )
} }

View File

@ -272,7 +272,8 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
visibleActions={{ visibleActions={{
edit: false, edit: false,
info: !slicerIntegration, info: !slicerIntegration,
control: !slicerIntegration control: !slicerIntegration,
newPrinterProfile: !slicerIntegration
}} }}
objectData={objectFormState.objectData} objectData={objectFormState.objectData}
/> />
@ -318,11 +319,13 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
/> />
</Space> </Space>
</Space> </Space>
<ObjectTableNavigationButtons {!slicerIntegration && (
disabled={objectFormState.loading} <ObjectTableNavigationButtons
_id={printerId} disabled={objectFormState.loading}
objectType='printer' _id={printerId}
/> objectType='printer'
/>
)}
</Flex> </Flex>
<AlertsDisplay <AlertsDisplay

View File

@ -37,54 +37,95 @@ function filterActionsByVisibility(actions, visibleActions) {
}) })
} }
function cleanDividers(items) {
if (!Array.isArray(items) || items.length === 0) return []
const cleaned = []
for (const item of items) {
if (!item) continue
if (item.type === 'divider') {
if (cleaned.length === 0) continue
if (cleaned[cleaned.length - 1]?.type === 'divider') continue
cleaned.push(item)
continue
}
if (item.children && Array.isArray(item.children)) {
const children = cleanDividers(item.children)
if (children.length === 0) continue
cleaned.push({ ...item, children })
continue
}
cleaned.push(item)
}
if (cleaned[cleaned.length - 1]?.type === 'divider') {
cleaned.pop()
}
return cleaned
}
// Recursively map actions to AntD Dropdown items // Recursively map actions to AntD Dropdown items
function mapActionsToMenuItems(actions, currentUrlWithActions, id, objectData) { function mapActionsToMenuItems(
return actions.map((action) => { actions,
if (action.type === 'divider') { currentUrlWithActions,
return { type: 'divider' } id,
} objectData,
const actionUrl = action.url ? action.url(id) : undefined userProfile
) {
var disabled = actionUrl && actionUrl === currentUrlWithActions return cleanDividers(
var visible = true actions.map((action) => {
if (action.type === 'divider') {
const { userProfile } = useContext(AuthContext) return { type: 'divider' }
if (action.disabled) {
if (typeof action.disabled === 'function') {
disabled = action.disabled({ ...objectData, _user: userProfile })
} else {
disabled = action.disabled
} }
} const actionUrl = action.url ? action.url(id) : undefined
if (action.visible) { var disabled = actionUrl && actionUrl === currentUrlWithActions
if (typeof action.visible === 'function') { var visible = true
visible = action.visible(objectData)
} else { if (action.disabled) {
visible = action.visible if (typeof action.disabled === 'function') {
disabled = action.disabled({ ...objectData, _user: userProfile })
} else {
disabled = action.disabled
}
} }
}
const item = { if (action.visible) {
key: action.key || action.name, if (typeof action.visible === 'function') {
label: action.label, visible = action.visible(objectData)
danger: action?.danger || false, } else {
icon: action.icon ? createElement(action.icon) : undefined, visible = action.visible
disabled }
} }
if (action.children && Array.isArray(action.children)) {
item.children = mapActionsToMenuItems( if (visible != true) {
action.children, return null
currentUrlWithActions, }
id,
objectData const item = {
) key: action.key || action.name,
} label: action.label,
if (visible == true) { danger: action?.danger || false,
icon: action.icon ? createElement(action.icon) : undefined,
disabled
}
if (action.children && Array.isArray(action.children)) {
item.children = mapActionsToMenuItems(
action.children,
currentUrlWithActions,
id,
objectData,
userProfile
)
}
return item return item
} })
}) )
} }
const stripActionParam = (pathname, search) => { const stripActionParam = (pathname, search) => {
@ -108,6 +149,7 @@ const ObjectActions = ({
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const { showActionsModal } = useActionsModal() const { showActionsModal } = useActionsModal()
const { userProfile } = useContext(AuthContext)
// Get current url without 'action' param // Get current url without 'action' param
const currentUrlWithoutActions = stripActionParam( const currentUrlWithoutActions = stripActionParam(
location.pathname, location.pathname,
@ -120,10 +162,13 @@ const ObjectActions = ({
visibleActions visibleActions
) )
const filteredActions = visibilityFilteredActions.filter( const filteredActions = cleanDividers(
(action) => visibilityFilteredActions.filter(
typeof action.url !== 'function' || (action) =>
action.url(id) !== currentUrlWithoutActions action.type === 'divider' ||
typeof action.url !== 'function' ||
action.url(id) !== currentUrlWithoutActions
)
) )
const currentUrlWithActions = location.pathname + location.search const currentUrlWithActions = location.pathname + location.search
@ -134,7 +179,8 @@ const ObjectActions = ({
filteredActions, filteredActions,
currentUrlWithActions, currentUrlWithActions,
id, id,
objectData objectData,
userProfile
), ),
onClick: (info) => { onClick: (info) => {
// Find the action by key // Find the action by key