2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-27 19:16: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:
Matthias Mair 2025-04-24 09:29:28 +02:00 committed by GitHub
parent b86b1d4c4d
commit 71cf9f5452
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 85 additions and 94 deletions

View File

@ -6,102 +6,28 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { type Layout, Responsive, WidthProvider } from 'react-grid-layout';
import { useDashboardItems } from '../../hooks/UseDashboardItems';
import { useUserState } from '../../states/UserState';
import { useLocalState } from '../../states/LocalState';
import DashboardMenu from './DashboardMenu';
import DashboardWidget, { type DashboardWidgetProps } from './DashboardWidget';
import DashboardWidgetDrawer from './DashboardWidgetDrawer';
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() {
const user = useUserState();
// Dashboard layout definition
const [layouts, setLayouts] = useState({});
// Dashboard widget selection
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 [removing, setRemoving] = useDisclosure(false);
@ -132,7 +58,7 @@ export default function DashboardLayout() {
// Save the selected widgets to local storage when the selection changes
useEffect(() => {
if (loaded) {
saveDashboardWidgets(widgetLabels, user.userId());
setRemoteWidgets(widgetLabels);
}
}, [widgetLabels]);
@ -232,7 +158,18 @@ export default function DashboardLayout() {
});
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);
}
},
@ -242,13 +179,10 @@ export default function DashboardLayout() {
// Load the dashboard layout from local storage
useEffect(() => {
if (availableWidgets.loaded) {
const initialLayouts = loadDashboardLayout(user.userId());
const initialWidgetLabels = loadDashboardWidgets(user.userId());
setLayouts(initialLayouts);
setLayouts(remoteLayouts);
setWidgets(
availableWidgets.items.filter((widget) =>
initialWidgetLabels.includes(widget.label)
remoteWidgets.includes(widget.label)
)
);
@ -256,6 +190,12 @@ export default function DashboardLayout() {
}
}, [availableWidgets.loaded]);
// Clear all widgets from the dashboard
const clearWidgets = useCallback(() => {
setWidgets([]);
setLayouts({});
}, []);
return (
<>
<DashboardWidgetDrawer
@ -267,6 +207,7 @@ export default function DashboardLayout() {
<DashboardMenu
onAddWidget={openWidgetDrawer}
onStartEdit={setEditing.open}
onClear={clearWidgets}
onStartRemove={setRemoving.open}
onAcceptLayout={() => {
setEditing.close();

View File

@ -30,6 +30,7 @@ export default function DashboardMenu({
onAddWidget,
onStartEdit,
onStartRemove,
onClear,
onAcceptLayout
}: Readonly<{
editing: boolean;
@ -37,6 +38,7 @@ export default function DashboardMenu({
onAddWidget: () => void;
onStartEdit: () => void;
onStartRemove: () => void;
onClear: () => void;
onAcceptLayout: () => void;
}>) {
const user = useUserState();
@ -119,6 +121,15 @@ export default function DashboardMenu({
</Menu.Item>
)}
{!editing && !removing && (
<Menu.Item
leftSection={<IconLayoutGridRemove color='red' size={14} />}
onClick={onClear}
>
<Trans>Clear Widgets</Trans>
</Menu.Item>
)}
{(editing || removing) && (
<Menu.Item
leftSection={<IconCircleCheck color='green' size={14} />}

View File

@ -183,7 +183,7 @@ function observeProfile() {
// overwrite language and theme info in session with profile info
const user = useUserState.getState().getUser();
const { language, setLanguage, userTheme, setTheme } =
const { language, setLanguage, userTheme, setTheme, setWidgets, setLayouts } =
useLocalState.getState();
if (user) {
if (user.profile?.language && language != user.profile.language) {
@ -216,6 +216,15 @@ function observeProfile() {
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);
}
}
}

View File

@ -28,6 +28,10 @@ interface LocalStateProps {
}[],
noPatch?: boolean
) => void;
widgets: string[];
setWidgets: (widgets: string[], noPatch?: boolean) => void;
layouts: any;
setLayouts: (layouts: any, noPatch?: boolean) => void;
// panels
lastUsedPanels: Record<string, string>;
setLastUsedPanel: (panelKey: string) => (value: string) => void;
@ -104,6 +108,28 @@ export const useLocalState = create<LocalStateProps>()(
set({ userTheme: 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
lastUsedPanels: {},
setLastUsedPanel: (panelKey) => (value) => {
@ -171,7 +197,7 @@ export const useLocalState = create<LocalStateProps>()(
/*
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();
if (uid) {
api.patch(apiUrl(ApiEndpoints.user_profile), { [key]: val });

View File

@ -78,7 +78,7 @@ export const doCachedLogin = async (
await page.waitForURL('**/web/**');
// Wait for the dashboard to load
await page.getByText('No widgets selected').waitFor();
//await page.getByText('No widgets selected').waitFor()
await page.waitForLoadState('load');
// Cache the login state

View File

@ -5,6 +5,10 @@ import { setPluginState } from '../settings.js';
test('Dashboard - Basic', async ({ 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();
// Let's add some widgets