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,
+});