feat: soft delete (#6576)
Implement soft delete on standards and custom objects. This is a temporary solution, when we drop `pg_graphql` we should rely on the `softDelete` functions of TypeORM. --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -13,7 +13,7 @@ import { isDefined } from '~/utils/isDefined';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useDeleteOneRecordProps = {
|
||||
type useDeleteManyRecordProps = {
|
||||
objectNameSingular: string;
|
||||
refetchFindManyQuery?: boolean;
|
||||
};
|
||||
@ -25,7 +25,7 @@ type DeleteManyRecordsOptions = {
|
||||
|
||||
export const useDeleteManyRecords = ({
|
||||
objectNameSingular,
|
||||
}: useDeleteOneRecordProps) => {
|
||||
}: useDeleteManyRecordProps) => {
|
||||
const apiConfig = useRecoilValue(apiConfigState);
|
||||
|
||||
const mutationPageSize =
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
|
||||
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useDestroyManyRecordsMutation = ({
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
if (isUndefinedOrNull(objectMetadataItem)) {
|
||||
return { destroyManyRecordsMutation: EMPTY_MUTATION };
|
||||
}
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.namePlural);
|
||||
|
||||
const mutationResponseField = getDestroyManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
const destroyManyRecordsMutation = gql`
|
||||
mutation DestroyMany${capitalizedObjectName}($filter: ${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}FilterInput!) {
|
||||
${mutationResponseField}(filter: $filter) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
destroyManyRecordsMutation,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,115 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
|
||||
import { useDestroyManyRecordsMutation } from '@/object-record/hooks/useDestroyManyRecordMutation';
|
||||
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useDestroyManyRecordProps = {
|
||||
objectNameSingular: string;
|
||||
refetchFindManyQuery?: boolean;
|
||||
};
|
||||
|
||||
type DestroyManyRecordsOptions = {
|
||||
skipOptimisticEffect?: boolean;
|
||||
delayInMsBetweenRequests?: number;
|
||||
};
|
||||
|
||||
export const useDestroyManyRecords = ({
|
||||
objectNameSingular,
|
||||
}: useDestroyManyRecordProps) => {
|
||||
const apiConfig = useRecoilValue(apiConfigState);
|
||||
|
||||
const mutationPageSize =
|
||||
apiConfig?.mutationMaximumAffectedRecords ?? DEFAULT_MUTATION_BATCH_SIZE;
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { destroyManyRecordsMutation } = useDestroyManyRecordsMutation({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const mutationResponseField = getDestroyManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
const destroyManyRecords = async (
|
||||
idsToDestroy: string[],
|
||||
options?: DestroyManyRecordsOptions,
|
||||
) => {
|
||||
const numberOfBatches = Math.ceil(idsToDestroy.length / mutationPageSize);
|
||||
|
||||
const destroyedRecords = [];
|
||||
|
||||
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
||||
const batchIds = idsToDestroy.slice(
|
||||
batchIndex * mutationPageSize,
|
||||
(batchIndex + 1) * mutationPageSize,
|
||||
);
|
||||
|
||||
const destroyedRecordsResponse = await apolloClient.mutate({
|
||||
mutation: destroyManyRecordsMutation,
|
||||
variables: {
|
||||
filter: { id: { in: batchIds } },
|
||||
},
|
||||
optimisticResponse: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: {
|
||||
[mutationResponseField]: batchIds.map((idToDestroy) => ({
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToDestroy,
|
||||
})),
|
||||
},
|
||||
update: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: (cache, { data }) => {
|
||||
const records = data?.[mutationResponseField];
|
||||
|
||||
if (!records?.length) return;
|
||||
|
||||
const cachedRecords = records
|
||||
.map((record) => getRecordFromCache(record.id, cache))
|
||||
.filter(isDefined);
|
||||
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToDelete: cachedRecords,
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const destroyedRecordsForThisBatch =
|
||||
destroyedRecordsResponse.data?.[mutationResponseField] ?? [];
|
||||
|
||||
destroyedRecords.push(...destroyedRecordsForThisBatch);
|
||||
|
||||
if (isDefined(options?.delayInMsBetweenRequests)) {
|
||||
await sleep(options.delayInMsBetweenRequests);
|
||||
}
|
||||
}
|
||||
|
||||
return destroyedRecords;
|
||||
};
|
||||
|
||||
return { destroyManyRecords };
|
||||
};
|
||||
@ -17,11 +17,13 @@ export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
|
||||
recordGqlFields,
|
||||
onCompleted,
|
||||
skip,
|
||||
withSoftDeleted = false,
|
||||
}: ObjectMetadataItemIdentifier & {
|
||||
objectRecordId: string | undefined;
|
||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||
onCompleted?: (data: T) => void;
|
||||
skip?: boolean;
|
||||
withSoftDeleted?: boolean;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
@ -33,6 +35,7 @@ export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
|
||||
const { findOneRecordQuery } = useFindOneRecordQuery({
|
||||
objectNameSingular,
|
||||
recordGqlFields: computedRecordGqlFields,
|
||||
withSoftDeleted,
|
||||
});
|
||||
|
||||
const { data, loading, error } = useQuery<{
|
||||
|
||||
@ -10,9 +10,11 @@ import { capitalize } from '~/utils/string/capitalize';
|
||||
export const useFindOneRecordQuery = ({
|
||||
objectNameSingular,
|
||||
recordGqlFields,
|
||||
withSoftDeleted = false,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||
withSoftDeleted?: boolean;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
@ -25,6 +27,16 @@ export const useFindOneRecordQuery = ({
|
||||
objectMetadataItem.nameSingular,
|
||||
)}($objectRecordId: ID!) {
|
||||
${objectMetadataItem.nameSingular}(filter: {
|
||||
${
|
||||
withSoftDeleted
|
||||
? `
|
||||
or: [
|
||||
{ deletedAt: { is: NULL } },
|
||||
{ deletedAt: { is: NOT_NULL } }
|
||||
],
|
||||
`
|
||||
: ''
|
||||
}
|
||||
id: {
|
||||
eq: $objectRecordId
|
||||
}
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
|
||||
import { useRestoreManyRecordsMutation } from '@/object-record/hooks/useRestoreManyRecordsMutation';
|
||||
import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useRestoreManyRecordProps = {
|
||||
objectNameSingular: string;
|
||||
refetchFindManyQuery?: boolean;
|
||||
};
|
||||
|
||||
type RestoreManyRecordsOptions = {
|
||||
skipOptimisticEffect?: boolean;
|
||||
delayInMsBetweenRequests?: number;
|
||||
};
|
||||
|
||||
export const useRestoreManyRecords = ({
|
||||
objectNameSingular,
|
||||
}: useRestoreManyRecordProps) => {
|
||||
const apiConfig = useRecoilValue(apiConfigState);
|
||||
|
||||
const mutationPageSize =
|
||||
apiConfig?.mutationMaximumAffectedRecords ?? DEFAULT_MUTATION_BATCH_SIZE;
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { restoreManyRecordsMutation } = useRestoreManyRecordsMutation({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const mutationResponseField = getRestoreManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
const restoreManyRecords = async (
|
||||
idsToRestore: string[],
|
||||
options?: RestoreManyRecordsOptions,
|
||||
) => {
|
||||
const numberOfBatches = Math.ceil(idsToRestore.length / mutationPageSize);
|
||||
|
||||
const restoredRecords = [];
|
||||
|
||||
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
||||
const batchIds = idsToRestore.slice(
|
||||
batchIndex * mutationPageSize,
|
||||
(batchIndex + 1) * mutationPageSize,
|
||||
);
|
||||
|
||||
// TODO: fix optimistic effect
|
||||
const findOneQueryName = `FindOne${capitalize(objectNameSingular)}`;
|
||||
const findManyQueryName = `FindMany${capitalize(objectMetadataItem.namePlural)}`;
|
||||
|
||||
const restoredRecordsResponse = await apolloClient.mutate({
|
||||
mutation: restoreManyRecordsMutation,
|
||||
refetchQueries: [findOneQueryName, findManyQueryName],
|
||||
variables: {
|
||||
filter: { id: { in: batchIds } },
|
||||
},
|
||||
optimisticResponse: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: {
|
||||
[mutationResponseField]: batchIds.map((idToRestore) => ({
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToRestore,
|
||||
deletedAt: null,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
const restoredRecordsForThisBatch =
|
||||
restoredRecordsResponse.data?.[mutationResponseField] ?? [];
|
||||
|
||||
restoredRecords.push(...restoredRecordsForThisBatch);
|
||||
|
||||
if (isDefined(options?.delayInMsBetweenRequests)) {
|
||||
await sleep(options.delayInMsBetweenRequests);
|
||||
}
|
||||
}
|
||||
|
||||
return restoredRecords;
|
||||
};
|
||||
|
||||
return { restoreManyRecords };
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
|
||||
import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useRestoreManyRecordsMutation = ({
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
if (isUndefinedOrNull(objectMetadataItem)) {
|
||||
return { restoreManyRecordsMutation: EMPTY_MUTATION };
|
||||
}
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.namePlural);
|
||||
|
||||
const mutationResponseField = getRestoreManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
const restoreManyRecordsMutation = gql`
|
||||
mutation RestoreMany${capitalizedObjectName}($filter: ${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}FilterInput!) {
|
||||
${mutationResponseField}(filter: $filter) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
restoreManyRecordsMutation,
|
||||
};
|
||||
};
|
||||
@ -4,6 +4,7 @@ import { FilterDefinition } from './FilterDefinition';
|
||||
|
||||
export type Filter = {
|
||||
id: string;
|
||||
variant?: 'default' | 'danger';
|
||||
fieldMetadataId: string;
|
||||
value: string;
|
||||
displayValue: string;
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
import { useCallback } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||
import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type UseHandleToggleTrashColumnFilterProps = {
|
||||
objectNameSingular: string;
|
||||
viewBarId: string;
|
||||
};
|
||||
|
||||
export const useHandleToggleTrashColumnFilter = ({
|
||||
viewBarId,
|
||||
objectNameSingular,
|
||||
}: UseHandleToggleTrashColumnFilterProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { columnDefinitions } =
|
||||
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
||||
|
||||
const { upsertCombinedViewFilter } = useCombinedViewFilters(viewBarId);
|
||||
|
||||
const handleToggleTrashColumnFilter = useCallback(() => {
|
||||
const trashFieldMetadata = objectMetadataItem.fields.find(
|
||||
(field) => field.name === 'deletedAt',
|
||||
);
|
||||
|
||||
if (!isDefined(trashFieldMetadata)) return;
|
||||
|
||||
const correspondingColumnDefinition = columnDefinitions.find(
|
||||
(columnDefinition) =>
|
||||
columnDefinition.fieldMetadataId === trashFieldMetadata.id,
|
||||
);
|
||||
|
||||
if (!isDefined(correspondingColumnDefinition)) return;
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(
|
||||
correspondingColumnDefinition?.type,
|
||||
);
|
||||
|
||||
const newFilter: Filter = {
|
||||
id: v4(),
|
||||
variant: 'danger',
|
||||
fieldMetadataId: trashFieldMetadata.id,
|
||||
operand: ViewFilterOperand.IsNotEmpty,
|
||||
displayValue: '',
|
||||
definition: {
|
||||
label: 'Trash',
|
||||
iconName: 'IconTrash',
|
||||
fieldMetadataId: trashFieldMetadata.id,
|
||||
type: filterType,
|
||||
},
|
||||
value: '',
|
||||
};
|
||||
|
||||
upsertCombinedViewFilter(newFilter);
|
||||
}, [columnDefinitions, objectMetadataItem.fields, upsertCombinedViewFilter]);
|
||||
|
||||
return handleToggleTrashColumnFilter;
|
||||
};
|
||||
@ -8,6 +8,7 @@ import {
|
||||
IconFileImport,
|
||||
IconSettings,
|
||||
IconTag,
|
||||
IconTrash,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
|
||||
@ -37,6 +38,7 @@ import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
|
||||
|
||||
type RecordIndexOptionsMenu = 'fields' | 'hiddenFields';
|
||||
|
||||
@ -88,6 +90,11 @@ export const RecordIndexOptionsDropdownContent = ({
|
||||
hiddenTableColumns,
|
||||
} = useRecordIndexOptionsForTable(recordIndexId);
|
||||
|
||||
const handleToggleTrashColumnFilter = useHandleToggleTrashColumnFilter({
|
||||
objectNameSingular,
|
||||
viewBarId: recordIndexId,
|
||||
});
|
||||
|
||||
const {
|
||||
visibleBoardFields,
|
||||
hiddenBoardFields,
|
||||
@ -153,6 +160,14 @@ export const RecordIndexOptionsDropdownContent = ({
|
||||
LeftIcon={IconFileExport}
|
||||
text={displayedExportProgress(progress)}
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleToggleTrashColumnFilter();
|
||||
closeDropdown();
|
||||
}}
|
||||
LeftIcon={IconTrash}
|
||||
text="Trash"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
{currentMenu === 'fields' && (
|
||||
|
||||
@ -4,6 +4,7 @@ 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 { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
@ -126,7 +127,11 @@ export const RecordShowContainer = ({
|
||||
);
|
||||
|
||||
const { inlineFieldMetadataItems, relationFieldMetadataItems } = groupBy(
|
||||
availableFieldMetadataItems,
|
||||
availableFieldMetadataItems.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.name !== 'createdAt' &&
|
||||
fieldMetadataItem.name !== 'deletedAt',
|
||||
),
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation
|
||||
? 'relationFieldMetadataItems'
|
||||
@ -301,25 +306,33 @@ export const RecordShowContainer = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<ShowPageContainer>
|
||||
<ShowPageLeftContainer forceMobile={isMobile}>
|
||||
{!isMobile && summaryCard}
|
||||
{!isMobile && fieldsBox}
|
||||
</ShowPageLeftContainer>
|
||||
<ShowPageRightContainer
|
||||
targetableObject={{
|
||||
id: objectRecordId,
|
||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||
}}
|
||||
timeline
|
||||
tasks
|
||||
notes
|
||||
emails
|
||||
isInRightDrawer={isInRightDrawer}
|
||||
summaryCard={isMobile ? summaryCard : <></>}
|
||||
fieldsBox={fieldsBox}
|
||||
loading={isPrefetchLoading || loading || recordLoading}
|
||||
/>
|
||||
</ShowPageContainer>
|
||||
<>
|
||||
{recordFromStore && recordFromStore.deletedAt && (
|
||||
<InformationBannerDeletedRecord
|
||||
recordId={objectRecordId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
)}
|
||||
<ShowPageContainer>
|
||||
<ShowPageLeftContainer forceMobile={isMobile}>
|
||||
{!isMobile && summaryCard}
|
||||
{!isMobile && fieldsBox}
|
||||
</ShowPageLeftContainer>
|
||||
<ShowPageRightContainer
|
||||
targetableObject={{
|
||||
id: objectRecordId,
|
||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||
}}
|
||||
timeline
|
||||
tasks
|
||||
notes
|
||||
emails
|
||||
isInRightDrawer={isInRightDrawer}
|
||||
summaryCard={isMobile ? summaryCard : <></>}
|
||||
fieldsBox={fieldsBox}
|
||||
loading={isPrefetchLoading || loading || recordLoading}
|
||||
/>
|
||||
</ShowPageContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -29,7 +29,7 @@ export const buildFindOneRecordForShowPageOperationSignature: RecordGqlOperation
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(objectMetadataItem.nameSingular === 'Note'
|
||||
...(objectMetadataItem.nameSingular === CoreObjectNameSingular.Note
|
||||
? {
|
||||
noteTargets: {
|
||||
id: true,
|
||||
|
||||
@ -50,6 +50,7 @@ export const useRecordShowPage = (
|
||||
objectRecordId,
|
||||
objectNameSingular,
|
||||
recordGqlFields: FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE.fields,
|
||||
withSoftDeleted: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getDestroyManyRecordsMutationResponseField = (
|
||||
objectNamePlural: string,
|
||||
) => `destroy${capitalize(objectNamePlural)}`;
|
||||
@ -0,0 +1,5 @@
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getRestoreManyRecordsMutationResponseField = (
|
||||
objectNamePlural: string,
|
||||
) => `restore${capitalize(objectNamePlural)}`;
|
||||
Reference in New Issue
Block a user