Right drawer to edit records (#5551)

This PR introduces a new side panel to edit records and the ability to
minimize the side panel.

The goal is leverage this sidepanel to be able to create records while
being in another show page.

I'm opening the PR for feedback since it involved refactoring and
therefore already touches a lot of files, even though it was quick to
implement.

<img width="1503" alt="Screenshot 2024-05-23 at 17 41 37"
src="https://github.com/twentyhq/twenty/assets/6399865/6f17e7a8-f4e9-4eb4-b392-c756db7198ac">
This commit is contained in:
Félix Malfait
2024-06-03 17:15:05 +02:00
committed by GitHub
parent 8e8078d596
commit 09bfb617b2
61 changed files with 957 additions and 452 deletions
@@ -1,3 +1,4 @@
import { useState } from 'react';
import { useApolloClient } from '@apollo/client';
import { v4 } from 'uuid';
@@ -28,6 +29,7 @@ export const useCreateOneRecord = <
skipPostOptmisticEffect = false,
}: useCreateOneRecordProps) => {
const apolloClient = useApolloClient();
const [loading, setLoading] = useState(false);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
@@ -50,6 +52,8 @@ export const useCreateOneRecord = <
const { objectMetadataItems } = useObjectMetadataItems();
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
setLoading(true);
const idForCreation = input.id ?? v4();
const sanitizedInput = {
@@ -94,6 +98,7 @@ export const useCreateOneRecord = <
recordsToCreate: [record],
objectMetadataItems,
});
setLoading(false);
},
});
@@ -102,5 +107,6 @@ export const useCreateOneRecord = <
return {
createOneRecord,
loading,
};
};
@@ -74,6 +74,7 @@ const RelationFieldInputWithContext = ({
relationObjectMetadataNameSingular:
CoreObjectNameSingular.WorkspaceMember,
objectMetadataNameSingular: 'person',
relationFieldMetadataId: '20202020-8c37-4163-ba06-1dada334ce3e',
},
}}
entityId={entityId}
@@ -0,0 +1,40 @@
import { useRecoilValue } from 'recoil';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
export const RightDrawerRecord = () => {
const viewableRecordNameSingular = useRecoilValue(
viewableRecordNameSingularState,
);
const viewableRecordId = useRecoilValue(viewableRecordIdState);
if (!viewableRecordNameSingular) {
throw new Error(`Object name is not defined`);
}
if (!viewableRecordId) {
throw new Error(`Record id is not defined`);
}
const { objectNameSingular, objectRecordId } = useRecordShowPage(
viewableRecordNameSingular ?? '',
viewableRecordId ?? '',
);
return (
<RecordFieldValueSelectorContextProvider>
<RecordValueSetterEffect recordId={objectRecordId} />
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
isInRightDrawer={true}
/>
</RecordFieldValueSelectorContextProvider>
);
};
@@ -0,0 +1,6 @@
import { createState } from 'twenty-ui';
export const viewableRecordIdState = createState<string | null>({
key: 'activities/viewable-record-id',
defaultValue: null,
});
@@ -0,0 +1,6 @@
import { createState } from 'twenty-ui';
export const viewableRecordNameSingularState = createState<string | null>({
key: 'activities/viewable-record-name-singular',
defaultValue: null,
});
@@ -27,6 +27,7 @@ import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPag
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
import { ShowPageRecoilScopeContext } from '@/ui/layout/states/ShowPageRecoilScopeContext';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import {
FieldMetadataType,
FileFolder,
@@ -39,12 +40,14 @@ type RecordShowContainerProps = {
objectNameSingular: string;
objectRecordId: string;
loading: boolean;
isInRightDrawer?: boolean;
};
export const RecordShowContainer = ({
objectNameSingular,
objectRecordId,
loading,
isInRightDrawer = false,
}: RecordShowContainerProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
@@ -129,114 +132,118 @@ export const RecordShowContainer = ({
);
const isReadOnly = objectMetadataItem.isRemote;
const isMobile = useIsMobile() || isInRightDrawer;
const isPrefetchLoading = useIsPrefetchLoading();
return (
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>
<ShowPageContainer>
<ShowPageLeftContainer>
{isDefined(recordFromStore) && (
<>
<ShowPageSummaryCard
id={objectRecordId}
logoOrAvatar={recordIdentifier?.avatarUrl ?? ''}
avatarPlaceholder={recordIdentifier?.name ?? ''}
date={recordFromStore.createdAt ?? ''}
loading={isPrefetchLoading || loading || recordLoading}
title={
<FieldContext.Provider
value={{
entityId: objectRecordId,
recoilScopeId:
objectRecordId + labelIdentifierFieldMetadataItem?.id,
isLabelIdentifier: false,
fieldDefinition: {
type:
labelIdentifierFieldMetadataItem?.type ||
FieldMetadataType.Text,
iconName: '',
fieldMetadataId:
labelIdentifierFieldMetadataItem?.id ?? '',
label: labelIdentifierFieldMetadataItem?.label || '',
metadata: {
fieldName:
labelIdentifierFieldMetadataItem?.name || '',
objectMetadataNameSingular: objectNameSingular,
},
defaultValue:
labelIdentifierFieldMetadataItem?.defaultValue,
},
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordInlineCell readonly={isReadOnly} />
</FieldContext.Provider>
}
avatarType={recordIdentifier?.avatarType ?? 'rounded'}
onUploadPicture={
objectNameSingular === 'person' ? onUploadPicture : undefined
}
/>
<PropertyBox>
{isPrefetchLoading ? (
<PropertyBoxSkeletonLoader />
) : (
inlineFieldMetadataItems.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
entityId: objectRecordId,
maxWidth: 200,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier: false,
fieldDefinition:
formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordInlineCell
loading={loading || recordLoading}
readonly={isReadOnly}
/>
</FieldContext.Provider>
))
)}
</PropertyBox>
<RecordDetailDuplicatesSection
objectRecordId={objectRecordId}
objectNameSingular={objectNameSingular}
/>
{relationFieldMetadataItems?.map((fieldMetadataItem, index) => (
const summary = (
<>
{isDefined(recordFromStore) && (
<>
<ShowPageSummaryCard
id={objectRecordId}
logoOrAvatar={recordIdentifier?.avatarUrl ?? ''}
avatarPlaceholder={recordIdentifier?.name ?? ''}
date={recordFromStore.createdAt ?? ''}
loading={isPrefetchLoading || loading || recordLoading}
title={
<FieldContext.Provider
value={{
entityId: objectRecordId,
recoilScopeId:
objectRecordId + labelIdentifierFieldMetadataItem?.id,
isLabelIdentifier: false,
fieldDefinition: {
type:
labelIdentifierFieldMetadataItem?.type ||
FieldMetadataType.Text,
iconName: '',
fieldMetadataId: labelIdentifierFieldMetadataItem?.id ?? '',
label: labelIdentifierFieldMetadataItem?.label || '',
metadata: {
fieldName: labelIdentifierFieldMetadataItem?.name || '',
objectMetadataNameSingular: objectNameSingular,
},
defaultValue:
labelIdentifierFieldMetadataItem?.defaultValue,
},
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordInlineCell readonly={isReadOnly} />
</FieldContext.Provider>
}
avatarType={recordIdentifier?.avatarType ?? 'rounded'}
onUploadPicture={
objectNameSingular === 'person' ? onUploadPicture : undefined
}
/>
<PropertyBox>
{isPrefetchLoading ? (
<PropertyBoxSkeletonLoader />
) : (
inlineFieldMetadataItems.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
entityId: objectRecordId,
maxWidth: 200,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordDetailRelationSection
loading={isPrefetchLoading || loading || recordLoading}
<RecordInlineCell
loading={loading || recordLoading}
readonly={isReadOnly}
/>
</FieldContext.Provider>
))}
</>
)}
))
)}
</PropertyBox>
<RecordDetailDuplicatesSection
objectRecordId={objectRecordId}
objectNameSingular={objectNameSingular}
/>
{relationFieldMetadataItems?.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
entityId: objectRecordId,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordDetailRelationSection
loading={isPrefetchLoading || loading || recordLoading}
/>
</FieldContext.Provider>
))}
</>
)}
</>
);
return (
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>
<ShowPageContainer>
<ShowPageLeftContainer forceMobile={isInRightDrawer}>
{!isMobile && summary}
</ShowPageLeftContainer>
{recordFromStore ? (
<ShowPageRightContainer
@@ -248,6 +255,8 @@ export const RecordShowContainer = ({
tasks
notes
emails
isRightDrawer={isInRightDrawer}
summary={summary}
loading={isPrefetchLoading || loading || recordLoading}
/>
) : (
@@ -1,41 +0,0 @@
import { useEffect } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { Activity } from '@/activities/types/Activity';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isDefined } from '~/utils/isDefined';
export const RecordShowContainer = ({
objectRecordId,
objectNameSingular,
}: {
objectRecordId: string;
objectNameSingular: string;
}) => {
const { record: activity, loading } = useFindOneRecord<Activity>({
objectRecordId,
objectNameSingular,
});
const setRecordStore = useSetRecoilState(
recordStoreFamilyState(objectRecordId),
);
const [recordLoading, setRecordLoading] = useRecoilState(
recordLoadingFamilyState(objectRecordId),
);
useEffect(() => {
if (loading !== recordLoading) {
setRecordLoading(loading);
}
}, [loading, recordLoading, setRecordLoading]);
useEffect(() => {
if (!loading && isDefined(activity)) {
setRecordStore(activity);
}
}, [loading, setRecordStore, activity]);
};
@@ -0,0 +1,98 @@
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { useIcons } from 'twenty-ui';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { findOneRecordForShowPageOperationSignatureFactory } from '@/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';
import { capitalize } from '~/utils/string/capitalize';
export const useRecordShowPage = (
propsObjectNameSingular: string,
propsObjectRecordId: string,
) => {
const {
objectNameSingular: paramObjectNameSingular,
objectRecordId: paramObjectRecordId,
} = useParams();
const objectNameSingular = propsObjectNameSingular || paramObjectNameSingular;
const objectRecordId = propsObjectRecordId || paramObjectRecordId;
if (!objectNameSingular || !objectRecordId) {
throw new Error('Object name or Record id is not defined');
}
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({ objectNameSingular });
const { favorites, createFavorite, deleteFavorite } = useFavorites();
const setEntityFields = useSetRecoilState(
recordStoreFamilyState(objectRecordId),
);
const { getIcon } = useIcons();
const headerIcon = getIcon(objectMetadataItem?.icon);
const FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE =
findOneRecordForShowPageOperationSignatureFactory({ objectMetadataItem });
const { record, loading } = useFindOneRecord({
objectRecordId,
objectNameSingular,
recordGqlFields: FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE.fields,
});
useEffect(() => {
if (isDefined(record)) {
setEntityFields(record);
}
}, [record, setEntityFields]);
const correspondingFavorite = favorites.find(
(favorite) => favorite.recordId === objectRecordId,
);
const isFavorite = isDefined(correspondingFavorite);
const handleFavoriteButtonClick = async () => {
if (!objectNameSingular || !record) return;
if (isFavorite) {
deleteFavorite(correspondingFavorite.id);
} else {
createFavorite(record, objectNameSingular);
}
};
const labelIdentifierFieldValue =
record?.[labelIdentifierFieldMetadataItem?.name ?? ''];
const pageName =
labelIdentifierFieldMetadataItem?.type === FieldMetadataType.FullName
? [
labelIdentifierFieldValue?.firstName,
labelIdentifierFieldValue?.lastName,
].join(' ')
: isDefined(labelIdentifierFieldValue)
? `${labelIdentifierFieldValue}`
: '';
const pageTitle = pageName.trim()
? `${pageName} - ${capitalize(objectNameSingular)}`
: capitalize(objectNameSingular);
return {
objectNameSingular,
objectRecordId,
headerIcon,
loading,
pageTitle,
pageName,
isFavorite,
handleFavoriteButtonClick,
record,
objectMetadataItem,
};
};
@@ -17,6 +17,7 @@ import { RecordDetailSectionHeader } from '@/object-record/record-show/record-de
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { SingleEntitySelectMenuItemsWithSearch } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/relation-picker/hooks/useAddNewRecordAndOpenRightDrawer';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
@@ -72,7 +73,7 @@ export const RecordDetailRelationSection = ({
const relationRecordIds = relationRecords.map(({ id }) => id);
const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}`;
const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}-${entityId}`;
const { closeDropdown, isDropdownOpen } = useDropdown(dropdownId);
@@ -138,6 +139,14 @@ export const RecordDetailRelationSection = ({
);
};
const { createNewRecordAndOpenRightDrawer } =
useAddNewRecordAndOpenRightDrawer({
relationObjectMetadataNameSingular,
relationObjectMetadataItem,
relationFieldMetadataItem,
entityId,
});
return (
<RecordDetailSection>
<RecordDetailSectionHeader
@@ -174,6 +183,7 @@ export const RecordDetailRelationSection = ({
relationObjectMetadataNameSingular
}
relationPickerScopeId={dropdownId}
onCreate={createNewRecordAndOpenRightDrawer}
/>
</RelationPickerScope>
}
@@ -54,6 +54,7 @@ export const RecordTableRow = ({
getBasePathToShowPage({
objectNameSingular: objectMetadataItem.nameSingular,
}) + recordId,
objectNameSingular: objectMetadataItem.nameSingular,
isSelected: currentRowSelected,
isReadOnly: objectMetadataItem.isRemote ?? false,
isPendingRow,
@@ -79,6 +79,8 @@ const meta: Meta = {
>
<RecordTableRowContext.Provider
value={{
objectNameSingular:
mockPerformance.entityValue.__typename.toLocaleLowerCase(),
recordId: mockPerformance.entityId,
rowIndex: 0,
pathToShowPage:
@@ -2,6 +2,7 @@ import { createContext } from 'react';
export type RecordTableRowContextProps = {
pathToShowPage: string;
objectNameSingular: string;
recordId: string;
rowIndex: number;
isSelected: boolean;
@@ -117,9 +117,21 @@ export const RecordTableCellSoftFocusMode = ({
}
};
const handleButtonClick = () => {
handleClick();
/*
Disabling sidepanel access for now, TODO: launch
if (!isFieldInputOnly) {
openTableCell(undefined, true);
}
*/
};
const isFirstColumn = columnIndex === 0;
const customButtonIcon = useGetButtonIcon();
const buttonIcon = isFirstColumn ? IconArrowUpRight : customButtonIcon;
const buttonIcon = isFirstColumn
? IconArrowUpRight // IconLayoutSidebarRightExpand - Disabling sidepanel access for now
: customButtonIcon;
const showButton =
isDefined(buttonIcon) &&
@@ -136,7 +148,7 @@ export const RecordTableCellSoftFocusMode = ({
{editModeContentOnly ? editModeContent : nonEditModeContent}
</RecordTableCellDisplayContainer>
{showButton && (
<RecordTableCellButton onClick={handleClick} Icon={buttonIcon} />
<RecordTableCellButton onClick={handleButtonClick} Icon={buttonIcon} />
)}
</>
);
@@ -8,6 +8,7 @@ export const recordTableRow: RecordTableRowContextProps = {
isSelected: false,
recordId: 'recordId',
pathToShowPage: '/',
objectNameSingular: 'objectNameSingular',
isReadOnly: false,
};
@@ -31,9 +31,14 @@ export const useOpenRecordTableCellFromCell = () => {
const cellPosition = useCurrentTableCellPosition();
const customCellHotkeyScope = useContext(CellHotkeyScopeContext);
const { entityId, fieldDefinition } = useContext(FieldContext);
const { isReadOnly, pathToShowPage } = useContext(RecordTableRowContext);
const { isReadOnly, pathToShowPage, objectNameSingular } = useContext(
RecordTableRowContext,
);
const openTableCell = (initialValue?: string) => {
const openTableCell = (
initialValue?: string,
isActionButtonClick = false,
) => {
onOpenTableCell({
cellPosition,
customCellHotkeyScope,
@@ -41,7 +46,9 @@ export const useOpenRecordTableCellFromCell = () => {
fieldDefinition,
isReadOnly,
pathToShowPage,
objectNameSingular,
initialValue,
isActionButtonClick,
});
};
@@ -1,15 +1,19 @@
import { useNavigate } from 'react-router-dom';
import { useRecoilCallback } from 'recoil';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId';
import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
import { useMoveEditModeToTableCellPosition } from '@/object-record/record-table/hooks/internal/useMoveEditModeToCellPosition';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
@@ -28,9 +32,11 @@ export type OpenTableCellArgs = {
cellPosition: TableCellPosition;
isReadOnly: boolean;
pathToShowPage: string;
objectNameSingular: string;
customCellHotkeyScope: HotkeyScope | null;
fieldDefinition: FieldDefinition<FieldMetadata>;
entityId: string;
isActionButtonClick: boolean;
};
export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
@@ -48,6 +54,12 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
const initDraftValue = useInitDraftValueV2();
const { openRightDrawer } = useRightDrawer();
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
const setViewableRecordNameSingular = useSetRecoilState(
viewableRecordNameSingularState,
);
const openTableCell = useRecoilCallback(
({ snapshot }) =>
({
@@ -55,9 +67,11 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
cellPosition,
isReadOnly,
pathToShowPage,
objectNameSingular,
customCellHotkeyScope,
fieldDefinition,
entityId,
isActionButtonClick,
}: OpenTableCellArgs) => {
if (isReadOnly) {
return;
@@ -78,9 +92,19 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
fieldValue,
});
if (isFirstColumnCell && !isEmpty) {
if (isFirstColumnCell && !isEmpty && !isActionButtonClick) {
leaveTableFocus();
navigate(pathToShowPage);
return;
}
if (isFirstColumnCell && !isEmpty && isActionButtonClick) {
leaveTableFocus();
setViewableRecordId(entityId);
setViewableRecordNameSingular(objectNameSingular);
openRightDrawer(RightDrawerPages.ViewRecord);
return;
}
@@ -112,10 +136,13 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
setDragSelectionStartEnabled,
toggleClickOutsideListener,
leaveTableFocus,
navigate,
setHotkeyScope,
initDraftValue,
moveEditModeToTableCellPosition,
openRightDrawer,
setViewableRecordId,
setViewableRecordNameSingular,
navigate,
],
);
@@ -1,9 +1,12 @@
import { useEffect } from 'react';
import { useContext, useEffect } from 'react';
import { IconForbid } from 'twenty-ui';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/relation-picker/hooks/useAddNewRecordAndOpenRightDrawer';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
@@ -39,11 +42,33 @@ export const RelationPicker = ({
selectedEntity: EntityForSelect | null | undefined,
) => onSubmit(selectedEntity ?? null);
const { objectMetadataItem: relationObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular:
fieldDefinition.metadata.relationObjectMetadataNameSingular,
});
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
({ id }) => id === fieldDefinition.metadata.relationFieldMetadataId,
);
const { entityId } = useContext(FieldContext);
const { createNewRecordAndOpenRightDrawer } =
useAddNewRecordAndOpenRightDrawer({
relationObjectMetadataNameSingular:
fieldDefinition.metadata.relationObjectMetadataNameSingular,
relationObjectMetadataItem,
relationFieldMetadataItem,
entityId,
});
return (
<SingleEntitySelect
EmptyIcon={IconForbid}
emptyLabel={'No ' + fieldDefinition.label}
onCancel={onCancel}
onCreate={createNewRecordAndOpenRightDrawer}
onEntitySelected={handleEntitySelected}
width={width}
relationObjectNameSingular={
@@ -122,7 +122,7 @@ export const SingleEntitySelectMenuItems = ({
selectedEntity={selectedEntity}
/>
))}
{showCreateButton && !loading && (
{showCreateButton && (
<>
{entitiesToSelect.length > 0 && <DropdownMenuSeparator />}
<CreateNewButton
@@ -15,7 +15,7 @@ import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';
export type SingleEntitySelectMenuItemsWithSearchProps = {
excludedRelationRecordIds?: string[];
onCreate?: () => void;
onCreate?: ((searchInput?: string) => void) | (() => void);
relationObjectNameSingular: string;
relationPickerScopeId?: string;
selectedRelationRecordIds: string[];
@@ -54,8 +54,7 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
relationPickerSearchFilterState,
);
const showCreateButton =
isDefined(onCreate) && relationPickerSearchFilter !== '';
const showCreateButton = isDefined(onCreate);
const entities = useFilteredSearchEntityQuery({
filters: [
@@ -71,6 +70,20 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
objectNameSingular: relationObjectNameSingular,
});
let onCreateWithInput = undefined;
if (isDefined(onCreate)) {
onCreateWithInput = () => {
if (onCreate.length > 0) {
(onCreate as (searchInput?: string) => void)(
relationPickerSearchFilter,
);
} else {
(onCreate as () => void)();
}
};
}
return (
<>
<ObjectMetadataItemsRelationPickerEffect
@@ -81,12 +94,17 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
<SingleEntitySelectMenuItems
entitiesToSelect={entities.entitiesToSelect}
loading={entities.loading}
selectedEntity={selectedEntity ?? entities.selectedEntities[0]}
selectedEntity={
selectedEntity ??
(entities.selectedEntities.length === 1
? entities.selectedEntities[0]
: undefined)
}
onCreate={onCreateWithInput}
{...{
EmptyIcon,
emptyLabel,
onCancel,
onCreate,
onEntitySelected,
showCreateButton,
}}
@@ -0,0 +1,115 @@
import { useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import {
FieldMetadataType,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';
type RecordDetailRelationSectionProps = {
relationObjectMetadataNameSingular: string;
relationObjectMetadataItem: ObjectMetadataItem;
relationFieldMetadataItem?: FieldMetadataItem;
entityId: string;
};
export const useAddNewRecordAndOpenRightDrawer = ({
relationObjectMetadataNameSingular,
relationObjectMetadataItem,
relationFieldMetadataItem,
entityId,
}: RecordDetailRelationSectionProps) => {
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
const setViewableRecordNameSingular = useSetRecoilState(
viewableRecordNameSingularState,
);
const { createOneRecord } = useCreateOneRecord({
objectNameSingular: relationObjectMetadataNameSingular,
});
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular:
relationFieldMetadataItem?.relationDefinition?.targetObjectMetadata
.nameSingular ?? 'workspaceMember',
});
const { openRightDrawer } = useRightDrawer();
if (
relationObjectMetadataNameSingular === 'workspaceMember' ||
!isDefined(
relationFieldMetadataItem?.relationDefinition?.targetObjectMetadata
.nameSingular,
)
) {
return {
createNewRecordAndOpenRightDrawer: undefined,
};
}
return {
createNewRecordAndOpenRightDrawer: async (searchInput?: string) => {
const newRecordId = v4();
const labelIdentifierType = getLabelIdentifierFieldMetadataItem(
relationObjectMetadataItem,
)?.type;
const createRecordPayload: {
id: string;
name:
| string
| { firstName: string | undefined; lastName: string | undefined };
[key: string]: any;
} =
labelIdentifierType === FieldMetadataType.FullName
? {
id: newRecordId,
name:
searchInput && searchInput.split(' ').length > 1
? {
firstName: searchInput.split(' ')[0],
lastName: searchInput.split(' ').slice(1).join(' '),
}
: { firstName: searchInput, lastName: '' },
}
: { id: newRecordId, name: searchInput ?? '' };
if (
relationFieldMetadataItem?.relationDefinition?.direction ===
RelationDefinitionType.ManyToOne
) {
createRecordPayload[
`${relationFieldMetadataItem?.relationDefinition?.targetFieldMetadata.name}Id`
] = entityId;
}
await createOneRecord(createRecordPayload);
if (
relationFieldMetadataItem?.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
) {
await updateOneRecord({
idToUpdate: entityId,
updateOneRecordInput: {
[`${relationFieldMetadataItem?.relationDefinition?.targetFieldMetadata.name}Id`]:
newRecordId,
},
});
}
setViewableRecordId(newRecordId);
setViewableRecordNameSingular(relationObjectMetadataNameSingular);
openRightDrawer(RightDrawerPages.ViewRecord);
},
};
};