Open showpage on workflow creation (#9714)

- Created an new component state
`isRecordEditableNameRenamingComponentState`
- Updated `useCreateNewTableRecord` to open the ShowPage on workflow
creation
- Refactored `RecordEditableName` and its components to remove the
useEffect (This was causing the recordName state to be updated after the
focus on `NavigationDrawerInput`, but we want the text so be selected
after the update).
- Introduced a new component `EditableBreadcrumbItem`
- Created an autosizing text input: This is done by a hack using a span
inside a div and the input position is set to absolute and takes the
size of the div. There are two problems that I didn't manage to fix:
If the text is too long, the title overflows, and the letter spacing is
different between the span and the input creating a small offset.


https://github.com/user-attachments/assets/4aa1e177-7458-4691-b0c8-96567b482206


New text input component:


https://github.com/user-attachments/assets/94565546-fe2b-457d-a1d8-907007e0e2ce
This commit is contained in:
Raphaël Bosi
2025-01-22 14:44:10 +01:00
committed by GitHub
parent 441b88b7e1
commit 8213995887
16 changed files with 429 additions and 179 deletions

View File

@ -0,0 +1,120 @@
import { isUpdatingRecordEditableNameState } from '@/object-record/states/isUpdatingRecordEditableName';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { useOpenEditableBreadCrumbItem } from '@/ui/navigation/bread-crumb/hooks/useOpenEditableBreadCrumbItem';
import { EditableBreadcrumbItemHotkeyScope } from '@/ui/navigation/bread-crumb/types/EditableBreadcrumbItemHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import styled from '@emotion/styled';
import { useRef, useState } from 'react';
import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-ui';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
type EditableBreadcrumbItemProps = {
className?: string;
defaultValue: string;
noValuePlaceholder?: string;
placeholder: string;
onSubmit: (value: string) => void;
hotkeyScope: string;
};
const StyledButton = styled('button')`
align-items: center;
background: inherit;
border: none;
border-radius: ${({ theme }) => theme.border.radius.sm};
box-sizing: content-box;
color: ${({ theme }) => theme.font.color.primary};
cursor: pointer;
display: flex;
font-family: ${({ theme }) => theme.font.family};
font-size: ${({ theme }) => theme.font.size.md};
height: 20px;
overflow: hidden;
text-decoration: none;
text-overflow: ellipsis;
white-space: nowrap;
:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
`;
export const EditableBreadcrumbItem = ({
className,
defaultValue,
noValuePlaceholder,
placeholder,
onSubmit,
}: EditableBreadcrumbItemProps) => {
const inputRef = useRef<HTMLInputElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const [isUpdatingRecordEditableName, setIsUpdatingRecordEditableName] =
useRecoilState(isUpdatingRecordEditableNameState);
// TODO: remove this and set the hokey scopes synchronously on page change inside the useNavigateApp hook
useHotkeyScopeOnMount(
EditableBreadcrumbItemHotkeyScope.EditableBreadcrumbItem,
);
useScopedHotkeys(
[Key.Escape],
() => {
setIsUpdatingRecordEditableName(false);
},
EditableBreadcrumbItemHotkeyScope.EditableBreadcrumbItem,
);
useScopedHotkeys(
[Key.Enter],
() => {
onSubmit(value);
setIsUpdatingRecordEditableName(false);
},
EditableBreadcrumbItemHotkeyScope.EditableBreadcrumbItem,
);
const clickOutsideRefs: Array<React.RefObject<HTMLElement>> = [
inputRef,
buttonRef,
];
useListenClickOutside({
refs: clickOutsideRefs,
callback: () => {
setIsUpdatingRecordEditableName(false);
},
listenerId: 'editable-breadcrumb-item',
});
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
if (isDefined(value)) {
event.target.select();
}
};
const [value, setValue] = useState<string>(defaultValue);
const { openEditableBreadCrumbItem } = useOpenEditableBreadCrumbItem();
return isUpdatingRecordEditableName ? (
<TextInputV2
className={className}
autoGrow
sizeVariant="sm"
ref={inputRef}
value={value}
onChange={setValue}
placeholder={placeholder}
onFocus={handleFocus}
autoFocus
/>
) : (
<StyledButton ref={buttonRef} onClick={openEditableBreadCrumbItem}>
{value || noValuePlaceholder}
</StyledButton>
);
};

View File

@ -0,0 +1,68 @@
import { expect, jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { RecoilRoot } from 'recoil';
import { EditableBreadcrumbItemHotkeyScope } from '@/ui/navigation/bread-crumb/types/EditableBreadcrumbItemHotkeyScope';
import { findByText, userEvent } from '@storybook/test';
import { ComponentDecorator } from 'twenty-ui';
import { EditableBreadcrumbItem } from '../EditableBreadcrumbItem';
const onSubmit = jest.fn();
const meta: Meta<typeof EditableBreadcrumbItem> = {
title: 'UI/Navigation/BreadCrumb/EditableBreadcrumbItem',
component: EditableBreadcrumbItem,
decorators: [
(Story) => (
<RecoilRoot>
<Story />
</RecoilRoot>
),
ComponentDecorator,
],
args: {
defaultValue: 'Company Name',
placeholder: 'Enter name',
hotkeyScope: EditableBreadcrumbItemHotkeyScope.EditableBreadcrumbItem,
onSubmit,
},
};
export default meta;
type Story = StoryObj<typeof EditableBreadcrumbItem>;
export const Default: Story = {
args: {},
play: async ({ canvasElement }) => {
const button = await findByText(canvasElement, 'Company Name');
expect(button).toBeInTheDocument();
},
};
export const Editing: Story = {
args: {},
play: async ({ canvasElement }) => {
const button = canvasElement.querySelector('button');
await userEvent.click(button);
await new Promise((resolve) => setTimeout(resolve, 100));
await userEvent.keyboard('New Name');
await userEvent.keyboard('{Enter}');
expect(onSubmit).toHaveBeenCalledWith('New Name');
},
};
export const WithNoValue: Story = {
args: {
defaultValue: '',
noValuePlaceholder: 'Untitled',
},
play: async ({ canvasElement }) => {
const button = await findByText(canvasElement, 'Untitled');
expect(button).toBeInTheDocument();
},
};

View File

@ -0,0 +1,19 @@
import { isUpdatingRecordEditableNameState } from '@/object-record/states/isUpdatingRecordEditableName';
import { EditableBreadcrumbItemHotkeyScope } from '@/ui/navigation/bread-crumb/types/EditableBreadcrumbItemHotkeyScope';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useSetRecoilState } from 'recoil';
export const useOpenEditableBreadCrumbItem = () => {
const setIsUpdatingRecordEditableName = useSetRecoilState(
isUpdatingRecordEditableNameState,
);
const setHotkeyScope = useSetHotkeyScope();
const openEditableBreadCrumbItem = () => {
setIsUpdatingRecordEditableName(true);
setHotkeyScope(EditableBreadcrumbItemHotkeyScope.EditableBreadcrumbItem);
};
return { openEditableBreadCrumbItem };
};

View File

@ -0,0 +1,3 @@
export enum EditableBreadcrumbItemHotkeyScope {
EditableBreadcrumbItem = 'editable-breadcrumb-item',
}

View File

@ -1,23 +1,16 @@
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
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 { FocusEvent, useRef } from 'react';
import { Key } from 'ts-key-enum';
import {
IconComponent,
isDefined,
TablerIconsProps,
TEXT_INPUT_STYLE,
} from 'twenty-ui';
import { IconComponent, TablerIconsProps, isDefined } from 'twenty-ui';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
type NavigationDrawerInputProps = {
className?: string;
Icon?: IconComponent | ((props: TablerIconsProps) => JSX.Element);
placeholder?: string;
value: string;
onChange: (value: string) => void;
onSubmit: (value: string) => void;
@ -26,38 +19,13 @@ type NavigationDrawerInputProps = {
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;
gap: ${({ theme }) => theme.spacing(2)};
display: flex;
width: 100%;
`;
const StyledTextInput = styled.input`
${TEXT_INPUT_STYLE}
margin: 0;
width: 100%;
padding: 0;
const StyledInput = styled(TextInputV2)`
background-color: white;
`;
export const NavigationDrawerInput = ({
className,
placeholder,
Icon,
value,
onChange,
@ -66,10 +34,6 @@ export const NavigationDrawerInput = ({
onClickOutside,
hotkeyScope,
}: NavigationDrawerInputProps) => {
const theme = useTheme();
const [isNavigationDrawerExpanded] = useRecoilState(
isNavigationDrawerExpandedState,
);
const inputRef = useRef<HTMLInputElement>(null);
useHotkeyScopeOnMount(hotkeyScope);
@ -99,10 +63,6 @@ export const NavigationDrawerInput = ({
listenerId: 'navigation-drawer-input',
});
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value);
};
const handleFocus = (event: FocusEvent<HTMLInputElement>) => {
if (isDefined(value)) {
event.target.select();
@ -110,29 +70,16 @@ export const NavigationDrawerInput = ({
};
return (
<StyledItem
<StyledInput
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>
LeftIcon={Icon}
ref={inputRef}
value={value}
onChange={onChange}
placeholder={placeholder}
onFocus={handleFocus}
fullWidth
autoFocus
/>
);
};

View File

@ -1,5 +0,0 @@
import { TextInput } from '@/ui/input/components/TextInput';
export const NavigationDrawerItemInput = () => {
return <TextInput />;
};