Favorite folders (#7998)

closes - #5755

---------

Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
nitin
2024-11-18 19:52:19 +05:30
committed by GitHub
parent 5115022355
commit 0125d58ba8
100 changed files with 24033 additions and 21488 deletions

View File

@ -0,0 +1,136 @@
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ChangeEvent, FocusEvent, useRef } from 'react';
import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import {
IconComponent,
isDefined,
TablerIconsProps,
TEXT_INPUT_STYLE,
} from 'twenty-ui';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
type NavigationDrawerInputProps = {
className?: string;
Icon: IconComponent | ((props: TablerIconsProps) => JSX.Element);
value: string;
onChange: (value: string) => void;
onSubmit: (value: string) => void;
onCancel: (value: string) => void;
onClickOutside: (event: MouseEvent | TouchEvent, value: string) => void;
hotkeyScope: string;
};
const StyledItem = styled.div<{ isNavigationDrawerExpanded: boolean }>`
align-items: center;
background-color: ${({ theme }) => theme.background.primary};
border: 1px solid ${({ theme }) => theme.color.blue};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-sizing: content-box;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
font-family: ${({ theme }) => theme.font.family};
font-size: ${({ theme }) => theme.font.size.md};
height: calc(${({ theme }) => theme.spacing(5)} - 2px);
padding: ${({ theme }) => theme.spacing(1)};
text-decoration: none;
user-select: none;
`;
const StyledItemElementsContainer = styled.span`
align-items: center;
display: flex;
width: 100%;
`;
const StyledTextInput = styled.input`
${TEXT_INPUT_STYLE}
margin: 0;
width: 100%;
`;
export const NavigationDrawerInput = ({
className,
Icon,
value,
onChange,
onSubmit,
onCancel,
onClickOutside,
hotkeyScope,
}: NavigationDrawerInputProps) => {
const theme = useTheme();
const [isNavigationDrawerExpanded] = useRecoilState(
isNavigationDrawerExpandedState,
);
const inputRef = useRef<HTMLInputElement>(null);
useHotkeyScopeOnMount(hotkeyScope);
useScopedHotkeys(
[Key.Escape],
() => {
onCancel(value);
},
hotkeyScope,
);
useScopedHotkeys(
[Key.Enter],
() => {
onSubmit(value);
},
hotkeyScope,
);
useListenClickOutside({
refs: [inputRef],
callback: (event) => {
event.stopImmediatePropagation();
onClickOutside(event, value);
},
});
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value);
};
const handleFocus = (event: FocusEvent<HTMLInputElement>) => {
if (isDefined(value)) {
event.target.select();
}
};
return (
<StyledItem
className={className}
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
>
<StyledItemElementsContainer>
{Icon && (
<Icon
style={{ minWidth: theme.icon.size.md }}
size={theme.icon.size.md}
stroke={theme.icon.stroke.md}
color="currentColor"
/>
)}
<NavigationDrawerAnimatedCollapseWrapper>
<StyledTextInput
ref={inputRef}
value={value}
onChange={handleChange}
onFocus={handleFocus}
autoFocus
/>
</NavigationDrawerAnimatedCollapseWrapper>
</StyledItemElementsContainer>
</StyledItem>
);
};

View File

@ -8,6 +8,7 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import isPropValid from '@emotion/is-prop-valid';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { useRecoilState } from 'recoil';
import {
@ -35,16 +36,19 @@ export type NavigationDrawerItemProps = {
soon?: boolean;
count?: number;
keyboard?: string[];
rightOptions?: ReactNode;
isDraggable?: boolean;
};
type StyledItemProps = Pick<
NavigationDrawerItemProps,
'active' | 'danger' | 'indentationLevel' | 'soon' | 'to'
'active' | 'danger' | 'indentationLevel' | 'soon' | 'to' | 'isDraggable'
> & { isNavigationDrawerExpanded: boolean };
const StyledItem = styled('button', {
shouldForwardProp: (prop) =>
!['active', 'danger', 'soon'].includes(prop) && isPropValid(prop),
!['active', 'danger', 'soon', 'isDraggable'].includes(prop) &&
isPropValid(prop),
})<StyledItemProps>`
box-sizing: content-box;
align-items: center;
@ -85,6 +89,15 @@ const StyledItem = styled('button', {
!props.isNavigationDrawerExpanded
? `${NAV_DRAWER_WIDTHS.menu.desktop.collapsed - 24}px`
: '100%'};
${({ isDraggable }) =>
isDraggable &&
`
cursor: grab;
&:active {
cursor: grabbing;
}
`}
:hover {
background: ${({ theme }) => theme.background.transparent.light};
@ -150,6 +163,27 @@ const StyledSpacer = styled.span`
flex-grow: 1;
`;
const StyledRightOptionsContainer = styled.div<{
isMobile: boolean;
active: boolean;
}>`
margin-left: auto;
visibility: ${({ isMobile, active }) =>
isMobile || active ? 'visible' : 'hidden'};
display: flex;
align-items: center;
justify-content: center;
:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
width: ${({ theme }) => theme.spacing(6)};
height: ${({ theme }) => theme.spacing(6)};
border-radius: ${({ theme }) => theme.border.radius.sm};
.navigation-drawer-item:hover & {
visibility: visible;
}
`;
export const NavigationDrawerItem = ({
className,
label,
@ -163,6 +197,8 @@ export const NavigationDrawerItem = ({
count,
keyboard,
subItemState,
rightOptions,
isDraggable,
}: NavigationDrawerItemProps) => {
const theme = useTheme();
const isMobile = useIsMobile();
@ -185,7 +221,7 @@ export const NavigationDrawerItem = ({
return (
<StyledNavigationDrawerItemContainer>
<StyledItem
className={className}
className={`navigation-drawer-item ${className || ''}`}
onClick={handleItemClick}
active={active}
aria-selected={active}
@ -195,6 +231,7 @@ export const NavigationDrawerItem = ({
to={to ? to : undefined}
indentationLevel={indentationLevel}
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
isDraggable={isDraggable}
>
{showBreadcrumb && (
<NavigationDrawerAnimatedCollapseWrapper>
@ -240,6 +277,20 @@ export const NavigationDrawerItem = ({
</StyledKeyBoardShortcut>
</NavigationDrawerAnimatedCollapseWrapper>
)}
<NavigationDrawerAnimatedCollapseWrapper>
{rightOptions && (
<StyledRightOptionsContainer
isMobile={isMobile}
active={active || false}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
{rightOptions}
</StyledRightOptionsContainer>
)}
</NavigationDrawerAnimatedCollapseWrapper>
</StyledItemElementsContainer>
</StyledItem>
</StyledNavigationDrawerItemContainer>

View File

@ -0,0 +1,5 @@
import { TextInput } from '@/ui/input/components/TextInput';
export const NavigationDrawerItemInput = () => {
return <TextInput />;
};

View File

@ -1,22 +1,22 @@
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { AnimationControls, motion, TargetAndTransition } from 'framer-motion';
import { ReactNode } from 'react';
import { useRecoilValue } from 'recoil';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { AnimationControls, motion, TargetAndTransition } from 'framer-motion';
import { useTheme } from '@emotion/react';
const StyledAnimationGroupContainer = styled(motion.div)``;
type NavigationDrawerItemsCollapsedContainerProps = {
type NavigationDrawerItemsCollapsableContainerProps = {
isGroup?: boolean;
children: ReactNode;
};
export const NavigationDrawerItemsCollapsedContainer = ({
export const NavigationDrawerItemsCollapsableContainer = ({
isGroup = false,
children,
}: NavigationDrawerItemsCollapsedContainerProps) => {
}: NavigationDrawerItemsCollapsableContainerProps) => {
const theme = useTheme();
const isSettingsPage = useIsSettingsPage();
const isNavigationDrawerExpanded = useRecoilValue(

View File

@ -7,7 +7,6 @@ const StyledSection = styled.div`
width: 100%;
margin-bottom: ${({ theme }) => theme.spacing(3)};
flex-shrink: 1;
overflow: hidden;
`;
export { StyledSection as NavigationDrawerSection };

View File

@ -1,20 +1,15 @@
import styled from '@emotion/styled';
import { currentUserState } from '@/auth/states/currentUserState';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { NavigationDrawerSectionTitleSkeletonLoader } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import styled from '@emotion/styled';
import React from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
type NavigationDrawerSectionTitleProps = {
onClick?: () => void;
label: string;
};
const StyledTitle = styled.div<{ onClick?: () => void }>`
const StyledTitle = styled.div`
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.light};
@ -23,38 +18,92 @@ const StyledTitle = styled.div<{ onClick?: () => void }>`
font-weight: ${({ theme }) => theme.font.weight.semiBold};
height: ${({ theme }) => theme.spacing(5)};
padding: ${({ theme }) => theme.spacing(1)};
justify-content: space-between;
${({ onClick, theme }) =>
!isUndefinedOrNull(onClick)
? `&:hover {
cursor: pointer;
background-color:${theme.background.transparent.light};
}`
: ''}
&:hover {
cursor: pointer;
background-color: ${({ theme }) => theme.background.transparent.light};
}
`;
const StyledLabel = styled.div`
flex-grow: 1;
`;
type StyledRightIconProps = {
isMobile: boolean;
};
const StyledRightIcon = styled.div<StyledRightIconProps>`
cursor: pointer;
margin-left: ${({ theme }) => theme.spacing(2)};
transition: opacity 150ms ease-in-out;
opacity: ${({ isMobile }) => (isMobile ? 1 : 0)};
display: flex;
align-items: center;
justify-content: center;
border-radius: ${({ theme }) => theme.border.radius.sm};
width: ${({ theme }) => theme.spacing(5)};
height: ${({ theme }) => theme.spacing(5)};
:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
.section-title-container:hover & {
opacity: 1;
}
&:active {
cursor: pointer;
}
`;
type NavigationDrawerSectionTitleProps = {
onClick?: () => void;
onRightIconClick?: () => void;
label: string;
rightIcon?: React.ReactNode;
};
export const NavigationDrawerSectionTitle = ({
onClick,
onRightIconClick,
label,
rightIcon,
}: NavigationDrawerSectionTitleProps) => {
const currentUser = useRecoilValue(currentUserState);
const loading = useIsPrefetchLoading();
const isMobile = useIsMobile();
const isNavigationDrawerExpanded = useRecoilValue(
isNavigationDrawerExpandedState,
);
const isSettingsPage = useIsSettingsPage();
const currentUser = useRecoilValue(currentUserState);
const loading = useIsPrefetchLoading();
const handleTitleClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
if (isDefined(onClick) && (isNavigationDrawerExpanded || isSettingsPage)) {
onClick();
}
};
const handleRightIconClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
if (isDefined(onRightIconClick)) {
onRightIconClick();
}
};
if (loading && isDefined(currentUser)) {
return <NavigationDrawerSectionTitleSkeletonLoader />;
}
return (
<StyledTitle
onClick={
isNavigationDrawerExpanded || isSettingsPage ? onClick : undefined
}
>
{label}
<StyledTitle className="section-title-container" onClick={handleTitleClick}>
<StyledLabel>{label}</StyledLabel>
{rightIcon && (
<StyledRightIcon isMobile={isMobile} onClick={handleRightIconClick}>
{rightIcon}
</StyledRightIcon>
)}
</StyledTitle>
);
};

View File

@ -17,6 +17,8 @@ export const NavigationDrawerSubItem = ({
count,
keyboard,
subItemState,
rightOptions,
isDraggable,
}: NavigationDrawerSubItemProps) => {
return (
<NavigationDrawerItem
@ -32,6 +34,8 @@ export const NavigationDrawerSubItem = ({
soon={soon}
count={count}
keyboard={keyboard}
rightOptions={rightOptions}
isDraggable={isDraggable}
/>
);
};

View File

@ -22,7 +22,7 @@ import { SettingsPath } from '@/types/SettingsPath';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { CurrentWorkspaceMemberFavorites } from '@/favorites/components/CurrentWorkspaceMemberFavorites';
import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import jsonPage from '../../../../../../../package.json';
import { NavigationDrawer } from '../NavigationDrawer';
@ -71,7 +71,7 @@ export const Default: Story = {
/>
</NavigationDrawerSection>
<CurrentWorkspaceMemberFavorites />
<CurrentWorkspaceMemberFavoritesFolders />
<NavigationDrawerSection>
<NavigationDrawerSectionTitle label="Workspace" />

View File

@ -1,6 +1,6 @@
import { NavigationDrawerSubItemState } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerSubItemState';
export const getNavigationSubItemState = ({
export const getNavigationSubItemLeftAdornment = ({
index,
arrayLength,
selectedIndex,