Open showpage on workflow creation (#9714)
- Created an new component state `isRecordEditableNameRenamingComponentState` - Updated `useCreateNewTableRecord` to open the ShowPage on workflow creation - Refactored `RecordEditableName` and its components to remove the useEffect (This was causing the recordName state to be updated after the focus on `NavigationDrawerInput`, but we want the text so be selected after the update). - Introduced a new component `EditableBreadcrumbItem` - Created an autosizing text input: This is done by a hack using a span inside a div and the input position is set to absolute and takes the size of the div. There are two problems that I didn't manage to fix: If the text is too long, the title overflows, and the letter spacing is different between the span and the input creating a small offset. https://github.com/user-attachments/assets/4aa1e177-7458-4691-b0c8-96567b482206 New text input component: https://github.com/user-attachments/assets/94565546-fe2b-457d-a1d8-907007e0e2ce
This commit is contained in:
@ -26,9 +26,6 @@ test('Create workflow', async ({ page }) => {
|
|||||||
await createWorkflowButton.click(),
|
await createWorkflowButton.click(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const nameInputClosedState = page.getByText('Name').first();
|
|
||||||
await nameInputClosedState.click();
|
|
||||||
|
|
||||||
const nameInput = page.getByRole('textbox');
|
const nameInput = page.getByRole('textbox');
|
||||||
await nameInput.fill(NEW_WORKFLOW_NAME);
|
await nameInput.fill(NEW_WORKFLOW_NAME);
|
||||||
await nameInput.press('Enter');
|
await nameInput.press('Enter');
|
||||||
@ -37,23 +34,11 @@ test('Create workflow', async ({ page }) => {
|
|||||||
const newWorkflowId = body.data.createWorkflow.id;
|
const newWorkflowId = body.data.createWorkflow.id;
|
||||||
|
|
||||||
try {
|
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 });
|
const workflowName = page.getByRole('button', { name: NEW_WORKFLOW_NAME });
|
||||||
|
|
||||||
await expect(workflowName).toBeVisible();
|
await expect(workflowName).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(`/object/workflow/${newWorkflowId}`);
|
||||||
} finally {
|
} finally {
|
||||||
await deleteWorkflow({
|
await deleteWorkflow({
|
||||||
page,
|
page,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { FavoriteFolderHotkeyScope } from '@/favorites/constants/FavoriteFolderR
|
|||||||
import { useCreateFavoriteFolder } from '@/favorites/hooks/useCreateFavoriteFolder';
|
import { useCreateFavoriteFolder } from '@/favorites/hooks/useCreateFavoriteFolder';
|
||||||
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
|
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
|
||||||
import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState';
|
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 { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
@ -62,15 +63,19 @@ export const FavoriteFolders = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isFavoriteFolderCreating && (
|
{isFavoriteFolderCreating && (
|
||||||
<NavigationDrawerInput
|
<NavigationDrawerAnimatedCollapseWrapper>
|
||||||
Icon={IconFolder}
|
<NavigationDrawerInput
|
||||||
value={newFolderName}
|
Icon={IconFolder}
|
||||||
onChange={handleFavoriteFolderNameChange}
|
value={newFolderName}
|
||||||
onSubmit={handleSubmitFavoriteFolderCreation}
|
onChange={handleFavoriteFolderNameChange}
|
||||||
onCancel={handleCancelFavoriteFolderCreation}
|
onSubmit={handleSubmitFavoriteFolderCreation}
|
||||||
onClickOutside={handleClickOutside}
|
onCancel={handleCancelFavoriteFolderCreation}
|
||||||
hotkeyScope={FavoriteFolderHotkeyScope.FavoriteFolderNavigationInput}
|
onClickOutside={handleClickOutside}
|
||||||
/>
|
hotkeyScope={
|
||||||
|
FavoriteFolderHotkeyScope.FavoriteFolderNavigationInput
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</NavigationDrawerAnimatedCollapseWrapper>
|
||||||
)}
|
)}
|
||||||
{favoritesByFolder.map((folder) => (
|
{favoritesByFolder.map((folder) => (
|
||||||
<CurrentWorkspaceMemberFavorites
|
<CurrentWorkspaceMemberFavorites
|
||||||
|
|||||||
@ -1,50 +1,49 @@
|
|||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
import { EditableBreadcrumbItem } from '@/ui/navigation/bread-crumb/components/EditableBreadcrumbItem';
|
||||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { capitalize } from 'twenty-shared';
|
import { capitalize } from 'twenty-shared';
|
||||||
|
|
||||||
const StyledEditableTitleContainer = styled.div`
|
const StyledEditableTitleContainer = styled.div`
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
overflow-x: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledEditableTitlePrefix = styled.div`
|
const StyledEditableTitlePrefix = styled.div`
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
line-height: 24px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1 0 auto;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
padding: ${({ theme }) => theme.spacing(0.75)};
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(0.75)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RecordEditableName = ({
|
export const ObjectRecordShowPageBreadcrumb = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
objectRecordId,
|
objectRecordId,
|
||||||
objectLabelPlural,
|
objectLabelPlural,
|
||||||
|
labelIdentifierFieldMetadataItem,
|
||||||
}: {
|
}: {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
objectRecordId: string;
|
objectRecordId: string;
|
||||||
objectLabelPlural: string;
|
objectLabelPlural: string;
|
||||||
|
labelIdentifierFieldMetadataItem?: FieldMetadataItem;
|
||||||
}) => {
|
}) => {
|
||||||
const [isRenaming, setIsRenaming] = useState(false);
|
|
||||||
const { record, loading } = useFindOneRecord({
|
const { record, loading } = useFindOneRecord({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
objectRecordId,
|
objectRecordId,
|
||||||
recordGqlFields: {
|
recordGqlFields: {
|
||||||
name: true,
|
[labelIdentifierFieldMetadataItem?.name ?? 'name']: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [recordName, setRecordName] = useState(record?.name);
|
|
||||||
|
|
||||||
const { updateOneRecord } = useUpdateOneRecord({
|
const { updateOneRecord } = useUpdateOneRecord({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordGqlFields: {
|
recordGqlFields: {
|
||||||
name: true,
|
[labelIdentifierFieldMetadataItem?.name ?? 'name']: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -55,18 +54,8 @@ export const RecordEditableName = ({
|
|||||||
name: value,
|
name: value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setIsRenaming(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setRecordName(record?.name);
|
|
||||||
setIsRenaming(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setRecordName(record?.name);
|
|
||||||
}, [record?.name]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -77,24 +66,13 @@ export const RecordEditableName = ({
|
|||||||
{capitalize(objectLabelPlural)}
|
{capitalize(objectLabelPlural)}
|
||||||
<span>{' / '}</span>
|
<span>{' / '}</span>
|
||||||
</StyledEditableTitlePrefix>
|
</StyledEditableTitlePrefix>
|
||||||
{isRenaming ? (
|
<EditableBreadcrumbItem
|
||||||
<NavigationDrawerInput
|
defaultValue={record?.name ?? ''}
|
||||||
value={recordName}
|
noValuePlaceholder={labelIdentifierFieldMetadataItem?.label ?? 'Name'}
|
||||||
onChange={setRecordName}
|
placeholder={labelIdentifierFieldMetadataItem?.label ?? 'Name'}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onCancel={handleCancel}
|
hotkeyScope="editable-breadcrumb-item"
|
||||||
onClickOutside={handleCancel}
|
/>
|
||||||
hotkeyScope="favorites-folder-input"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<NavigationDrawerItem
|
|
||||||
label={recordName}
|
|
||||||
onClick={() => setIsRenaming(true)}
|
|
||||||
rightOptions={undefined}
|
|
||||||
className="navigation-drawer-item"
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</StyledEditableTitleContainer>
|
</StyledEditableTitleContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -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 { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
|
||||||
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
|
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
|
||||||
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
|
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
|
||||||
|
import { isUpdatingRecordEditableNameState } from '@/object-record/states/isUpdatingRecordEditableName';
|
||||||
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
|
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 { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
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 { useRecoilCallback } from 'recoil';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { FeatureFlagKey } from '~/generated/graphql';
|
import { FeatureFlagKey } from '~/generated/graphql';
|
||||||
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const useCreateNewTableRecord = ({
|
export const useCreateNewTableRecord = ({
|
||||||
@ -54,30 +58,69 @@ export const useCreateNewTableRecord = ({
|
|||||||
shouldMatchRootQueryFilter: true,
|
shouldMatchRootQueryFilter: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createNewTableRecord = async () => {
|
const navigate = useNavigateApp();
|
||||||
const recordId = v4();
|
|
||||||
|
|
||||||
if (isCommandMenuV2Enabled) {
|
const createNewTableRecord = useRecoilCallback(
|
||||||
await createOneRecord({ id: recordId });
|
({ set }) =>
|
||||||
|
async () => {
|
||||||
|
const recordId = v4();
|
||||||
|
|
||||||
openRecordInCommandMenu(recordId, objectMetadataItem.nameSingular);
|
if (isCommandMenuV2Enabled) {
|
||||||
return;
|
// 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);
|
navigate(AppPath.RecordShowPage, {
|
||||||
setSelectedTableCellEditMode(-1, 0);
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes);
|
objectRecordId: recordId,
|
||||||
|
});
|
||||||
|
|
||||||
if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
|
set(isUpdatingRecordEditableNameState, true);
|
||||||
setActiveDropdownFocusIdAndMemorizePrevious(
|
return;
|
||||||
getDropdownFocusIdForRecordField(
|
}
|
||||||
recordId,
|
|
||||||
objectMetadataItem.labelIdentifierFieldMetadataId,
|
await createOneRecord({ id: recordId });
|
||||||
'table-cell',
|
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(
|
const createNewTableRecordInGroup = useRecoilCallback(
|
||||||
({ set }) =>
|
({ set }) =>
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const isUpdatingRecordEditableNameState = createState<boolean>({
|
||||||
|
key: 'isUpdatingRecordEditableNameState',
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
|
||||||
|
export const shouldRedirectToShowPageOnCreation = (
|
||||||
|
objectNameSingular: string,
|
||||||
|
) => {
|
||||||
|
if (objectNameSingular === CoreObjectNameSingular.Workflow) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {
|
import {
|
||||||
@ -10,28 +11,37 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} 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 { useCombinedRefs } from '~/hooks/useCombinedRefs';
|
||||||
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||||
import { InputLabel } from './InputLabel';
|
|
||||||
|
|
||||||
const StyledContainer = styled.div<
|
const StyledContainer = styled.div<
|
||||||
Pick<TextInputV2ComponentProps, 'fullWidth'>
|
Pick<TextInputV2ComponentProps, 'fullWidth'>
|
||||||
>`
|
>`
|
||||||
|
box-sizing: border-box;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
|
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledInputContainer = styled.div`
|
const StyledInputContainer = styled.div`
|
||||||
|
background-color: inherit;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledInput = styled.input<
|
const StyledInput = styled.input<
|
||||||
Pick<TextInputV2ComponentProps, 'fullWidth' | 'LeftIcon' | 'error'>
|
Pick<
|
||||||
|
TextInputV2ComponentProps,
|
||||||
|
'LeftIcon' | 'error' | 'sizeVariant' | 'width'
|
||||||
|
>
|
||||||
>`
|
>`
|
||||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
border: 1px solid
|
border: 1px solid
|
||||||
@ -44,12 +54,14 @@ const StyledInput = styled.input<
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-family: ${({ theme }) => theme.font.family};
|
font-family: ${({ theme }) => theme.font.family};
|
||||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
height: 32px;
|
height: ${({ sizeVariant }) => (sizeVariant === 'sm' ? '20px' : '32px')};
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
padding: ${({ theme, sizeVariant }) =>
|
||||||
|
sizeVariant === 'sm' ? `${theme.spacing(2)} 0` : theme.spacing(2)};
|
||||||
padding-left: ${({ theme, LeftIcon }) =>
|
padding-left: ${({ theme, LeftIcon }) =>
|
||||||
LeftIcon ? `calc(${theme.spacing(4)} + 16px)` : theme.spacing(2)};
|
LeftIcon ? `px` : theme.spacing(2)};
|
||||||
width: 100%;
|
width: ${({ theme, width }) =>
|
||||||
|
width ? `calc(${width}px + ${theme.spacing(5)})` : '100%'};
|
||||||
|
|
||||||
&::placeholder,
|
&::placeholder,
|
||||||
&::-webkit-input-placeholder {
|
&::-webkit-input-placeholder {
|
||||||
@ -111,6 +123,8 @@ const StyledTrailingIcon = styled.div`
|
|||||||
|
|
||||||
const INPUT_TYPE_PASSWORD = 'password';
|
const INPUT_TYPE_PASSWORD = 'password';
|
||||||
|
|
||||||
|
export type TextInputV2Size = 'sm' | 'md';
|
||||||
|
|
||||||
export type TextInputV2ComponentProps = Omit<
|
export type TextInputV2ComponentProps = Omit<
|
||||||
InputHTMLAttributes<HTMLInputElement>,
|
InputHTMLAttributes<HTMLInputElement>,
|
||||||
'onChange' | 'onKeyDown'
|
'onChange' | 'onKeyDown'
|
||||||
@ -123,11 +137,15 @@ export type TextInputV2ComponentProps = Omit<
|
|||||||
noErrorHelper?: boolean;
|
noErrorHelper?: boolean;
|
||||||
RightIcon?: IconComponent;
|
RightIcon?: IconComponent;
|
||||||
LeftIcon?: IconComponent;
|
LeftIcon?: IconComponent;
|
||||||
|
autoGrow?: boolean;
|
||||||
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||||
dataTestId?: string;
|
dataTestId?: string;
|
||||||
|
sizeVariant?: TextInputV2Size;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TextInputV2WithAutoGrowWrapperProps = TextInputV2ComponentProps;
|
||||||
|
|
||||||
const TextInputV2Component = (
|
const TextInputV2Component = (
|
||||||
{
|
{
|
||||||
className,
|
className,
|
||||||
@ -138,6 +156,7 @@ const TextInputV2Component = (
|
|||||||
onBlur,
|
onBlur,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
fullWidth,
|
fullWidth,
|
||||||
|
width,
|
||||||
error,
|
error,
|
||||||
noErrorHelper = false,
|
noErrorHelper = false,
|
||||||
required,
|
required,
|
||||||
@ -150,6 +169,7 @@ const TextInputV2Component = (
|
|||||||
LeftIcon,
|
LeftIcon,
|
||||||
autoComplete,
|
autoComplete,
|
||||||
maxLength,
|
maxLength,
|
||||||
|
sizeVariant = 'md',
|
||||||
dataTestId,
|
dataTestId,
|
||||||
}: TextInputV2ComponentProps,
|
}: TextInputV2ComponentProps,
|
||||||
// eslint-disable-next-line @nx/workspace-component-props-naming
|
// eslint-disable-next-line @nx/workspace-component-props-naming
|
||||||
@ -183,8 +203,10 @@ const TextInputV2Component = (
|
|||||||
</StyledTrailingIcon>
|
</StyledTrailingIcon>
|
||||||
</StyledLeftIconContainer>
|
</StyledLeftIconContainer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<StyledInput
|
<StyledInput
|
||||||
id={inputId}
|
id={inputId}
|
||||||
|
width={width}
|
||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
autoComplete={autoComplete || 'off'}
|
autoComplete={autoComplete || 'off'}
|
||||||
ref={combinedRef}
|
ref={combinedRef}
|
||||||
@ -207,8 +229,10 @@ const TextInputV2Component = (
|
|||||||
LeftIcon,
|
LeftIcon,
|
||||||
maxLength,
|
maxLength,
|
||||||
error,
|
error,
|
||||||
|
sizeVariant,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StyledTrailingIconContainer {...{ error }}>
|
<StyledTrailingIconContainer {...{ error }}>
|
||||||
{!error && type === INPUT_TYPE_PASSWORD && (
|
{!error && type === INPUT_TYPE_PASSWORD && (
|
||||||
<StyledTrailingIcon
|
<StyledTrailingIcon
|
||||||
@ -236,4 +260,22 @@ const TextInputV2Component = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextInputV2 = forwardRef(TextInputV2Component);
|
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} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TextInputV2 = forwardRef(TextInputV2WithAutoGrowWrapper);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { ComponentDecorator } from 'twenty-ui';
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -40,3 +40,19 @@ export const Filled: Story = {
|
|||||||
export const Disabled: Story = {
|
export const Disabled: Story = {
|
||||||
args: { disabled: true, value: 'Tim' },
|
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' },
|
||||||
|
};
|
||||||
|
|||||||
@ -34,9 +34,9 @@ const StyledTopBarContainer = styled.div`
|
|||||||
padding: ${({ theme }) => theme.spacing(2)};
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: ${({ theme }) => theme.spacing(3)};
|
padding-right: ${({ theme }) => theme.spacing(3)};
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
|
||||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: ${({ theme }) => theme.spacing(3)};
|
padding: ${({ theme }) => theme.spacing(3)};
|
||||||
}
|
}
|
||||||
@ -48,7 +48,7 @@ const StyledLeftContainer = styled.div`
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||||
width: 100%;
|
overflow-x: hidden;
|
||||||
|
|
||||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||||
@ -60,21 +60,19 @@ const StyledTitleContainer = styled.div`
|
|||||||
font-size: ${({ theme }) => theme.font.size.md};
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||||
width: 100%;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTopBarIconStyledTitleContainer = styled.div`
|
const StyledTopBarIconStyledTitleContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0 auto;
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: 100%;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledPageActionContainer = styled.div`
|
const StyledPageActionContainer = styled.div`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
flex: 1 0 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTopBarButtonContainer = styled.div`
|
const StyledTopBarButtonContainer = styled.div`
|
||||||
@ -82,6 +80,13 @@ const StyledTopBarButtonContainer = styled.div`
|
|||||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledIconContainer = styled.div`
|
||||||
|
flex: 1 0 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
type PageHeaderProps = {
|
type PageHeaderProps = {
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
hasClosePageButton?: boolean;
|
hasClosePageButton?: boolean;
|
||||||
@ -149,7 +154,9 @@ export const PageHeader = ({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{Icon && <Icon size={theme.icon.size.md} />}
|
<StyledIconContainer>
|
||||||
|
{Icon && <Icon size={theme.icon.size.md} />}
|
||||||
|
</StyledIconContainer>
|
||||||
{title && (
|
{title && (
|
||||||
<StyledTitleContainer data-testid="top-bar-title">
|
<StyledTitleContainer data-testid="top-bar-title">
|
||||||
{typeof title === 'string' ? (
|
{typeof title === 'string' ? (
|
||||||
|
|||||||
@ -0,0 +1,120 @@
|
|||||||
|
import { isUpdatingRecordEditableNameState } from '@/object-record/states/isUpdatingRecordEditableName';
|
||||||
|
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||||
|
import { useOpenEditableBreadCrumbItem } from '@/ui/navigation/bread-crumb/hooks/useOpenEditableBreadCrumbItem';
|
||||||
|
import { EditableBreadcrumbItemHotkeyScope } from '@/ui/navigation/bread-crumb/types/EditableBreadcrumbItemHotkeyScope';
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
|
||||||
|
|
||||||
|
type EditableBreadcrumbItemProps = {
|
||||||
|
className?: string;
|
||||||
|
defaultValue: string;
|
||||||
|
noValuePlaceholder?: string;
|
||||||
|
placeholder: string;
|
||||||
|
onSubmit: (value: string) => void;
|
||||||
|
hotkeyScope: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledButton = styled('button')`
|
||||||
|
align-items: center;
|
||||||
|
background: inherit;
|
||||||
|
border: none;
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
box-sizing: content-box;
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
font-family: ${({ theme }) => theme.font.family};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
height: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
background: ${({ theme }) => theme.background.transparent.light};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EditableBreadcrumbItem = ({
|
||||||
|
className,
|
||||||
|
defaultValue,
|
||||||
|
noValuePlaceholder,
|
||||||
|
placeholder,
|
||||||
|
onSubmit,
|
||||||
|
}: EditableBreadcrumbItemProps) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const [isUpdatingRecordEditableName, setIsUpdatingRecordEditableName] =
|
||||||
|
useRecoilState(isUpdatingRecordEditableNameState);
|
||||||
|
|
||||||
|
// TODO: remove this and set the hokey scopes synchronously on page change inside the useNavigateApp hook
|
||||||
|
useHotkeyScopeOnMount(
|
||||||
|
EditableBreadcrumbItemHotkeyScope.EditableBreadcrumbItem,
|
||||||
|
);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
[Key.Escape],
|
||||||
|
() => {
|
||||||
|
setIsUpdatingRecordEditableName(false);
|
||||||
|
},
|
||||||
|
EditableBreadcrumbItemHotkeyScope.EditableBreadcrumbItem,
|
||||||
|
);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
[Key.Enter],
|
||||||
|
() => {
|
||||||
|
onSubmit(value);
|
||||||
|
setIsUpdatingRecordEditableName(false);
|
||||||
|
},
|
||||||
|
EditableBreadcrumbItemHotkeyScope.EditableBreadcrumbItem,
|
||||||
|
);
|
||||||
|
|
||||||
|
const clickOutsideRefs: Array<React.RefObject<HTMLElement>> = [
|
||||||
|
inputRef,
|
||||||
|
buttonRef,
|
||||||
|
];
|
||||||
|
|
||||||
|
useListenClickOutside({
|
||||||
|
refs: clickOutsideRefs,
|
||||||
|
callback: () => {
|
||||||
|
setIsUpdatingRecordEditableName(false);
|
||||||
|
},
|
||||||
|
listenerId: 'editable-breadcrumb-item',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
if (isDefined(value)) {
|
||||||
|
event.target.select();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [value, setValue] = useState<string>(defaultValue);
|
||||||
|
|
||||||
|
const { openEditableBreadCrumbItem } = useOpenEditableBreadCrumbItem();
|
||||||
|
|
||||||
|
return isUpdatingRecordEditableName ? (
|
||||||
|
<TextInputV2
|
||||||
|
className={className}
|
||||||
|
autoGrow
|
||||||
|
sizeVariant="sm"
|
||||||
|
ref={inputRef}
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<StyledButton ref={buttonRef} onClick={openEditableBreadCrumbItem}>
|
||||||
|
{value || noValuePlaceholder}
|
||||||
|
</StyledButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import { expect, jest } from '@storybook/jest';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import { EditableBreadcrumbItemHotkeyScope } from '@/ui/navigation/bread-crumb/types/EditableBreadcrumbItemHotkeyScope';
|
||||||
|
|
||||||
|
import { findByText, userEvent } from '@storybook/test';
|
||||||
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
import { EditableBreadcrumbItem } from '../EditableBreadcrumbItem';
|
||||||
|
|
||||||
|
const onSubmit = jest.fn();
|
||||||
|
|
||||||
|
const meta: Meta<typeof EditableBreadcrumbItem> = {
|
||||||
|
title: 'UI/Navigation/BreadCrumb/EditableBreadcrumbItem',
|
||||||
|
component: EditableBreadcrumbItem,
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<RecoilRoot>
|
||||||
|
<Story />
|
||||||
|
</RecoilRoot>
|
||||||
|
),
|
||||||
|
ComponentDecorator,
|
||||||
|
],
|
||||||
|
args: {
|
||||||
|
defaultValue: 'Company Name',
|
||||||
|
placeholder: 'Enter name',
|
||||||
|
hotkeyScope: EditableBreadcrumbItemHotkeyScope.EditableBreadcrumbItem,
|
||||||
|
onSubmit,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof EditableBreadcrumbItem>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const button = await findByText(canvasElement, 'Company Name');
|
||||||
|
expect(button).toBeInTheDocument();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Editing: Story = {
|
||||||
|
args: {},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const button = canvasElement.querySelector('button');
|
||||||
|
await userEvent.click(button);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
await userEvent.keyboard('New Name');
|
||||||
|
await userEvent.keyboard('{Enter}');
|
||||||
|
|
||||||
|
expect(onSubmit).toHaveBeenCalledWith('New Name');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithNoValue: Story = {
|
||||||
|
args: {
|
||||||
|
defaultValue: '',
|
||||||
|
noValuePlaceholder: 'Untitled',
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const button = await findByText(canvasElement, 'Untitled');
|
||||||
|
|
||||||
|
expect(button).toBeInTheDocument();
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { isUpdatingRecordEditableNameState } from '@/object-record/states/isUpdatingRecordEditableName';
|
||||||
|
import { EditableBreadcrumbItemHotkeyScope } from '@/ui/navigation/bread-crumb/types/EditableBreadcrumbItemHotkeyScope';
|
||||||
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
export const useOpenEditableBreadCrumbItem = () => {
|
||||||
|
const setIsUpdatingRecordEditableName = useSetRecoilState(
|
||||||
|
isUpdatingRecordEditableNameState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
|
const openEditableBreadCrumbItem = () => {
|
||||||
|
setIsUpdatingRecordEditableName(true);
|
||||||
|
setHotkeyScope(EditableBreadcrumbItemHotkeyScope.EditableBreadcrumbItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { openEditableBreadCrumbItem };
|
||||||
|
};
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export enum EditableBreadcrumbItemHotkeyScope {
|
||||||
|
EditableBreadcrumbItem = 'editable-breadcrumb-item',
|
||||||
|
}
|
||||||
@ -1,23 +1,16 @@
|
|||||||
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { ChangeEvent, FocusEvent, useRef } from 'react';
|
import { FocusEvent, useRef } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import {
|
import { IconComponent, TablerIconsProps, isDefined } from 'twenty-ui';
|
||||||
IconComponent,
|
|
||||||
isDefined,
|
|
||||||
TablerIconsProps,
|
|
||||||
TEXT_INPUT_STYLE,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
|
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
|
||||||
|
|
||||||
type NavigationDrawerInputProps = {
|
type NavigationDrawerInputProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
Icon?: IconComponent | ((props: TablerIconsProps) => JSX.Element);
|
Icon?: IconComponent | ((props: TablerIconsProps) => JSX.Element);
|
||||||
|
placeholder?: string;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
onSubmit: (value: string) => void;
|
onSubmit: (value: string) => void;
|
||||||
@ -26,38 +19,13 @@ type NavigationDrawerInputProps = {
|
|||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledItem = styled.div<{ isNavigationDrawerExpanded: boolean }>`
|
const StyledInput = styled(TextInputV2)`
|
||||||
align-items: center;
|
background-color: white;
|
||||||
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;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const NavigationDrawerInput = ({
|
export const NavigationDrawerInput = ({
|
||||||
className,
|
className,
|
||||||
|
placeholder,
|
||||||
Icon,
|
Icon,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@ -66,10 +34,6 @@ export const NavigationDrawerInput = ({
|
|||||||
onClickOutside,
|
onClickOutside,
|
||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
}: NavigationDrawerInputProps) => {
|
}: NavigationDrawerInputProps) => {
|
||||||
const theme = useTheme();
|
|
||||||
const [isNavigationDrawerExpanded] = useRecoilState(
|
|
||||||
isNavigationDrawerExpandedState,
|
|
||||||
);
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useHotkeyScopeOnMount(hotkeyScope);
|
useHotkeyScopeOnMount(hotkeyScope);
|
||||||
@ -99,10 +63,6 @@ export const NavigationDrawerInput = ({
|
|||||||
listenerId: 'navigation-drawer-input',
|
listenerId: 'navigation-drawer-input',
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onChange(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFocus = (event: FocusEvent<HTMLInputElement>) => {
|
const handleFocus = (event: FocusEvent<HTMLInputElement>) => {
|
||||||
if (isDefined(value)) {
|
if (isDefined(value)) {
|
||||||
event.target.select();
|
event.target.select();
|
||||||
@ -110,29 +70,16 @@ export const NavigationDrawerInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledItem
|
<StyledInput
|
||||||
className={className}
|
className={className}
|
||||||
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
|
LeftIcon={Icon}
|
||||||
>
|
ref={inputRef}
|
||||||
<StyledItemElementsContainer>
|
value={value}
|
||||||
{Icon && (
|
onChange={onChange}
|
||||||
<Icon
|
placeholder={placeholder}
|
||||||
style={{ minWidth: theme.icon.size.md }}
|
onFocus={handleFocus}
|
||||||
size={theme.icon.size.md}
|
fullWidth
|
||||||
stroke={theme.icon.stroke.md}
|
autoFocus
|
||||||
color="currentColor"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<NavigationDrawerAnimatedCollapseWrapper>
|
|
||||||
<StyledTextInput
|
|
||||||
ref={inputRef}
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</NavigationDrawerAnimatedCollapseWrapper>
|
|
||||||
</StyledItemElementsContainer>
|
|
||||||
</StyledItem>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
|
||||||
|
|
||||||
export const NavigationDrawerItemInput = () => {
|
|
||||||
return <TextInput />;
|
|
||||||
};
|
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
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 { useRecordShowContainerTabs } from '@/object-record/record-show/hooks/useRecordShowContainerTabs';
|
||||||
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
|
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
|
||||||
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
|
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
|
||||||
@ -34,14 +35,18 @@ export const RecordShowPageHeader = ({
|
|||||||
|
|
||||||
const hasEditableName = layout.hideSummaryAndFields === true;
|
const hasEditableName = layout.hideSummaryAndFields === true;
|
||||||
|
|
||||||
|
const { labelIdentifierFieldMetadataItem } =
|
||||||
|
getObjectMetadataIdentifierFields({ objectMetadataItem });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={
|
title={
|
||||||
hasEditableName ? (
|
hasEditableName ? (
|
||||||
<RecordEditableName
|
<ObjectRecordShowPageBreadcrumb
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
objectRecordId={objectRecordId}
|
objectRecordId={objectRecordId}
|
||||||
objectLabelPlural={objectMetadataItem.labelPlural}
|
objectLabelPlural={objectMetadataItem.labelPlural}
|
||||||
|
labelIdentifierFieldMetadataItem={labelIdentifierFieldMetadataItem}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
viewName
|
viewName
|
||||||
|
|||||||
Reference in New Issue
Block a user