mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-27 11:03:40 +03:00
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:
parent
bf05e5917d
commit
10b68618d3
@ -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>
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ export function ActivityRelationEditableField({ activity }: OwnProps) {
|
||||
displayModeContent={
|
||||
<ActivityTargetChips targets={activity?.activityTargets} />
|
||||
}
|
||||
isDisplayModeContentEmpty={activity?.activityTargets?.length === 0}
|
||||
/>
|
||||
</RecoilScope>
|
||||
</RecoilScope>
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 ? (
|
||||
|
@ -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();
|
||||
|
||||
|
@ -11,8 +11,6 @@ const StyledEditableFieldEditModeContainer = styled.div<OwnProps>`
|
||||
|
||||
margin-left: -${({ theme }) => theme.spacing(1)};
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
`;
|
||||
|
||||
|
@ -37,6 +37,7 @@ export function GenericEditableURLField() {
|
||||
editModeContent={<GenericEditableURLFieldEditMode />}
|
||||
displayModeContent={<FieldDisplayURL URL={fieldValue} />}
|
||||
isDisplayModeContentEmpty={!fieldValue}
|
||||
isDisplayModeFixHeight
|
||||
/>
|
||||
</RecoilScope>
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -1,5 +1,4 @@
|
||||
export enum RightDrawerPages {
|
||||
Timeline = 'timeline',
|
||||
CreateActivity = 'create-activity',
|
||||
EditActivity = 'edit-activity',
|
||||
}
|
||||
|
@ -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;
|
31
front/src/pages/not-found/__stories__/NotFound.stories.tsx
Normal file
31
front/src/pages/not-found/__stories__/NotFound.stories.tsx
Normal 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');
|
||||
},
|
||||
};
|
@ -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 () => {
|
||||
|
@ -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>
|
||||
|
@ -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 = {};
|
@ -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 = {};
|
Loading…
Reference in New Issue
Block a user