Refactoring show page (#7838)

@ehconitin following your question I did a quick refactoring of the show
page - we can push it much further but it would be better to start from
this code than from main

Edit: I will merge to avoid conflicts, this is very far from perfect but
still much better than the mess we had before
This commit is contained in:
Félix Malfait
2024-10-19 00:39:10 +02:00
committed by GitHub
parent d4457d756c
commit c285f0a9df
9 changed files with 600 additions and 460 deletions

View File

@ -3,7 +3,7 @@ import { IconCheckbox, IconNotes, IconPaperclip } from 'twenty-ui';
import { Button } from '@/ui/input/button/components/Button';
import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup';
import { TAB_LIST_COMPONENT_ID } from '@/ui/layout/show-page/components/ShowPageRightContainer';
import { TAB_LIST_COMPONENT_ID } from '@/ui/layout/show-page/components/ShowPageSubContainer';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
export const TimelineCreateButtonGroup = ({

View File

@ -0,0 +1,188 @@
import groupBy from 'lodash.groupby';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { RecordDetailDuplicatesSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection';
import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection';
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
type FieldsCardProps = {
objectNameSingular: string;
objectRecordId: string;
};
export const FieldsCard = ({
objectNameSingular,
objectRecordId,
}: FieldsCardProps) => {
const {
recordFromStore,
recordLoading,
objectMetadataItem,
labelIdentifierFieldMetadataItem,
isPrefetchLoading,
objectMetadataItems,
} = useRecordShowContainerData({
objectNameSingular,
objectRecordId,
});
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
objectNameSingular,
objectRecordId,
recordFromStore,
});
const availableFieldMetadataItems = objectMetadataItem.fields
.filter(
(fieldMetadataItem) =>
isFieldCellSupported(fieldMetadataItem, objectMetadataItems) &&
fieldMetadataItem.id !== labelIdentifierFieldMetadataItem?.id,
)
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
);
const { inlineFieldMetadataItems, relationFieldMetadataItems } = groupBy(
availableFieldMetadataItems.filter(
(fieldMetadataItem) =>
fieldMetadataItem.name !== 'createdAt' &&
fieldMetadataItem.name !== 'deletedAt',
),
(fieldMetadataItem) =>
fieldMetadataItem.type === FieldMetadataType.Relation
? 'relationFieldMetadataItems'
: 'inlineFieldMetadataItems',
);
const inlineRelationFieldMetadataItems = relationFieldMetadataItems?.filter(
(fieldMetadataItem) =>
(objectNameSingular === CoreObjectNameSingular.Note &&
fieldMetadataItem.name === 'noteTargets') ||
(objectNameSingular === CoreObjectNameSingular.Task &&
fieldMetadataItem.name === 'taskTargets'),
);
const boxedRelationFieldMetadataItems = relationFieldMetadataItems?.filter(
(fieldMetadataItem) =>
objectNameSingular !== CoreObjectNameSingular.Note &&
fieldMetadataItem.name !== 'noteTargets' &&
objectNameSingular !== CoreObjectNameSingular.Task &&
fieldMetadataItem.name !== 'taskTargets',
);
const isReadOnly = objectMetadataItem.isRemote;
return (
<>
{isDefined(recordFromStore) && (
<>
<PropertyBox>
{isPrefetchLoading ? (
<PropertyBoxSkeletonLoader />
) : (
<>
{inlineRelationFieldMetadataItems?.map(
(fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: 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,
}}
>
<ActivityTargetsInlineCell
activityObjectNameSingular={
objectNameSingular as
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task
}
activity={recordFromStore as Task | Note}
showLabel={true}
maxWidth={200}
/>
</FieldContext.Provider>
),
)}
{inlineFieldMetadataItems?.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: 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={recordLoading}
readonly={isReadOnly}
/>
</FieldContext.Provider>
))}
</>
)}
</PropertyBox>
<RecordDetailDuplicatesSection
objectRecordId={objectRecordId}
objectNameSingular={objectNameSingular}
/>
{boxedRelationFieldMetadataItems?.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: objectRecordId,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordDetailRelationSection
loading={isPrefetchLoading || recordLoading}
/>
</FieldContext.Provider>
))}
</>
)}
</>
);
};

View File

@ -1,47 +1,10 @@
import groupBy from 'lodash.groupby';
import { useRecoilState, useRecoilValue } from 'recoil';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task';
import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord';
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import {
FieldContext,
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { RecordDetailDuplicatesSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection';
import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection';
import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { ShowPageContainer } from '@/ui/layout/page/ShowPageContainer';
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
import { ShowPageSummaryCardSkeletonLoader } from '@/ui/layout/show-page/components/ShowPageSummaryCardSkeletonLoader';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import {
FieldMetadataType,
FileFolder,
useUploadImageMutation,
} from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { useRecordShowContainerTabs } from '@/object-record/record-show/hooks/useRecordShowContainerTabs';
import { ShowPageSubContainer } from '@/ui/layout/show-page/components/ShowPageSubContainer';
type RecordShowContainerProps = {
objectNameSingular: string;
@ -58,261 +21,20 @@ export const RecordShowContainer = ({
isInRightDrawer = false,
isNewRightDrawerItemLoading = false,
}: RecordShowContainerProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
const {
recordFromStore,
objectMetadataItem,
isPrefetchLoading,
recordLoading,
} = useRecordShowContainerData({
objectNameSingular,
objectRecordId,
});
const { objectMetadataItems } = useObjectMetadataItems();
const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({
objectNameSingular,
});
const [recordLoading] = useRecoilState(
recordLoadingFamilyState(objectRecordId),
);
const [recordFromStore] = useRecoilState<ObjectRecord | null>(
recordStoreFamilyState(objectRecordId),
);
const recordIdentifier = useRecoilValue(
recordStoreIdentifierFamilySelector({
objectNameSingular,
recordId: objectRecordId,
}),
);
const [uploadImage] = useUploadImageMutation();
const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular });
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneRecord?.({
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
};
return [updateEntity, { loading: false }];
};
const onUploadPicture = async (file: File) => {
if (objectNameSingular !== 'person') {
return;
}
const result = await uploadImage({
variables: {
file,
fileFolder: FileFolder.PersonPicture,
},
});
const avatarUrl = result?.data?.uploadImage;
if (!avatarUrl || isUndefinedOrNull(updateOneRecord) || !recordFromStore) {
return;
}
await updateOneRecord({
idToUpdate: objectRecordId,
updateOneRecordInput: {
avatarUrl,
},
});
};
const availableFieldMetadataItems = objectMetadataItem.fields
.filter(
(fieldMetadataItem) =>
isFieldCellSupported(fieldMetadataItem, objectMetadataItems) &&
fieldMetadataItem.id !== labelIdentifierFieldMetadataItem?.id,
)
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
);
const { inlineFieldMetadataItems, relationFieldMetadataItems } = groupBy(
availableFieldMetadataItems.filter(
(fieldMetadataItem) =>
fieldMetadataItem.name !== 'createdAt' &&
fieldMetadataItem.name !== 'deletedAt',
),
(fieldMetadataItem) =>
fieldMetadataItem.type === FieldMetadataType.Relation
? 'relationFieldMetadataItems'
: 'inlineFieldMetadataItems',
);
const inlineRelationFieldMetadataItems = relationFieldMetadataItems?.filter(
(fieldMetadataItem) =>
(objectNameSingular === CoreObjectNameSingular.Note &&
fieldMetadataItem.name === 'noteTargets') ||
(objectNameSingular === CoreObjectNameSingular.Task &&
fieldMetadataItem.name === 'taskTargets'),
);
const boxedRelationFieldMetadataItems = relationFieldMetadataItems?.filter(
(fieldMetadataItem) =>
objectNameSingular !== CoreObjectNameSingular.Note &&
fieldMetadataItem.name !== 'noteTargets' &&
objectNameSingular !== CoreObjectNameSingular.Task &&
fieldMetadataItem.name !== 'taskTargets',
);
const { Icon, IconColor } = useGetStandardObjectIcon(objectNameSingular);
const isReadOnly = objectMetadataItem.isRemote;
const isMobile = useIsMobile() || isInRightDrawer;
const isPrefetchLoading = useIsPrefetchLoading();
const summaryCard =
!isNewRightDrawerItemLoading && isDefined(recordFromStore) ? (
<ShowPageSummaryCard
isMobile={isMobile}
id={objectRecordId}
logoOrAvatar={recordIdentifier?.avatarUrl ?? ''}
icon={Icon}
iconColor={IconColor}
avatarPlaceholder={recordIdentifier?.name ?? ''}
date={recordFromStore.createdAt ?? ''}
loading={isPrefetchLoading || loading || recordLoading}
title={
<FieldContext.Provider
value={{
recordId: 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,
isCentered: !isMobile,
}}
>
<RecordInlineCell readonly={isReadOnly} />
</FieldContext.Provider>
}
avatarType={recordIdentifier?.avatarType ?? 'rounded'}
onUploadPicture={
objectNameSingular === 'person' ? onUploadPicture : undefined
}
/>
) : (
<ShowPageSummaryCardSkeletonLoader />
);
const fieldsBox = (
<>
{isDefined(recordFromStore) && (
<>
<PropertyBox>
{isPrefetchLoading ? (
<PropertyBoxSkeletonLoader />
) : (
<>
{inlineRelationFieldMetadataItems?.map(
(fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: 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,
}}
>
<ActivityTargetsInlineCell
activityObjectNameSingular={
objectNameSingular as
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task
}
activity={recordFromStore as Task | Note}
showLabel={true}
maxWidth={200}
/>
</FieldContext.Provider>
),
)}
{inlineFieldMetadataItems?.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: 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}
/>
{boxedRelationFieldMetadataItems?.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: 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>
))}
</>
)}
</>
const tabs = useRecordShowContainerTabs(
loading,
objectNameSingular as CoreObjectNameSingular,
isInRightDrawer,
);
return (
@ -324,23 +46,15 @@ export const RecordShowContainer = ({
/>
)}
<ShowPageContainer>
<ShowPageLeftContainer forceMobile={isMobile}>
{!isMobile && summaryCard}
{!isMobile && fieldsBox}
</ShowPageLeftContainer>
<ShowPageRightContainer
<ShowPageSubContainer
tabs={tabs}
targetableObject={{
id: objectRecordId,
targetObjectNameSingular: objectMetadataItem?.nameSingular,
}}
timeline
tasks
notes
emails
isInRightDrawer={isInRightDrawer}
summaryCard={isMobile ? summaryCard : <></>}
fieldsBox={fieldsBox}
loading={isPrefetchLoading || loading || recordLoading}
isNewRightDrawerItemLoading={isNewRightDrawerItemLoading}
/>
</ShowPageContainer>
</>

View File

@ -0,0 +1,100 @@
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
import { ShowPageSummaryCardSkeletonLoader } from '@/ui/layout/show-page/components/ShowPageSummaryCardSkeletonLoader';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
type SummaryCardProps = {
objectNameSingular: string;
objectRecordId: string;
isNewRightDrawerItemLoading: boolean;
isInRightDrawer: boolean;
};
export const SummaryCard = ({
objectNameSingular,
objectRecordId,
isNewRightDrawerItemLoading,
isInRightDrawer,
}: SummaryCardProps) => {
const {
recordFromStore,
recordLoading,
objectMetadataItem,
labelIdentifierFieldMetadataItem,
isPrefetchLoading,
recordIdentifier,
} = useRecordShowContainerData({
objectNameSingular,
objectRecordId,
});
const { onUploadPicture, useUpdateOneObjectRecordMutation } =
useRecordShowContainerActions({
objectNameSingular,
objectRecordId,
recordFromStore,
});
const { Icon, IconColor } = useGetStandardObjectIcon(objectNameSingular);
const isMobile = useIsMobile() || isInRightDrawer;
const isReadOnly = objectMetadataItem.isRemote;
if (isNewRightDrawerItemLoading || !isDefined(recordFromStore)) {
return <ShowPageSummaryCardSkeletonLoader />;
}
return (
<ShowPageSummaryCard
isMobile={isMobile}
id={objectRecordId}
logoOrAvatar={recordIdentifier?.avatarUrl ?? ''}
icon={Icon}
iconColor={IconColor}
avatarPlaceholder={recordIdentifier?.name ?? ''}
date={recordFromStore.createdAt ?? ''}
loading={isPrefetchLoading || recordLoading}
title={
<FieldContext.Provider
value={{
recordId: 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,
isCentered: !isMobile,
}}
>
<RecordInlineCell readonly={isReadOnly} />
</FieldContext.Provider>
}
avatarType={recordIdentifier?.avatarType ?? 'rounded'}
onUploadPicture={
objectNameSingular === CoreObjectNameSingular.Person
? onUploadPicture
: undefined
}
/>
);
};

View File

@ -0,0 +1,66 @@
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import {
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { FileFolder } from '~/generated-metadata/graphql';
import { useUploadImageMutation } from '~/generated/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
interface UseRecordShowContainerActionsProps {
objectNameSingular: string;
objectRecordId: string;
recordFromStore: ObjectRecord | null;
}
export const useRecordShowContainerActions = ({
objectNameSingular,
objectRecordId,
recordFromStore,
}: UseRecordShowContainerActionsProps) => {
const [uploadImage] = useUploadImageMutation();
const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular });
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneRecord?.({
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
};
return [updateEntity, { loading: false }];
};
const onUploadPicture = async (file: File) => {
if (objectNameSingular !== 'person') {
return;
}
const result = await uploadImage({
variables: {
file,
fileFolder: FileFolder.PersonPicture,
},
});
const avatarUrl = result?.data?.uploadImage;
if (!avatarUrl || isUndefinedOrNull(updateOneRecord) || !recordFromStore) {
return;
}
await updateOneRecord({
idToUpdate: objectRecordId,
updateOneRecordInput: {
avatarUrl,
},
});
};
return {
onUploadPicture,
useUpdateOneObjectRecordMutation,
};
};

View File

@ -0,0 +1,57 @@
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { useRecoilState, useRecoilValue } from 'recoil';
type UseRecordShowContainerDataProps = {
objectNameSingular: string;
objectRecordId: string;
};
export const useRecordShowContainerData = ({
objectNameSingular,
objectRecordId,
}: UseRecordShowContainerDataProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({
objectNameSingular,
});
const [recordLoading] = useRecoilState(
recordLoadingFamilyState(objectRecordId),
);
const [recordFromStore] = useRecoilState<ObjectRecord | null>(
recordStoreFamilyState(objectRecordId),
);
const recordIdentifier = useRecoilValue(
recordStoreIdentifierFamilySelector({
objectNameSingular,
recordId: objectRecordId,
}),
);
const isPrefetchLoading = useIsPrefetchLoading();
const { objectMetadataItems } = useObjectMetadataItems();
return {
recordFromStore,
recordLoading,
objectMetadataItem,
labelIdentifierFieldMetadataItem,
isPrefetchLoading,
recordIdentifier,
objectMetadataItems,
};
};

View File

@ -0,0 +1,110 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import {
IconCalendarEvent,
IconCheckbox,
IconList,
IconMail,
IconNotes,
IconPaperclip,
IconSettings,
IconTimelineEvent,
} from 'twenty-ui';
export const useRecordShowContainerTabs = (
loading: boolean,
targetObjectNameSingular: CoreObjectNameSingular,
isInRightDrawer: boolean,
) => {
const isMobile = useIsMobile();
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
const isWorkflow =
isWorkflowEnabled &&
targetObjectNameSingular === CoreObjectNameSingular.Workflow;
const isWorkflowVersion =
isWorkflowEnabled &&
targetObjectNameSingular === CoreObjectNameSingular.WorkflowVersion;
const isCompanyOrPerson = [
CoreObjectNameSingular.Company,
CoreObjectNameSingular.Person,
].includes(targetObjectNameSingular);
const shouldDisplayCalendarTab = isCompanyOrPerson;
const shouldDisplayEmailsTab = isCompanyOrPerson;
return [
{
id: 'richText',
title: 'Note',
Icon: IconNotes,
hide:
loading ||
(targetObjectNameSingular !== CoreObjectNameSingular.Note &&
targetObjectNameSingular !== CoreObjectNameSingular.Task),
},
{
id: 'fields',
title: 'Fields',
Icon: IconList,
hide: !(isMobile || isInRightDrawer),
},
{
id: 'timeline',
title: 'Timeline',
Icon: IconTimelineEvent,
hide: isInRightDrawer || isWorkflow || isWorkflowVersion,
},
{
id: 'tasks',
title: 'Tasks',
Icon: IconCheckbox,
hide:
targetObjectNameSingular === CoreObjectNameSingular.Note ||
targetObjectNameSingular === CoreObjectNameSingular.Task ||
isWorkflow ||
isWorkflowVersion,
},
{
id: 'notes',
title: 'Notes',
Icon: IconNotes,
hide:
targetObjectNameSingular === CoreObjectNameSingular.Note ||
targetObjectNameSingular === CoreObjectNameSingular.Task ||
isWorkflow ||
isWorkflowVersion,
},
{
id: 'files',
title: 'Files',
Icon: IconPaperclip,
hide: isWorkflow || isWorkflowVersion,
},
{
id: 'emails',
title: 'Emails',
Icon: IconMail,
hide: !shouldDisplayEmailsTab,
},
{
id: 'calendar',
title: 'Calendar',
Icon: IconCalendarEvent,
hide: !shouldDisplayCalendarTab,
},
{
id: 'workflow',
title: 'Workflow',
Icon: IconSettings,
hide: !isWorkflow,
},
{
id: 'workflowVersion',
title: 'Workflow Version',
Icon: IconSettings,
hide: !isWorkflowVersion,
},
];
};

View File

@ -8,32 +8,24 @@ import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableE
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
import { FieldsCard } from '@/object-record/record-show/components/FieldsCard';
import { SummaryCard } from '@/object-record/record-show/components/SummaryCard';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { Button } from '@/ui/input/button/components/Button';
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
import { TabList } from '@/ui/layout/tab/components/TabList';
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { WorkflowVersionVisualizer } from '@/workflow/components/WorkflowVersionVisualizer';
import { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowVersionVisualizerEffect';
import { WorkflowVisualizer } from '@/workflow/components/WorkflowVisualizer';
import { WorkflowVisualizerEffect } from '@/workflow/components/WorkflowVisualizerEffect';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled';
import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import {
IconCalendarEvent,
IconCheckbox,
IconList,
IconMail,
IconNotes,
IconPaperclip,
IconSettings,
IconTimelineEvent,
IconTrash,
} from 'twenty-ui';
import { IconTrash } from 'twenty-ui';
const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>`
display: flex;
@ -89,145 +81,51 @@ const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
export const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list';
type ShowPageRightContainerProps = {
type ShowPageSubContainerProps = {
tabs: SingleTabProps[];
targetableObject: Pick<
ActivityTargetableObject,
'targetObjectNameSingular' | 'id'
>;
timeline?: boolean;
tasks?: boolean;
notes?: boolean;
emails?: boolean;
fieldsBox?: JSX.Element;
summaryCard?: JSX.Element;
isInRightDrawer?: boolean;
loading: boolean;
isNewRightDrawerItemLoading?: boolean;
};
export const ShowPageRightContainer = ({
export const ShowPageSubContainer = ({
tabs,
targetableObject,
timeline,
tasks,
notes,
emails,
loading,
fieldsBox,
summaryCard,
isInRightDrawer = false,
}: ShowPageRightContainerProps) => {
isNewRightDrawerItemLoading = false,
}: ShowPageSubContainerProps) => {
const { activeTabIdState } = useTabList(
`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`,
);
const activeTabId = useRecoilValue(activeTabIdState);
const targetObjectNameSingular =
targetableObject.targetObjectNameSingular as CoreObjectNameSingular;
const isCompanyOrPerson = [
CoreObjectNameSingular.Company,
CoreObjectNameSingular.Person,
].includes(targetObjectNameSingular);
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
const isWorkflow =
isWorkflowEnabled &&
targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Workflow;
const isWorkflowVersion =
isWorkflowEnabled &&
targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.WorkflowVersion;
const shouldDisplayCalendarTab = isCompanyOrPerson;
const shouldDisplayEmailsTab = emails && isCompanyOrPerson;
const isMobile = useIsMobile();
const isNewViewableRecordLoading = useRecoilValue(
isNewViewableRecordLoadingState,
);
const tabs = [
{
id: 'richText',
title: 'Note',
Icon: IconNotes,
hide:
loading ||
(targetableObject.targetObjectNameSingular !==
CoreObjectNameSingular.Note &&
targetableObject.targetObjectNameSingular !==
CoreObjectNameSingular.Task),
},
{
id: 'fields',
title: 'Fields',
Icon: IconList,
hide: !(isMobile || isInRightDrawer),
},
{
id: 'timeline',
title: 'Timeline',
Icon: IconTimelineEvent,
hide: !timeline || isInRightDrawer || isWorkflow || isWorkflowVersion,
},
{
id: 'tasks',
title: 'Tasks',
Icon: IconCheckbox,
hide:
!tasks ||
targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Note ||
targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Task ||
isWorkflow ||
isWorkflowVersion,
},
{
id: 'notes',
title: 'Notes',
Icon: IconNotes,
hide:
!notes ||
targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Note ||
targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Task ||
isWorkflow ||
isWorkflowVersion,
},
{
id: 'files',
title: 'Files',
Icon: IconPaperclip,
hide: !notes || isWorkflow || isWorkflowVersion,
},
{
id: 'emails',
title: 'Emails',
Icon: IconMail,
hide: !shouldDisplayEmailsTab,
},
{
id: 'calendar',
title: 'Calendar',
Icon: IconCalendarEvent,
hide: !shouldDisplayCalendarTab,
},
{
id: 'workflow',
title: 'Workflow',
Icon: IconSettings,
hide: !isWorkflow,
},
{
id: 'workflowVersion',
title: 'Workflow Version',
Icon: IconSettings,
hide: !isWorkflowVersion,
},
];
const summaryCard = (
<SummaryCard
objectNameSingular={targetableObject.targetObjectNameSingular}
objectRecordId={targetableObject.id}
isNewRightDrawerItemLoading={isNewRightDrawerItemLoading}
isInRightDrawer={isInRightDrawer}
/>
);
const fieldsCard = (
<FieldsCard
objectNameSingular={targetableObject.targetObjectNameSingular}
objectRecordId={targetableObject.id}
/>
);
const renderActiveTabContent = () => {
switch (activeTabId) {
case 'timeline':
@ -251,10 +149,9 @@ export const ShowPageRightContainer = ({
case 'fields':
return (
<StyledGreyBox isInRightDrawer={isInRightDrawer}>
{fieldsBox}
{fieldsCard}
</StyledGreyBox>
);
case 'tasks':
return <ObjectTasks targetableObject={targetableObject} />;
case 'notes':
@ -307,28 +204,36 @@ export const ShowPageRightContainer = ({
);
return (
<StyledShowPageRightContainer isMobile={isMobile}>
<StyledTabListContainer>
<TabList
loading={loading || isNewViewableRecordLoading}
tabListId={`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`}
tabs={tabs}
/>
</StyledTabListContainer>
{summaryCard}
<StyledContentContainer isInRightDrawer={isInRightDrawer}>
{renderActiveTabContent()}
</StyledContentContainer>
{isInRightDrawer && recordFromStore && !recordFromStore.deletedAt && (
<StyledButtonContainer>
<Button
Icon={IconTrash}
onClick={handleDelete}
disabled={isDeleting}
title={isDeleting ? 'Deleting...' : 'Delete'}
></Button>
</StyledButtonContainer>
<>
{!isMobile && !isInRightDrawer && (
<ShowPageLeftContainer forceMobile={isMobile}>
{summaryCard}
{fieldsCard}
</ShowPageLeftContainer>
)}
</StyledShowPageRightContainer>
<StyledShowPageRightContainer isMobile={isMobile}>
<StyledTabListContainer>
<TabList
loading={loading || isNewViewableRecordLoading}
tabListId={`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`}
tabs={tabs}
/>
</StyledTabListContainer>
{(isMobile || isInRightDrawer) && summaryCard}
<StyledContentContainer isInRightDrawer={isInRightDrawer}>
{renderActiveTabContent()}
</StyledContentContainer>
{isInRightDrawer && recordFromStore && !recordFromStore.deletedAt && (
<StyledButtonContainer>
<Button
Icon={IconTrash}
onClick={handleDelete}
disabled={isDeleting}
title={isDeleting ? 'Deleting...' : 'Delete'}
></Button>
</StyledButtonContainer>
)}
</StyledShowPageRightContainer>
</>
);
};

View File

@ -9,7 +9,7 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { Tab } from './Tab';
type SingleTabProps = {
export type SingleTabProps = {
title: string;
Icon?: IconComponent;
id: string;