New behavior for editable fields (#1300)

* New behavior for editable fields

* fix

* fix

* fix coverage

* Add tests on NotFound

* fix

* fix

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Weiko
2023-08-24 15:56:43 +02:00
committed by GitHub
parent bf05e5917d
commit 10b68618d3
17 changed files with 134 additions and 136 deletions

View File

@ -15,12 +15,12 @@ import { People } from '~/pages/people/People';
import { PersonShow } from '~/pages/people/PersonShow'; import { PersonShow } from '~/pages/people/PersonShow';
import { SettingsExperience } from '~/pages/settings/SettingsExperience'; import { SettingsExperience } from '~/pages/settings/SettingsExperience';
import { SettingsProfile } from '~/pages/settings/SettingsProfile'; import { SettingsProfile } from '~/pages/settings/SettingsProfile';
import { SettingsWorksapce } from '~/pages/settings/SettingsWorkspace'; import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers'; import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
import { Tasks } from '~/pages/tasks/Tasks'; import { Tasks } from '~/pages/tasks/Tasks';
import { AppInternalHooks } from '~/sync-hooks/AppInternalHooks'; import { AppInternalHooks } from '~/sync-hooks/AppInternalHooks';
import { NotFound } from './NotFound'; import { NotFound } from './pages/not-found/NotFound';
// TEMP FEATURE FLAG FOR VIEW FIELDS // TEMP FEATURE FLAG FOR VIEW FIELDS
export const ACTIVATE_VIEW_FIELDS = true; export const ACTIVATE_VIEW_FIELDS = true;
@ -64,7 +64,7 @@ export function App() {
/> />
<Route <Route
path={SettingsPath.Workspace} path={SettingsPath.Workspace}
element={<SettingsWorksapce />} element={<SettingsWorkspace />}
/> />
</Routes> </Routes>
} }

View File

@ -36,6 +36,7 @@ export function ActivityRelationEditableField({ activity }: OwnProps) {
displayModeContent={ displayModeContent={
<ActivityTargetChips targets={activity?.activityTargets} /> <ActivityTargetChips targets={activity?.activityTargets} />
} }
isDisplayModeContentEmpty={activity?.activityTargets?.length === 0}
/> />
</RecoilScope> </RecoilScope>
</RecoilScope> </RecoilScope>

View File

@ -1,26 +0,0 @@
import { useRecoilState } from 'recoil';
import { useRightDrawer } from '@/ui/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { activityTargetableEntityArrayState } from '../states/activityTargetableEntityArrayState';
import { ActivityTargetableEntity } from '../types/ActivityTargetableEntity';
// TODO: refactor with recoil callback to avoid rerender
export function useOpenTimelineRightDrawer() {
const { openRightDrawer } = useRightDrawer();
const [, setActivityTargetableEntityArray] = useRecoilState(
activityTargetableEntityArrayState,
);
const setHotkeyScope = useSetHotkeyScope();
return function openTimelineRightDrawer(
activityTargetableEntityArray: ActivityTargetableEntity[],
) {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setActivityTargetableEntityArray(activityTargetableEntityArray);
openRightDrawer(RightDrawerPages.Timeline);
};
}

View File

@ -1,24 +0,0 @@
import { useRecoilValue } from 'recoil';
import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
import { Timeline } from '@/activities/timeline/components/Timeline';
export function RightDrawerTimeline() {
const activityTargetableEntityArray = useRecoilValue(
activityTargetableEntityArrayState,
);
return (
<>
{activityTargetableEntityArray.map((targetableEntity) => (
<Timeline
key={targetableEntity.id}
entity={{
id: targetableEntity?.id ?? '',
type: targetableEntity.type,
}}
/>
))}
</>
);
}

View File

@ -34,7 +34,6 @@ const StyledLabelAndIconContainer = styled.div`
const StyledValueContainer = styled.div` const StyledValueContainer = styled.div`
display: flex; display: flex;
flex: 1;
max-width: calc(100% - ${({ theme }) => theme.spacing(4)}); max-width: calc(100% - ${({ theme }) => theme.spacing(4)});
`; `;
@ -46,21 +45,28 @@ const StyledLabel = styled.div<Pick<OwnProps, 'labelFixedWidth'>>`
`; `;
const StyledEditButtonContainer = styled(motion.div)` const StyledEditButtonContainer = styled(motion.div)`
position: absolute; align-items: center;
right: 0; display: flex;
`; `;
export const StyledEditableFieldBaseContainer = styled.div` const StyledClickableContainer = styled.div`
cursor: pointer;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
width: 100%;
`;
const StyledEditableFieldBaseContainer = styled.div`
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.spacing(1)};
justify-content: space-between;
position: relative; position: relative;
user-select: none; user-select: none;
width: 100%; width: 100%;
`; `;
@ -71,11 +77,11 @@ type OwnProps = {
useEditButton?: boolean; useEditButton?: boolean;
editModeContent?: React.ReactNode; editModeContent?: React.ReactNode;
displayModeContentOnly?: boolean; displayModeContentOnly?: boolean;
disableHoverEffect?: boolean;
displayModeContent: React.ReactNode; displayModeContent: React.ReactNode;
customEditHotkeyScope?: HotkeyScope; customEditHotkeyScope?: HotkeyScope;
isDisplayModeContentEmpty?: boolean; isDisplayModeContentEmpty?: boolean;
isDisplayModeFixHeight?: boolean; isDisplayModeFixHeight?: boolean;
disableHoverEffect?: boolean;
onSubmit?: () => void; onSubmit?: () => void;
onCancel?: () => void; onCancel?: () => void;
}; };
@ -88,10 +94,10 @@ export function EditableField({
editModeContent, editModeContent,
displayModeContent, displayModeContent,
customEditHotkeyScope, customEditHotkeyScope,
disableHoverEffect,
isDisplayModeContentEmpty, isDisplayModeContentEmpty,
displayModeContentOnly, displayModeContentOnly,
isDisplayModeFixHeight, isDisplayModeFixHeight,
disableHoverEffect,
onSubmit, onSubmit,
onCancel, onCancel,
}: OwnProps) { }: OwnProps) {
@ -124,33 +130,35 @@ export function EditableField({
<StyledLabel labelFixedWidth={labelFixedWidth}>{label}</StyledLabel> <StyledLabel labelFixedWidth={labelFixedWidth}>{label}</StyledLabel>
)} )}
</StyledLabelAndIconContainer> </StyledLabelAndIconContainer>
<StyledValueContainer> <StyledValueContainer>
{isFieldInEditMode && !displayModeContentOnly ? ( {isFieldInEditMode && !displayModeContentOnly ? (
<EditableFieldEditMode onSubmit={onSubmit} onCancel={onCancel}> <EditableFieldEditMode onSubmit={onSubmit} onCancel={onCancel}>
{editModeContent} {editModeContent}
</EditableFieldEditMode> </EditableFieldEditMode>
) : ( ) : (
<EditableFieldDisplayMode <StyledClickableContainer onClick={handleDisplayModeClick}>
disableHoverEffect={disableHoverEffect} <EditableFieldDisplayMode
disableClick={useEditButton} disableHoverEffect={disableHoverEffect}
onClick={handleDisplayModeClick} isDisplayModeContentEmpty={isDisplayModeContentEmpty}
isDisplayModeContentEmpty={isDisplayModeContentEmpty} isDisplayModeFixHeight={isDisplayModeFixHeight}
isDisplayModeFixHeight={isDisplayModeFixHeight} isHovered={isHovered}
> >
{displayModeContent} {displayModeContent}
</EditableFieldDisplayMode> </EditableFieldDisplayMode>
{showEditButton && (
<StyledEditButtonContainer
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }}
>
<EditableFieldEditButton />
</StyledEditButtonContainer>
)}
</StyledClickableContainer>
)} )}
</StyledValueContainer> </StyledValueContainer>
{showEditButton && (
<StyledEditButtonContainer
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }}
>
<EditableFieldEditButton />
</StyledEditButtonContainer>
)}
</StyledEditableFieldBaseContainer> </StyledEditableFieldBaseContainer>
); );
} }

View File

@ -4,10 +4,10 @@ import styled from '@emotion/styled';
const StyledEditableFieldNormalModeOuterContainer = styled.div< const StyledEditableFieldNormalModeOuterContainer = styled.div<
Pick< Pick<
OwnProps, OwnProps,
| 'disableClick'
| 'isDisplayModeContentEmpty' | 'isDisplayModeContentEmpty'
| 'disableHoverEffect' | 'disableHoverEffect'
| 'isDisplayModeFixHeight' | 'isDisplayModeFixHeight'
| 'isHovered'
> >
>` >`
align-items: center; align-items: center;
@ -20,26 +20,13 @@ const StyledEditableFieldNormalModeOuterContainer = styled.div<
padding: ${({ theme }) => theme.spacing(1)}; padding: ${({ theme }) => theme.spacing(1)};
${(props) => { ${(props) => {
if (!props.isDisplayModeContentEmpty) { if (props.isHovered) {
return css` return css`
width: fit-content; background-color: ${!props.disableHoverEffect
`; ? props.theme.background.transparent.lighter
} : 'transparent'};
}}
${(props) => {
if (props.disableClick) {
return css`
cursor: default;
`;
} else {
return css`
cursor: pointer; cursor: pointer;
&:hover {
background-color: ${!props.disableHoverEffect &&
props.theme.background.transparent.light};
}
`; `;
} }
}} }}
@ -64,28 +51,25 @@ const StyledEmptyField = styled.div`
`; `;
type OwnProps = { type OwnProps = {
disableClick?: boolean;
onClick?: () => void;
isDisplayModeContentEmpty?: boolean; isDisplayModeContentEmpty?: boolean;
disableHoverEffect?: boolean; disableHoverEffect?: boolean;
isDisplayModeFixHeight?: boolean; isDisplayModeFixHeight?: boolean;
isHovered?: boolean;
}; };
export function EditableFieldDisplayMode({ export function EditableFieldDisplayMode({
children, children,
disableClick,
onClick,
isDisplayModeContentEmpty, isDisplayModeContentEmpty,
disableHoverEffect, disableHoverEffect,
isDisplayModeFixHeight, isDisplayModeFixHeight,
isHovered,
}: React.PropsWithChildren<OwnProps>) { }: React.PropsWithChildren<OwnProps>) {
return ( return (
<StyledEditableFieldNormalModeOuterContainer <StyledEditableFieldNormalModeOuterContainer
onClick={disableClick ? undefined : onClick}
disableClick={disableClick}
isDisplayModeContentEmpty={isDisplayModeContentEmpty} isDisplayModeContentEmpty={isDisplayModeContentEmpty}
disableHoverEffect={disableHoverEffect} disableHoverEffect={disableHoverEffect}
isDisplayModeFixHeight={isDisplayModeFixHeight} isDisplayModeFixHeight={isDisplayModeFixHeight}
isHovered={isHovered}
> >
<StyledEditableFieldNormalModeInnerContainer> <StyledEditableFieldNormalModeInnerContainer>
{isDisplayModeContentEmpty || !children ? ( {isDisplayModeContentEmpty || !children ? (

View File

@ -1,30 +1,8 @@
import styled from '@emotion/styled';
import { IconButton } from '@/ui/button/components/IconButton'; import { IconButton } from '@/ui/button/components/IconButton';
import { IconPencil } from '@/ui/icon'; import { IconPencil } from '@/ui/icon';
import { overlayBackground } from '@/ui/theme/constants/effects';
import { useEditableField } from '../hooks/useEditableField'; import { useEditableField } from '../hooks/useEditableField';
export const StyledEditableFieldEditButton = styled.div`
align-items: center;
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;
display: flex;
height: 20px;
justify-content: center;
margin-left: -2px;
width: 20px;
z-index: 1;
${overlayBackground}
`;
export function EditableFieldEditButton() { export function EditableFieldEditButton() {
const { openEditableField } = useEditableField(); const { openEditableField } = useEditableField();

View File

@ -11,8 +11,6 @@ const StyledEditableFieldEditModeContainer = styled.div<OwnProps>`
margin-left: -${({ theme }) => theme.spacing(1)}; margin-left: -${({ theme }) => theme.spacing(1)};
position: relative; position: relative;
width: 100%;
z-index: 10; z-index: 10;
`; `;

View File

@ -37,6 +37,7 @@ export function GenericEditableURLField() {
editModeContent={<GenericEditableURLFieldEditMode />} editModeContent={<GenericEditableURLFieldEditMode />}
displayModeContent={<FieldDisplayURL URL={fieldValue} />} displayModeContent={<FieldDisplayURL URL={fieldValue} />}
isDisplayModeContentEmpty={!fieldValue} isDisplayModeContentEmpty={!fieldValue}
isDisplayModeFixHeight
/> />
</RecoilScope> </RecoilScope>
); );

View File

@ -3,7 +3,6 @@ import { useRecoilState } from 'recoil';
import { RightDrawerCreateActivity } from '@/activities/right-drawer/components/create/RightDrawerCreateActivity'; import { RightDrawerCreateActivity } from '@/activities/right-drawer/components/create/RightDrawerCreateActivity';
import { RightDrawerEditActivity } from '@/activities/right-drawer/components/edit/RightDrawerEditActivity'; import { RightDrawerEditActivity } from '@/activities/right-drawer/components/edit/RightDrawerEditActivity';
import { RightDrawerTimeline } from '@/activities/right-drawer/components/RightDrawerTimeline';
import { rightDrawerPageState } from '../states/rightDrawerPageState'; import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerPages } from '../types/RightDrawerPages'; import { RightDrawerPages } from '../types/RightDrawerPages';
@ -33,9 +32,6 @@ export function RightDrawerRouter() {
let page = <></>; let page = <></>;
switch (rightDrawerPage) { switch (rightDrawerPage) {
case RightDrawerPages.Timeline:
page = <RightDrawerTimeline />;
break;
case RightDrawerPages.CreateActivity: case RightDrawerPages.CreateActivity:
page = <RightDrawerCreateActivity />; page = <RightDrawerCreateActivity />;
break; break;

View File

@ -1,5 +1,4 @@
export enum RightDrawerPages { export enum RightDrawerPages {
Timeline = 'timeline',
CreateActivity = 'create-activity', CreateActivity = 'create-activity',
EditActivity = 'edit-activity', EditActivity = 'edit-activity',
} }

View File

@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import { MainButton } from '@/ui/button/components/MainButton'; import { MainButton } from '@/ui/button/components/MainButton';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { CompaniesMockMode } from './pages/companies/CompaniesMockMode'; import { CompaniesMockMode } from '../companies/CompaniesMockMode';
const StyledBackDrop = styled.div` const StyledBackDrop = styled.div`
align-items: center; align-items: center;

View File

@ -0,0 +1,31 @@
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/testing-library';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { PageDecoratorArgs } from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { NotFound } from '../NotFound';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/NotFound/Default',
component: NotFound,
decorators: [ComponentWithRouterDecorator],
args: {
routePath: 'toto-not-found',
},
parameters: {
docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof NotFound>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Page not found');
},
};

View File

@ -41,7 +41,7 @@ export const AddCompanyFromHeader: Story = {
await button.click(); await button.click();
await canvas.findByText('Airbnb'); await canvas.findByText('Algolia');
}); });
await step('Change pipeline stage', async () => { await step('Change pipeline stage', async () => {

View File

@ -16,7 +16,7 @@ const StyledContainer = styled.div`
width: 350px; width: 350px;
`; `;
export function SettingsWorksapce() { export function SettingsWorkspace() {
return ( return (
<SubMenuTopBarContainer icon={<IconSettings size={16} />} title="Settings"> <SubMenuTopBarContainer icon={<IconSettings size={16} />} title="Settings">
<div> <div>

View File

@ -0,0 +1,26 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
PageDecorator,
type PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { SettingsExperience } from '../SettingsExperience';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/SettingsExperience',
component: SettingsExperience,
decorators: [PageDecorator],
args: { routePath: '/settings/experience' },
parameters: {
docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsExperience>;
export const Default: Story = {};

View File

@ -0,0 +1,26 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
PageDecorator,
type PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { SettingsWorkspace } from '../SettingsWorkspace';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/SettingsWorkspace',
component: SettingsWorkspace,
decorators: [PageDecorator],
args: { routePath: '/settings/workspace' },
parameters: {
docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsWorkspace>;
export const Default: Story = {};