From 82139958877cb9a9fdbdcabf7c890735f8007b6d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?=
<71827178+bosiraphael@users.noreply.github.com>
Date: Wed, 22 Jan 2025 14:44:10 +0100
Subject: [PATCH] Open showpage on workflow creation (#9714)
- Created an new component state
`isRecordEditableNameRenamingComponentState`
- Updated `useCreateNewTableRecord` to open the ShowPage on workflow
creation
- Refactored `RecordEditableName` and its components to remove the
useEffect (This was causing the recordName state to be updated after the
focus on `NavigationDrawerInput`, but we want the text so be selected
after the update).
- Introduced a new component `EditableBreadcrumbItem`
- Created an autosizing text input: This is done by a hack using a span
inside a div and the input position is set to absolute and takes the
size of the div. There are two problems that I didn't manage to fix:
If the text is too long, the title overflows, and the letter spacing is
different between the span and the input creating a small offset.
https://github.com/user-attachments/assets/4aa1e177-7458-4691-b0c8-96567b482206
New text input component:
https://github.com/user-attachments/assets/94565546-fe2b-457d-a1d8-907007e0e2ce
---
.../tests/workflow-creation.spec.ts | 19 +--
.../favorites/components/FavoritesFolders.tsx | 23 ++--
.../ObjectRecordShowPageBreadcrumb.tsx} | 58 +++------
.../hooks/useCreateNewTableRecords.ts | 83 +++++++++---
.../states/isUpdatingRecordEditableName.ts | 6 +
.../shouldRedirectToShowPageOnCreation.ts | 11 ++
.../ui/input/components/TextInputV2.tsx | 60 +++++++--
.../__stories__/TextInputV2.stories.tsx | 18 ++-
.../ui/layout/page/components/PageHeader.tsx | 19 ++-
.../components/EditableBreadcrumbItem.tsx | 120 ++++++++++++++++++
.../EditableBreadcrumbItem.stories.tsx | 68 ++++++++++
.../hooks/useOpenEditableBreadCrumbItem.ts | 19 +++
.../EditableBreadcrumbItemHotkeyScope.ts | 3 +
.../components/NavigationDrawerInput.tsx | 87 +++----------
.../components/NavigationDrawerItemInput.tsx | 5 -
.../object-record/RecordShowPageHeader.tsx | 9 +-
16 files changed, 429 insertions(+), 179 deletions(-)
rename packages/twenty-front/src/modules/object-record/{components/RecordEditableName.tsx => record-show/components/ObjectRecordShowPageBreadcrumb.tsx} (53%)
create mode 100644 packages/twenty-front/src/modules/object-record/states/isUpdatingRecordEditableName.ts
create mode 100644 packages/twenty-front/src/modules/object-record/utils/shouldRedirectToShowPageOnCreation.ts
create mode 100644 packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/EditableBreadcrumbItem.tsx
create mode 100644 packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/__stories__/EditableBreadcrumbItem.stories.tsx
create mode 100644 packages/twenty-front/src/modules/ui/navigation/bread-crumb/hooks/useOpenEditableBreadCrumbItem.ts
create mode 100644 packages/twenty-front/src/modules/ui/navigation/bread-crumb/types/EditableBreadcrumbItemHotkeyScope.ts
delete mode 100644 packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemInput.tsx
diff --git a/packages/twenty-e2e-testing/tests/workflow-creation.spec.ts b/packages/twenty-e2e-testing/tests/workflow-creation.spec.ts
index 026dd5b7b..049837891 100644
--- a/packages/twenty-e2e-testing/tests/workflow-creation.spec.ts
+++ b/packages/twenty-e2e-testing/tests/workflow-creation.spec.ts
@@ -26,9 +26,6 @@ test('Create workflow', async ({ page }) => {
await createWorkflowButton.click(),
]);
- const nameInputClosedState = page.getByText('Name').first();
- await nameInputClosedState.click();
-
const nameInput = page.getByRole('textbox');
await nameInput.fill(NEW_WORKFLOW_NAME);
await nameInput.press('Enter');
@@ -37,23 +34,11 @@ test('Create workflow', async ({ page }) => {
const newWorkflowId = body.data.createWorkflow.id;
try {
- const newWorkflowRowEntryName = page
- .getByTestId(`row-id-${newWorkflowId}`)
- .locator('div')
- .filter({ hasText: NEW_WORKFLOW_NAME })
- .nth(2);
-
- await Promise.all([
- page.waitForURL(
- (url) => url.pathname === `/object/workflow/${newWorkflowId}`,
- ),
-
- newWorkflowRowEntryName.click(),
- ]);
-
const workflowName = page.getByRole('button', { name: NEW_WORKFLOW_NAME });
await expect(workflowName).toBeVisible();
+
+ await expect(page).toHaveURL(`/object/workflow/${newWorkflowId}`);
} finally {
await deleteWorkflow({
page,
diff --git a/packages/twenty-front/src/modules/favorites/components/FavoritesFolders.tsx b/packages/twenty-front/src/modules/favorites/components/FavoritesFolders.tsx
index 3fd41f5ef..fb7c53603 100644
--- a/packages/twenty-front/src/modules/favorites/components/FavoritesFolders.tsx
+++ b/packages/twenty-front/src/modules/favorites/components/FavoritesFolders.tsx
@@ -3,6 +3,7 @@ import { FavoriteFolderHotkeyScope } from '@/favorites/constants/FavoriteFolderR
import { useCreateFavoriteFolder } from '@/favorites/hooks/useCreateFavoriteFolder';
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState';
+import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
@@ -62,15 +63,19 @@ export const FavoriteFolders = ({
return (
<>
{isFavoriteFolderCreating && (
-
+
+
+
)}
{favoritesByFolder.map((folder) => (
theme.font.color.tertiary};
- line-height: 24px;
display: flex;
+ flex: 1 0 auto;
flex-direction: row;
- padding: ${({ theme }) => theme.spacing(0.75)};
gap: ${({ theme }) => theme.spacing(1)};
+ padding: ${({ theme }) => theme.spacing(0.75)};
`;
-export const RecordEditableName = ({
+export const ObjectRecordShowPageBreadcrumb = ({
objectNameSingular,
objectRecordId,
objectLabelPlural,
+ labelIdentifierFieldMetadataItem,
}: {
objectNameSingular: string;
objectRecordId: string;
objectLabelPlural: string;
+ labelIdentifierFieldMetadataItem?: FieldMetadataItem;
}) => {
- const [isRenaming, setIsRenaming] = useState(false);
const { record, loading } = useFindOneRecord({
objectNameSingular,
objectRecordId,
recordGqlFields: {
- name: true,
+ [labelIdentifierFieldMetadataItem?.name ?? 'name']: true,
},
});
- const [recordName, setRecordName] = useState(record?.name);
-
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular,
recordGqlFields: {
- name: true,
+ [labelIdentifierFieldMetadataItem?.name ?? 'name']: true,
},
});
@@ -55,18 +54,8 @@ export const RecordEditableName = ({
name: value,
},
});
- setIsRenaming(false);
};
- const handleCancel = () => {
- setRecordName(record?.name);
- setIsRenaming(false);
- };
-
- useEffect(() => {
- setRecordName(record?.name);
- }, [record?.name]);
-
if (loading) {
return null;
}
@@ -77,24 +66,13 @@ export const RecordEditableName = ({
{capitalize(objectLabelPlural)}
{' / '}
- {isRenaming ? (
-
- ) : (
- setIsRenaming(true)}
- rightOptions={undefined}
- className="navigation-drawer-item"
- active
- />
- )}
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts
index a5718cb27..189385d73 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts
@@ -5,7 +5,10 @@ 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 { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
+import { shouldRedirectToShowPageOnCreation } from '@/object-record/utils/shouldRedirectToShowPageOnCreation';
+import { AppPath } from '@/types/AppPath';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
@@ -14,6 +17,7 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { FeatureFlagKey } from '~/generated/graphql';
+import { useNavigateApp } from '~/hooks/useNavigateApp';
import { isDefined } from '~/utils/isDefined';
export const useCreateNewTableRecord = ({
@@ -54,30 +58,69 @@ export const useCreateNewTableRecord = ({
shouldMatchRootQueryFilter: true,
});
- const createNewTableRecord = async () => {
- const recordId = v4();
+ const navigate = useNavigateApp();
- if (isCommandMenuV2Enabled) {
- await createOneRecord({ id: recordId });
+ const createNewTableRecord = useRecoilCallback(
+ ({ set }) =>
+ async () => {
+ const recordId = v4();
- openRecordInCommandMenu(recordId, objectMetadataItem.nameSingular);
- return;
- }
+ 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',
+ });
- setPendingRecordId(recordId);
- setSelectedTableCellEditMode(-1, 0);
- setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes);
+ navigate(AppPath.RecordShowPage, {
+ objectNameSingular: objectMetadataItem.nameSingular,
+ objectRecordId: recordId,
+ });
- if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
- setActiveDropdownFocusIdAndMemorizePrevious(
- getDropdownFocusIdForRecordField(
- recordId,
- objectMetadataItem.labelIdentifierFieldMetadataId,
- 'table-cell',
- ),
- );
- }
- };
+ set(isUpdatingRecordEditableNameState, true);
+ return;
+ }
+
+ await createOneRecord({ id: recordId });
+ openRecordInCommandMenu(recordId, objectMetadataItem.nameSingular);
+
+ 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',
+ ),
+ );
+ }
+ },
+ [
+ createOneRecord,
+ isCommandMenuV2Enabled,
+ navigate,
+ objectMetadataItem.labelIdentifierFieldMetadataId,
+ objectMetadataItem.nameSingular,
+ openRecordInCommandMenu,
+ setActiveDropdownFocusIdAndMemorizePrevious,
+ setHotkeyScope,
+ setPendingRecordId,
+ setSelectedTableCellEditMode,
+ ],
+ );
const createNewTableRecordInGroup = useRecoilCallback(
({ set }) =>
diff --git a/packages/twenty-front/src/modules/object-record/states/isUpdatingRecordEditableName.ts b/packages/twenty-front/src/modules/object-record/states/isUpdatingRecordEditableName.ts
new file mode 100644
index 000000000..5829911a5
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/states/isUpdatingRecordEditableName.ts
@@ -0,0 +1,6 @@
+import { createState } from 'twenty-ui';
+
+export const isUpdatingRecordEditableNameState = createState({
+ key: 'isUpdatingRecordEditableNameState',
+ defaultValue: false,
+});
diff --git a/packages/twenty-front/src/modules/object-record/utils/shouldRedirectToShowPageOnCreation.ts b/packages/twenty-front/src/modules/object-record/utils/shouldRedirectToShowPageOnCreation.ts
new file mode 100644
index 000000000..d9a98b5ff
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/utils/shouldRedirectToShowPageOnCreation.ts
@@ -0,0 +1,11 @@
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+
+export const shouldRedirectToShowPageOnCreation = (
+ objectNameSingular: string,
+) => {
+ if (objectNameSingular === CoreObjectNameSingular.Workflow) {
+ return true;
+ }
+
+ return false;
+};
diff --git a/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx b/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx
index d770d8a1b..f3a371084 100644
--- a/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx
@@ -1,3 +1,4 @@
+import { InputLabel } from '@/ui/input/components/InputLabel';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
@@ -10,28 +11,37 @@ import {
useRef,
useState,
} from 'react';
-import { IconComponent, IconEye, IconEyeOff, RGBA } from 'twenty-ui';
+import {
+ ComputeNodeDimensions,
+ IconComponent,
+ IconEye,
+ IconEyeOff,
+ RGBA,
+} from 'twenty-ui';
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
-import { InputLabel } from './InputLabel';
const StyledContainer = styled.div<
Pick
>`
+ box-sizing: border-box;
display: inline-flex;
flex-direction: column;
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
`;
const StyledInputContainer = styled.div`
+ background-color: inherit;
display: flex;
flex-direction: row;
- width: 100%;
position: relative;
`;
const StyledInput = styled.input<
- Pick
+ Pick<
+ TextInputV2ComponentProps,
+ 'LeftIcon' | 'error' | 'sizeVariant' | 'width'
+ >
>`
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid
@@ -44,12 +54,14 @@ const StyledInput = styled.input<
flex-grow: 1;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.regular};
- height: 32px;
+ height: ${({ sizeVariant }) => (sizeVariant === 'sm' ? '20px' : '32px')};
outline: none;
- padding: ${({ theme }) => theme.spacing(2)};
+ padding: ${({ theme, sizeVariant }) =>
+ sizeVariant === 'sm' ? `${theme.spacing(2)} 0` : theme.spacing(2)};
padding-left: ${({ theme, LeftIcon }) =>
- LeftIcon ? `calc(${theme.spacing(4)} + 16px)` : theme.spacing(2)};
- width: 100%;
+ LeftIcon ? `px` : theme.spacing(2)};
+ width: ${({ theme, width }) =>
+ width ? `calc(${width}px + ${theme.spacing(5)})` : '100%'};
&::placeholder,
&::-webkit-input-placeholder {
@@ -111,6 +123,8 @@ const StyledTrailingIcon = styled.div`
const INPUT_TYPE_PASSWORD = 'password';
+export type TextInputV2Size = 'sm' | 'md';
+
export type TextInputV2ComponentProps = Omit<
InputHTMLAttributes,
'onChange' | 'onKeyDown'
@@ -123,11 +137,15 @@ export type TextInputV2ComponentProps = Omit<
noErrorHelper?: boolean;
RightIcon?: IconComponent;
LeftIcon?: IconComponent;
+ autoGrow?: boolean;
onKeyDown?: (event: React.KeyboardEvent) => void;
onBlur?: FocusEventHandler;
dataTestId?: string;
+ sizeVariant?: TextInputV2Size;
};
+type TextInputV2WithAutoGrowWrapperProps = TextInputV2ComponentProps;
+
const TextInputV2Component = (
{
className,
@@ -138,6 +156,7 @@ const TextInputV2Component = (
onBlur,
onKeyDown,
fullWidth,
+ width,
error,
noErrorHelper = false,
required,
@@ -150,6 +169,7 @@ const TextInputV2Component = (
LeftIcon,
autoComplete,
maxLength,
+ sizeVariant = 'md',
dataTestId,
}: TextInputV2ComponentProps,
// eslint-disable-next-line @nx/workspace-component-props-naming
@@ -183,8 +203,10 @@ const TextInputV2Component = (
)}
+
+
{!error && type === INPUT_TYPE_PASSWORD && (
(
+ <>
+ {props.autoGrow ? (
+
+ {(nodeDimensions) => (
+ // eslint-disable-next-line
+
+ )}
+
+ ) : (
+ // eslint-disable-next-line
+
+ )}
+ >
+);
+
+export const TextInputV2 = forwardRef(TextInputV2WithAutoGrowWrapper);
diff --git a/packages/twenty-front/src/modules/ui/input/components/__stories__/TextInputV2.stories.tsx b/packages/twenty-front/src/modules/ui/input/components/__stories__/TextInputV2.stories.tsx
index 8cd86abc2..e9ffe6d6d 100644
--- a/packages/twenty-front/src/modules/ui/input/components/__stories__/TextInputV2.stories.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/__stories__/TextInputV2.stories.tsx
@@ -1,5 +1,5 @@
-import { useState } from 'react';
import { Meta, StoryObj } from '@storybook/react';
+import { useState } from 'react';
import { ComponentDecorator } from 'twenty-ui';
import {
@@ -40,3 +40,19 @@ 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' },
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx
index 5e5da2122..d2acad3e8 100644
--- a/packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx
@@ -34,9 +34,9 @@ const StyledTopBarContainer = styled.div`
padding: ${({ theme }) => theme.spacing(2)};
padding-left: 0;
padding-right: ${({ theme }) => theme.spacing(3)};
+ gap: ${({ theme }) => theme.spacing(2)};
@media (max-width: ${MOBILE_VIEWPORT}px) {
- width: 100%;
box-sizing: border-box;
padding: ${({ theme }) => theme.spacing(3)};
}
@@ -48,7 +48,7 @@ const StyledLeftContainer = styled.div`
flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(1)};
- width: 100%;
+ overflow-x: hidden;
@media (max-width: ${MOBILE_VIEWPORT}px) {
padding-left: ${({ theme }) => theme.spacing(1)};
@@ -60,21 +60,19 @@ 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%;
`;
const StyledTopBarIconStyledTitleContainer = styled.div`
align-items: center;
display: flex;
- flex: 1 0 auto;
gap: ${({ theme }) => theme.spacing(1)};
flex-direction: row;
- width: 100%;
`;
const StyledPageActionContainer = styled.div`
display: inline-flex;
gap: ${({ theme }) => theme.spacing(2)};
+ flex: 1 0 1;
`;
const StyledTopBarButtonContainer = styled.div`
@@ -82,6 +80,13 @@ const StyledTopBarButtonContainer = styled.div`
margin-right: ${({ theme }) => theme.spacing(1)};
`;
+const StyledIconContainer = styled.div`
+ flex: 1 0 1;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+`;
+
type PageHeaderProps = {
title?: ReactNode;
hasClosePageButton?: boolean;
@@ -149,7 +154,9 @@ export const PageHeader = ({
/>
>
)}
- {Icon && }
+
+ {Icon && }
+
{title && (
{typeof title === 'string' ? (
diff --git a/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/EditableBreadcrumbItem.tsx b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/EditableBreadcrumbItem.tsx
new file mode 100644
index 000000000..b3956a044
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/EditableBreadcrumbItem.tsx
@@ -0,0 +1,120 @@
+import { isUpdatingRecordEditableNameState } from '@/object-record/states/isUpdatingRecordEditableName';
+import { TextInputV2 } from '@/ui/input/components/TextInputV2';
+import { useOpenEditableBreadCrumbItem } from '@/ui/navigation/bread-crumb/hooks/useOpenEditableBreadCrumbItem';
+import { EditableBreadcrumbItemHotkeyScope } from '@/ui/navigation/bread-crumb/types/EditableBreadcrumbItemHotkeyScope';
+import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
+import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
+import styled from '@emotion/styled';
+import { useRef, useState } from 'react';
+import { useRecoilState } from 'recoil';
+import { Key } from 'ts-key-enum';
+import { isDefined } from 'twenty-ui';
+import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
+
+type EditableBreadcrumbItemProps = {
+ className?: string;
+ defaultValue: string;
+ noValuePlaceholder?: string;
+ placeholder: string;
+ onSubmit: (value: string) => void;
+ hotkeyScope: string;
+};
+
+const StyledButton = styled('button')`
+ align-items: center;
+ background: inherit;
+ border: none;
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ box-sizing: content-box;
+ color: ${({ theme }) => theme.font.color.primary};
+ cursor: pointer;
+ display: flex;
+ font-family: ${({ theme }) => theme.font.family};
+ font-size: ${({ theme }) => theme.font.size.md};
+ height: 20px;
+ overflow: hidden;
+ text-decoration: none;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ :hover {
+ background: ${({ theme }) => theme.background.transparent.light};
+ }
+`;
+
+export const EditableBreadcrumbItem = ({
+ className,
+ defaultValue,
+ noValuePlaceholder,
+ placeholder,
+ onSubmit,
+}: EditableBreadcrumbItemProps) => {
+ const inputRef = useRef(null);
+ const buttonRef = useRef(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> = [
+ inputRef,
+ buttonRef,
+ ];
+
+ useListenClickOutside({
+ refs: clickOutsideRefs,
+ callback: () => {
+ setIsUpdatingRecordEditableName(false);
+ },
+ listenerId: 'editable-breadcrumb-item',
+ });
+
+ const handleFocus = (event: React.FocusEvent) => {
+ if (isDefined(value)) {
+ event.target.select();
+ }
+ };
+
+ const [value, setValue] = useState(defaultValue);
+
+ const { openEditableBreadCrumbItem } = useOpenEditableBreadCrumbItem();
+
+ return isUpdatingRecordEditableName ? (
+
+ ) : (
+
+ {value || noValuePlaceholder}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/__stories__/EditableBreadcrumbItem.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/__stories__/EditableBreadcrumbItem.stories.tsx
new file mode 100644
index 000000000..2a1ec4f9e
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/__stories__/EditableBreadcrumbItem.stories.tsx
@@ -0,0 +1,68 @@
+import { expect, jest } from '@storybook/jest';
+import { Meta, StoryObj } from '@storybook/react';
+import { RecoilRoot } from 'recoil';
+
+import { EditableBreadcrumbItemHotkeyScope } from '@/ui/navigation/bread-crumb/types/EditableBreadcrumbItemHotkeyScope';
+
+import { findByText, userEvent } from '@storybook/test';
+import { ComponentDecorator } from 'twenty-ui';
+import { EditableBreadcrumbItem } from '../EditableBreadcrumbItem';
+
+const onSubmit = jest.fn();
+
+const meta: Meta = {
+ title: 'UI/Navigation/BreadCrumb/EditableBreadcrumbItem',
+ component: EditableBreadcrumbItem,
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ComponentDecorator,
+ ],
+ args: {
+ defaultValue: 'Company Name',
+ placeholder: 'Enter name',
+ hotkeyScope: EditableBreadcrumbItemHotkeyScope.EditableBreadcrumbItem,
+ onSubmit,
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+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();
+ },
+};
diff --git a/packages/twenty-front/src/modules/ui/navigation/bread-crumb/hooks/useOpenEditableBreadCrumbItem.ts b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/hooks/useOpenEditableBreadCrumbItem.ts
new file mode 100644
index 000000000..e42dca770
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/hooks/useOpenEditableBreadCrumbItem.ts
@@ -0,0 +1,19 @@
+import { isUpdatingRecordEditableNameState } from '@/object-record/states/isUpdatingRecordEditableName';
+import { EditableBreadcrumbItemHotkeyScope } from '@/ui/navigation/bread-crumb/types/EditableBreadcrumbItemHotkeyScope';
+import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
+import { useSetRecoilState } from 'recoil';
+
+export const useOpenEditableBreadCrumbItem = () => {
+ const setIsUpdatingRecordEditableName = useSetRecoilState(
+ isUpdatingRecordEditableNameState,
+ );
+
+ const setHotkeyScope = useSetHotkeyScope();
+
+ const openEditableBreadCrumbItem = () => {
+ setIsUpdatingRecordEditableName(true);
+ setHotkeyScope(EditableBreadcrumbItemHotkeyScope.EditableBreadcrumbItem);
+ };
+
+ return { openEditableBreadCrumbItem };
+};
diff --git a/packages/twenty-front/src/modules/ui/navigation/bread-crumb/types/EditableBreadcrumbItemHotkeyScope.ts b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/types/EditableBreadcrumbItemHotkeyScope.ts
new file mode 100644
index 000000000..0bc4d86dd
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/types/EditableBreadcrumbItemHotkeyScope.ts
@@ -0,0 +1,3 @@
+export enum EditableBreadcrumbItemHotkeyScope {
+ EditableBreadcrumbItem = 'editable-breadcrumb-item',
+}
diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerInput.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerInput.tsx
index f76e5bcbf..0266bb39b 100644
--- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerInput.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerInput.tsx
@@ -1,23 +1,16 @@
-import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
-import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
+import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
-import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
-import { ChangeEvent, FocusEvent, useRef } from 'react';
-import { useRecoilState } from 'recoil';
+import { FocusEvent, useRef } from 'react';
import { Key } from 'ts-key-enum';
-import {
- IconComponent,
- isDefined,
- TablerIconsProps,
- TEXT_INPUT_STYLE,
-} from 'twenty-ui';
+import { IconComponent, TablerIconsProps, isDefined } from 'twenty-ui';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
type NavigationDrawerInputProps = {
className?: string;
Icon?: IconComponent | ((props: TablerIconsProps) => JSX.Element);
+ placeholder?: string;
value: string;
onChange: (value: string) => void;
onSubmit: (value: string) => void;
@@ -26,38 +19,13 @@ type NavigationDrawerInputProps = {
hotkeyScope: string;
};
-const StyledItem = styled.div<{ isNavigationDrawerExpanded: boolean }>`
- align-items: center;
- background-color: ${({ theme }) => theme.background.primary};
- border: 1px solid ${({ theme }) => theme.color.blue};
- border-radius: ${({ theme }) => theme.border.radius.sm};
- box-sizing: content-box;
- color: ${({ theme }) => theme.font.color.primary};
- display: flex;
- font-family: ${({ theme }) => theme.font.family};
- font-size: ${({ theme }) => theme.font.size.md};
- height: calc(${({ theme }) => theme.spacing(5)} - 2px);
- padding: ${({ theme }) => theme.spacing(1)};
- text-decoration: none;
- user-select: none;
-`;
-
-const StyledItemElementsContainer = styled.span`
- align-items: center;
- gap: ${({ theme }) => theme.spacing(2)};
- display: flex;
- width: 100%;
-`;
-
-const StyledTextInput = styled.input`
- ${TEXT_INPUT_STYLE}
- margin: 0;
- width: 100%;
- padding: 0;
+const StyledInput = styled(TextInputV2)`
+ background-color: white;
`;
export const NavigationDrawerInput = ({
className,
+ placeholder,
Icon,
value,
onChange,
@@ -66,10 +34,6 @@ export const NavigationDrawerInput = ({
onClickOutside,
hotkeyScope,
}: NavigationDrawerInputProps) => {
- const theme = useTheme();
- const [isNavigationDrawerExpanded] = useRecoilState(
- isNavigationDrawerExpandedState,
- );
const inputRef = useRef(null);
useHotkeyScopeOnMount(hotkeyScope);
@@ -99,10 +63,6 @@ export const NavigationDrawerInput = ({
listenerId: 'navigation-drawer-input',
});
- const handleChange = (event: ChangeEvent) => {
- onChange(event.target.value);
- };
-
const handleFocus = (event: FocusEvent) => {
if (isDefined(value)) {
event.target.select();
@@ -110,29 +70,16 @@ export const NavigationDrawerInput = ({
};
return (
-
-
- {Icon && (
-
- )}
-
-
-
-
-
+ LeftIcon={Icon}
+ ref={inputRef}
+ value={value}
+ onChange={onChange}
+ placeholder={placeholder}
+ onFocus={handleFocus}
+ fullWidth
+ autoFocus
+ />
);
};
diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemInput.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemInput.tsx
deleted file mode 100644
index 3e920f251..000000000
--- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemInput.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { TextInput } from '@/ui/input/components/TextInput';
-
-export const NavigationDrawerItemInput = () => {
- return ;
-};
diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPageHeader.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPageHeader.tsx
index ecaf938f9..37549ebc4 100644
--- a/packages/twenty-front/src/pages/object-record/RecordShowPageHeader.tsx
+++ b/packages/twenty-front/src/pages/object-record/RecordShowPageHeader.tsx
@@ -1,5 +1,6 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
-import { RecordEditableName } from '@/object-record/components/RecordEditableName';
+import { getObjectMetadataIdentifierFields } from '@/object-metadata/utils/getObjectMetadataIdentifierFields';
+import { ObjectRecordShowPageBreadcrumb } from '@/object-record/record-show/components/ObjectRecordShowPageBreadcrumb';
import { useRecordShowContainerTabs } from '@/object-record/record-show/hooks/useRecordShowContainerTabs';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
@@ -34,14 +35,18 @@ export const RecordShowPageHeader = ({
const hasEditableName = layout.hideSummaryAndFields === true;
+ const { labelIdentifierFieldMetadataItem } =
+ getObjectMetadataIdentifierFields({ objectMetadataItem });
+
return (
) : (
viewName