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:
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
export const NavigationDrawerItemInput = () => {
|
||||
return <TextInput />;
|
||||
};
|
||||
@ -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(
|
||||
@ -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 };
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NavigationDrawerSubItemState } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerSubItemState';
|
||||
|
||||
export const getNavigationSubItemState = ({
|
||||
export const getNavigationSubItemLeftAdornment = ({
|
||||
index,
|
||||
arrayLength,
|
||||
selectedIndex,
|
||||
Reference in New Issue
Block a user