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