mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
[FR] Save widget state per user (#9567)
* [FR] Save widget state per user Fixes #9562 * cleanup * fix doc strings * add reset stage
This commit is contained in:
parent
b86b1d4c4d
commit
71cf9f5452
@ -6,102 +6,28 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import { type Layout, Responsive, WidthProvider } from 'react-grid-layout';
|
import { type Layout, Responsive, WidthProvider } from 'react-grid-layout';
|
||||||
|
|
||||||
import { useDashboardItems } from '../../hooks/UseDashboardItems';
|
import { useDashboardItems } from '../../hooks/UseDashboardItems';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useLocalState } from '../../states/LocalState';
|
||||||
import DashboardMenu from './DashboardMenu';
|
import DashboardMenu from './DashboardMenu';
|
||||||
import DashboardWidget, { type DashboardWidgetProps } from './DashboardWidget';
|
import DashboardWidget, { type DashboardWidgetProps } from './DashboardWidget';
|
||||||
import DashboardWidgetDrawer from './DashboardWidgetDrawer';
|
import DashboardWidgetDrawer from './DashboardWidgetDrawer';
|
||||||
|
|
||||||
const ReactGridLayout = WidthProvider(Responsive);
|
const ReactGridLayout = WidthProvider(Responsive);
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the dashboard layout to local storage
|
|
||||||
*/
|
|
||||||
function saveDashboardLayout(layouts: any, userId: number | undefined): void {
|
|
||||||
const reducedLayouts: any = {};
|
|
||||||
|
|
||||||
// Reduce the layouts to exclude default attributes from the dataset
|
|
||||||
Object.keys(layouts).forEach((key) => {
|
|
||||||
reducedLayouts[key] = layouts[key].map((item: Layout) => {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
moved: item.moved ? true : undefined,
|
|
||||||
static: item.static ? true : undefined
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = JSON.stringify(reducedLayouts);
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
localStorage?.setItem(`dashboard-layout-${userId}`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage?.setItem('dashboard-layout', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the dashboard layout from local storage
|
|
||||||
*/
|
|
||||||
function loadDashboardLayout(
|
|
||||||
userId: number | undefined
|
|
||||||
): Record<string, Layout[]> {
|
|
||||||
let layout = userId && localStorage?.getItem(`dashboard-layout-${userId}`);
|
|
||||||
|
|
||||||
if (!layout) {
|
|
||||||
// Fallback to global layout
|
|
||||||
layout = localStorage?.getItem('dashboard-layout');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layout) {
|
|
||||||
return JSON.parse(layout);
|
|
||||||
} else {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the list of selected widgets to local storage
|
|
||||||
*/
|
|
||||||
function saveDashboardWidgets(
|
|
||||||
widgets: string[],
|
|
||||||
userId: number | undefined
|
|
||||||
): void {
|
|
||||||
const data = JSON.stringify(widgets);
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
localStorage?.setItem(`dashboard-widgets-${userId}`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage?.setItem('dashboard-widgets', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the list of selected widgets from local storage
|
|
||||||
*/
|
|
||||||
function loadDashboardWidgets(userId: number | undefined): string[] {
|
|
||||||
let widgets = userId && localStorage?.getItem(`dashboard-widgets-${userId}`);
|
|
||||||
|
|
||||||
if (!widgets) {
|
|
||||||
// Fallback to global widget list
|
|
||||||
widgets = localStorage?.getItem('dashboard-widgets');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widgets) {
|
|
||||||
return JSON.parse(widgets);
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DashboardLayout() {
|
export default function DashboardLayout() {
|
||||||
const user = useUserState();
|
|
||||||
|
|
||||||
// Dashboard layout definition
|
// Dashboard layout definition
|
||||||
const [layouts, setLayouts] = useState({});
|
const [layouts, setLayouts] = useState({});
|
||||||
|
|
||||||
// Dashboard widget selection
|
// Dashboard widget selection
|
||||||
const [widgets, setWidgets] = useState<DashboardWidgetProps[]>([]);
|
const [widgets, setWidgets] = useState<DashboardWidgetProps[]>([]);
|
||||||
|
|
||||||
|
// local/remote storage values for widget / layout
|
||||||
|
const [remoteWidgets, setRemoteWidgets, remoteLayouts, setRemoteLayouts] =
|
||||||
|
useLocalState((state) => [
|
||||||
|
state.widgets,
|
||||||
|
state.setWidgets,
|
||||||
|
state.layouts,
|
||||||
|
state.setLayouts
|
||||||
|
]);
|
||||||
|
|
||||||
const [editing, setEditing] = useDisclosure(false);
|
const [editing, setEditing] = useDisclosure(false);
|
||||||
const [removing, setRemoving] = useDisclosure(false);
|
const [removing, setRemoving] = useDisclosure(false);
|
||||||
|
|
||||||
@ -132,7 +58,7 @@ export default function DashboardLayout() {
|
|||||||
// Save the selected widgets to local storage when the selection changes
|
// Save the selected widgets to local storage when the selection changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
saveDashboardWidgets(widgetLabels, user.userId());
|
setRemoteWidgets(widgetLabels);
|
||||||
}
|
}
|
||||||
}, [widgetLabels]);
|
}, [widgetLabels]);
|
||||||
|
|
||||||
@ -232,7 +158,18 @@ export default function DashboardLayout() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (layouts && loaded && availableWidgets.loaded) {
|
if (layouts && loaded && availableWidgets.loaded) {
|
||||||
saveDashboardLayout(newLayouts, user.userId());
|
const reducedLayouts: any = {};
|
||||||
|
// Reduce the layouts to exclude default attributes from the dataset
|
||||||
|
Object.keys(newLayouts).forEach((key) => {
|
||||||
|
reducedLayouts[key] = newLayouts[key].map((item: Layout) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
moved: item.moved ? true : undefined,
|
||||||
|
static: item.static ? true : undefined
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setRemoteLayouts(reducedLayouts);
|
||||||
setLayouts(newLayouts);
|
setLayouts(newLayouts);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -242,13 +179,10 @@ export default function DashboardLayout() {
|
|||||||
// Load the dashboard layout from local storage
|
// Load the dashboard layout from local storage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (availableWidgets.loaded) {
|
if (availableWidgets.loaded) {
|
||||||
const initialLayouts = loadDashboardLayout(user.userId());
|
setLayouts(remoteLayouts);
|
||||||
const initialWidgetLabels = loadDashboardWidgets(user.userId());
|
|
||||||
|
|
||||||
setLayouts(initialLayouts);
|
|
||||||
setWidgets(
|
setWidgets(
|
||||||
availableWidgets.items.filter((widget) =>
|
availableWidgets.items.filter((widget) =>
|
||||||
initialWidgetLabels.includes(widget.label)
|
remoteWidgets.includes(widget.label)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -256,6 +190,12 @@ export default function DashboardLayout() {
|
|||||||
}
|
}
|
||||||
}, [availableWidgets.loaded]);
|
}, [availableWidgets.loaded]);
|
||||||
|
|
||||||
|
// Clear all widgets from the dashboard
|
||||||
|
const clearWidgets = useCallback(() => {
|
||||||
|
setWidgets([]);
|
||||||
|
setLayouts({});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DashboardWidgetDrawer
|
<DashboardWidgetDrawer
|
||||||
@ -267,6 +207,7 @@ export default function DashboardLayout() {
|
|||||||
<DashboardMenu
|
<DashboardMenu
|
||||||
onAddWidget={openWidgetDrawer}
|
onAddWidget={openWidgetDrawer}
|
||||||
onStartEdit={setEditing.open}
|
onStartEdit={setEditing.open}
|
||||||
|
onClear={clearWidgets}
|
||||||
onStartRemove={setRemoving.open}
|
onStartRemove={setRemoving.open}
|
||||||
onAcceptLayout={() => {
|
onAcceptLayout={() => {
|
||||||
setEditing.close();
|
setEditing.close();
|
||||||
|
@ -30,6 +30,7 @@ export default function DashboardMenu({
|
|||||||
onAddWidget,
|
onAddWidget,
|
||||||
onStartEdit,
|
onStartEdit,
|
||||||
onStartRemove,
|
onStartRemove,
|
||||||
|
onClear,
|
||||||
onAcceptLayout
|
onAcceptLayout
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
editing: boolean;
|
editing: boolean;
|
||||||
@ -37,6 +38,7 @@ export default function DashboardMenu({
|
|||||||
onAddWidget: () => void;
|
onAddWidget: () => void;
|
||||||
onStartEdit: () => void;
|
onStartEdit: () => void;
|
||||||
onStartRemove: () => void;
|
onStartRemove: () => void;
|
||||||
|
onClear: () => void;
|
||||||
onAcceptLayout: () => void;
|
onAcceptLayout: () => void;
|
||||||
}>) {
|
}>) {
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
@ -119,6 +121,15 @@ export default function DashboardMenu({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!editing && !removing && (
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<IconLayoutGridRemove color='red' size={14} />}
|
||||||
|
onClick={onClear}
|
||||||
|
>
|
||||||
|
<Trans>Clear Widgets</Trans>
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
{(editing || removing) && (
|
{(editing || removing) && (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<IconCircleCheck color='green' size={14} />}
|
leftSection={<IconCircleCheck color='green' size={14} />}
|
||||||
|
@ -183,7 +183,7 @@ function observeProfile() {
|
|||||||
// overwrite language and theme info in session with profile info
|
// overwrite language and theme info in session with profile info
|
||||||
|
|
||||||
const user = useUserState.getState().getUser();
|
const user = useUserState.getState().getUser();
|
||||||
const { language, setLanguage, userTheme, setTheme } =
|
const { language, setLanguage, userTheme, setTheme, setWidgets, setLayouts } =
|
||||||
useLocalState.getState();
|
useLocalState.getState();
|
||||||
if (user) {
|
if (user) {
|
||||||
if (user.profile?.language && language != user.profile.language) {
|
if (user.profile?.language && language != user.profile.language) {
|
||||||
@ -216,6 +216,15 @@ function observeProfile() {
|
|||||||
setTheme(newTheme);
|
setTheme(newTheme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.profile?.widgets) {
|
||||||
|
const data = user.profile.widgets;
|
||||||
|
// split data into widgets and layouts (either might be undefined)
|
||||||
|
const widgets = data.widgets ?? [];
|
||||||
|
const layouts = data.layouts ?? {};
|
||||||
|
setWidgets(widgets, true);
|
||||||
|
setLayouts(layouts, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +28,10 @@ interface LocalStateProps {
|
|||||||
}[],
|
}[],
|
||||||
noPatch?: boolean
|
noPatch?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
|
widgets: string[];
|
||||||
|
setWidgets: (widgets: string[], noPatch?: boolean) => void;
|
||||||
|
layouts: any;
|
||||||
|
setLayouts: (layouts: any, noPatch?: boolean) => void;
|
||||||
// panels
|
// panels
|
||||||
lastUsedPanels: Record<string, string>;
|
lastUsedPanels: Record<string, string>;
|
||||||
setLastUsedPanel: (panelKey: string) => (value: string) => void;
|
setLastUsedPanel: (panelKey: string) => (value: string) => void;
|
||||||
@ -104,6 +108,28 @@ export const useLocalState = create<LocalStateProps>()(
|
|||||||
set({ userTheme: newTheme });
|
set({ userTheme: newTheme });
|
||||||
if (!noPatch) patchUser('theme', newTheme);
|
if (!noPatch) patchUser('theme', newTheme);
|
||||||
},
|
},
|
||||||
|
widgets: [],
|
||||||
|
setWidgets: (newWidgets, noPatch = false) => {
|
||||||
|
// check for difference
|
||||||
|
if (JSON.stringify(newWidgets) === JSON.stringify(get().widgets)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ widgets: newWidgets });
|
||||||
|
if (!noPatch)
|
||||||
|
patchUser('widgets', { widgets: newWidgets, layouts: get().layouts });
|
||||||
|
},
|
||||||
|
layouts: {},
|
||||||
|
setLayouts: (newLayouts, noPatch) => {
|
||||||
|
// check for difference
|
||||||
|
if (JSON.stringify(newLayouts) === JSON.stringify(get().layouts)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ layouts: newLayouts });
|
||||||
|
if (!noPatch)
|
||||||
|
patchUser('widgets', { widgets: get().widgets, layouts: newLayouts });
|
||||||
|
},
|
||||||
// panels
|
// panels
|
||||||
lastUsedPanels: {},
|
lastUsedPanels: {},
|
||||||
setLastUsedPanel: (panelKey) => (value) => {
|
setLastUsedPanel: (panelKey) => (value) => {
|
||||||
@ -171,7 +197,7 @@ export const useLocalState = create<LocalStateProps>()(
|
|||||||
/*
|
/*
|
||||||
pushes changes in user profile to backend
|
pushes changes in user profile to backend
|
||||||
*/
|
*/
|
||||||
function patchUser(key: 'language' | 'theme', val: any) {
|
function patchUser(key: 'language' | 'theme' | 'widgets', val: any) {
|
||||||
const uid = useUserState.getState().userId();
|
const uid = useUserState.getState().userId();
|
||||||
if (uid) {
|
if (uid) {
|
||||||
api.patch(apiUrl(ApiEndpoints.user_profile), { [key]: val });
|
api.patch(apiUrl(ApiEndpoints.user_profile), { [key]: val });
|
||||||
|
@ -78,7 +78,7 @@ export const doCachedLogin = async (
|
|||||||
await page.waitForURL('**/web/**');
|
await page.waitForURL('**/web/**');
|
||||||
|
|
||||||
// Wait for the dashboard to load
|
// Wait for the dashboard to load
|
||||||
await page.getByText('No widgets selected').waitFor();
|
//await page.getByText('No widgets selected').waitFor()
|
||||||
await page.waitForLoadState('load');
|
await page.waitForLoadState('load');
|
||||||
|
|
||||||
// Cache the login state
|
// Cache the login state
|
||||||
|
@ -5,6 +5,10 @@ import { setPluginState } from '../settings.js';
|
|||||||
test('Dashboard - Basic', async ({ browser }) => {
|
test('Dashboard - Basic', async ({ browser }) => {
|
||||||
const page = await doCachedLogin(browser);
|
const page = await doCachedLogin(browser);
|
||||||
|
|
||||||
|
// Reset wizards
|
||||||
|
await page.getByLabel('dashboard-menu').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Clear Widgets' }).click();
|
||||||
|
|
||||||
await page.getByText('Use the menu to add widgets').waitFor();
|
await page.getByText('Use the menu to add widgets').waitFor();
|
||||||
|
|
||||||
// Let's add some widgets
|
// Let's add some widgets
|
||||||
|
Loading…
x
Reference in New Issue
Block a user