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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 { SettingsExperience } from '~/pages/settings/SettingsExperience';
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 { Tasks } from '~/pages/tasks/Tasks';
import { AppInternalHooks } from '~/sync-hooks/AppInternalHooks';
import { NotFound } from './NotFound';
import { NotFound } from './pages/not-found/NotFound';
// TEMP FEATURE FLAG FOR VIEW FIELDS
export const ACTIVATE_VIEW_FIELDS = true;
@ -64,7 +64,7 @@ export function App() {
/>
<Route
path={SettingsPath.Workspace}
element={<SettingsWorksapce />}
element={<SettingsWorkspace />}
/>
</Routes>
}

View File

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

View File

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

View File

@ -1,30 +1,8 @@
import styled from '@emotion/styled';
import { IconButton } from '@/ui/button/components/IconButton';
import { IconPencil } from '@/ui/icon';
import { overlayBackground } from '@/ui/theme/constants/effects';
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() {
const { openEditableField } = useEditableField();

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import { MainButton } from '@/ui/button/components/MainButton';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { CompaniesMockMode } from './pages/companies/CompaniesMockMode';
import { CompaniesMockMode } from '../companies/CompaniesMockMode';
const StyledBackDrop = styled.div`
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 canvas.findByText('Airbnb');
await canvas.findByText('Algolia');
});
await step('Change pipeline stage', async () => {

View File

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