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:
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user