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:
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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();
|
||||
},
|
||||
};
|
||||
@ -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 };
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
export enum EditableBreadcrumbItemHotkeyScope {
|
||||
EditableBreadcrumbItem = 'editable-breadcrumb-item',
|
||||
}
|
||||
@ -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
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
export const NavigationDrawerItemInput = () => {
|
||||
return <TextInput />;
|
||||
};
|
||||
Reference in New Issue
Block a user