From 67f036c0abb7a77ba0da48f74718a38f915dc3e4 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 8 Oct 2024 08:21:23 +0200 Subject: [PATCH] feat: store dashboard state (#8382) This PR stores the dashboard state (selected project and flag) in localstorage so that you get taken back to the same project and flag when you refresh the page or navigate away and back. It also handles scrolling the selected items into view in case they're below the fold. --- .../personalDashboard/MyProjects.tsx | 86 +++++++++------ .../PersonalDashboard.test.tsx | 3 + .../personalDashboard/PersonalDashboard.tsx | 104 ++++++++++++++---- .../usePersonalDashboardProjectDetails.ts | 2 +- 4 files changed, 138 insertions(+), 57 deletions(-) diff --git a/frontend/src/component/personalDashboard/MyProjects.tsx b/frontend/src/component/personalDashboard/MyProjects.tsx index 614342837ca0..4b664221a4ab 100644 --- a/frontend/src/component/personalDashboard/MyProjects.tsx +++ b/frontend/src/component/personalDashboard/MyProjects.tsx @@ -14,7 +14,7 @@ import { ProjectSetupComplete } from './ProjectSetupComplete'; import { ConnectSDK, CreateFlag, ExistingFlag } from './ConnectSDK'; import { LatestProjectEvents } from './LatestProjectEvents'; import { RoleAndOwnerInfo } from './RoleAndOwnerInfo'; -import type { FC } from 'react'; +import { useEffect, useRef, type FC } from 'react'; import { StyledCardTitle } from './PersonalDashboard'; import type { PersonalDashboardProjectDetailsSchema, @@ -63,6 +63,51 @@ const ActiveProjectDetails: FC<{ ); }; +const ProjectListItem: FC<{ + project: PersonalDashboardSchemaProjectsItem; + selected: boolean; + onClick: () => void; +}> = ({ project, selected, onClick }) => { + const activeProjectRef = useRef(null); + + useEffect(() => { + if (activeProjectRef.current) { + activeProjectRef.current.scrollIntoView({ + block: 'nearest', + inline: 'start', + }); + } + }, []); + + return ( + + + + + {project.name} + + + + + {selected ? : null} + + + ); +}; + export const MyProjects: FC<{ projects: PersonalDashboardSchemaProjectsItem[]; personalDashboardProjectDetails?: PersonalDashboardProjectDetailsSchema; @@ -104,41 +149,12 @@ export const MyProjects: FC<{ > {projects.map((project) => { return ( - - - setActiveProject(project.id) - } - > - - - - {project.name} - - - - - - {project.id === activeProject ? ( - - ) : null} - - + project={project} + selected={project.id === activeProject} + onClick={() => setActiveProject(project.id)} + /> ); })} diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.test.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.test.tsx index e28caced5082..260721e5109d 100644 --- a/frontend/src/component/personalDashboard/PersonalDashboard.test.tsx +++ b/frontend/src/component/personalDashboard/PersonalDashboard.test.tsx @@ -117,6 +117,9 @@ const setupNewProject = () => { // @ts-ignore HTMLCanvasElement.prototype.getContext = () => {}; +//scrollIntoView is not implemented in jsdom +HTMLElement.prototype.scrollIntoView = () => {}; + test('Render personal dashboard for a long running project', async () => { setupLongRunningProject(); render(); diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.tsx index a6e66ab81f1f..a8bc0cf41b74 100644 --- a/frontend/src/component/personalDashboard/PersonalDashboard.tsx +++ b/frontend/src/component/personalDashboard/PersonalDashboard.tsx @@ -8,14 +8,14 @@ import { styled, Typography, } from '@mui/material'; -import React, { type FC, useEffect, useState } from 'react'; +import React, { type FC, useEffect, useRef } from 'react'; import LinkIcon from '@mui/icons-material/ArrowForward'; import { WelcomeDialog } from './WelcomeDialog'; import { useLocalStorageState } from 'hooks/useLocalStorageState'; import { usePersonalDashboard } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboard'; import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; import type { - PersonalDashboardSchema, + PersonalDashboardSchemaFlagsItem, PersonalDashboardSchemaProjectsItem, } from '../../openapi'; import { FlagExposure } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure'; @@ -61,9 +61,24 @@ const FlagListItem: FC<{ selected: boolean; onClick: () => void; }> = ({ flag, selected, onClick }) => { + const activeFlagRef = useRef(null); + + useEffect(() => { + if (activeFlagRef.current) { + activeFlagRef.current.scrollIntoView({ + block: 'nearest', + inline: 'start', + }); + } + }, []); const IconComponent = getFeatureTypeIcons(flag.type); return ( - + { - const [activeProject, setActiveProject] = useState(projects[0]?.id); +const useDashboardState = ( + projects: PersonalDashboardSchemaProjectsItem[], + flags: PersonalDashboardSchemaFlagsItem[], +) => { + type State = { + activeProject: string | undefined; + activeFlag: PersonalDashboardSchemaFlagsItem | undefined; + }; + + const defaultState = { + activeProject: undefined, + activeFlag: undefined, + }; + + const [state, setState] = useLocalStorageState( + 'personal-dashboard:v1', + defaultState, + ); useEffect(() => { - if (!activeProject && projects.length > 0) { - setActiveProject(projects[0].id); + const setDefaultFlag = + flags.length && + (!state.activeFlag || + !flags.some((flag) => flag.name === state.activeFlag?.name)); + const setDefaultProject = + projects.length && + (!state.activeProject || + !projects.some( + (project) => project.id === state.activeProject, + )); + + if (setDefaultFlag || setDefaultProject) { + setState({ + activeFlag: setDefaultFlag ? flags[0] : state.activeFlag, + activeProject: setDefaultProject + ? projects[0].id + : state.activeProject, + }); } - }, [JSON.stringify(projects)]); + }); + + const { activeFlag, activeProject } = state; + + const setActiveFlag = (flag: PersonalDashboardSchemaFlagsItem) => { + setState({ + ...state, + activeFlag: flag, + }); + }; - return [activeProject, setActiveProject] as const; + const setActiveProject = (projectId: string) => { + setState({ + ...state, + activeProject: projectId, + }); + }; + + return { + activeFlag, + setActiveFlag, + activeProject, + setActiveProject, + }; }; export const PersonalDashboard = () => { @@ -110,22 +178,16 @@ export const PersonalDashboard = () => { refetch: refetchDashboard, loading: personalDashboardLoading, } = usePersonalDashboard(); - const [activeFlag, setActiveFlag] = useState< - PersonalDashboardSchema['flags'][0] | null - >(null); - useEffect(() => { - if (personalDashboard?.flags.length) { - setActiveFlag(personalDashboard.flags[0]); - } - }, [JSON.stringify(personalDashboard?.flags)]); + + const projects = personalDashboard?.projects || []; + + const { activeProject, setActiveProject, activeFlag, setActiveFlag } = + useDashboardState(projects, personalDashboard?.flags ?? []); const [welcomeDialog, setWelcomeDialog] = useLocalStorageState< 'open' | 'closed' >('welcome-dialog:v1', 'open'); - const projects = personalDashboard?.projects || []; - const [activeProject, setActiveProject] = useActiveProject(projects); - const { personalDashboardProjectDetails, loading: loadingDetails } = usePersonalDashboardProjectDetails(activeProject); @@ -174,7 +236,7 @@ export const PersonalDashboard = () => { ) : ( { const { data, error, mutate } = useConditionalSWR( Boolean(project),