feat: enable export of deleted records (#12776)

resolve #12662
This PR enables exporting deleted records by detecting when deleted view
mode is active and adding deletedAt: { is: 'NOT_NULL' } to
graphqlFilter.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Naifer
2025-07-04 21:58:24 +01:00
committed by GitHub
parent 1729970836
commit 324dadca63
15 changed files with 186 additions and 48 deletions

View File

@ -2,7 +2,10 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { displayedExportProgress, generateCsv } from '../useExportRecords';
import {
displayedExportProgress,
generateCsv,
} from '../useRecordIndexExportRecords';
jest.useFakeTimers();

View File

@ -3,8 +3,8 @@ import { act } from 'react';
import {
percentage,
sleep,
useExportFetchRecords,
} from '../useExportFetchRecords';
useRecordIndexLazyFetchRecords,
} from '../useRecordIndexLazyFetchRecords';
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
@ -107,7 +107,7 @@ describe('useRecordData', () => {
const { result } = renderHook(
() =>
useExportFetchRecords({
useRecordIndexLazyFetchRecords({
recordIndexId,
objectMetadataItem,
pageSize: 30,
@ -134,7 +134,7 @@ describe('useRecordData', () => {
mockFetchAllRecords.mockReturnValue([mockPerson]);
const { result } = renderHook(
() =>
useExportFetchRecords({
useRecordIndexLazyFetchRecords({
recordIndexId,
objectMetadataItem,
callback,
@ -167,7 +167,7 @@ describe('useRecordData', () => {
);
return {
tableData: useExportFetchRecords({
tableData: useRecordIndexLazyFetchRecords({
recordIndexId,
objectMetadataItem,
callback,
@ -260,7 +260,7 @@ describe('useRecordData', () => {
);
return {
tableData: useExportFetchRecords({
tableData: useRecordIndexLazyFetchRecords({
recordIndexId,
objectMetadataItem,
callback,

View File

@ -7,8 +7,8 @@ import { useExportProcessRecordsForCSV } from '@/object-record/object-options-dr
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import {
UseRecordDataOptions,
useExportFetchRecords,
} from '@/object-record/record-index/export/hooks/useExportFetchRecords';
useRecordIndexLazyFetchRecords,
} from '@/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
@ -20,7 +20,10 @@ import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
type GenerateExportOptions = {
columns: ColumnDefinition<FieldMetadata>[];
columns: Pick<
ColumnDefinition<FieldMetadata>,
'size' | 'label' | 'type' | 'metadata'
>[];
rows: Record<string, any>[];
};
@ -121,7 +124,7 @@ type UseExportTableDataOptions = Omit<UseRecordDataOptions, 'callback'> & {
filename: string;
};
export const useExportRecords = ({
export const useRecordIndexExportRecords = ({
delayMs,
filename,
maximumRequests = 100,
@ -144,7 +147,7 @@ export const useExportRecords = ({
[filename, processRecordsForCSVExport],
);
const { getTableData: download, progress } = useExportFetchRecords({
const { getTableData: download, progress } = useRecordIndexLazyFetchRecords({
delayMs,
maximumRequests,
objectMetadataItem,

View File

@ -36,7 +36,7 @@ export type UseRecordDataOptions = {
viewType?: ViewType;
};
export const useExportFetchRecords = ({
export const useRecordIndexLazyFetchRecords = ({
objectMetadataItem,
delayMs,
maximumRequests = 100,
@ -74,6 +74,10 @@ export const useExportFetchRecords = ({
const { filterValueDependencies } = useFilterValueDependencies();
const findManyRecordsParams = useFindManyRecordIndexTableParams(
objectMetadataItem.nameSingular,
);
const queryFilter = computeContextStoreFilters(
contextStoreTargetedRecordsRule,
contextStoreFilters,
@ -81,10 +85,6 @@ export const useExportFetchRecords = ({
filterValueDependencies,
);
const findManyRecordsParams = useFindManyRecordIndexTableParams(
objectMetadataItem.nameSingular,
);
const finalColumns = [
...columns,
...(hiddenKanbanFieldColumn && viewType === ViewType.Kanban

View File

@ -0,0 +1,69 @@
import { useMemo } from 'react';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useExportProcessRecordsForCSV } from '@/object-record/object-options-dropdown/hooks/useExportProcessRecordsForCSV';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { csvDownloader } from '@/object-record/record-index/export/hooks/useRecordIndexExportRecords';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
export type UseSingleExportTableDataOptions = {
filename: string;
objectMetadataItem: ObjectMetadataItem;
recordId: string;
};
export const useExportSingleRecord = ({
filename,
objectMetadataItem,
recordId,
}: UseSingleExportTableDataOptions) => {
const { processRecordsForCSVExport } = useExportProcessRecordsForCSV(
objectMetadataItem.nameSingular,
);
const downloadCsv = useMemo(
() =>
(
record: ObjectRecord,
columns: Pick<
ColumnDefinition<FieldMetadata>,
'size' | 'label' | 'type' | 'metadata'
>[],
) => {
const recordToArray = [record];
const recordsProcessedForExport =
processRecordsForCSVExport(recordToArray);
csvDownloader(filename, { rows: recordsProcessedForExport, columns });
},
[filename, processRecordsForCSVExport],
);
const columns: Pick<
ColumnDefinition<FieldMetadata>,
'size' | 'label' | 'type' | 'metadata'
>[] = objectMetadataItem.fields
.filter((field) => field.isActive)
.map((field, index) =>
formatFieldMetadataItemAsColumnDefinition({
field,
objectMetadataItem,
position: index,
}),
);
const { record, error } = useFindOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
objectRecordId: recordId,
withSoftDeleted: true,
});
const download = () => {
if (isDefined(error) || !isDefined(record)) {
return;
}
downloadCsv(record, columns);
};
return { download };
};