384 update the input of the record show page inside the command menu (#10213)
Created a new component `RecordTitleCell` with an API close to `RecordInlineCell`. This new component is an autogrowing input. It consumes the `FieldContext`. It uses some hooks and states from `RecordInlineCell` because I didn't want to duplicate all the logic, but this logic could be duplicated. Two issues that I didn't solve in this PR: - There is a flashing glitch inside the input when typing - The input of a workflow isn't focused when creating a new one. This is because of an issue with the `useHotkeyScopeOnMount` hook which is deprecated but still used in some components. Upon redirection on the workflow showpage, the hokey scope of the input is overridden by the hokey scopes of the components which use `useHotkeyScopeOnMount`. I decided not to open the input for now. ## Command menu record show page ### Single input https://github.com/user-attachments/assets/50dc235c-8f34-4445-8b04-586125606bd5 ### Double input https://github.com/user-attachments/assets/bdcfd6eb-d25e-4006-a87f-6e615e8a6e7e ## Workflow breadcrumb https://github.com/user-attachments/assets/ded38dd6-5794-4779-a4ae-b3948567595a ## Record show page ### Single input https://github.com/user-attachments/assets/8ad7a606-556a-416b-8788-93415f7989e1 ### Double input https://github.com/user-attachments/assets/55aae40b-36ae-40f1-8171-06f1a5db3532
This commit is contained in:
@ -1,26 +1,35 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { EditableBreadcrumbItem } from '@/ui/navigation/bread-crumb/components/EditableBreadcrumbItem';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
|
||||
import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell';
|
||||
import styled from '@emotion/styled';
|
||||
import { capitalize } from 'twenty-shared';
|
||||
import { FieldMetadataType, capitalize } from 'twenty-shared';
|
||||
|
||||
const StyledEditableTitleContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledEditableTitlePrefix = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding: ${({ theme }) => theme.spacing(0.75)};
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
width: fit-content;
|
||||
`;
|
||||
|
||||
export const ObjectRecordShowPageBreadcrumb = ({
|
||||
objectNameSingular,
|
||||
objectRecordId,
|
||||
@ -40,22 +49,12 @@ export const ObjectRecordShowPageBreadcrumb = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { updateOneRecord } = useUpdateOneRecord({
|
||||
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
|
||||
objectNameSingular,
|
||||
recordGqlFields: {
|
||||
[labelIdentifierFieldMetadataItem?.name ?? 'name']: true,
|
||||
},
|
||||
objectRecordId,
|
||||
recordFromStore: record ?? null,
|
||||
});
|
||||
|
||||
const handleSubmit = (value: string) => {
|
||||
updateOneRecord({
|
||||
idToUpdate: objectRecordId,
|
||||
updateOneRecordInput: {
|
||||
name: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
@ -66,13 +65,35 @@ export const ObjectRecordShowPageBreadcrumb = ({
|
||||
{capitalize(objectLabelPlural)}
|
||||
<span>{' / '}</span>
|
||||
</StyledEditableTitlePrefix>
|
||||
<EditableBreadcrumbItem
|
||||
defaultValue={record?.name ?? ''}
|
||||
noValuePlaceholder={labelIdentifierFieldMetadataItem?.label ?? 'Name'}
|
||||
placeholder={labelIdentifierFieldMetadataItem?.label ?? 'Name'}
|
||||
onSubmit={handleSubmit}
|
||||
hotkeyScope="editable-breadcrumb-item"
|
||||
/>
|
||||
<StyledTitle>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
recordId: objectRecordId,
|
||||
recoilScopeId:
|
||||
objectRecordId + labelIdentifierFieldMetadataItem?.id,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
type:
|
||||
labelIdentifierFieldMetadataItem?.type ||
|
||||
FieldMetadataType.TEXT,
|
||||
iconName: '',
|
||||
fieldMetadataId: labelIdentifierFieldMetadataItem?.id ?? '',
|
||||
label: labelIdentifierFieldMetadataItem?.label || '',
|
||||
metadata: {
|
||||
fieldName: labelIdentifierFieldMetadataItem?.name || '',
|
||||
objectMetadataNameSingular: objectNameSingular,
|
||||
},
|
||||
defaultValue: labelIdentifierFieldMetadataItem?.defaultValue,
|
||||
},
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
isCentered: false,
|
||||
isDisplayModeFixHeight: true,
|
||||
}}
|
||||
>
|
||||
<RecordTitleCell sizeVariant="sm" />
|
||||
</FieldContext.Provider>
|
||||
</StyledTitle>
|
||||
</StyledEditableTitleContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -7,11 +7,13 @@ import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/
|
||||
import { RightDrawerTitleRecordInlineCell } from '@/object-record/record-right-drawer/components/RightDrawerTitleRecordInlineCell';
|
||||
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
|
||||
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
|
||||
import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell';
|
||||
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
|
||||
import { ShowPageSummaryCardSkeletonLoader } from '@/ui/layout/show-page/components/ShowPageSummaryCardSkeletonLoader';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
import { FeatureFlagKey, FieldMetadataType } from '~/generated/graphql';
|
||||
|
||||
type SummaryCardProps = {
|
||||
objectNameSingular: string;
|
||||
@ -53,6 +55,10 @@ export const SummaryCard = ({
|
||||
isRecordDeleted: recordFromStore?.isDeleted,
|
||||
});
|
||||
|
||||
const isCommandMenuV2Enabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsCommandMenuV2Enabled,
|
||||
);
|
||||
|
||||
if (isNewRightDrawerItemLoading || !isDefined(recordFromStore)) {
|
||||
return <ShowPageSummaryCardSkeletonLoader />;
|
||||
}
|
||||
@ -93,7 +99,9 @@ export const SummaryCard = ({
|
||||
isDisplayModeFixHeight: true,
|
||||
}}
|
||||
>
|
||||
{isInRightDrawer ? (
|
||||
{isCommandMenuV2Enabled ? (
|
||||
<RecordTitleCell sizeVariant="md" />
|
||||
) : isInRightDrawer ? (
|
||||
<RightDrawerTitleRecordInlineCell />
|
||||
) : (
|
||||
<RecordInlineCell readonly={isReadOnly} />
|
||||
|
||||
@ -5,7 +5,7 @@ import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-ce
|
||||
import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
|
||||
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
|
||||
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
|
||||
import { isUpdatingRecordEditableNameState } from '@/object-record/states/isUpdatingRecordEditableName';
|
||||
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
|
||||
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
|
||||
import { shouldRedirectToShowPageOnCreation } from '@/object-record/utils/shouldRedirectToShowPageOnCreation';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
@ -60,67 +60,59 @@ export const useCreateNewTableRecord = ({
|
||||
|
||||
const navigate = useNavigateApp();
|
||||
|
||||
const createNewTableRecord = useRecoilCallback(
|
||||
({ set }) =>
|
||||
async () => {
|
||||
const recordId = v4();
|
||||
const { openRecordTitleCell } = useRecordTitleCell();
|
||||
|
||||
if (isCommandMenuV2Enabled) {
|
||||
// TODO: Generalize this behaviour, there will be a view setting to specify
|
||||
// if the new record should be displayed in the side panel or on the record page
|
||||
if (
|
||||
shouldRedirectToShowPageOnCreation(objectMetadataItem.nameSingular)
|
||||
) {
|
||||
await createOneRecord({
|
||||
id: recordId,
|
||||
name: 'Untitled',
|
||||
});
|
||||
const createNewTableRecord = async () => {
|
||||
const recordId = v4();
|
||||
|
||||
navigate(AppPath.RecordShowPage, {
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
objectRecordId: recordId,
|
||||
});
|
||||
if (isCommandMenuV2Enabled) {
|
||||
// TODO: Generalize this behaviour, there will be a view setting to specify
|
||||
// if the new record should be displayed in the side panel or on the record page
|
||||
if (shouldRedirectToShowPageOnCreation(objectMetadataItem.nameSingular)) {
|
||||
await createOneRecord({
|
||||
id: recordId,
|
||||
name: 'Untitled',
|
||||
});
|
||||
|
||||
set(isUpdatingRecordEditableNameState, true);
|
||||
return;
|
||||
}
|
||||
navigate(AppPath.RecordShowPage, {
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
objectRecordId: recordId,
|
||||
});
|
||||
|
||||
await createOneRecord({ id: recordId });
|
||||
openRecordInCommandMenu(recordId, objectMetadataItem.nameSingular);
|
||||
// TODO: we should open the record title cell here but because
|
||||
// we are redirecting to the record show page, the hotkey scope will
|
||||
// be overridden by the hotkey scope on mount. We need to deprecate
|
||||
// the useHotkeyScopeOnMount hook.
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingRecordId(recordId);
|
||||
setSelectedTableCellEditMode(-1, 0);
|
||||
setHotkeyScope(
|
||||
DEFAULT_CELL_SCOPE.scope,
|
||||
DEFAULT_CELL_SCOPE.customScopes,
|
||||
);
|
||||
await createOneRecord({ id: recordId });
|
||||
|
||||
if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
|
||||
setActiveDropdownFocusIdAndMemorizePrevious(
|
||||
getDropdownFocusIdForRecordField(
|
||||
recordId,
|
||||
objectMetadataItem.labelIdentifierFieldMetadataId,
|
||||
'table-cell',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
createOneRecord,
|
||||
isCommandMenuV2Enabled,
|
||||
navigate,
|
||||
objectMetadataItem.labelIdentifierFieldMetadataId,
|
||||
objectMetadataItem.nameSingular,
|
||||
openRecordInCommandMenu,
|
||||
setActiveDropdownFocusIdAndMemorizePrevious,
|
||||
setHotkeyScope,
|
||||
setPendingRecordId,
|
||||
setSelectedTableCellEditMode,
|
||||
],
|
||||
);
|
||||
openRecordInCommandMenu(recordId, objectMetadataItem.nameSingular);
|
||||
|
||||
openRecordTitleCell({
|
||||
recordId,
|
||||
fieldMetadataId: objectMetadataItem.labelIdentifierFieldMetadataId,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingRecordId(recordId);
|
||||
setSelectedTableCellEditMode(-1, 0);
|
||||
setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes);
|
||||
|
||||
if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
|
||||
setActiveDropdownFocusIdAndMemorizePrevious(
|
||||
getDropdownFocusIdForRecordField(
|
||||
recordId,
|
||||
objectMetadataItem.labelIdentifierFieldMetadataId,
|
||||
'table-cell',
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const createNewTableRecordInGroup = useRecoilCallback(
|
||||
({ set }) =>
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
|
||||
import { useInlineCell } from '../../record-inline-cell/hooks/useInlineCell';
|
||||
|
||||
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
|
||||
import { RecordTitleCellContainer } from '@/object-record/record-title-cell/components/RecordTitleCellContainer';
|
||||
import {
|
||||
RecordTitleCellContext,
|
||||
RecordTitleCellContextProps,
|
||||
} from '@/object-record/record-title-cell/components/RecordTitleCellContext';
|
||||
import { RecordTitleCellFieldDisplay } from '@/object-record/record-title-cell/components/RecordTitleCellFieldDisplay';
|
||||
import { RecordTitleCellFieldInput } from '@/object-record/record-title-cell/components/RecordTitleCellFieldInput';
|
||||
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
|
||||
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
type RecordTitleCellProps = {
|
||||
loading?: boolean;
|
||||
sizeVariant?: 'sm' | 'md';
|
||||
};
|
||||
|
||||
export const RecordTitleCell = ({
|
||||
loading,
|
||||
sizeVariant,
|
||||
}: RecordTitleCellProps) => {
|
||||
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
const { closeInlineCell } = useInlineCell();
|
||||
|
||||
const handleEnter: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
closeInlineCell();
|
||||
};
|
||||
|
||||
const handleEscape = () => {
|
||||
closeInlineCell();
|
||||
};
|
||||
|
||||
const handleTab: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
closeInlineCell();
|
||||
};
|
||||
|
||||
const handleShiftTab: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
closeInlineCell();
|
||||
};
|
||||
|
||||
const handleClickOutside: FieldInputClickOutsideEvent = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(persistField, event) => {
|
||||
const recordFieldDropdownId = getDropdownFocusIdForRecordField(
|
||||
recordId,
|
||||
fieldDefinition.fieldMetadataId,
|
||||
'inline-cell',
|
||||
);
|
||||
|
||||
const activeDropdownFocusId = snapshot
|
||||
.getLoadable(activeDropdownFocusIdState)
|
||||
.getValue();
|
||||
|
||||
if (recordFieldDropdownId !== activeDropdownFocusId) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
persistField();
|
||||
closeInlineCell();
|
||||
},
|
||||
[closeInlineCell, fieldDefinition.fieldMetadataId, recordId],
|
||||
);
|
||||
|
||||
const recordTitleCellContextValue: RecordTitleCellContextProps = {
|
||||
editModeContent: (
|
||||
<RecordTitleCellFieldInput
|
||||
recordFieldInputId={getRecordFieldInputId(
|
||||
recordId,
|
||||
fieldDefinition?.metadata?.fieldName,
|
||||
)}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onTab={handleTab}
|
||||
onShiftTab={handleShiftTab}
|
||||
onClickOutside={handleClickOutside}
|
||||
sizeVariant={sizeVariant}
|
||||
/>
|
||||
),
|
||||
displayModeContent: <RecordTitleCellFieldDisplay />,
|
||||
editModeContentOnly: isFieldInputOnly,
|
||||
loading: loading,
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldFocusContextProvider>
|
||||
<RecordTitleCellContext.Provider value={recordTitleCellContextValue}>
|
||||
<RecordTitleCellContainer />
|
||||
</RecordTitleCellContext.Provider>
|
||||
</FieldFocusContextProvider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,13 @@
|
||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||
import { RecordTitleCellContext } from '@/object-record/record-title-cell/components/RecordTitleCellContext';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const RecordTitleCellContainer = () => {
|
||||
const { displayModeContent, editModeContent } = useContext(
|
||||
RecordTitleCellContext,
|
||||
);
|
||||
|
||||
const { isInlineCellInEditMode } = useInlineCell();
|
||||
|
||||
return <>{isInlineCellInEditMode ? editModeContent : displayModeContent}</>;
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { createContext, ReactElement } from 'react';
|
||||
|
||||
export type RecordTitleCellContextProps = {
|
||||
editModeContent?: ReactElement;
|
||||
editModeContentOnly?: boolean;
|
||||
displayModeContent?: ReactElement;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
const defaultRecordTitleCellContextProp: RecordTitleCellContextProps = {
|
||||
editModeContent: undefined,
|
||||
editModeContentOnly: false,
|
||||
displayModeContent: undefined,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
export const RecordTitleCellContext =
|
||||
createContext<RecordTitleCellContextProps>(defaultRecordTitleCellContextProp);
|
||||
@ -0,0 +1,24 @@
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
||||
import { RecordTitleCellSingleTextDisplayMode } from '@/object-record/record-title-cell/components/RecordTitleCellTextFieldDisplay';
|
||||
import { RecordTitleFullNameFieldDisplay } from '@/object-record/record-title-cell/components/RecordTitleFullNameFieldDisplay';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const RecordTitleCellFieldDisplay = () => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
if (!isFieldText(fieldDefinition) && !isFieldFullName(fieldDefinition)) {
|
||||
throw new Error('Field definition is not a text or full name field');
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFieldText(fieldDefinition) ? (
|
||||
<RecordTitleCellSingleTextDisplayMode />
|
||||
) : isFieldFullName(fieldDefinition) ? (
|
||||
<RecordTitleFullNameFieldDisplay />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,66 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
|
||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
||||
import { RecordTitleCellTextFieldInput } from '@/object-record/record-title-cell/components/RecordTitleCellTextFieldInput';
|
||||
import { RecordTitleFullNameFieldInput } from '@/object-record/record-title-cell/components/RecordTitleFullNameFieldInput';
|
||||
|
||||
type RecordTitleCellFieldInputProps = {
|
||||
recordFieldInputId: string;
|
||||
onClickOutside?: (
|
||||
persist: () => void,
|
||||
event: MouseEvent | TouchEvent,
|
||||
) => void;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
sizeVariant?: 'sm' | 'md';
|
||||
};
|
||||
|
||||
export const RecordTitleCellFieldInput = ({
|
||||
sizeVariant,
|
||||
recordFieldInputId,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onShiftTab,
|
||||
onTab,
|
||||
onClickOutside,
|
||||
}: RecordTitleCellFieldInputProps) => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
if (!isFieldText(fieldDefinition) && !isFieldFullName(fieldDefinition)) {
|
||||
throw new Error('Field definition is not a text or full name field');
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordFieldInputScope
|
||||
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputId)}
|
||||
>
|
||||
{isFieldText(fieldDefinition) ? (
|
||||
<RecordTitleCellTextFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
sizeVariant={sizeVariant}
|
||||
/>
|
||||
) : isFieldFullName(fieldDefinition) ? (
|
||||
<RecordTitleFullNameFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
sizeVariant={sizeVariant}
|
||||
/>
|
||||
) : null}
|
||||
</RecordFieldInputScope>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||
import { useRecordValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui';
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
align-items: center;
|
||||
background: inherit;
|
||||
border: none;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
|
||||
export const RecordTitleCellSingleTextDisplayMode = () => {
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const recordValue = useRecordValue(recordId);
|
||||
|
||||
const { openInlineCell } = useInlineCell();
|
||||
|
||||
return (
|
||||
<StyledDiv onClick={() => openInlineCell()}>
|
||||
<OverflowingTextWithTooltip
|
||||
text={
|
||||
recordValue?.[fieldDefinition.metadata.fieldName] ||
|
||||
fieldDefinition.label
|
||||
}
|
||||
/>
|
||||
</StyledDiv>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,78 @@
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { useTextField } from '@/object-record/record-field/meta-types/hooks/useTextField';
|
||||
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
|
||||
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
import { useRef } from 'react';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
|
||||
|
||||
type RecordTitleCellTextFieldInputProps = {
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
sizeVariant?: 'sm' | 'md';
|
||||
};
|
||||
|
||||
export const RecordTitleCellTextFieldInput = ({
|
||||
sizeVariant,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: RecordTitleCellTextFieldInputProps) => {
|
||||
const { fieldDefinition, draftValue, hotkeyScope, setDraftValue } =
|
||||
useTextField();
|
||||
|
||||
const wrapperRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleChange = (newText: string) => {
|
||||
setDraftValue(turnIntoUndefinedIfWhitespacesOnly(newText));
|
||||
};
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
useRegisterInputEvents<string>({
|
||||
inputRef: wrapperRef,
|
||||
inputValue: draftValue ?? '',
|
||||
onEnter: (inputValue) => {
|
||||
onEnter?.(() => persistField(inputValue));
|
||||
},
|
||||
onEscape: (inputValue) => {
|
||||
onEscape?.(() => persistField(inputValue));
|
||||
},
|
||||
onClickOutside: (event, inputValue) => {
|
||||
onClickOutside?.(() => persistField(inputValue), event);
|
||||
},
|
||||
onTab: (inputValue) => {
|
||||
onTab?.(() => persistField(inputValue));
|
||||
},
|
||||
onShiftTab: (inputValue) => {
|
||||
onShiftTab?.(() => persistField(inputValue));
|
||||
},
|
||||
hotkeyScope,
|
||||
});
|
||||
|
||||
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (isDefined(draftValue)) {
|
||||
event.target.select();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInputV2
|
||||
autoGrow
|
||||
sizeVariant={sizeVariant}
|
||||
inheritFontStyles
|
||||
value={draftValue ?? ''}
|
||||
onChange={handleChange}
|
||||
placeholder={fieldDefinition.label}
|
||||
onFocus={handleFocus}
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,235 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { ClipboardEvent, useEffect, useRef, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText';
|
||||
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 { isDefined } from 'twenty-shared';
|
||||
import { splitFullName } from '~/utils/format/spiltFullName';
|
||||
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: inherit;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTextInputWrapper = styled.div`
|
||||
max-width: 50%;
|
||||
`;
|
||||
|
||||
type RecordTitleDoubleTextInputProps = {
|
||||
firstValue: string;
|
||||
secondValue: string;
|
||||
firstValuePlaceholder: string;
|
||||
secondValuePlaceholder: string;
|
||||
hotkeyScope: string;
|
||||
onEnter: (newDoubleTextValue: FieldDoubleText) => void;
|
||||
onEscape: (newDoubleTextValue: FieldDoubleText) => void;
|
||||
onTab?: (newDoubleTextValue: FieldDoubleText) => void;
|
||||
onShiftTab?: (newDoubleTextValue: FieldDoubleText) => void;
|
||||
onClickOutside: (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newDoubleTextValue: FieldDoubleText,
|
||||
) => void;
|
||||
onChange?: (newDoubleTextValue: FieldDoubleText) => void;
|
||||
onPaste?: (newDoubleTextValue: FieldDoubleText) => void;
|
||||
sizeVariant?: 'sm' | 'md';
|
||||
};
|
||||
|
||||
export const RecordTitleDoubleTextInput = ({
|
||||
firstValue,
|
||||
secondValue,
|
||||
firstValuePlaceholder,
|
||||
secondValuePlaceholder,
|
||||
hotkeyScope,
|
||||
onClickOutside,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onShiftTab,
|
||||
onTab,
|
||||
onChange,
|
||||
onPaste,
|
||||
sizeVariant,
|
||||
}: RecordTitleDoubleTextInputProps) => {
|
||||
const [firstInternalValue, setFirstInternalValue] = useState(firstValue);
|
||||
const [secondInternalValue, setSecondInternalValue] = useState(secondValue);
|
||||
|
||||
const firstValueInputRef = useRef<HTMLInputElement>(null);
|
||||
const secondValueInputRef = useRef<HTMLInputElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setFirstInternalValue(firstValue);
|
||||
setSecondInternalValue(secondValue);
|
||||
}, [firstValue, secondValue]);
|
||||
|
||||
const handleChange = (
|
||||
newFirstValue: string,
|
||||
newSecondValue: string,
|
||||
): void => {
|
||||
setFirstInternalValue(newFirstValue);
|
||||
setSecondInternalValue(newSecondValue);
|
||||
|
||||
onChange?.({
|
||||
firstValue: newFirstValue,
|
||||
secondValue: newSecondValue,
|
||||
});
|
||||
};
|
||||
|
||||
const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left');
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
() => {
|
||||
onEnter({
|
||||
firstValue: firstInternalValue,
|
||||
secondValue: secondInternalValue,
|
||||
});
|
||||
},
|
||||
hotkeyScope,
|
||||
[onEnter, firstInternalValue, secondInternalValue],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
onEscape({
|
||||
firstValue: firstInternalValue,
|
||||
secondValue: secondInternalValue,
|
||||
});
|
||||
},
|
||||
hotkeyScope,
|
||||
[onEscape, firstInternalValue, secondInternalValue],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'tab',
|
||||
() => {
|
||||
if (focusPosition === 'left') {
|
||||
setFocusPosition('right');
|
||||
secondValueInputRef.current?.focus();
|
||||
} else {
|
||||
onTab?.({
|
||||
firstValue: firstInternalValue,
|
||||
secondValue: secondInternalValue,
|
||||
});
|
||||
}
|
||||
},
|
||||
hotkeyScope,
|
||||
[onTab, firstInternalValue, secondInternalValue, focusPosition],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'shift+tab',
|
||||
() => {
|
||||
if (focusPosition === 'right') {
|
||||
setFocusPosition('left');
|
||||
firstValueInputRef.current?.focus();
|
||||
} else {
|
||||
onShiftTab?.({
|
||||
firstValue: firstInternalValue,
|
||||
secondValue: secondInternalValue,
|
||||
});
|
||||
}
|
||||
},
|
||||
hotkeyScope,
|
||||
[onShiftTab, firstInternalValue, secondInternalValue, focusPosition],
|
||||
);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
callback: (event) => {
|
||||
onClickOutside?.(event, {
|
||||
firstValue: firstInternalValue,
|
||||
secondValue: secondInternalValue,
|
||||
});
|
||||
},
|
||||
enabled: isDefined(onClickOutside),
|
||||
listenerId: 'record-title-double-text-input',
|
||||
});
|
||||
|
||||
const handleOnPaste = (event: ClipboardEvent<HTMLInputElement>) => {
|
||||
if (firstInternalValue.length > 0 || secondInternalValue.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const name = event.clipboardData.getData('Text');
|
||||
|
||||
const splittedName = splitFullName(name);
|
||||
|
||||
onPaste?.({
|
||||
firstValue: splittedName[0],
|
||||
secondValue: splittedName[1],
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickToPreventParentClickEvents = (
|
||||
event: React.MouseEvent<HTMLInputElement>,
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer ref={containerRef}>
|
||||
<StyledTextInputWrapper>
|
||||
<TextInputV2
|
||||
autoGrow
|
||||
sizeVariant={sizeVariant}
|
||||
autoComplete="off"
|
||||
inheritFontStyles
|
||||
autoFocus
|
||||
onFocus={(event: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (isDefined(firstInternalValue)) {
|
||||
event.target.select();
|
||||
}
|
||||
setFocusPosition('left');
|
||||
}}
|
||||
ref={firstValueInputRef}
|
||||
placeholder={firstValuePlaceholder}
|
||||
value={firstInternalValue}
|
||||
onChange={(text: string) => {
|
||||
handleChange(
|
||||
turnIntoEmptyStringIfWhitespacesOnly(text),
|
||||
secondInternalValue,
|
||||
);
|
||||
}}
|
||||
onPaste={(event: ClipboardEvent<HTMLInputElement>) =>
|
||||
handleOnPaste(event)
|
||||
}
|
||||
onClick={handleClickToPreventParentClickEvents}
|
||||
/>
|
||||
</StyledTextInputWrapper>
|
||||
<StyledTextInputWrapper>
|
||||
<TextInputV2
|
||||
autoGrow
|
||||
sizeVariant={sizeVariant}
|
||||
autoComplete="off"
|
||||
inheritFontStyles
|
||||
onFocus={(event: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (isDefined(secondInternalValue)) {
|
||||
event.target.select();
|
||||
}
|
||||
setFocusPosition('right');
|
||||
}}
|
||||
ref={secondValueInputRef}
|
||||
placeholder={secondValuePlaceholder}
|
||||
value={secondInternalValue}
|
||||
onChange={(text: string) => {
|
||||
handleChange(
|
||||
firstInternalValue,
|
||||
turnIntoEmptyStringIfWhitespacesOnly(text),
|
||||
);
|
||||
}}
|
||||
onClick={handleClickToPreventParentClickEvents}
|
||||
/>
|
||||
</StyledTextInputWrapper>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useFullNameFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useFullNameFieldDisplay';
|
||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui';
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
align-items: center;
|
||||
background: inherit;
|
||||
border: none;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
|
||||
export const RecordTitleFullNameFieldDisplay = () => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const { openInlineCell } = useInlineCell();
|
||||
|
||||
const { fieldValue } = useFullNameFieldDisplay();
|
||||
|
||||
const content = [fieldValue?.firstName, fieldValue?.lastName]
|
||||
.filter(isNonEmptyString)
|
||||
.join(' ')
|
||||
.trim();
|
||||
|
||||
return (
|
||||
<StyledDiv onClick={() => openInlineCell()}>
|
||||
<OverflowingTextWithTooltip
|
||||
text={isNonEmptyString(content) ? content : fieldDefinition.label}
|
||||
/>
|
||||
</StyledDiv>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,102 @@
|
||||
import { useFullNameField } from '@/object-record/record-field/meta-types/hooks/useFullNameField';
|
||||
import {
|
||||
FieldInputClickOutsideEvent,
|
||||
FieldInputEvent,
|
||||
} from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
|
||||
import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder';
|
||||
import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder';
|
||||
import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty';
|
||||
import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText';
|
||||
import { RecordTitleDoubleTextInput } from './RecordTitleDoubleTextInput';
|
||||
|
||||
type RecordTitleFullNameFieldInputProps = {
|
||||
onClickOutside?: FieldInputClickOutsideEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
sizeVariant?: 'sm' | 'md';
|
||||
};
|
||||
|
||||
export const RecordTitleFullNameFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
sizeVariant,
|
||||
}: RecordTitleFullNameFieldInputProps) => {
|
||||
const { hotkeyScope, draftValue, setDraftValue, persistFullNameField } =
|
||||
useFullNameField();
|
||||
|
||||
const convertToFullName = (newDoubleText: FieldDoubleText) => {
|
||||
return {
|
||||
firstName: newDoubleText.firstValue.trim(),
|
||||
lastName: newDoubleText.secondValue.trim(),
|
||||
};
|
||||
};
|
||||
|
||||
const getRequiredDraftValueFromDoubleText = (
|
||||
newDoubleText: FieldDoubleText,
|
||||
) => {
|
||||
return isDoubleTextFieldEmpty(newDoubleText)
|
||||
? undefined
|
||||
: convertToFullName(newDoubleText);
|
||||
};
|
||||
|
||||
const handleEnter = (newDoubleText: FieldDoubleText) => {
|
||||
onEnter?.(() => persistFullNameField(convertToFullName(newDoubleText)));
|
||||
};
|
||||
|
||||
const handleEscape = (newDoubleText: FieldDoubleText) => {
|
||||
onEscape?.(() => persistFullNameField(convertToFullName(newDoubleText)));
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newDoubleText: FieldDoubleText,
|
||||
) => {
|
||||
onClickOutside?.(
|
||||
() => persistFullNameField(convertToFullName(newDoubleText)),
|
||||
event,
|
||||
);
|
||||
};
|
||||
|
||||
const handleTab = (newDoubleText: FieldDoubleText) => {
|
||||
onTab?.(() => persistFullNameField(convertToFullName(newDoubleText)));
|
||||
};
|
||||
|
||||
const handleShiftTab = (newDoubleText: FieldDoubleText) => {
|
||||
onShiftTab?.(() => persistFullNameField(convertToFullName(newDoubleText)));
|
||||
};
|
||||
|
||||
const handleChange = (newDoubleText: FieldDoubleText) => {
|
||||
setDraftValue(getRequiredDraftValueFromDoubleText(newDoubleText));
|
||||
};
|
||||
|
||||
const handlePaste = (newDoubleText: FieldDoubleText) => {
|
||||
setDraftValue(getRequiredDraftValueFromDoubleText(newDoubleText));
|
||||
};
|
||||
|
||||
return (
|
||||
<RecordTitleDoubleTextInput
|
||||
firstValue={draftValue?.firstName ?? ''}
|
||||
secondValue={draftValue?.lastName ?? ''}
|
||||
firstValuePlaceholder={
|
||||
FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS
|
||||
}
|
||||
secondValuePlaceholder={
|
||||
LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS
|
||||
}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
onPaste={handlePaste}
|
||||
hotkeyScope={hotkeyScope}
|
||||
onChange={handleChange}
|
||||
sizeVariant={sizeVariant}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,73 @@
|
||||
import { isInlineCellInEditModeScopedState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeScopedState';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const useRecordTitleCell = () => {
|
||||
const { goBackToPreviousDropdownFocusId } =
|
||||
useGoBackToPreviousDropdownFocusId();
|
||||
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const closeRecordTitleCell = useRecoilCallback(
|
||||
({ set }) =>
|
||||
({
|
||||
recordId,
|
||||
fieldMetadataId,
|
||||
}: {
|
||||
recordId: string;
|
||||
fieldMetadataId: string;
|
||||
}) => {
|
||||
set(
|
||||
isInlineCellInEditModeScopedState(recordId + fieldMetadataId),
|
||||
false,
|
||||
);
|
||||
|
||||
goBackToPreviousHotkeyScope();
|
||||
|
||||
goBackToPreviousDropdownFocusId();
|
||||
},
|
||||
[goBackToPreviousDropdownFocusId, goBackToPreviousHotkeyScope],
|
||||
);
|
||||
|
||||
const openRecordTitleCell = useRecoilCallback(
|
||||
({ set }) =>
|
||||
({
|
||||
recordId,
|
||||
fieldMetadataId,
|
||||
customEditHotkeyScopeForField,
|
||||
}: {
|
||||
recordId: string;
|
||||
fieldMetadataId: string;
|
||||
customEditHotkeyScopeForField?: HotkeyScope;
|
||||
}) => {
|
||||
set(
|
||||
isInlineCellInEditModeScopedState(recordId + fieldMetadataId),
|
||||
true,
|
||||
);
|
||||
|
||||
if (isDefined(customEditHotkeyScopeForField)) {
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
customEditHotkeyScopeForField.scope,
|
||||
customEditHotkeyScopeForField.customScopes,
|
||||
);
|
||||
} else {
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
InlineCellHotkeyScope.InlineCell,
|
||||
);
|
||||
}
|
||||
},
|
||||
[setHotkeyScopeAndMemorizePreviousScope],
|
||||
);
|
||||
|
||||
return {
|
||||
closeRecordTitleCell,
|
||||
openRecordTitleCell,
|
||||
};
|
||||
};
|
||||
@ -1,6 +0,0 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const isUpdatingRecordEditableNameState = createState<boolean>({
|
||||
key: 'isUpdatingRecordEditableNameState',
|
||||
defaultValue: false,
|
||||
});
|
||||
@ -1,10 +1,10 @@
|
||||
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,
|
||||
ForwardedRef,
|
||||
InputHTMLAttributes,
|
||||
forwardRef,
|
||||
useId,
|
||||
@ -19,7 +19,6 @@ import {
|
||||
} from 'twenty-ui';
|
||||
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
|
||||
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||
import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper';
|
||||
|
||||
const StyledContainer = styled.div<
|
||||
Pick<TextInputV2ComponentProps, 'fullWidth'>
|
||||
@ -40,7 +39,12 @@ const StyledInputContainer = styled.div`
|
||||
const StyledInput = styled.input<
|
||||
Pick<
|
||||
TextInputV2ComponentProps,
|
||||
'LeftIcon' | 'error' | 'sizeVariant' | 'width'
|
||||
| 'LeftIcon'
|
||||
| 'error'
|
||||
| 'sizeVariant'
|
||||
| 'width'
|
||||
| 'inheritFontStyles'
|
||||
| 'autoGrow'
|
||||
>
|
||||
>`
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
@ -52,17 +56,30 @@ const StyledInput = styled.input<
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
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 === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'};
|
||||
outline: none;
|
||||
padding: ${({ theme, sizeVariant }) =>
|
||||
sizeVariant === 'sm' ? `${theme.spacing(2)} 0` : theme.spacing(2)};
|
||||
padding-left: ${({ theme, LeftIcon }) =>
|
||||
LeftIcon ? `calc(${theme.spacing(3)} + 16px)` : theme.spacing(2)};
|
||||
padding: ${({ theme, sizeVariant, autoGrow }) =>
|
||||
autoGrow
|
||||
? theme.spacing(1)
|
||||
: sizeVariant === 'sm'
|
||||
? `${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)};
|
||||
width: ${({ theme, width }) =>
|
||||
width ? `calc(${width}px + ${theme.spacing(5)})` : '100%'};
|
||||
width ? `calc(${width}px + ${theme.spacing(0.5)})` : '100%'};
|
||||
max-width: ${({ autoGrow }) => (autoGrow ? '100%' : 'none')};
|
||||
|
||||
&::placeholder,
|
||||
&::-webkit-input-placeholder {
|
||||
@ -144,149 +161,180 @@ export type TextInputV2ComponentProps = Omit<
|
||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||
dataTestId?: string;
|
||||
sizeVariant?: TextInputV2Size;
|
||||
inheritFontStyles?: boolean;
|
||||
};
|
||||
|
||||
type TextInputV2WithAutoGrowWrapperProps = TextInputV2ComponentProps;
|
||||
|
||||
const TextInputV2Component = (
|
||||
{
|
||||
className,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
fullWidth,
|
||||
width,
|
||||
error,
|
||||
noErrorHelper = false,
|
||||
required,
|
||||
type,
|
||||
autoFocus,
|
||||
placeholder,
|
||||
disabled,
|
||||
tabIndex,
|
||||
RightIcon,
|
||||
LeftIcon,
|
||||
autoComplete,
|
||||
maxLength,
|
||||
sizeVariant = 'lg',
|
||||
dataTestId,
|
||||
}: TextInputV2ComponentProps,
|
||||
// eslint-disable-next-line @nx/workspace-component-props-naming
|
||||
ref: ForwardedRef<HTMLInputElement>,
|
||||
): JSX.Element => {
|
||||
const theme = useTheme();
|
||||
const TextInputV2Component = forwardRef<
|
||||
HTMLInputElement,
|
||||
TextInputV2ComponentProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
fullWidth,
|
||||
width,
|
||||
error,
|
||||
noErrorHelper = false,
|
||||
required,
|
||||
type,
|
||||
autoFocus,
|
||||
placeholder,
|
||||
disabled,
|
||||
tabIndex,
|
||||
RightIcon,
|
||||
LeftIcon,
|
||||
autoComplete,
|
||||
maxLength,
|
||||
sizeVariant = 'md',
|
||||
inheritFontStyles = false,
|
||||
dataTestId,
|
||||
autoGrow = false,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const combinedRef = useCombinedRefs(ref, inputRef);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const combinedRef = useCombinedRefs(ref, inputRef);
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const handleTogglePasswordVisibility = () => {
|
||||
setPasswordVisible(!passwordVisible);
|
||||
};
|
||||
|
||||
const handleTogglePasswordVisibility = () => {
|
||||
setPasswordVisible(!passwordVisible);
|
||||
};
|
||||
const handleFocus: FocusEventHandler<HTMLInputElement> = (event) => {
|
||||
setIsFocused(true);
|
||||
onFocus?.(event);
|
||||
};
|
||||
|
||||
const handleFocus: FocusEventHandler<HTMLInputElement> = (event) => {
|
||||
setIsFocused(true);
|
||||
onFocus?.(event);
|
||||
};
|
||||
const handleBlur: FocusEventHandler<HTMLInputElement> = (event) => {
|
||||
setIsFocused(false);
|
||||
onBlur?.(event);
|
||||
};
|
||||
|
||||
const handleBlur: FocusEventHandler<HTMLInputElement> = (event) => {
|
||||
setIsFocused(false);
|
||||
onBlur?.(event);
|
||||
};
|
||||
const inputId = useId();
|
||||
|
||||
const inputId = useId();
|
||||
|
||||
return (
|
||||
<StyledContainer className={className} fullWidth={fullWidth ?? false}>
|
||||
{label && (
|
||||
<InputLabel htmlFor={inputId}>
|
||||
{label + (required ? '*' : '')}
|
||||
</InputLabel>
|
||||
)}
|
||||
<StyledInputContainer>
|
||||
{!!LeftIcon && (
|
||||
<StyledLeftIconContainer sizeVariant={sizeVariant}>
|
||||
<StyledTrailingIcon isFocused={isFocused}>
|
||||
<LeftIcon size={theme.icon.size.md} />
|
||||
</StyledTrailingIcon>
|
||||
</StyledLeftIconContainer>
|
||||
return (
|
||||
<StyledContainer className={className} fullWidth={fullWidth ?? false}>
|
||||
{label && (
|
||||
<InputLabel htmlFor={inputId}>
|
||||
{label + (required ? '*' : '')}
|
||||
</InputLabel>
|
||||
)}
|
||||
|
||||
<StyledInput
|
||||
id={inputId}
|
||||
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,
|
||||
placeholder,
|
||||
required,
|
||||
value,
|
||||
LeftIcon,
|
||||
maxLength,
|
||||
error,
|
||||
sizeVariant,
|
||||
}}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<StyledInputContainer>
|
||||
{!!LeftIcon && (
|
||||
<StyledLeftIconContainer sizeVariant={sizeVariant}>
|
||||
<StyledTrailingIcon isFocused={isFocused}>
|
||||
<LeftIcon size={theme.icon.size.md} />
|
||||
</StyledTrailingIcon>
|
||||
</StyledLeftIconContainer>
|
||||
)}
|
||||
{!error && type !== INPUT_TYPE_PASSWORD && !!RightIcon && (
|
||||
<StyledTrailingIcon>
|
||||
<RightIcon size={theme.icon.size.md} />
|
||||
</StyledTrailingIcon>
|
||||
)}
|
||||
</StyledTrailingIconContainer>
|
||||
</StyledInputContainer>
|
||||
<InputErrorHelper isVisible={!noErrorHelper}>{error}</InputErrorHelper>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const TextInputV2WithAutoGrowWrapper = (
|
||||
props: TextInputV2WithAutoGrowWrapperProps,
|
||||
) => (
|
||||
<>
|
||||
{props.autoGrow ? (
|
||||
<ComputeNodeDimensions node={props.value || props.placeholder}>
|
||||
{(nodeDimensions) => (
|
||||
// eslint-disable-next-line
|
||||
<TextInputV2Component {...props} width={nodeDimensions?.width} />
|
||||
)}
|
||||
</ComputeNodeDimensions>
|
||||
) : (
|
||||
// eslint-disable-next-line
|
||||
<TextInputV2Component {...props} />
|
||||
)}
|
||||
</>
|
||||
<StyledInput
|
||||
id={inputId}
|
||||
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,
|
||||
placeholder,
|
||||
required,
|
||||
value,
|
||||
LeftIcon,
|
||||
maxLength,
|
||||
error,
|
||||
sizeVariant,
|
||||
inheritFontStyles,
|
||||
autoGrow,
|
||||
}}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<RightIcon size={theme.icon.size.md} />
|
||||
</StyledTrailingIcon>
|
||||
)}
|
||||
</StyledTrailingIconContainer>
|
||||
</StyledInputContainer>
|
||||
<InputErrorHelper isVisible={!noErrorHelper}>{error}</InputErrorHelper>
|
||||
</StyledContainer>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const TextInputV2 = forwardRef(TextInputV2WithAutoGrowWrapper);
|
||||
const StyledComputeNodeDimensions = styled(ComputeNodeDimensions)<{
|
||||
sizeVariant?: TextInputV2Size;
|
||||
}>`
|
||||
border: 1px solid transparent;
|
||||
height: ${({ sizeVariant }) =>
|
||||
sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'};
|
||||
padding: 0 ${({ theme }) => theme.spacing(1)};
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
const TextInputV2WithAutoGrowWrapper = forwardRef<
|
||||
HTMLInputElement,
|
||||
TextInputV2WithAutoGrowWrapperProps
|
||||
>((props, ref) => {
|
||||
return (
|
||||
<>
|
||||
{props.autoGrow ? (
|
||||
<StyledComputeNodeDimensions
|
||||
sizeVariant={props.sizeVariant}
|
||||
node={props.value || props.placeholder}
|
||||
>
|
||||
{(nodeDimensions) => (
|
||||
<TextInputV2Component
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
ref={ref}
|
||||
width={nodeDimensions?.width}
|
||||
/>
|
||||
)}
|
||||
</StyledComputeNodeDimensions>
|
||||
) : (
|
||||
<TextInputV2Component
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const TextInputV2 = TextInputV2WithAutoGrowWrapper;
|
||||
|
||||
@ -49,7 +49,7 @@ const StyledLeftContainer = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
overflow-x: hidden;
|
||||
|
||||
width: 100%;
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
}
|
||||
@ -60,6 +60,8 @@ const StyledTitleContainer = styled.div`
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledTopBarIconStyledTitleContainer = styled.div`
|
||||
@ -67,6 +69,8 @@ const StyledTopBarIconStyledTitleContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledPageActionContainer = styled.div`
|
||||
@ -81,10 +85,9 @@ const StyledTopBarButtonContainer = styled.div`
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
flex: 1 0 1;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
type PageHeaderProps = {
|
||||
|
||||
@ -62,7 +62,7 @@ const StyledTitle = styled.div<{ isMobile: boolean }>`
|
||||
font-size: ${({ theme }) => theme.font.size.xl};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
justify-content: ${({ isMobile }) => (isMobile ? 'flex-start' : 'center')};
|
||||
max-width: 90%;
|
||||
width: 90%;
|
||||
`;
|
||||
|
||||
const StyledAvatarWrapper = styled.div<{ isAvatarEditable: boolean }>`
|
||||
|
||||
@ -1,119 +0,0 @@
|
||||
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-shared';
|
||||
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>
|
||||
);
|
||||
};
|
||||
@ -1,68 +0,0 @@
|
||||
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();
|
||||
},
|
||||
};
|
||||
@ -1,19 +0,0 @@
|
||||
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 };
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
export enum EditableBreadcrumbItemHotkeyScope {
|
||||
EditableBreadcrumbItem = 'editable-breadcrumb-item',
|
||||
}
|
||||
Reference in New Issue
Block a user