Removed all v2 in naming of state management logic (#13675)

This PR removes any V2 naming in state management logic and some minor
utils and hooks.

It has a lot of changes but nearly all of them were made by the rename
functionality of vscode which is deterministic, so it shouldn't
introduce any regression.

QA has been made on this PR on the main features of the app without any
noticeable issue.

Also renamed some other v2 naming related items : 
- TextInputV2 => TextInput
- TextInput => SettingsTextInput
- ObjectFilterDropdownFilterSelectMenuItemV2 =>
ObjectFilterDropdownFilterSelectMenuItem
- useInitDraftValueV2 => useInitDraftValue
- useOpenRecordTableCellV2 => useOpenRecordTableCell
This commit is contained in:
Lucas Bordeau
2025-08-06 12:09:15 +02:00
committed by GitHub
parent d8b24812b0
commit de802b1447
773 changed files with 3355 additions and 3462 deletions
@@ -14,7 +14,7 @@ import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { t } from '@lingui/core/macro';
import { IconApps, IconComponent, useIcons } from 'twenty-ui/display';
import {
@@ -87,7 +87,7 @@ const IconPickerIcon = ({
Icon,
focusedIconKey,
}: IconPickerIconProps) => {
const isSelectedItemId = useRecoilComponentValueV2(
const isSelectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
iconKey,
);
@@ -200,7 +200,7 @@ export const IconPicker = ({
const selectableListInstanceId = 'icon-list';
const focusedIconKey =
useRecoilComponentValueV2(
useRecoilComponentValue(
selectedItemIdComponentState,
selectableListInstanceId,
) ?? undefined;
@@ -16,7 +16,7 @@ import { SelectableList } from '@/ui/layout/selectable-list/components/Selectabl
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { isDefined } from 'twenty-shared/utils';
import { IconComponent } from 'twenty-ui/display';
import { SelectOption } from 'twenty-ui/input';
@@ -124,7 +124,7 @@ export const Select = <Value extends SelectValue>({
const selectableItemIdArray = filteredOptions.map((option) => option.label);
const selectedItemId = useRecoilComponentValueV2(
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
dropdownId,
);
@@ -7,7 +7,7 @@ import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-l
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useEffect, useMemo, useRef, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { TagColor } from 'twenty-ui/components';
@@ -41,7 +41,7 @@ export const SelectInput = ({
SelectableListComponentInstanceContext,
);
const selectedItemId = useRecoilComponentValueV2(
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
selectableListInstanceId,
);
@@ -0,0 +1,132 @@
import { FocusEventHandler, useEffect, useRef, useState } from 'react';
import { Key } from 'ts-key-enum';
import {
TextInput,
TextInputComponentProps,
} from '@/ui/input/components/TextInput';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { isDefined } from 'twenty-shared/utils';
export type SettingsTextInputProps = TextInputComponentProps & {
instanceId: string;
disableHotkeys?: boolean;
onInputEnter?: () => void;
dataTestId?: string;
autoFocusOnMount?: boolean;
autoSelectOnMount?: boolean;
};
export const SettingsTextInput = ({
instanceId,
onFocus,
onBlur,
onInputEnter,
disableHotkeys = false,
autoFocusOnMount,
autoSelectOnMount,
dataTestId,
...props
}: SettingsTextInputProps) => {
const inputRef = useRef<HTMLInputElement>(null);
const [isFocused, setIsFocused] = useState(false);
useEffect(() => {
if (autoFocusOnMount === true) {
inputRef.current?.focus();
setIsFocused(true);
}
}, [autoFocusOnMount]);
useEffect(() => {
if (autoSelectOnMount === true) {
inputRef.current?.select();
}
}, [autoSelectOnMount]);
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
onFocus?.(e);
setIsFocused(true);
if (!disableHotkeys) {
pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.TEXT_INPUT,
instanceId: instanceId,
},
globalHotkeysConfig: {
enableGlobalHotkeysConflictingWithKeyboard: false,
},
});
}
};
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
onBlur?.(e);
setIsFocused(false);
if (!disableHotkeys) {
removeFocusItemFromFocusStackById({ focusId: instanceId });
}
};
const handleEscape = () => {
if (!isFocused) {
return;
}
if (isDefined(inputRef) && 'current' in inputRef) {
inputRef.current?.blur();
setIsFocused(false);
}
};
const handleEnter = () => {
if (!isFocused) {
return;
}
onInputEnter?.();
if (isDefined(inputRef) && 'current' in inputRef) {
setIsFocused(false);
}
};
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: handleEscape,
focusId: instanceId,
dependencies: [handleEscape],
options: {
preventDefault: false,
},
});
useHotkeysOnFocusedElement({
keys: [Key.Enter],
callback: handleEnter,
focusId: instanceId,
dependencies: [handleEnter],
options: {
preventDefault: false,
},
});
return (
<TextInput
ref={inputRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
dataTestId={dataTestId}
onFocus={handleFocus}
onBlur={handleBlur}
/>
);
};
@@ -1,132 +1,444 @@
import { FocusEventHandler, useEffect, useRef, useState } from 'react';
import { Key } from 'ts-key-enum';
import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
TextInputV2,
TextInputV2ComponentProps,
} from '@/ui/input/components/TextInputV2';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { isDefined } from 'twenty-shared/utils';
ChangeEvent,
FocusEventHandler,
InputHTMLAttributes,
forwardRef,
useId,
useRef,
useState,
} from 'react';
import { IconComponent, IconEye, IconEyeOff } from 'twenty-ui/display';
import { AutogrowWrapper } from 'twenty-ui/utilities';
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
export type TextInputProps = TextInputV2ComponentProps & {
instanceId: string;
disableHotkeys?: boolean;
onInputEnter?: () => void;
const StyledContainer = styled.div<Pick<TextInputComponentProps, 'fullWidth'>>`
box-sizing: border-box;
display: inline-flex;
flex-direction: column;
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
position: relative;
`;
const StyledInputContainer = styled.div`
align-items: center;
background-color: inherit;
display: flex;
flex-direction: row;
position: relative;
`;
type StyledAdornmentContainerProps = {
sizeVariant: TextInputSize;
position: 'left' | 'right';
};
const StyledAdornmentContainer = styled.div<StyledAdornmentContainerProps>`
align-items: center;
background-color: ${({ theme }) => theme.background.transparent.light};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme, position }) =>
position === 'left'
? `${theme.border.radius.sm} 0 0 ${theme.border.radius.sm}`
: `0 ${theme.border.radius.sm} ${theme.border.radius.sm} 0`};
box-sizing: border-box;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
height: ${({ sizeVariant }) =>
sizeVariant === 'xs'
? '20px'
: sizeVariant === 'sm'
? '24px'
: sizeVariant === 'md'
? '28px'
: '32px'};
justify-content: center;
min-width: fit-content;
padding: ${({ theme }) => theme.spacing(2)};
width: auto;
line-height: ${({ sizeVariant }) =>
sizeVariant === 'xs'
? '20px'
: sizeVariant === 'sm'
? '24px'
: sizeVariant === 'md'
? '28px'
: '32px'};
${({ position }) =>
position === 'left' ? 'border-right: none;' : 'border-left: none;'}
`;
const StyledInput = styled.input<
Pick<
TextInputComponentProps,
| 'LeftIcon'
| 'RightIcon'
| 'error'
| 'sizeVariant'
| 'width'
| 'inheritFontStyles'
| 'autoGrow'
| 'rightAdornment'
| 'leftAdornment'
>
>`
background-color: ${({ theme }) => theme.background.transparent.lighter};
border-radius: ${({ theme, leftAdornment, rightAdornment }) =>
leftAdornment
? `0 ${theme.border.radius.sm} ${theme.border.radius.sm} 0`
: rightAdornment
? `${theme.border.radius.sm} 0 0 ${theme.border.radius.sm}`
: theme.border.radius.sm};
border: 1px solid
${({ theme, error }) =>
error ? theme.border.color.danger : theme.border.color.medium};
box-sizing: border-box;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex-grow: 1;
font-family: ${({ theme, inheritFontStyles }) =>
inheritFontStyles ? 'inherit' : theme.font.family};
font-size: ${({ theme, inheritFontStyles }) =>
inheritFontStyles ? 'inherit' : theme.font.size.md};
font-weight: ${({ theme, inheritFontStyles }) =>
inheritFontStyles ? 'inherit' : theme.font.weight.regular};
height: ${({ sizeVariant }) =>
sizeVariant === 'xs'
? '20px'
: sizeVariant === 'sm'
? '24px'
: sizeVariant === 'md'
? '28px'
: '32px'};
outline: none;
padding: ${({ theme, sizeVariant, autoGrow }) =>
autoGrow
? 0
: sizeVariant === 'xs'
? `${theme.spacing(2)} 0`
: theme.spacing(2)};
padding-left: ${({ theme, LeftIcon, autoGrow }) =>
autoGrow
? theme.spacing(1)
: LeftIcon
? `calc(${theme.spacing(3)} + 16px)`
: theme.spacing(2)};
padding-right: ${({ theme, RightIcon, autoGrow }) =>
autoGrow
? theme.spacing(1)
: RightIcon
? `calc(${theme.spacing(3)} + 16px)`
: theme.spacing(2)};
width: ${({ theme, width }) =>
width ? `calc(${width}px + ${theme.spacing(0.5)})` : '100%'};
max-width: ${({ autoGrow }) => (autoGrow ? '100%' : 'none')};
text-overflow: ellipsis;
&::placeholder,
&::-webkit-input-placeholder {
color: ${({ theme }) => theme.font.color.light};
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.medium};
}
&:disabled {
color: ${({ theme }) => theme.font.color.tertiary};
}
&[readonly] {
pointer-events: none;
}
&:focus {
${({ theme, error }) => {
return `
border-color: ${error ? theme.border.color.danger : theme.color.blue};
`;
}};
}
`;
const StyledLeftIconContainer = styled.div<{ sizeVariant: TextInputSize }>`
align-items: center;
display: flex;
justify-content: center;
padding-left: ${({ theme, sizeVariant }) =>
sizeVariant === 'xs'
? theme.spacing(0.5)
: sizeVariant === 'md' || sizeVariant === 'sm'
? theme.spacing(1)
: theme.spacing(2)};
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
`;
const StyledTrailingIconContainer = styled.div<
Pick<TextInputComponentProps, 'error'>
>`
align-items: center;
display: flex;
justify-content: center;
padding-right: ${({ theme }) => theme.spacing(2)};
position: absolute;
top: 0;
bottom: 0;
right: 0;
margin: auto 0;
`;
const StyledTrailingIcon = styled.div<{
isFocused?: boolean;
onClick?: () => void;
}>`
align-items: center;
color: ${({ theme, isFocused }) =>
isFocused ? theme.font.color.secondary : theme.font.color.light};
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
display: flex;
justify-content: center;
`;
const INPUT_TYPE_PASSWORD = 'password';
export type TextInputSize = 'xs' | 'sm' | 'md' | 'lg';
export type TextInputComponentProps = Omit<
InputHTMLAttributes<HTMLInputElement>,
'onChange' | 'onKeyDown'
> & {
className?: string;
label?: string;
onChange?: (text: string) => void;
fullWidth?: boolean;
error?: string;
noErrorHelper?: boolean;
RightIcon?: IconComponent;
onRightIconClick?: () => void;
LeftIcon?: IconComponent;
autoGrow?: boolean;
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
onBlur?: FocusEventHandler<HTMLInputElement>;
dataTestId?: string;
autoFocusOnMount?: boolean;
autoSelectOnMount?: boolean;
sizeVariant?: TextInputSize;
inheritFontStyles?: boolean;
rightAdornment?: string;
leftAdornment?: string;
textClickOutsideId?: string;
};
export const TextInput = ({
instanceId,
onFocus,
onBlur,
onInputEnter,
disableHotkeys = false,
autoFocusOnMount,
autoSelectOnMount,
dataTestId,
...props
}: TextInputProps) => {
const inputRef = useRef<HTMLInputElement>(null);
type TextInputWithAutoGrowWrapperProps = TextInputComponentProps;
const [isFocused, setIsFocused] = useState(false);
const TextInputComponent = forwardRef<
HTMLInputElement,
TextInputComponentProps
>(
(
{
className,
label,
value,
onChange,
onFocus,
onBlur,
onKeyDown,
fullWidth,
width,
error,
noErrorHelper = false,
required,
type,
autoFocus,
placeholder,
disabled,
readOnly,
tabIndex,
RightIcon,
onRightIconClick,
LeftIcon,
autoComplete,
maxLength,
sizeVariant = 'lg',
inheritFontStyles = false,
dataTestId,
autoGrow = false,
rightAdornment,
leftAdornment,
textClickOutsideId,
},
ref,
) => {
const theme = useTheme();
const inputRef = useRef<HTMLInputElement>(null);
const combinedRef = useCombinedRefs(ref, inputRef);
useEffect(() => {
if (autoFocusOnMount === true) {
inputRef.current?.focus();
const [passwordVisible, setPasswordVisible] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const handleTogglePasswordVisibility = () => {
setPasswordVisible(!passwordVisible);
};
const handleFocus: FocusEventHandler<HTMLInputElement> = (event) => {
setIsFocused(true);
}
}, [autoFocusOnMount]);
onFocus?.(event);
};
useEffect(() => {
if (autoSelectOnMount === true) {
inputRef.current?.select();
}
}, [autoSelectOnMount]);
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
onFocus?.(e);
setIsFocused(true);
if (!disableHotkeys) {
pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.TEXT_INPUT,
instanceId: instanceId,
},
globalHotkeysConfig: {
enableGlobalHotkeysConflictingWithKeyboard: false,
},
});
}
};
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
onBlur?.(e);
setIsFocused(false);
if (!disableHotkeys) {
removeFocusItemFromFocusStackById({ focusId: instanceId });
}
};
const handleEscape = () => {
if (!isFocused) {
return;
}
if (isDefined(inputRef) && 'current' in inputRef) {
inputRef.current?.blur();
const handleBlur: FocusEventHandler<HTMLInputElement> = (event) => {
setIsFocused(false);
}
};
onBlur?.(event);
};
const handleEnter = () => {
if (!isFocused) {
return;
}
onInputEnter?.();
if (isDefined(inputRef) && 'current' in inputRef) {
setIsFocused(false);
}
};
const instanceId = useId();
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: handleEscape,
focusId: instanceId,
dependencies: [handleEscape],
options: {
preventDefault: false,
},
});
return (
<StyledContainer
className={className}
fullWidth={fullWidth ?? false}
data-click-outside-id={textClickOutsideId}
>
{label && (
<InputLabel htmlFor={instanceId}>
{label + (required ? '*' : '')}
</InputLabel>
)}
<StyledInputContainer>
{leftAdornment && (
<StyledAdornmentContainer sizeVariant={sizeVariant} position="left">
{leftAdornment}
</StyledAdornmentContainer>
)}
useHotkeysOnFocusedElement({
keys: [Key.Enter],
callback: handleEnter,
focusId: instanceId,
dependencies: [handleEnter],
options: {
preventDefault: false,
},
});
{!!LeftIcon && (
<StyledLeftIconContainer sizeVariant={sizeVariant}>
<StyledTrailingIcon isFocused={isFocused}>
<LeftIcon size={theme.icon.size.md} />
</StyledTrailingIcon>
</StyledLeftIconContainer>
)}
<StyledInput
id={instanceId}
width={width}
data-testid={dataTestId}
autoComplete={autoComplete || 'off'}
ref={combinedRef}
tabIndex={tabIndex ?? 0}
onFocus={handleFocus}
onBlur={handleBlur}
type={passwordVisible ? 'text' : type}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange?.(
turnIntoEmptyStringIfWhitespacesOnly(event.target.value),
);
}}
onKeyDown={onKeyDown}
{...{
autoFocus,
disabled,
readOnly,
placeholder,
required,
value,
LeftIcon,
RightIcon,
maxLength,
error,
sizeVariant,
inheritFontStyles,
autoGrow,
leftAdornment,
rightAdornment,
}}
/>
{rightAdornment && (
<StyledAdornmentContainer
sizeVariant={sizeVariant}
position="right"
>
{rightAdornment}
</StyledAdornmentContainer>
)}
<StyledTrailingIconContainer {...{ error }}>
{!error && type === INPUT_TYPE_PASSWORD && (
<StyledTrailingIcon
onClick={handleTogglePasswordVisibility}
data-testid="reveal-password-button"
>
{passwordVisible ? (
<IconEyeOff size={theme.icon.size.md} />
) : (
<IconEye size={theme.icon.size.md} />
)}
</StyledTrailingIcon>
)}
{!error && type !== INPUT_TYPE_PASSWORD && !!RightIcon && (
<StyledTrailingIcon
onClick={onRightIconClick ? onRightIconClick : undefined}
>
<RightIcon size={theme.icon.size.md} />
</StyledTrailingIcon>
)}
</StyledTrailingIconContainer>
</StyledInputContainer>
{!noErrorHelper && error && (
<InputErrorHelper>{error}</InputErrorHelper>
)}
</StyledContainer>
);
},
);
const StyledAutogrowWrapper = styled(AutogrowWrapper)<{
sizeVariant?: TextInputSize;
}>`
box-sizing: border-box;
height: ${({ sizeVariant }) =>
sizeVariant === 'xs'
? '20px'
: sizeVariant === 'sm'
? '24px'
: sizeVariant === 'md'
? '28px'
: '32px'};
padding: 0 ${({ theme }) => theme.spacing(1.25)};
`;
const TextInputWithAutoGrowWrapper = forwardRef<
HTMLInputElement,
TextInputWithAutoGrowWrapperProps
>((props, ref) => {
return (
<TextInputV2
ref={inputRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
dataTestId={dataTestId}
onFocus={handleFocus}
onBlur={handleBlur}
/>
<>
{props.autoGrow ? (
<StyledAutogrowWrapper
sizeVariant={props.sizeVariant}
node={props.value || props.placeholder}
>
<TextInputComponent
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
fullWidth={true}
/>
</StyledAutogrowWrapper>
) : (
<TextInputComponent
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
/>
)}
</>
);
};
});
export const TextInput = TextInputWithAutoGrowWrapper;
@@ -1,446 +0,0 @@
import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
ChangeEvent,
FocusEventHandler,
InputHTMLAttributes,
forwardRef,
useId,
useRef,
useState,
} from 'react';
import { IconComponent, IconEye, IconEyeOff } from 'twenty-ui/display';
import { AutogrowWrapper } from 'twenty-ui/utilities';
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
const StyledContainer = styled.div<
Pick<TextInputV2ComponentProps, 'fullWidth'>
>`
box-sizing: border-box;
display: inline-flex;
flex-direction: column;
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
position: relative;
`;
const StyledInputContainer = styled.div`
align-items: center;
background-color: inherit;
display: flex;
flex-direction: row;
position: relative;
`;
type StyledAdornmentContainerProps = {
sizeVariant: TextInputV2Size;
position: 'left' | 'right';
};
const StyledAdornmentContainer = styled.div<StyledAdornmentContainerProps>`
align-items: center;
background-color: ${({ theme }) => theme.background.transparent.light};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme, position }) =>
position === 'left'
? `${theme.border.radius.sm} 0 0 ${theme.border.radius.sm}`
: `0 ${theme.border.radius.sm} ${theme.border.radius.sm} 0`};
box-sizing: border-box;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
height: ${({ sizeVariant }) =>
sizeVariant === 'xs'
? '20px'
: sizeVariant === 'sm'
? '24px'
: sizeVariant === 'md'
? '28px'
: '32px'};
justify-content: center;
min-width: fit-content;
padding: ${({ theme }) => theme.spacing(2)};
width: auto;
line-height: ${({ sizeVariant }) =>
sizeVariant === 'xs'
? '20px'
: sizeVariant === 'sm'
? '24px'
: sizeVariant === 'md'
? '28px'
: '32px'};
${({ position }) =>
position === 'left' ? 'border-right: none;' : 'border-left: none;'}
`;
const StyledInput = styled.input<
Pick<
TextInputV2ComponentProps,
| 'LeftIcon'
| 'RightIcon'
| 'error'
| 'sizeVariant'
| 'width'
| 'inheritFontStyles'
| 'autoGrow'
| 'rightAdornment'
| 'leftAdornment'
>
>`
background-color: ${({ theme }) => theme.background.transparent.lighter};
border-radius: ${({ theme, leftAdornment, rightAdornment }) =>
leftAdornment
? `0 ${theme.border.radius.sm} ${theme.border.radius.sm} 0`
: rightAdornment
? `${theme.border.radius.sm} 0 0 ${theme.border.radius.sm}`
: theme.border.radius.sm};
border: 1px solid
${({ theme, error }) =>
error ? theme.border.color.danger : theme.border.color.medium};
box-sizing: border-box;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex-grow: 1;
font-family: ${({ theme, inheritFontStyles }) =>
inheritFontStyles ? 'inherit' : theme.font.family};
font-size: ${({ theme, inheritFontStyles }) =>
inheritFontStyles ? 'inherit' : theme.font.size.md};
font-weight: ${({ theme, inheritFontStyles }) =>
inheritFontStyles ? 'inherit' : theme.font.weight.regular};
height: ${({ sizeVariant }) =>
sizeVariant === 'xs'
? '20px'
: sizeVariant === 'sm'
? '24px'
: sizeVariant === 'md'
? '28px'
: '32px'};
outline: none;
padding: ${({ theme, sizeVariant, autoGrow }) =>
autoGrow
? 0
: sizeVariant === 'xs'
? `${theme.spacing(2)} 0`
: theme.spacing(2)};
padding-left: ${({ theme, LeftIcon, autoGrow }) =>
autoGrow
? theme.spacing(1)
: LeftIcon
? `calc(${theme.spacing(3)} + 16px)`
: theme.spacing(2)};
padding-right: ${({ theme, RightIcon, autoGrow }) =>
autoGrow
? theme.spacing(1)
: RightIcon
? `calc(${theme.spacing(3)} + 16px)`
: theme.spacing(2)};
width: ${({ theme, width }) =>
width ? `calc(${width}px + ${theme.spacing(0.5)})` : '100%'};
max-width: ${({ autoGrow }) => (autoGrow ? '100%' : 'none')};
text-overflow: ellipsis;
&::placeholder,
&::-webkit-input-placeholder {
color: ${({ theme }) => theme.font.color.light};
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.medium};
}
&:disabled {
color: ${({ theme }) => theme.font.color.tertiary};
}
&[readonly] {
pointer-events: none;
}
&:focus {
${({ theme, error }) => {
return `
border-color: ${error ? theme.border.color.danger : theme.color.blue};
`;
}};
}
`;
const StyledLeftIconContainer = styled.div<{ sizeVariant: TextInputV2Size }>`
align-items: center;
display: flex;
justify-content: center;
padding-left: ${({ theme, sizeVariant }) =>
sizeVariant === 'xs'
? theme.spacing(0.5)
: sizeVariant === 'md' || sizeVariant === 'sm'
? theme.spacing(1)
: theme.spacing(2)};
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
`;
const StyledTrailingIconContainer = styled.div<
Pick<TextInputV2ComponentProps, 'error'>
>`
align-items: center;
display: flex;
justify-content: center;
padding-right: ${({ theme }) => theme.spacing(2)};
position: absolute;
top: 0;
bottom: 0;
right: 0;
margin: auto 0;
`;
const StyledTrailingIcon = styled.div<{
isFocused?: boolean;
onClick?: () => void;
}>`
align-items: center;
color: ${({ theme, isFocused }) =>
isFocused ? theme.font.color.secondary : theme.font.color.light};
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
display: flex;
justify-content: center;
`;
const INPUT_TYPE_PASSWORD = 'password';
export type TextInputV2Size = 'xs' | 'sm' | 'md' | 'lg';
export type TextInputV2ComponentProps = Omit<
InputHTMLAttributes<HTMLInputElement>,
'onChange' | 'onKeyDown'
> & {
className?: string;
label?: string;
onChange?: (text: string) => void;
fullWidth?: boolean;
error?: string;
noErrorHelper?: boolean;
RightIcon?: IconComponent;
onRightIconClick?: () => void;
LeftIcon?: IconComponent;
autoGrow?: boolean;
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
onBlur?: FocusEventHandler<HTMLInputElement>;
dataTestId?: string;
sizeVariant?: TextInputV2Size;
inheritFontStyles?: boolean;
rightAdornment?: string;
leftAdornment?: string;
textClickOutsideId?: string;
};
type TextInputV2WithAutoGrowWrapperProps = TextInputV2ComponentProps;
const TextInputV2Component = forwardRef<
HTMLInputElement,
TextInputV2ComponentProps
>(
(
{
className,
label,
value,
onChange,
onFocus,
onBlur,
onKeyDown,
fullWidth,
width,
error,
noErrorHelper = false,
required,
type,
autoFocus,
placeholder,
disabled,
readOnly,
tabIndex,
RightIcon,
onRightIconClick,
LeftIcon,
autoComplete,
maxLength,
sizeVariant = 'lg',
inheritFontStyles = false,
dataTestId,
autoGrow = false,
rightAdornment,
leftAdornment,
textClickOutsideId,
},
ref,
) => {
const theme = useTheme();
const inputRef = useRef<HTMLInputElement>(null);
const combinedRef = useCombinedRefs(ref, inputRef);
const [passwordVisible, setPasswordVisible] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const handleTogglePasswordVisibility = () => {
setPasswordVisible(!passwordVisible);
};
const handleFocus: FocusEventHandler<HTMLInputElement> = (event) => {
setIsFocused(true);
onFocus?.(event);
};
const handleBlur: FocusEventHandler<HTMLInputElement> = (event) => {
setIsFocused(false);
onBlur?.(event);
};
const instanceId = useId();
return (
<StyledContainer
className={className}
fullWidth={fullWidth ?? false}
data-click-outside-id={textClickOutsideId}
>
{label && (
<InputLabel htmlFor={instanceId}>
{label + (required ? '*' : '')}
</InputLabel>
)}
<StyledInputContainer>
{leftAdornment && (
<StyledAdornmentContainer sizeVariant={sizeVariant} position="left">
{leftAdornment}
</StyledAdornmentContainer>
)}
{!!LeftIcon && (
<StyledLeftIconContainer sizeVariant={sizeVariant}>
<StyledTrailingIcon isFocused={isFocused}>
<LeftIcon size={theme.icon.size.md} />
</StyledTrailingIcon>
</StyledLeftIconContainer>
)}
<StyledInput
id={instanceId}
width={width}
data-testid={dataTestId}
autoComplete={autoComplete || 'off'}
ref={combinedRef}
tabIndex={tabIndex ?? 0}
onFocus={handleFocus}
onBlur={handleBlur}
type={passwordVisible ? 'text' : type}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange?.(
turnIntoEmptyStringIfWhitespacesOnly(event.target.value),
);
}}
onKeyDown={onKeyDown}
{...{
autoFocus,
disabled,
readOnly,
placeholder,
required,
value,
LeftIcon,
RightIcon,
maxLength,
error,
sizeVariant,
inheritFontStyles,
autoGrow,
leftAdornment,
rightAdornment,
}}
/>
{rightAdornment && (
<StyledAdornmentContainer
sizeVariant={sizeVariant}
position="right"
>
{rightAdornment}
</StyledAdornmentContainer>
)}
<StyledTrailingIconContainer {...{ error }}>
{!error && type === INPUT_TYPE_PASSWORD && (
<StyledTrailingIcon
onClick={handleTogglePasswordVisibility}
data-testid="reveal-password-button"
>
{passwordVisible ? (
<IconEyeOff size={theme.icon.size.md} />
) : (
<IconEye size={theme.icon.size.md} />
)}
</StyledTrailingIcon>
)}
{!error && type !== INPUT_TYPE_PASSWORD && !!RightIcon && (
<StyledTrailingIcon
onClick={onRightIconClick ? onRightIconClick : undefined}
>
<RightIcon size={theme.icon.size.md} />
</StyledTrailingIcon>
)}
</StyledTrailingIconContainer>
</StyledInputContainer>
{!noErrorHelper && error && (
<InputErrorHelper>{error}</InputErrorHelper>
)}
</StyledContainer>
);
},
);
const StyledAutogrowWrapper = styled(AutogrowWrapper)<{
sizeVariant?: TextInputV2Size;
}>`
box-sizing: border-box;
height: ${({ sizeVariant }) =>
sizeVariant === 'xs'
? '20px'
: sizeVariant === 'sm'
? '24px'
: sizeVariant === 'md'
? '28px'
: '32px'};
padding: 0 ${({ theme }) => theme.spacing(1.25)};
`;
const TextInputV2WithAutoGrowWrapper = forwardRef<
HTMLInputElement,
TextInputV2WithAutoGrowWrapperProps
>((props, ref) => {
return (
<>
{props.autoGrow ? (
<StyledAutogrowWrapper
sizeVariant={props.sizeVariant}
node={props.value || props.placeholder}
>
<TextInputV2Component
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
fullWidth={true}
/>
</StyledAutogrowWrapper>
) : (
<TextInputV2Component
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
/>
)}
</>
);
});
export const TextInputV2 = TextInputV2WithAutoGrowWrapper;
@@ -1,7 +1,4 @@
import {
TextInputV2,
TextInputV2Size,
} from '@/ui/input/components/TextInputV2';
import { TextInput, TextInputSize } from '@/ui/input/components/TextInput';
import { useRef, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
@@ -22,7 +19,7 @@ type InputProps = {
onClickOutside?: () => void;
onTab?: () => void;
onShiftTab?: () => void;
sizeVariant?: TextInputV2Size;
sizeVariant?: TextInputSize;
};
export type TitleInputProps = {
@@ -30,7 +27,7 @@ export type TitleInputProps = {
} & InputProps;
const StyledDiv = styled.div<{
sizeVariant: TextInputV2Size;
sizeVariant: TextInputSize;
disabled?: boolean;
}>`
background: inherit;
@@ -116,7 +113,7 @@ const Input = ({
});
return (
<TextInputV2
<TextInput
ref={wrapperRef}
autoGrow
sizeVariant={sizeVariant}
@@ -1,10 +1,13 @@
import { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { TextInput, TextInputProps } from '../TextInput';
import {
TextInput,
TextInputComponentProps,
} from '@/ui/input/components/TextInput';
import { ComponentDecorator } from 'twenty-ui/testing';
type RenderProps = TextInputProps;
type RenderProps = TextInputComponentProps;
const Render = (args: RenderProps) => {
const [value, setValue] = useState(args.value);
@@ -37,3 +40,31 @@ export const Filled: Story = {
export const Disabled: Story = {
args: { disabled: true, value: 'Tim' },
};
export const AutoGrow: Story = {
args: { autoGrow: true, value: 'Tim' },
};
export const AutoGrowWithPlaceholder: Story = {
args: { autoGrow: true, placeholder: 'Tim' },
};
export const Small: Story = {
args: { sizeVariant: 'sm', value: 'Tim' },
};
export const AutoGrowSmall: Story = {
args: { autoGrow: true, sizeVariant: 'sm', value: 'Tim' },
};
export const WithLeftAdornment: Story = {
args: {
leftAdornment: 'https://',
},
};
export const WithRightAdornment: Story = {
args: {
rightAdornment: '@twenty.com',
},
};
@@ -1,70 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import {
TextInputV2,
TextInputV2ComponentProps,
} from '@/ui/input/components/TextInputV2';
import { ComponentDecorator } from 'twenty-ui/testing';
type RenderProps = TextInputV2ComponentProps;
const Render = (args: RenderProps) => {
const [value, setValue] = useState(args.value);
const handleChange = (text: string) => {
args.onChange?.(text);
setValue(text);
};
// eslint-disable-next-line react/jsx-props-no-spreading
return <TextInputV2 {...args} value={value} onChange={handleChange} />;
};
const meta: Meta<typeof TextInputV2> = {
title: 'UI/Input/TextInputV2',
component: TextInputV2,
decorators: [ComponentDecorator],
args: { placeholder: 'Tim' },
render: Render,
};
export default meta;
type Story = StoryObj<typeof TextInputV2>;
export const Default: Story = {};
export const Filled: Story = {
args: { value: 'Tim' },
};
export const Disabled: Story = {
args: { disabled: true, value: 'Tim' },
};
export const AutoGrow: Story = {
args: { autoGrow: true, value: 'Tim' },
};
export const AutoGrowWithPlaceholder: Story = {
args: { autoGrow: true, placeholder: 'Tim' },
};
export const Small: Story = {
args: { sizeVariant: 'sm', value: 'Tim' },
};
export const AutoGrowSmall: Story = {
args: { autoGrow: true, sizeVariant: 'sm', value: 'Tim' },
};
export const WithLeftAdornment: Story = {
args: {
leftAdornment: 'https://',
},
};
export const WithRightAdornment: Story = {
args: {
rightAdornment: '@twenty.com',
},
};
@@ -1,5 +1,5 @@
import { Select } from '@/ui/input/components/Select';
import { TextInput } from '@/ui/input/components/TextInput';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { RELATIVE_DATE_DIRECTION_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions';
import { RELATIVE_DATE_UNITS_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions';
import {
@@ -71,7 +71,7 @@ export const RelativeDatePickerHeader = (
options={RELATIVE_DATE_DIRECTION_SELECT_OPTIONS}
fullWidth
/>
<TextInput
<SettingsTextInput
instanceId="relative-date-picker-amount"
width={50}
value={textInputValue}
@@ -9,7 +9,7 @@ import { PhoneCountryPickerDropdownSelect } from './PhoneCountryPickerDropdownSe
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import 'react-phone-number-input/style.css';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronDown, IconWorld } from 'twenty-ui/display';
@@ -79,7 +79,7 @@ export const PhoneCountryPickerDropdownButton = ({
const dropdownId = 'country-picker-dropdown-id';
const isDropdownOpen = useRecoilComponentValueV2(
const isDropdownOpen = useRecoilComponentValue(
isDropdownOpenComponentState,
dropdownId,
);
@@ -2,7 +2,7 @@ import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { isSlashMenuOpenComponentState } from '@/ui/input/editor/states/isSlashMenuOpenComponentState';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
export type BlockEditorDropdownFocusEffectProps = {
@@ -12,7 +12,7 @@ export type BlockEditorDropdownFocusEffectProps = {
export const BlockEditorDropdownFocusEffect = ({
editor,
}: BlockEditorDropdownFocusEffectProps) => {
const isSlashMenuOpenState = useRecoilComponentCallbackStateV2(
const isSlashMenuOpenState = useRecoilComponentCallbackState(
isSlashMenuOpenComponentState,
);
@@ -3,7 +3,7 @@ import { SuggestionItem } from '@/ui/input/editor/components/types';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { MenuItemSuggestion } from 'twenty-ui/navigation';
export type CustomSlashMenuListItemProps = {
@@ -15,7 +15,7 @@ export const CustomSlashMenuListItem = ({
}: CustomSlashMenuListItemProps) => {
const { resetSelectedItem } = useSelectableList(SLASH_MENU_LIST_ID);
const isSelectedItem = useRecoilComponentFamilyValueV2(
const isSelectedItem = useRecoilComponentFamilyValue(
isSelectedItemIdComponentFamilySelector,
item.title,
);
@@ -1,7 +1,7 @@
import { BlockEditorComponentInstanceContext } from '@/ui/input/editor/contexts/BlockEditorCompoponeInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const isSlashMenuOpenComponentState = createComponentStateV2<boolean>({
export const isSlashMenuOpenComponentState = createComponentState<boolean>({
key: 'isSlashMenuOpenComponentState',
defaultValue: false,
componentInstanceContext: BlockEditorComponentInstanceContext,