[PUI] URl / panel fix (#6078)

* Add debug messages for auth - trying to track down issues

* Add explicit PartIndex page

* Remove debug statements

* Simplify panel state management

- Do not encode in URL
- Reduce number of component loads
- Ensure a valid panel is always selected

* Implement StockIndex page
This commit is contained in:
Oliver 2023-12-13 21:37:41 +11:00 committed by GitHub
parent 6cb66a7a70
commit 902eafcc75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 111 additions and 101 deletions

View File

@ -15,8 +15,14 @@ export function setApiDefaults() {
const token = useSessionState.getState().token; const token = useSessionState.getState().token;
api.defaults.baseURL = host; api.defaults.baseURL = host;
api.defaults.headers.common['Authorization'] = `Token ${token}`;
if (token) {
api.defaults.headers.common['Authorization'] = `Token ${token}`;
} else {
api.defaults.headers.common['Authorization'] = null;
}
} }
export const queryClient = new QueryClient(); export const queryClient = new QueryClient();
function checkMobile() { function checkMobile() {

View File

@ -10,15 +10,8 @@ import {
IconLayoutSidebarLeftCollapse, IconLayoutSidebarLeftCollapse,
IconLayoutSidebarRightCollapse IconLayoutSidebarRightCollapse
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react'; import { ReactNode, useCallback, useMemo } from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import {
Navigate,
Route,
Routes,
useNavigate,
useParams
} from 'react-router-dom';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
import { PlaceholderPanel } from '../items/Placeholder'; import { PlaceholderPanel } from '../items/Placeholder';
@ -44,61 +37,57 @@ export type PanelProps = {
collapsible?: boolean; collapsible?: boolean;
}; };
function BasePanelGroup({ export function PanelGroup({
pageKey, pageKey,
panels, panels,
onPanelChange, onPanelChange,
selectedPanel, selectedPanel,
collapsible = true collapsible = true
}: PanelProps): ReactNode { }: PanelProps): ReactNode {
const navigate = useNavigate(); const localState = useLocalState();
const { panel } = useParams();
const [panel, setPanel] = useState<string>(selectedPanel ?? '');
// Keep a list of active panels (hidden and disabled panels are not active)
const activePanels = useMemo( const activePanels = useMemo(
() => panels.filter((panel) => !panel.hidden && !panel.disabled), () => panels.filter((panel) => !panel.hidden && !panel.disabled),
[panels] [panels]
); );
const setLastUsedPanel = useLocalState((state) => // Set selected panel when component is initially loaded, or when the selected panel changes
state.setLastUsedPanel(pageKey)
);
useEffect(() => { useEffect(() => {
if (panel) { let first_panel: string = activePanels[0]?.name ?? '';
setLastUsedPanel(panel); let active_panel: string =
useLocalState.getState().getLastUsedPanel(pageKey)() ?? '';
let panelName = selectedPanel || active_panel || first_panel;
if (panelName != panel) {
setPanel(panelName);
} }
// panel is intentionally no dependency as this should only run on initial render
}, [setLastUsedPanel]); if (panelName != active_panel) {
useLocalState.getState().setLastUsedPanel(pageKey)(panelName);
}
}, [activePanels, panels, selectedPanel]);
// Callback when the active panel changes // Callback when the active panel changes
function handlePanelChange(panel: string) { const handlePanelChange = useCallback(
if (activePanels.findIndex((p) => p.name === panel) === -1) { (panelName: string) => {
setLastUsedPanel(''); // Ensure that the panel name is valid
return navigate('../'); if (!activePanels.some((panel) => panel.name == panelName)) {
} return;
}
navigate(`../${panel}`); setPanel(panelName);
localState.setLastUsedPanel(pageKey)(panelName);
// Optionally call external callback hook if (onPanelChange) {
if (onPanelChange) { onPanelChange(panelName);
onPanelChange(panel); }
} },
} [onPanelChange, pageKey]
);
// if the selected panel state changes update the current panel
useEffect(() => {
if (selectedPanel && selectedPanel !== panel) {
handlePanelChange(selectedPanel);
}
}, [selectedPanel, panel]);
// Update the active panel when panels changes and the active is no longer available
useEffect(() => {
if (activePanels.findIndex((p) => p.name === panel) === -1) {
setLastUsedPanel('');
return navigate('../');
}
}, [activePanels, panel]);
const [expanded, setExpanded] = useState<boolean>(true); const [expanded, setExpanded] = useState<boolean>(true);
@ -169,38 +158,3 @@ function BasePanelGroup({
</Paper> </Paper>
); );
} }
function IndexPanelComponent({ pageKey, selectedPanel, panels }: PanelProps) {
const lastUsedPanel = useLocalState((state) => {
const panelName =
selectedPanel || state.lastUsedPanels[pageKey] || panels[0]?.name;
if (
panels.findIndex(
(p) => p.name === panelName && !p.disabled && !p.hidden
) === -1
) {
return panels[0]?.name;
}
return panelName;
});
return <Navigate to={lastUsedPanel} replace />;
}
/**
* Render a panel group. The current panel will be appended to the current url.
* The last opened panel will be stored in local storage and opened if no panel is provided via url param
* @param panels - The list of panels to display
* @param onPanelChange - Callback when the active panel changes
* @param collapsible - If true, the panel group can be collapsed (defaults to true)
*/
export function PanelGroup(props: PanelProps) {
return (
<Routes>
<Route index element={<IndexPanelComponent {...props} />} />
<Route path="/:panel/*" element={<BasePanelGroup {...props} />} />
</Routes>
);
}

View File

@ -27,7 +27,9 @@ export const doClassicLogin = async (username: string, password: string) => {
name: 'inventree-web-app' name: 'inventree-web-app'
} }
}) })
.then((response) => response.data.token) .then((response) => {
return response.status == 200 ? response.data?.token : undefined;
})
.catch((error) => { .catch((error) => {
showNotification({ showNotification({
title: t`Login failed`, title: t`Login failed`,
@ -37,7 +39,7 @@ export const doClassicLogin = async (username: string, password: string) => {
return false; return false;
}); });
if (token === false) return token; if (!token) return false;
// log in with token // log in with token
doTokenLogin(token); doTokenLogin(token);
@ -51,6 +53,7 @@ export const doClassicLogout = async () => {
// TODO @matmair - logout from the server session // TODO @matmair - logout from the server session
// Set token in context // Set token in context
const { setToken } = useSessionState.getState(); const { setToken } = useSessionState.getState();
setToken(undefined); setToken(undefined);
notifications.show({ notifications.show({

View File

@ -22,9 +22,11 @@ import { useInstance } from '../../hooks/UseInstance';
* *
* Note: If no category ID is supplied, this acts as the top-level part category page * Note: If no category ID is supplied, this acts as the top-level part category page
*/ */
export default function CategoryDetail({}: {}) { export function CategoryPage({
const { id } = useParams(); categoryId
}: {
categoryId?: string | undefined;
}) {
const [treeOpen, setTreeOpen] = useState(false); const [treeOpen, setTreeOpen] = useState(false);
const { const {
@ -33,7 +35,8 @@ export default function CategoryDetail({}: {}) {
instanceQuery instanceQuery
} = useInstance({ } = useInstance({
endpoint: ApiPaths.category_list, endpoint: ApiPaths.category_list,
pk: id, pk: categoryId,
hasPrimaryKey: true,
params: { params: {
path_detail: true path_detail: true
} }
@ -49,7 +52,7 @@ export default function CategoryDetail({}: {}) {
<PartListTable <PartListTable
props={{ props={{
params: { params: {
category: id category: categoryId
} }
}} }}
/> />
@ -62,7 +65,7 @@ export default function CategoryDetail({}: {}) {
content: ( content: (
<PartCategoryTable <PartCategoryTable
params={{ params={{
parent: id parent: categoryId ?? 'null'
}} }}
/> />
) )
@ -74,7 +77,7 @@ export default function CategoryDetail({}: {}) {
content: <PlaceholderPanel /> content: <PlaceholderPanel />
} }
], ],
[category, id] [categoryId]
); );
const breadcrumbs = useMemo( const breadcrumbs = useMemo(
@ -110,3 +113,13 @@ export default function CategoryDetail({}: {}) {
</Stack> </Stack>
); );
} }
/**
* Detail page for a specific Part Category instance.
* Uses the :id parameter in the URL to determine which category to display.admin in
*/
export default function CategoryDetail({}: {}) {
const { id } = useParams();
return <CategoryPage categoryId={id} />;
}

View File

@ -0,0 +1,5 @@
import { CategoryPage } from './CategoryDetail';
export default function PartIndex({}: {}) {
return <CategoryPage />;
}

View File

@ -12,9 +12,11 @@ import { StockLocationTable } from '../../components/tables/stock/StockLocationT
import { ApiPaths } from '../../enums/ApiEndpoints'; import { ApiPaths } from '../../enums/ApiEndpoints';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
export default function Stock() { export function LocationPage({
const { id } = useParams(); locationId
}: {
locationId?: string | undefined;
}) {
const [treeOpen, setTreeOpen] = useState(false); const [treeOpen, setTreeOpen] = useState(false);
const { const {
@ -23,7 +25,8 @@ export default function Stock() {
instanceQuery instanceQuery
} = useInstance({ } = useInstance({
endpoint: ApiPaths.stock_location_list, endpoint: ApiPaths.stock_location_list,
pk: id, pk: locationId,
hasPrimaryKey: true,
params: { params: {
path_detail: true path_detail: true
} }
@ -38,7 +41,7 @@ export default function Stock() {
content: ( content: (
<StockItemTable <StockItemTable
params={{ params={{
location: id location: locationId
}} }}
/> />
) )
@ -50,13 +53,13 @@ export default function Stock() {
content: ( content: (
<StockLocationTable <StockLocationTable
params={{ params={{
parent: id parent: locationId ?? 'null'
}} }}
/> />
) )
} }
]; ];
}, [location, id]); }, [locationId]);
const breadcrumbs = useMemo( const breadcrumbs = useMemo(
() => [ () => [
@ -91,3 +94,13 @@ export default function Stock() {
</> </>
); );
} }
/**
* Detail page for a specific Stock Location instance
* Uses the :id parameter in the URL to determine which location to display
*/
export default function LocationDetail({}: {}) {
const { id } = useParams();
return <LocationPage locationId={id} />;
}

View File

@ -0,0 +1,5 @@
import { LocationPage } from './LocationDetail';
export default function StockIndex({}: {}) {
return <LocationPage />;
}

View File

@ -28,9 +28,12 @@ export const ManufacturerDetail = Loadable(
lazy(() => import('./pages/company/ManufacturerDetail')) lazy(() => import('./pages/company/ManufacturerDetail'))
); );
export const PartIndex = Loadable(lazy(() => import('./pages/part/PartIndex')));
export const CategoryDetail = Loadable( export const CategoryDetail = Loadable(
lazy(() => import('./pages/part/CategoryDetail')) lazy(() => import('./pages/part/CategoryDetail'))
); );
export const PartDetail = Loadable( export const PartDetail = Loadable(
lazy(() => import('./pages/part/PartDetail')) lazy(() => import('./pages/part/PartDetail'))
); );
@ -39,6 +42,10 @@ export const LocationDetail = Loadable(
lazy(() => import('./pages/stock/LocationDetail')) lazy(() => import('./pages/stock/LocationDetail'))
); );
export const StockIndex = Loadable(
lazy(() => import('./pages/stock/StockIndex'))
);
export const StockDetail = Loadable( export const StockDetail = Loadable(
lazy(() => import('./pages/stock/StockDetail')) lazy(() => import('./pages/stock/StockDetail'))
); );
@ -119,13 +126,13 @@ export const routes = (
<Route path="user/*" element={<UserSettings />} /> <Route path="user/*" element={<UserSettings />} />
</Route> </Route>
<Route path="part/"> <Route path="part/">
<Route index element={<Navigate to="category/" />} /> <Route index element={<PartIndex />} />
<Route path="category/:id?/*" element={<CategoryDetail />} /> <Route path="category/:id/*" element={<CategoryDetail />} />
<Route path=":id/*" element={<PartDetail />} /> <Route path=":id/*" element={<PartDetail />} />
</Route> </Route>
<Route path="stock/"> <Route path="stock/">
<Route index element={<Navigate to="location/" />} /> <Route index element={<StockIndex />} />
<Route path="location/:id?/*" element={<LocationDetail />} /> <Route path="location/:id/*" element={<LocationDetail />} />
<Route path="item/:id/*" element={<StockDetail />} /> <Route path="item/:id/*" element={<StockDetail />} />
</Route> </Route>
<Route path="build/"> <Route path="build/">

View File

@ -24,6 +24,7 @@ interface LocalStateProps {
loader: LoaderType; loader: LoaderType;
lastUsedPanels: Record<string, string>; lastUsedPanels: Record<string, string>;
setLastUsedPanel: (panelKey: string) => (value: string) => void; setLastUsedPanel: (panelKey: string) => (value: string) => void;
getLastUsedPanel: (panelKey: string) => () => string | undefined;
} }
export const useLocalState = create<LocalStateProps>()( export const useLocalState = create<LocalStateProps>()(
@ -55,6 +56,9 @@ export const useLocalState = create<LocalStateProps>()(
lastUsedPanels: { ...get().lastUsedPanels, [panelKey]: value } lastUsedPanels: { ...get().lastUsedPanels, [panelKey]: value }
}); });
} }
},
getLastUsedPanel(panelKey) {
return () => get().lastUsedPanels[panelKey];
} }
}), }),
{ {