From 0e525caf018310bd0db3743447f43b664daf0ae3 Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 16:36:28 +0200 Subject: [PATCH] Implement (#5086) ### Description Implement <ScrollRestoration /> ### Refs [https://github.com/twentyhq/twenty/issues/4357](https://github.com/twentyhq/twenty/issues/4183) ### Demo https://github.com/twentyhq/twenty/assets/140154534/321242e1-4751-4204-8c86-e9b921c1733e Fixes #4357 --------- Co-authored-by: gitstart-twenty Co-authored-by: Lucas Bordeau Co-authored-by: v1b3m Co-authored-by: RubensRafael --- packages/twenty-front/src/App.tsx | 99 ++++++++++++++++--- .../src/hooks/useScrollRestoration.ts | 34 +++++++ packages/twenty-front/src/index.tsx | 62 ++---------- .../record-board/components/RecordBoard.tsx | 7 ++ .../components/RecordTableBodyEffect.tsx | 6 ++ .../scroll/components/ScrollWrapper.tsx | 11 ++- .../scroll/states/overlayScrollbarsState.ts | 7 ++ .../scroll/states/scrollPositionState.ts | 6 ++ 8 files changed, 161 insertions(+), 71 deletions(-) create mode 100644 packages/twenty-front/src/hooks/useScrollRestoration.ts create mode 100644 packages/twenty-front/src/modules/ui/utilities/scroll/states/overlayScrollbarsState.ts create mode 100644 packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollPositionState.ts diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx index fd21a9c0701..72e7f41053f 100644 --- a/packages/twenty-front/src/App.tsx +++ b/packages/twenty-front/src/App.tsx @@ -1,15 +1,39 @@ -import { Route, Routes, useLocation } from 'react-router-dom'; +import { StrictMode } from 'react'; +import { + createBrowserRouter, + createRoutesFromElements, + Outlet, + redirect, + Route, + RouterProvider, + Routes, + useLocation, +} from 'react-router-dom'; import { useRecoilValue } from 'recoil'; +import { ApolloProvider } from '@/apollo/components/ApolloProvider'; import { VerifyEffect } from '@/auth/components/VerifyEffect'; +import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; +import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect'; import { billingState } from '@/client-config/states/billingState'; +import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect'; +import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider'; +import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; +import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; +import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager'; +import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; +import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider'; import { BlankLayout } from '@/ui/layout/page/BlankLayout'; import { DefaultLayout } from '@/ui/layout/page/DefaultLayout'; +import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; +import { UserProvider } from '@/users/components/UserProvider'; +import { UserProviderEffect } from '@/users/components/UserProviderEffect'; import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect'; import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect'; +import { PageChangeEffect } from '~/effect-components/PageChangeEffect'; import { Authorize } from '~/pages/auth/Authorize'; import { ChooseYourPlan } from '~/pages/auth/ChooseYourPlan'; import { CreateProfile } from '~/pages/auth/CreateProfile'; @@ -54,17 +78,53 @@ import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMemb import { Tasks } from '~/pages/tasks/Tasks'; import { getPageTitleFromPath } from '~/utils/title-utils'; -export const App = () => { - const billing = useRecoilValue(billingState); +const ProvidersThatNeedRouterContext = () => { const { pathname } = useLocation(); const pageTitle = getPageTitleFromPath(pathname); return ( - <> - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const createRouter = (isBillingEnabled?: boolean) => + createBrowserRouter( + createRoutesFromElements( + } + // To switch state to `loading` temporarily to enable us + // to set scroll position before the page is rendered + loader={async () => Promise.resolve(null)} + > }> } /> } /> @@ -119,12 +179,14 @@ export const App = () => { path={SettingsPath.AccountsEmailsInboxSettings} element={} /> - {billing?.isBillingEnabled && ( - } - /> - )} + } + loader={() => { + if (!isBillingEnabled) return redirect(AppPath.Index); + return null; + }} + /> } @@ -217,7 +279,12 @@ export const App = () => { }> } /> - - + , + ), ); + +export const App = () => { + const billing = useRecoilValue(billingState); + + return ; }; diff --git a/packages/twenty-front/src/hooks/useScrollRestoration.ts b/packages/twenty-front/src/hooks/useScrollRestoration.ts new file mode 100644 index 00000000000..1b145940a96 --- /dev/null +++ b/packages/twenty-front/src/hooks/useScrollRestoration.ts @@ -0,0 +1,34 @@ +import { useEffect } from 'react'; +import { useLocation, useNavigation } from 'react-router-dom'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { overlayScrollbarsState } from '@/ui/utilities/scroll/states/overlayScrollbarsState'; +import { scrollPositionState } from '@/ui/utilities/scroll/states/scrollPositionState'; +import { isDefined } from '~/utils/isDefined'; + +/** + * Note that `location.key` is used in the cache key, not `location.pathname`, + * so the same path navigated to at different points in the history stack will + * not share the same scroll position. + */ +export const useScrollRestoration = (viewportHeight?: number) => { + const key = `scroll-position-${useLocation().key}`; + const { state } = useNavigation(); + + const [scrollPosition, setScrollPosition] = useRecoilState( + scrollPositionState(key), + ); + + const overlayScrollbars = useRecoilValue(overlayScrollbarsState); + + const scrollWrapper = overlayScrollbars?.elements().viewport; + const skip = isDefined(viewportHeight) && scrollPosition > viewportHeight; + + useEffect(() => { + if (state === 'loading') { + setScrollPosition(scrollWrapper?.scrollTop ?? 0); + } else if (state === 'idle' && isDefined(scrollWrapper) && !skip) { + scrollWrapper.scrollTo({ top: scrollPosition }); + } + }, [key, state, scrollWrapper, skip, scrollPosition, setScrollPosition]); +}; diff --git a/packages/twenty-front/src/index.tsx b/packages/twenty-front/src/index.tsx index dfdc65277ac..06527d80050 100644 --- a/packages/twenty-front/src/index.tsx +++ b/packages/twenty-front/src/index.tsx @@ -1,30 +1,14 @@ -import { StrictMode } from 'react'; import ReactDOM from 'react-dom/client'; import { HelmetProvider } from 'react-helmet-async'; -import { BrowserRouter } from 'react-router-dom'; import { RecoilRoot } from 'recoil'; import { IconsProvider } from 'twenty-ui'; -import { ApolloProvider } from '@/apollo/components/ApolloProvider'; import { CaptchaProvider } from '@/captcha/components/CaptchaProvider'; -import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; -import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect'; import { ApolloDevLogEffect } from '@/debug/components/ApolloDevLogEffect'; import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver'; import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider'; -import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect'; -import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider'; -import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; -import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider'; -import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager'; -import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; -import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; -import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; -import { UserProvider } from '@/users/components/UserProvider'; -import { UserProviderEffect } from '@/users/components/UserProviderEffect'; -import { PageChangeEffect } from '~/effect-components/PageChangeEffect'; import '@emotion/react'; @@ -43,43 +27,15 @@ root.render( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + , diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index b8840686e10..d1e011e5904 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -16,6 +16,7 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import { useScrollRestoration } from '~/hooks/useScrollRestoration'; export type RecordBoardProps = { recordBoardId: string; @@ -42,6 +43,11 @@ const StyledBoardHeader = styled.div` z-index: 1; `; +const RecordBoardScrollRestoreEffect = () => { + useScrollRestoration(); + return null; +}; + export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => { const { updateOneRecord, selectFieldMetadataItem } = useContext(RecordBoardContext); @@ -152,6 +158,7 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => { ))} + { if (!loading) { setRecordTableData(records, totalCount); diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx index 028f3f1db51..5f1f24d9069 100644 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx @@ -2,8 +2,9 @@ import { createContext, RefObject, useEffect, useRef } from 'react'; import styled from '@emotion/styled'; import { OverlayScrollbars } from 'overlayscrollbars'; import { useOverlayScrollbars } from 'overlayscrollbars-react'; -import { useRecoilCallback } from 'recoil'; +import { useRecoilCallback, useSetRecoilState } from 'recoil'; +import { overlayScrollbarsState } from '@/ui/utilities/scroll/states/overlayScrollbarsState'; import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState'; import { scrollTopState } from '@/ui/utilities/scroll/states/scrollTopState'; @@ -48,7 +49,9 @@ export const ScrollWrapper = ({ [], ); - const [initialize] = useOverlayScrollbars({ + const setOverlayScrollbars = useSetRecoilState(overlayScrollbarsState); + + const [initialize, instance] = useOverlayScrollbars({ options: { scrollbars: { autoHide: 'scroll' }, overflow: { @@ -67,6 +70,10 @@ export const ScrollWrapper = ({ } }, [initialize, scrollableRef]); + useEffect(() => { + setOverlayScrollbars(instance()); + }, [instance, setOverlayScrollbars]); + return ( diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/states/overlayScrollbarsState.ts b/packages/twenty-front/src/modules/ui/utilities/scroll/states/overlayScrollbarsState.ts new file mode 100644 index 00000000000..784fc12d02b --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/states/overlayScrollbarsState.ts @@ -0,0 +1,7 @@ +import { OverlayScrollbars } from 'overlayscrollbars'; +import { createState } from 'twenty-ui'; + +export const overlayScrollbarsState = createState({ + key: 'scroll/overlayScrollbarsState', + defaultValue: null, +}); diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollPositionState.ts b/packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollPositionState.ts new file mode 100644 index 00000000000..9dbd6a9aedd --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollPositionState.ts @@ -0,0 +1,6 @@ +import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState'; + +export const scrollPositionState = createFamilyState({ + key: 'scroll/scrollPositionState', + defaultValue: 0, +});