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:
@ -3,7 +3,7 @@ import { IconCheckbox, IconNotes, IconPaperclip } from 'twenty-ui';
|
|||||||
|
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup';
|
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';
|
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
|
|
||||||
export const TimelineCreateButtonGroup = ({
|
export const TimelineCreateButtonGroup = ({
|
||||||
|
|||||||
@ -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>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 { 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 { 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 { 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 { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
|
||||||
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
|
import { useRecordShowContainerTabs } from '@/object-record/record-show/hooks/useRecordShowContainerTabs';
|
||||||
import { ShowPageSummaryCardSkeletonLoader } from '@/ui/layout/show-page/components/ShowPageSummaryCardSkeletonLoader';
|
import { ShowPageSubContainer } from '@/ui/layout/show-page/components/ShowPageSubContainer';
|
||||||
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';
|
|
||||||
|
|
||||||
type RecordShowContainerProps = {
|
type RecordShowContainerProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
@ -58,261 +21,20 @@ export const RecordShowContainer = ({
|
|||||||
isInRightDrawer = false,
|
isInRightDrawer = false,
|
||||||
isNewRightDrawerItemLoading = false,
|
isNewRightDrawerItemLoading = false,
|
||||||
}: RecordShowContainerProps) => {
|
}: RecordShowContainerProps) => {
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const {
|
||||||
|
recordFromStore,
|
||||||
|
objectMetadataItem,
|
||||||
|
isPrefetchLoading,
|
||||||
|
recordLoading,
|
||||||
|
} = useRecordShowContainerData({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
objectRecordId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { objectMetadataItems } = useObjectMetadataItems();
|
const tabs = useRecordShowContainerTabs(
|
||||||
|
loading,
|
||||||
const { labelIdentifierFieldMetadataItem } =
|
objectNameSingular as CoreObjectNameSingular,
|
||||||
useLabelIdentifierFieldMetadataItem({
|
isInRightDrawer,
|
||||||
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>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -324,23 +46,15 @@ export const RecordShowContainer = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ShowPageContainer>
|
<ShowPageContainer>
|
||||||
<ShowPageLeftContainer forceMobile={isMobile}>
|
<ShowPageSubContainer
|
||||||
{!isMobile && summaryCard}
|
tabs={tabs}
|
||||||
{!isMobile && fieldsBox}
|
|
||||||
</ShowPageLeftContainer>
|
|
||||||
<ShowPageRightContainer
|
|
||||||
targetableObject={{
|
targetableObject={{
|
||||||
id: objectRecordId,
|
id: objectRecordId,
|
||||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||||
}}
|
}}
|
||||||
timeline
|
|
||||||
tasks
|
|
||||||
notes
|
|
||||||
emails
|
|
||||||
isInRightDrawer={isInRightDrawer}
|
isInRightDrawer={isInRightDrawer}
|
||||||
summaryCard={isMobile ? summaryCard : <></>}
|
|
||||||
fieldsBox={fieldsBox}
|
|
||||||
loading={isPrefetchLoading || loading || recordLoading}
|
loading={isPrefetchLoading || loading || recordLoading}
|
||||||
|
isNewRightDrawerItemLoading={isNewRightDrawerItemLoading}
|
||||||
/>
|
/>
|
||||||
</ShowPageContainer>
|
</ShowPageContainer>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
@ -8,32 +8,24 @@ import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableE
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||||
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
|
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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
|
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 { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import { WorkflowVersionVisualizer } from '@/workflow/components/WorkflowVersionVisualizer';
|
import { WorkflowVersionVisualizer } from '@/workflow/components/WorkflowVersionVisualizer';
|
||||||
import { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowVersionVisualizerEffect';
|
import { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowVersionVisualizerEffect';
|
||||||
import { WorkflowVisualizer } from '@/workflow/components/WorkflowVisualizer';
|
import { WorkflowVisualizer } from '@/workflow/components/WorkflowVisualizer';
|
||||||
import { WorkflowVisualizerEffect } from '@/workflow/components/WorkflowVisualizerEffect';
|
import { WorkflowVisualizerEffect } from '@/workflow/components/WorkflowVisualizerEffect';
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import {
|
import { IconTrash } from 'twenty-ui';
|
||||||
IconCalendarEvent,
|
|
||||||
IconCheckbox,
|
|
||||||
IconList,
|
|
||||||
IconMail,
|
|
||||||
IconNotes,
|
|
||||||
IconPaperclip,
|
|
||||||
IconSettings,
|
|
||||||
IconTimelineEvent,
|
|
||||||
IconTrash,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
|
|
||||||
const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>`
|
const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -89,145 +81,51 @@ const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
|
|||||||
|
|
||||||
export const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list';
|
export const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list';
|
||||||
|
|
||||||
type ShowPageRightContainerProps = {
|
type ShowPageSubContainerProps = {
|
||||||
|
tabs: SingleTabProps[];
|
||||||
targetableObject: Pick<
|
targetableObject: Pick<
|
||||||
ActivityTargetableObject,
|
ActivityTargetableObject,
|
||||||
'targetObjectNameSingular' | 'id'
|
'targetObjectNameSingular' | 'id'
|
||||||
>;
|
>;
|
||||||
timeline?: boolean;
|
|
||||||
tasks?: boolean;
|
|
||||||
notes?: boolean;
|
|
||||||
emails?: boolean;
|
|
||||||
fieldsBox?: JSX.Element;
|
|
||||||
summaryCard?: JSX.Element;
|
|
||||||
isInRightDrawer?: boolean;
|
isInRightDrawer?: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
isNewRightDrawerItemLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ShowPageRightContainer = ({
|
export const ShowPageSubContainer = ({
|
||||||
|
tabs,
|
||||||
targetableObject,
|
targetableObject,
|
||||||
timeline,
|
|
||||||
tasks,
|
|
||||||
notes,
|
|
||||||
emails,
|
|
||||||
loading,
|
loading,
|
||||||
fieldsBox,
|
|
||||||
summaryCard,
|
|
||||||
isInRightDrawer = false,
|
isInRightDrawer = false,
|
||||||
}: ShowPageRightContainerProps) => {
|
isNewRightDrawerItemLoading = false,
|
||||||
|
}: ShowPageSubContainerProps) => {
|
||||||
const { activeTabIdState } = useTabList(
|
const { activeTabIdState } = useTabList(
|
||||||
`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`,
|
`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`,
|
||||||
);
|
);
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
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 isMobile = useIsMobile();
|
||||||
|
|
||||||
const isNewViewableRecordLoading = useRecoilValue(
|
const isNewViewableRecordLoading = useRecoilValue(
|
||||||
isNewViewableRecordLoadingState,
|
isNewViewableRecordLoadingState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const tabs = [
|
const summaryCard = (
|
||||||
{
|
<SummaryCard
|
||||||
id: 'richText',
|
objectNameSingular={targetableObject.targetObjectNameSingular}
|
||||||
title: 'Note',
|
objectRecordId={targetableObject.id}
|
||||||
Icon: IconNotes,
|
isNewRightDrawerItemLoading={isNewRightDrawerItemLoading}
|
||||||
hide:
|
isInRightDrawer={isInRightDrawer}
|
||||||
loading ||
|
/>
|
||||||
(targetableObject.targetObjectNameSingular !==
|
);
|
||||||
CoreObjectNameSingular.Note &&
|
|
||||||
targetableObject.targetObjectNameSingular !==
|
const fieldsCard = (
|
||||||
CoreObjectNameSingular.Task),
|
<FieldsCard
|
||||||
},
|
objectNameSingular={targetableObject.targetObjectNameSingular}
|
||||||
{
|
objectRecordId={targetableObject.id}
|
||||||
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 renderActiveTabContent = () => {
|
const renderActiveTabContent = () => {
|
||||||
switch (activeTabId) {
|
switch (activeTabId) {
|
||||||
case 'timeline':
|
case 'timeline':
|
||||||
@ -251,10 +149,9 @@ export const ShowPageRightContainer = ({
|
|||||||
case 'fields':
|
case 'fields':
|
||||||
return (
|
return (
|
||||||
<StyledGreyBox isInRightDrawer={isInRightDrawer}>
|
<StyledGreyBox isInRightDrawer={isInRightDrawer}>
|
||||||
{fieldsBox}
|
{fieldsCard}
|
||||||
</StyledGreyBox>
|
</StyledGreyBox>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'tasks':
|
case 'tasks':
|
||||||
return <ObjectTasks targetableObject={targetableObject} />;
|
return <ObjectTasks targetableObject={targetableObject} />;
|
||||||
case 'notes':
|
case 'notes':
|
||||||
@ -307,28 +204,36 @@ export const ShowPageRightContainer = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledShowPageRightContainer isMobile={isMobile}>
|
<>
|
||||||
<StyledTabListContainer>
|
{!isMobile && !isInRightDrawer && (
|
||||||
<TabList
|
<ShowPageLeftContainer forceMobile={isMobile}>
|
||||||
loading={loading || isNewViewableRecordLoading}
|
{summaryCard}
|
||||||
tabListId={`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`}
|
{fieldsCard}
|
||||||
tabs={tabs}
|
</ShowPageLeftContainer>
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
)}
|
)}
|
||||||
</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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -9,7 +9,7 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
|||||||
|
|
||||||
import { Tab } from './Tab';
|
import { Tab } from './Tab';
|
||||||
|
|
||||||
type SingleTabProps = {
|
export type SingleTabProps = {
|
||||||
title: string;
|
title: string;
|
||||||
Icon?: IconComponent;
|
Icon?: IconComponent;
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user