From 73f12d35047ce868c36c48d6ec876bc667576958 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:18:36 +0530 Subject: [PATCH] [admin-panel] - add scroll restoration and filter persistence on config variables (#13363) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit addressing https://github.com/twentyhq/twenty/issues/13323#issuecomment-3101630375 --------- Co-authored-by: Félix Malfait Co-authored-by: Félix Malfait --- .../SettingsAdminConfigVariables.tsx | 71 ++++++++---------- .../states/configVariableGroupFilterState.ts | 8 ++ .../states/configVariableSourceFilterState.ts | 8 ++ .../states/showHiddenGroupVariablesState.ts | 6 ++ .../components/SettingsPageContainer.tsx | 35 +++++++-- .../scroll/hooks/useScrollRestoration.ts | 73 +++++++++++++++++++ 6 files changed, 154 insertions(+), 47 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/config-variables/states/configVariableGroupFilterState.ts create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/config-variables/states/configVariableSourceFilterState.ts create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/config-variables/states/showHiddenGroupVariablesState.ts create mode 100644 packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useScrollRestoration.ts diff --git a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigVariables.tsx b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigVariables.tsx index b835868ecee..06cc5444b20 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigVariables.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigVariables.tsx @@ -3,11 +3,13 @@ import { ConfigVariableFilterContainer } from '@/settings/admin-panel/config-var import { ConfigVariableFilterDropdown } from '@/settings/admin-panel/config-variables/components/ConfigVariableFilterDropdown'; import { SettingsAdminConfigVariablesTable } from '@/settings/admin-panel/config-variables/components/SettingsAdminConfigVariablesTable'; import { ConfigVariableSourceOptions } from '@/settings/admin-panel/config-variables/constants/ConfigVariableSourceOptions'; -import { ConfigVariableGroupFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableGroupFilter'; -import { ConfigVariableSourceFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableSourceFilter'; +import { configVariableGroupFilterState } from '@/settings/admin-panel/config-variables/states/configVariableGroupFilterState'; +import { configVariableSourceFilterState } from '@/settings/admin-panel/config-variables/states/configVariableSourceFilterState'; +import { showHiddenGroupVariablesState } from '@/settings/admin-panel/config-variables/states/showHiddenGroupVariablesState'; import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; import { useMemo, useState } from 'react'; +import { useRecoilState } from 'recoil'; import { H2Title } from 'twenty-ui/display'; import { Section } from 'twenty-ui/layout'; import { @@ -34,19 +36,17 @@ export const SettingsAdminConfigVariables = () => { const [search, setSearch] = useState(''); const [showHiddenGroupVariables, setShowHiddenGroupVariables] = - useState(false); - const [sourceFilter, setSourceFilter] = - useState('all'); - const [groupFilter, setGroupFilter] = - useState('all'); + useRecoilState(showHiddenGroupVariablesState); + const [configVariableSourceFilter, setConfigVariableSourceFilter] = + useRecoilState(configVariableSourceFilterState); + const [configVariableGroupFilter, setConfigVariableGroupFilter] = + useRecoilState(configVariableGroupFilterState); - // Get all groups, not filtered by visibility const allGroups = useMemo( () => configVariables?.getConfigVariablesGrouped.groups ?? [], [configVariables], ); - // Compute group options from all groups, not just visible ones const groupOptions = useMemo( () => [ { value: 'all', label: 'All Groups' }, @@ -58,7 +58,6 @@ export const SettingsAdminConfigVariables = () => { [allGroups], ); - // Flatten all variables for filtering, attaching isHiddenOnLoad and groupName from group const allVariables = useMemo( () => configVariables?.getConfigVariablesGrouped.groups.flatMap((group) => @@ -71,31 +70,23 @@ export const SettingsAdminConfigVariables = () => { [configVariables], ); - // Filtering logic const filteredVariables = useMemo(() => { const isSearching = search.trim().length > 0; - const hasSelectedSpecificGroup = groupFilter !== 'all'; + const hasSelectedSpecificGroup = configVariableGroupFilter !== 'all'; return allVariables.filter((v) => { - // Search filter const matchesSearch = v.name.toLowerCase().includes(search.toLowerCase()) || (v.description?.toLowerCase() || '').includes(search.toLowerCase()); if (isSearching && !matchesSearch) return false; - // Group filter const matchesGroup = hasSelectedSpecificGroup - ? v.groupName === groupFilter + ? v.groupName === configVariableGroupFilter : true; if (hasSelectedSpecificGroup && !matchesGroup) return false; - // Hidden filter - Only apply if: - // 1. User is not searching - // 2. Show hidden is off - // 3. Item is from a hidden group - // 4. No specific group is selected (if a specific group is selected, show all its variables) if ( !isSearching && !showHiddenGroupVariables && @@ -105,13 +96,12 @@ export const SettingsAdminConfigVariables = () => { return false; } - // Source filter let matchesSource = true; - if (sourceFilter === 'database') + if (configVariableSourceFilter === 'database') matchesSource = v.source === ConfigSource.DATABASE; - if (sourceFilter === 'environment') + if (configVariableSourceFilter === 'environment') matchesSource = v.source === ConfigSource.ENVIRONMENT; - if (sourceFilter === 'default') + if (configVariableSourceFilter === 'default') matchesSource = v.source === ConfigSource.DEFAULT; return matchesSource; @@ -120,30 +110,31 @@ export const SettingsAdminConfigVariables = () => { allVariables, search, showHiddenGroupVariables, - sourceFilter, - groupFilter, + configVariableSourceFilter, + configVariableGroupFilter, ]); - // Build activeChips for current filters const activeChips = []; - if (sourceFilter !== 'all') { + if (configVariableSourceFilter !== 'all') { activeChips.push({ label: - ConfigVariableSourceOptions.find((o) => o.value === sourceFilter) - ?.label || '', - onRemove: () => setSourceFilter('all'), + ConfigVariableSourceOptions.find( + (o) => o.value === configVariableSourceFilter, + )?.label || '', + onRemove: () => setConfigVariableSourceFilter('all'), variant: 'default' as const, }); } - if (groupFilter !== 'all') { + if (configVariableGroupFilter !== 'all') { activeChips.push({ - label: groupOptions.find((o) => o.value === groupFilter)?.label || '', - onRemove: () => setGroupFilter('all'), + label: + groupOptions.find((o) => o.value === configVariableGroupFilter) + ?.label || '', + onRemove: () => setConfigVariableGroupFilter('all'), variant: 'danger' as const, }); } - // Group variables by groupName for rendering const groupedVariables = useMemo(() => { const groupMap = new Map(); filteredVariables.forEach((v) => { @@ -172,18 +163,17 @@ export const SettingsAdminConfigVariables = () => { - {groupedVariables.size === 0 && (
@@ -194,7 +184,6 @@ export const SettingsAdminConfigVariables = () => {
)} - {[...groupedVariables.entries()].map(([groupName, groupData]) => ( diff --git a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/states/configVariableGroupFilterState.ts b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/states/configVariableGroupFilterState.ts new file mode 100644 index 00000000000..bea4fa268ec --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/states/configVariableGroupFilterState.ts @@ -0,0 +1,8 @@ +import { ConfigVariableGroupFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableGroupFilter'; +import { createState } from 'twenty-ui/utilities'; + +export const configVariableGroupFilterState = + createState({ + key: 'configVariableGroupFilterState', + defaultValue: 'all', + }); diff --git a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/states/configVariableSourceFilterState.ts b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/states/configVariableSourceFilterState.ts new file mode 100644 index 00000000000..f45ac9f70f5 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/states/configVariableSourceFilterState.ts @@ -0,0 +1,8 @@ +import { ConfigVariableSourceFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableSourceFilter'; +import { createState } from 'twenty-ui/utilities'; + +export const configVariableSourceFilterState = + createState({ + key: 'configVariableSourceFilterState', + defaultValue: 'all', + }); diff --git a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/states/showHiddenGroupVariablesState.ts b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/states/showHiddenGroupVariablesState.ts new file mode 100644 index 00000000000..5d176fbb9b2 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/states/showHiddenGroupVariablesState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui/utilities'; + +export const showHiddenGroupVariablesState = createState({ + key: 'showHiddenGroupVariablesState', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx b/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx index 921e3efc441..f914210e510 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx @@ -1,9 +1,13 @@ import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings'; +import { SettingsPath } from '@/types/SettingsPath'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import { useScrollRestoration } from '@/ui/utilities/scroll/hooks/useScrollRestoration'; import styled from '@emotion/styled'; -import { ReactNode } from 'react'; +import { ReactNode, useMemo } from 'react'; +import { matchPath, useLocation } from 'react-router-dom'; import { isDefined } from 'twenty-shared/utils'; +import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; const StyledSettingsPageContainer = styled.div<{ width?: number; @@ -29,8 +33,27 @@ export const SettingsPageContainer = ({ children, }: { children: ReactNode; -}) => ( - - {children} - -); +}) => { + const location = useLocation(); + const settingsPath = useMemo(() => { + const sortedPaths = Object.values(SettingsPath).sort( + (a, b) => b.length - a.length, + ); + + return sortedPaths.find((path) => { + const settingsPath = getSettingsPath(path); + const match = matchPath(settingsPath, location.pathname); + return isDefined(match); + }); + }, [location.pathname]); + + const componentInstanceId = `scroll-wrapper-settings-page-container-${settingsPath}`; + + useScrollRestoration(componentInstanceId); + + return ( + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useScrollRestoration.ts b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useScrollRestoration.ts new file mode 100644 index 00000000000..9eceed5c3f5 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useScrollRestoration.ts @@ -0,0 +1,73 @@ +import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useCallback, useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { isDefined } from 'twenty-shared/utils'; + +export const useScrollRestoration = (componentInstanceId: string) => { + const location = useLocation(); + const storageKey = `scroll-${location.pathname}`; + const [isRestoring, setIsRestoring] = useState(false); + + const scrollTop = useRecoilComponentValueV2( + scrollWrapperScrollTopComponentState, + componentInstanceId, + ); + + const restoreScrollPosition = useCallback( + (position: number, elementId: string) => { + const attemptRestore = () => { + const element = document.getElementById(elementId); + + if (!isDefined(element)) { + requestAnimationFrame(attemptRestore); + return; + } + + const isScrollable = element.scrollHeight > element.clientHeight; + if (!isScrollable) { + requestAnimationFrame(attemptRestore); + return; + } + + element.scrollTo({ top: position }); + + requestAnimationFrame(() => { + setIsRestoring(false); + }); + }; + + requestAnimationFrame(attemptRestore); + }, + [], + ); + + useEffect(() => { + if (scrollTop > 0 && !isRestoring) { + sessionStorage.setItem(storageKey, scrollTop.toString()); + } + }, [scrollTop, storageKey, isRestoring]); + + useEffect(() => { + const savedPosition = sessionStorage.getItem(storageKey); + const expectedElementId = `scroll-wrapper-${componentInstanceId}`; + + if (!isDefined(savedPosition)) { + return; + } + + const position = parseInt(savedPosition, 10); + + if (position <= 0) { + return; + } + + setIsRestoring(true); + restoreScrollPosition(position, expectedElementId); + }, [ + location.pathname, + storageKey, + componentInstanceId, + restoreScrollPosition, + ]); +};