Animated the Sidebar Objects Tree view opening/closing (#9287)
closes #6485 https://github.com/user-attachments/assets/79efca87-1d9b-4fa2-a457-3117be679c6e --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -22,6 +22,7 @@ import { createPortal } from 'react-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import {
|
||||
AnimatedExpandableContainer,
|
||||
IconFolder,
|
||||
IconFolderOpen,
|
||||
IconHeartOff,
|
||||
@ -158,7 +159,12 @@ export const CurrentWorkspaceMemberFavorites = ({
|
||||
</FavoritesDroppable>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
>
|
||||
<Droppable droppableId={`folder-${folder.folderId}`}>
|
||||
{(provided) => (
|
||||
<div
|
||||
@ -202,7 +208,7 @@ export const CurrentWorkspaceMemberFavorites = ({
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
)}
|
||||
</AnimatedExpandableContainer>
|
||||
</NavigationDrawerItemsCollapsableContainer>
|
||||
|
||||
{createPortal(
|
||||
|
||||
@ -9,7 +9,7 @@ import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-dr
|
||||
import { View } from '@/views/types/View';
|
||||
import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useIcons } from 'twenty-ui';
|
||||
import { AnimatedExpandableContainer, useIcons } from 'twenty-ui';
|
||||
|
||||
export type NavigationDrawerItemForObjectMetadataItemProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
@ -66,8 +66,14 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
Icon={getIcon(objectMetadataItem.icon)}
|
||||
active={isActive}
|
||||
/>
|
||||
{shouldSubItemsBeDisplayed &&
|
||||
sortedObjectMetadataViews.map((view, index) => (
|
||||
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={shouldSubItemsBeDisplayed}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
>
|
||||
{sortedObjectMetadataViews.map((view, index) => (
|
||||
<NavigationDrawerSubItem
|
||||
label={view.name}
|
||||
to={`/objects/${objectMetadataItem.namePlural}?view=${view.id}`}
|
||||
@ -81,6 +87,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
key={view.id}
|
||||
/>
|
||||
))}
|
||||
</AnimatedExpandableContainer>
|
||||
</NavigationDrawerItemsCollapsableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,26 +1,37 @@
|
||||
import { useExpandedAnimation } from '@/settings/hooks/useExpandedAnimation';
|
||||
import { ADVANCED_SETTINGS_ANIMATION_DURATION } from '@/settings/constants/AdvancedSettingsAnimationDurations';
|
||||
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
||||
import styled from '@emotion/styled';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconPoint, MAIN_COLORS } from 'twenty-ui';
|
||||
import { AnimatedExpandableContainer, IconPoint, MAIN_COLORS } from 'twenty-ui';
|
||||
|
||||
const StyledAdvancedWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
const StyledIconContainer = styled.div<{ navigationDrawerItem: boolean }>`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
left: ${({ theme }) => theme.spacing(-4)};
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
${({ navigationDrawerItem, theme }) => {
|
||||
if (navigationDrawerItem) {
|
||||
return `
|
||||
height: 100%;
|
||||
left: ${theme.spacing(-5)};
|
||||
align-items: center;
|
||||
`;
|
||||
}
|
||||
return `
|
||||
left: ${theme.spacing(-4)};
|
||||
top: ${theme.spacing(1)};
|
||||
`;
|
||||
}}
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledIconPoint = styled(IconPoint)`
|
||||
margin-right: 0;
|
||||
`;
|
||||
@ -29,43 +40,37 @@ type AdvancedSettingsWrapperProps = {
|
||||
children: React.ReactNode;
|
||||
dimension?: 'width' | 'height';
|
||||
hideIcon?: boolean;
|
||||
navigationDrawerItem?: boolean;
|
||||
};
|
||||
|
||||
export const AdvancedSettingsWrapper = ({
|
||||
children,
|
||||
dimension = 'height',
|
||||
hideIcon = false,
|
||||
navigationDrawerItem = false,
|
||||
}: AdvancedSettingsWrapperProps) => {
|
||||
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
||||
const { contentRef, motionAnimationVariants } = useExpandedAnimation(
|
||||
isAdvancedModeEnabled,
|
||||
dimension,
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isAdvancedModeEnabled && (
|
||||
<motion.div
|
||||
ref={contentRef}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
variants={motionAnimationVariants}
|
||||
>
|
||||
<StyledAdvancedWrapper>
|
||||
{!hideIcon && (
|
||||
<StyledIconContainer>
|
||||
<StyledIconPoint
|
||||
size={12}
|
||||
color={MAIN_COLORS.yellow}
|
||||
fill={MAIN_COLORS.yellow}
|
||||
/>
|
||||
</StyledIconContainer>
|
||||
)}
|
||||
<StyledContent>{children}</StyledContent>
|
||||
</StyledAdvancedWrapper>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isAdvancedModeEnabled}
|
||||
dimension={dimension}
|
||||
animationDurations={ADVANCED_SETTINGS_ANIMATION_DURATION}
|
||||
mode="scroll-height"
|
||||
containAnimation={false}
|
||||
>
|
||||
<StyledAdvancedWrapper>
|
||||
{!hideIcon && (
|
||||
<StyledIconContainer navigationDrawerItem={navigationDrawerItem}>
|
||||
<StyledIconPoint
|
||||
size={12}
|
||||
color={MAIN_COLORS.yellow}
|
||||
fill={MAIN_COLORS.yellow}
|
||||
/>
|
||||
</StyledIconContainer>
|
||||
)}
|
||||
<StyledContent>{children}</StyledContent>
|
||||
</StyledAdvancedWrapper>
|
||||
</AnimatedExpandableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -12,20 +12,18 @@ import {
|
||||
IconHierarchy2,
|
||||
IconKey,
|
||||
IconMail,
|
||||
IconPoint,
|
||||
IconRocket,
|
||||
IconServer,
|
||||
IconSettings,
|
||||
IconUserCircle,
|
||||
IconUsers,
|
||||
MAIN_COLORS,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
|
||||
import { SettingsNavigationDrawerItem } from '@/settings/components/SettingsNavigationDrawerItem';
|
||||
import { useExpandedAnimation } from '@/settings/hooks/useExpandedAnimation';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import {
|
||||
@ -35,11 +33,8 @@ import {
|
||||
import { NavigationDrawerItemGroup } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemGroup';
|
||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
||||
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import styled from '@emotion/styled';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { matchPath, resolvePath, useLocation } from 'react-router-dom';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
@ -51,32 +46,7 @@ type SettingsNavigationItem = {
|
||||
matchSubPages?: boolean;
|
||||
};
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
position: absolute;
|
||||
left: ${({ theme }) => theme.spacing(-5)};
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledIconPoint = styled(IconPoint)`
|
||||
margin-right: 0;
|
||||
`;
|
||||
|
||||
export const SettingsNavigationDrawerItems = () => {
|
||||
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
||||
const {
|
||||
contentRef: securityRef,
|
||||
motionAnimationVariants: securityAnimationVariants,
|
||||
} = useExpandedAnimation(isAdvancedModeEnabled);
|
||||
const {
|
||||
contentRef: developersRef,
|
||||
motionAnimationVariants: developersAnimationVariants,
|
||||
} = useExpandedAnimation(isAdvancedModeEnabled);
|
||||
const { signOut } = useAuth();
|
||||
|
||||
const billing = useRecoilValue(billingState);
|
||||
@ -198,81 +168,36 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
Icon={IconCode}
|
||||
/>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{isAdvancedModeEnabled && (
|
||||
<motion.div
|
||||
ref={securityRef}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
variants={securityAnimationVariants}
|
||||
>
|
||||
<StyledContainer>
|
||||
<StyledIconContainer>
|
||||
<StyledIconPoint
|
||||
size={12}
|
||||
color={MAIN_COLORS.yellow}
|
||||
fill={MAIN_COLORS.yellow}
|
||||
/>
|
||||
</StyledIconContainer>
|
||||
<SettingsNavigationDrawerItem
|
||||
label="Security"
|
||||
path={SettingsPath.Security}
|
||||
Icon={IconKey}
|
||||
/>
|
||||
</StyledContainer>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<AdvancedSettingsWrapper navigationDrawerItem={true}>
|
||||
<SettingsNavigationDrawerItem
|
||||
label="Security"
|
||||
path={SettingsPath.Security}
|
||||
Icon={IconKey}
|
||||
/>
|
||||
</AdvancedSettingsWrapper>
|
||||
</NavigationDrawerSection>
|
||||
|
||||
<AnimatePresence>
|
||||
{isAdvancedModeEnabled && (
|
||||
<motion.div
|
||||
ref={developersRef}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
variants={developersAnimationVariants}
|
||||
>
|
||||
<NavigationDrawerSection>
|
||||
<NavigationDrawerSectionTitle label="Developers" />
|
||||
<StyledContainer>
|
||||
<StyledIconContainer>
|
||||
<StyledIconPoint
|
||||
size={12}
|
||||
color={MAIN_COLORS.yellow}
|
||||
fill={MAIN_COLORS.yellow}
|
||||
/>
|
||||
</StyledIconContainer>
|
||||
|
||||
<SettingsNavigationDrawerItem
|
||||
label="API & Webhooks"
|
||||
path={SettingsPath.Developers}
|
||||
Icon={IconCode}
|
||||
/>
|
||||
</StyledContainer>
|
||||
{isFunctionSettingsEnabled && (
|
||||
<StyledContainer>
|
||||
<StyledIconContainer>
|
||||
<StyledIconPoint
|
||||
size={12}
|
||||
color={MAIN_COLORS.yellow}
|
||||
fill={MAIN_COLORS.yellow}
|
||||
/>
|
||||
</StyledIconContainer>
|
||||
|
||||
<SettingsNavigationDrawerItem
|
||||
label="Functions"
|
||||
path={SettingsPath.ServerlessFunctions}
|
||||
Icon={IconFunction}
|
||||
/>
|
||||
</StyledContainer>
|
||||
)}
|
||||
</NavigationDrawerSection>
|
||||
</motion.div>
|
||||
<NavigationDrawerSection>
|
||||
<AdvancedSettingsWrapper hideIcon>
|
||||
<NavigationDrawerSectionTitle label="Developers" />
|
||||
</AdvancedSettingsWrapper>
|
||||
<AdvancedSettingsWrapper navigationDrawerItem={true}>
|
||||
<SettingsNavigationDrawerItem
|
||||
label="API & Webhooks"
|
||||
path={SettingsPath.Developers}
|
||||
Icon={IconCode}
|
||||
/>
|
||||
</AdvancedSettingsWrapper>
|
||||
{isFunctionSettingsEnabled && (
|
||||
<AdvancedSettingsWrapper navigationDrawerItem={true}>
|
||||
<SettingsNavigationDrawerItem
|
||||
label="Functions"
|
||||
path={SettingsPath.ServerlessFunctions}
|
||||
Icon={IconFunction}
|
||||
/>
|
||||
</AdvancedSettingsWrapper>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</NavigationDrawerSection>
|
||||
<NavigationDrawerSection>
|
||||
<NavigationDrawerSectionTitle label="Other" />
|
||||
{isAdminPageEnabled && (
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
import { ADVANCED_SETTINGS_ANIMATION_DURATION } from '@/settings/constants/AdvancedSettingsAnimationDurations';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
type AnimationDimension = 'width' | 'height';
|
||||
|
||||
const getTransitionValues = (dimension: AnimationDimension) => ({
|
||||
transition: {
|
||||
opacity: { duration: ADVANCED_SETTINGS_ANIMATION_DURATION.opacity },
|
||||
[dimension]: { duration: ADVANCED_SETTINGS_ANIMATION_DURATION.size },
|
||||
},
|
||||
});
|
||||
|
||||
const commonStyles = (dimension: AnimationDimension) => ({
|
||||
opacity: 0,
|
||||
[dimension]: 0,
|
||||
...getTransitionValues(dimension),
|
||||
});
|
||||
|
||||
const advancedSectionAnimationConfig = (
|
||||
isExpanded: boolean,
|
||||
dimension: AnimationDimension,
|
||||
measuredValue?: number,
|
||||
) => ({
|
||||
initial: {
|
||||
...commonStyles(dimension),
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
[dimension]: isExpanded
|
||||
? dimension === 'width'
|
||||
? '100%'
|
||||
: measuredValue
|
||||
: 0,
|
||||
...getTransitionValues(dimension),
|
||||
},
|
||||
exit: {
|
||||
...commonStyles(dimension),
|
||||
},
|
||||
});
|
||||
|
||||
export const useExpandedAnimation = (
|
||||
isExpanded: boolean,
|
||||
dimension: AnimationDimension = 'height',
|
||||
) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [measuredValue, setMeasuredValue] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (dimension === 'height' && isDefined(contentRef.current)) {
|
||||
setMeasuredValue(contentRef.current.scrollHeight);
|
||||
}
|
||||
}, [isExpanded, dimension]);
|
||||
|
||||
return {
|
||||
contentRef,
|
||||
motionAnimationVariants: advancedSectionAnimationConfig(
|
||||
isExpanded,
|
||||
dimension,
|
||||
dimension === 'height' ? measuredValue : undefined,
|
||||
),
|
||||
};
|
||||
};
|
||||
@ -5,7 +5,7 @@ import { Column } from '@/spreadsheet-import/steps/components/MatchColumnsStep/M
|
||||
import { Fields } from '@/spreadsheet-import/types';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { ExpandableContainer, isDefined } from 'twenty-ui';
|
||||
import { AnimatedExpandableContainer, isDefined } from 'twenty-ui';
|
||||
|
||||
const getExpandableContainerTitle = <T extends string>(
|
||||
fields: Fields<T>,
|
||||
@ -59,7 +59,12 @@ export const UnmatchColumn = <T extends string>({
|
||||
buttonOnClick={() => setIsExpanded(!isExpanded)}
|
||||
isExpanded={isExpanded}
|
||||
/>
|
||||
<ExpandableContainer isExpanded={isExpanded}>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isExpanded}
|
||||
dimension="height"
|
||||
mode="scroll-height"
|
||||
containAnimation
|
||||
>
|
||||
<StyledContentWrapper>
|
||||
{column.matchedOptions.map((option) => (
|
||||
<SubMatchingSelect
|
||||
@ -71,7 +76,7 @@ export const UnmatchColumn = <T extends string>({
|
||||
/>
|
||||
))}
|
||||
</StyledContentWrapper>
|
||||
</ExpandableContainer>
|
||||
</AnimatedExpandableContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user