mirror of
https://github.com/twentyhq/twenty.git
synced 2024-09-11 15:35:36 +03:00
1259/add compact view in opportunities (#2182)
* icons added * recoil family state added for checking compact view in each card * recoil state added for toggle button. Wether compact view show or not * menu item modifed for right side content * compact view toggle added in dropdown options * dropdown width increased because compact view text was overflowing * compact view added in boardcard * new animation added for in and out * compact view enabled state added * old state deleted * sizes added in toggle component * removed extra added code form navigation * toggle size added in menuitem toggle * MenuItemToggle added instead of MenuItemNavigate * Compact view improved
This commit is contained in:
parent
350410b0fe
commit
bd0b886081
@ -6,11 +6,15 @@ import { FieldContext } from '@/ui/data/field/contexts/FieldContext';
|
||||
import { InlineCell } from '@/ui/data/inline-cell/components/InlineCell';
|
||||
import { InlineCellHotkeyScope } from '@/ui/data/inline-cell/types/InlineCellHotkeyScope';
|
||||
import { EntityChipVariant } from '@/ui/display/chip/components/EntityChip';
|
||||
import { IconEye } from '@/ui/display/icon/index';
|
||||
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
|
||||
import { BoardCardIdContext } from '@/ui/layout/board/contexts/BoardCardIdContext';
|
||||
import { useBoardContext } from '@/ui/layout/board/hooks/useBoardContext';
|
||||
import { useCurrentCardSelected } from '@/ui/layout/board/hooks/useCurrentCardSelected';
|
||||
import { isCardInCompactViewState } from '@/ui/layout/board/states/isCardInCompactViewState';
|
||||
import { isCompactViewEnabledState } from '@/ui/layout/board/states/isCompactViewEnabledState';
|
||||
import { visibleBoardCardFieldsScopedSelector } from '@/ui/layout/board/states/selectors/visibleBoardCardFieldsScopedSelector';
|
||||
import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { useUpdateOnePipelineProgressMutation } from '~/generated/graphql';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
@ -38,12 +42,21 @@ const StyledBoardCard = styled.div<{ selected: boolean }>`
|
||||
cursor: pointer;
|
||||
|
||||
.checkbox-container {
|
||||
transition: all ease-in-out 160ms;
|
||||
opacity: ${({ selected }) => (selected ? 1 : 0)};
|
||||
}
|
||||
|
||||
&:hover .checkbox-container {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.compact-icon-container {
|
||||
transition: all ease-in-out 160ms;
|
||||
opacity: 0;
|
||||
}
|
||||
&:hover .compact-icon-container {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledBoardCardWrapper = styled.div`
|
||||
@ -51,16 +64,21 @@ const StyledBoardCardWrapper = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledBoardCardHeader = styled.div`
|
||||
const StyledBoardCardHeader = styled.div<{
|
||||
showCompactView: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
height: 24px;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
padding-bottom: ${({ theme, showCompactView }) =>
|
||||
theme.spacing(showCompactView ? 0 : 1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
transition: padding ease-in-out 160ms;
|
||||
|
||||
img {
|
||||
height: ${({ theme }) => theme.icon.size.md}px;
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
@ -99,6 +117,27 @@ const StyledFieldContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledCompactIconContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledIconEye = styled(IconEye)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
height: 24px;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(0.2)};
|
||||
padding-left: ${({ theme }) => theme.spacing(0.5)};
|
||||
padding-right: ${({ theme }) => theme.spacing(0.5)};
|
||||
|
||||
padding-top: ${({ theme }) => theme.spacing(0.2)};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.background.transparent.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
}
|
||||
`;
|
||||
|
||||
export const CompanyBoardCard = () => {
|
||||
const { BoardRecoilScopeContext } = useBoardContext();
|
||||
|
||||
@ -109,6 +148,15 @@ export const CompanyBoardCard = () => {
|
||||
const [companyProgress] = useRecoilState(
|
||||
companyProgressesFamilyState(boardCardId ?? ''),
|
||||
);
|
||||
|
||||
const [isCompactViewEnabled] = useRecoilState(isCompactViewEnabledState);
|
||||
|
||||
const [isCardInCompactView, setIsCardInCompactView] = useRecoilState(
|
||||
isCardInCompactViewState(boardCardId ?? ''),
|
||||
);
|
||||
|
||||
const showCompactView = isCompactViewEnabled && isCardInCompactView;
|
||||
|
||||
const { pipelineProgress, company } = companyProgress ?? {};
|
||||
|
||||
const visibleBoardCardFields = useRecoilScopedValue(
|
||||
@ -135,19 +183,34 @@ export const CompanyBoardCard = () => {
|
||||
</StyledFieldContainer>
|
||||
);
|
||||
|
||||
const OnMouseLeaveBoard = () => {
|
||||
setIsCardInCompactView(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledBoardCardWrapper>
|
||||
<StyledBoardCard
|
||||
selected={currentCardSelected}
|
||||
onMouseLeave={OnMouseLeaveBoard}
|
||||
onClick={() => setCurrentCardSelected(!currentCardSelected)}
|
||||
>
|
||||
<StyledBoardCardHeader>
|
||||
<StyledBoardCardHeader showCompactView={showCompactView}>
|
||||
<CompanyChip
|
||||
id={company.id}
|
||||
name={company.name}
|
||||
pictureUrl={getLogoUrlFromDomainName(company.domainName)}
|
||||
variant={EntityChipVariant.Transparent}
|
||||
/>
|
||||
{showCompactView && (
|
||||
<StyledCompactIconContainer className="compact-icon-container">
|
||||
<StyledIconEye
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsCardInCompactView(false);
|
||||
}}
|
||||
/>
|
||||
</StyledCompactIconContainer>
|
||||
)}
|
||||
<StyledCheckboxContainer className="checkbox-container">
|
||||
<Checkbox
|
||||
checked={currentCardSelected}
|
||||
@ -157,28 +220,32 @@ export const CompanyBoardCard = () => {
|
||||
</StyledCheckboxContainer>
|
||||
</StyledBoardCardHeader>
|
||||
<StyledBoardCardBody>
|
||||
{visibleBoardCardFields.map((viewField) => (
|
||||
<PreventSelectOnClickContainer key={viewField.key}>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: boardCardId,
|
||||
recoilScopeId: boardCardId + viewField.key,
|
||||
fieldDefinition: {
|
||||
key: viewField.key,
|
||||
name: viewField.name,
|
||||
Icon: viewField.Icon,
|
||||
type: viewField.type,
|
||||
metadata: viewField.metadata,
|
||||
entityChipDisplayMapper: viewField.entityChipDisplayMapper,
|
||||
},
|
||||
useUpdateEntityMutation: useUpdateOnePipelineProgressMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
}}
|
||||
>
|
||||
<InlineCell />
|
||||
</FieldContext.Provider>
|
||||
</PreventSelectOnClickContainer>
|
||||
))}
|
||||
<AnimatedEaseInOut isOpen={!showCompactView}>
|
||||
{visibleBoardCardFields.map((viewField) => (
|
||||
<PreventSelectOnClickContainer key={viewField.key}>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: boardCardId,
|
||||
recoilScopeId: boardCardId + viewField.key,
|
||||
fieldDefinition: {
|
||||
key: viewField.key,
|
||||
name: viewField.name,
|
||||
Icon: viewField.Icon,
|
||||
type: viewField.type,
|
||||
metadata: viewField.metadata,
|
||||
entityChipDisplayMapper:
|
||||
viewField.entityChipDisplayMapper,
|
||||
},
|
||||
useUpdateEntityMutation:
|
||||
useUpdateOnePipelineProgressMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
}}
|
||||
>
|
||||
<InlineCell />
|
||||
</FieldContext.Provider>
|
||||
</PreventSelectOnClickContainer>
|
||||
))}
|
||||
</AnimatedEaseInOut>
|
||||
</StyledBoardCardBody>
|
||||
</StyledBoardCard>
|
||||
</StyledBoardCardWrapper>
|
||||
|
@ -11,6 +11,7 @@ export {
|
||||
IconArrowRight,
|
||||
IconArrowUp,
|
||||
IconArrowUpRight,
|
||||
IconBaselineDensitySmall,
|
||||
IconBell,
|
||||
IconBox,
|
||||
IconBrandGithub,
|
||||
|
@ -1,10 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export type ToggleSize = 'small' | 'medium';
|
||||
|
||||
type ContainerProps = {
|
||||
isOn: boolean;
|
||||
color?: string;
|
||||
toggleSize: ToggleSize;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div<ContainerProps>`
|
||||
@ -14,32 +17,40 @@ const StyledContainer = styled.div<ContainerProps>`
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 20px;
|
||||
height: ${({ toggleSize }) => (toggleSize === 'small' ? 16 : 20)}px;
|
||||
transition: background-color 0.3s ease;
|
||||
width: 32px;
|
||||
width: ${({ toggleSize }) => (toggleSize === 'small' ? 24 : 32)}px;
|
||||
`;
|
||||
|
||||
const StyledCircle = styled(motion.div)`
|
||||
const StyledCircle = styled(motion.div)<{
|
||||
toggleSize: ToggleSize;
|
||||
}>`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border-radius: 50%;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
height: ${({ toggleSize }) => (toggleSize === 'small' ? 12 : 16)}px;
|
||||
width: ${({ toggleSize }) => (toggleSize === 'small' ? 12 : 16)}px;
|
||||
`;
|
||||
|
||||
const circleVariants = {
|
||||
on: { x: 14 },
|
||||
off: { x: 2 },
|
||||
};
|
||||
|
||||
export type ToggleProps = {
|
||||
value?: boolean;
|
||||
onChange?: (value: boolean) => void;
|
||||
color?: string;
|
||||
toggleSize?: ToggleSize;
|
||||
};
|
||||
|
||||
export const Toggle = ({ value, onChange, color }: ToggleProps) => {
|
||||
export const Toggle = ({
|
||||
value,
|
||||
onChange,
|
||||
color,
|
||||
toggleSize = 'medium',
|
||||
}: ToggleProps) => {
|
||||
const [isOn, setIsOn] = useState(value ?? false);
|
||||
|
||||
const circleVariants = {
|
||||
on: { x: toggleSize === 'small' ? 10 : 14 },
|
||||
off: { x: 2 },
|
||||
};
|
||||
|
||||
const handleChange = () => {
|
||||
setIsOn(!isOn);
|
||||
|
||||
@ -56,8 +67,17 @@ export const Toggle = ({ value, onChange, color }: ToggleProps) => {
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<StyledContainer onClick={handleChange} isOn={isOn} color={color}>
|
||||
<StyledCircle animate={isOn ? 'on' : 'off'} variants={circleVariants} />
|
||||
<StyledContainer
|
||||
onClick={handleChange}
|
||||
isOn={isOn}
|
||||
color={color}
|
||||
toggleSize={toggleSize}
|
||||
>
|
||||
<StyledCircle
|
||||
animate={isOn ? 'on' : 'off'}
|
||||
variants={circleVariants}
|
||||
toggleSize={toggleSize}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
@ -35,6 +35,7 @@ export const BoardOptionsDropdown = ({
|
||||
}
|
||||
dropdownHotkeyScope={customHotkeyScope}
|
||||
onClickOutside={resetViewEditMode}
|
||||
dropdownMenuWidth={170}
|
||||
/>
|
||||
</DropdownScope>
|
||||
);
|
||||
|
@ -15,6 +15,7 @@ import { currentViewScopedSelector } from '@/ui/data/view-bar/states/selectors/c
|
||||
import { viewsByIdScopedSelector } from '@/ui/data/view-bar/states/selectors/viewsByIdScopedSelector';
|
||||
import { viewEditModeState } from '@/ui/data/view-bar/states/viewEditModeState';
|
||||
import {
|
||||
IconBaselineDensitySmall,
|
||||
IconChevronLeft,
|
||||
IconLayoutKanban,
|
||||
IconPlus,
|
||||
@ -28,6 +29,7 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MenuItemNavigate } from '@/ui/navigation/menu-item/components/MenuItemNavigate';
|
||||
import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemToggle';
|
||||
import { ThemeColor } from '@/ui/theme/constants/colors';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
@ -37,6 +39,7 @@ import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilSco
|
||||
import { useBoardCardFields } from '../hooks/useBoardCardFields';
|
||||
import { boardCardFieldsScopedState } from '../states/boardCardFieldsScopedState';
|
||||
import { boardColumnsState } from '../states/boardColumnsState';
|
||||
import { isCompactViewEnabledState } from '../states/isCompactViewEnabledState';
|
||||
import { savedBoardCardFieldsFamilyState } from '../states/savedBoardCardFieldsFamilyState';
|
||||
import { hiddenBoardCardFieldsScopedSelector } from '../states/selectors/hiddenBoardCardFieldsScopedSelector';
|
||||
import { visibleBoardCardFieldsScopedSelector } from '../states/selectors/visibleBoardCardFieldsScopedSelector';
|
||||
@ -72,6 +75,9 @@ export const BoardOptionsDropdownContent = ({
|
||||
>();
|
||||
|
||||
const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState);
|
||||
const [isCompactViewEnabled, setIsCompactViewEnabled] = useRecoilState(
|
||||
isCompactViewEnabledState,
|
||||
);
|
||||
|
||||
const hiddenBoardCardFields = useRecoilScopedValue(
|
||||
hiddenBoardCardFieldsScopedSelector,
|
||||
@ -194,6 +200,16 @@ export const BoardOptionsDropdownContent = ({
|
||||
text="Stages"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconBaselineDensitySmall}
|
||||
onToggleChange={setIsCompactViewEnabled}
|
||||
toggled={isCompactViewEnabled}
|
||||
text="Compact view"
|
||||
toggleSize="small"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
)}
|
||||
{currentMenu === 'stages' && (
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isCardInCompactViewState = atomFamily<boolean, string>({
|
||||
key: 'isCardInCompactViewState',
|
||||
default: true,
|
||||
});
|
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isCompactViewEnabledState = atom<boolean>({
|
||||
key: 'isCompactViewEnabledState',
|
||||
default: false,
|
||||
});
|
@ -1,15 +1,19 @@
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
import { Toggle } from '@/ui/input/components/Toggle';
|
||||
import { Toggle, ToggleSize } from '@/ui/input/components/Toggle';
|
||||
|
||||
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
|
||||
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
|
||||
import {
|
||||
StyledMenuItemBase,
|
||||
StyledMenuItemRightContent,
|
||||
} from '../internals/components/StyledMenuItemBase';
|
||||
|
||||
type MenuItemToggleProps = {
|
||||
LeftIcon?: IconComponent;
|
||||
toggled: boolean;
|
||||
text: string;
|
||||
className: string;
|
||||
className?: string;
|
||||
onToggleChange?: (toggled: boolean) => void;
|
||||
toggleSize?: ToggleSize;
|
||||
};
|
||||
|
||||
export const MenuItemToggle = ({
|
||||
@ -18,6 +22,7 @@ export const MenuItemToggle = ({
|
||||
toggled,
|
||||
className,
|
||||
onToggleChange,
|
||||
toggleSize,
|
||||
}: MenuItemToggleProps) => {
|
||||
const handleOnClick = () => {
|
||||
onToggleChange?.(!toggled);
|
||||
@ -26,7 +31,13 @@ export const MenuItemToggle = ({
|
||||
return (
|
||||
<StyledMenuItemBase className={className} onClick={handleOnClick}>
|
||||
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
|
||||
<Toggle value={toggled} onChange={onToggleChange} />
|
||||
<StyledMenuItemRightContent>
|
||||
<Toggle
|
||||
value={toggled}
|
||||
onChange={onToggleChange}
|
||||
toggleSize={toggleSize}
|
||||
/>
|
||||
</StyledMenuItemRightContent>
|
||||
</StyledMenuItemBase>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,45 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
import { AnimationDuration } from '@/ui/theme/constants/animation';
|
||||
|
||||
type AnimatedEaseInOutProps = {
|
||||
isOpen: boolean;
|
||||
children: React.ReactNode;
|
||||
duration?: AnimationDuration;
|
||||
marginBottom?: string;
|
||||
marginTop?: string;
|
||||
};
|
||||
|
||||
export const AnimatedEaseInOut = ({
|
||||
children,
|
||||
isOpen,
|
||||
marginBottom,
|
||||
marginTop,
|
||||
duration = 'normal',
|
||||
}: AnimatedEaseInOutProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{
|
||||
marginBottom: marginBottom ?? 0,
|
||||
marginTop: marginTop ?? 0,
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{ height: 100, opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0, marginBottom: 0, marginTop: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration[duration],
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user