Import v2 - add label for subfield in csv export (#12355)
To test : Export companies record - "Domain Name PrimaryLinkUrl" >> "Domain Name / Link URL" closes https://github.com/twentyhq/core-team-issues/issues/907
This commit is contained in:
@ -1,7 +1,10 @@
|
|||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||||
|
|
||||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
import {
|
||||||
|
FieldMetadataType,
|
||||||
|
RelationDefinitionType,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
import { displayedExportProgress, generateCsv } from '../useExportRecords';
|
import { displayedExportProgress, generateCsv } from '../useExportRecords';
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
@ -11,7 +14,11 @@ describe('generateCsv', () => {
|
|||||||
const columns = [
|
const columns = [
|
||||||
{ label: 'Foo', metadata: { fieldName: 'foo' } },
|
{ label: 'Foo', metadata: { fieldName: 'foo' } },
|
||||||
{ label: 'Empty', metadata: { fieldName: 'empty' } },
|
{ label: 'Empty', metadata: { fieldName: 'empty' } },
|
||||||
{ label: 'Nested', metadata: { fieldName: 'nested' } },
|
{
|
||||||
|
label: 'Nested link field',
|
||||||
|
type: FieldMetadataType.LINKS,
|
||||||
|
metadata: { fieldName: 'nestedLinkField' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Relation',
|
label: 'Relation',
|
||||||
metadata: {
|
metadata: {
|
||||||
@ -26,13 +33,21 @@ describe('generateCsv', () => {
|
|||||||
bar: 'another field',
|
bar: 'another field',
|
||||||
empty: null,
|
empty: null,
|
||||||
foo: 'some field',
|
foo: 'some field',
|
||||||
nested: { __typename: 'type', foo: 'foo', nested: 'nested' },
|
nestedLinkField: {
|
||||||
|
__typename: 'Links',
|
||||||
|
primaryLinkUrl: 'https://www.test.com',
|
||||||
|
secondaryLinks: [
|
||||||
|
{ label: 'secondary link 1', url: 'https://www.test.com' },
|
||||||
|
{ label: 'secondary link 2', url: 'https://www.test.com' },
|
||||||
|
],
|
||||||
|
},
|
||||||
relation: 'a relation',
|
relation: 'a relation',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const csv = generateCsv({ columns, rows });
|
const csv = generateCsv({ columns, rows });
|
||||||
expect(csv).toEqual(`Id,Foo,Empty,Nested Foo,Nested Nested,Relation
|
expect(csv)
|
||||||
1,some field,,foo,nested,a relation`);
|
.toEqual(`Id,Foo,Empty,Nested link field / Link URL,Nested link field / Secondary Links,Relation
|
||||||
|
1,some field,,https://www.test.com,"[{""label"":""secondary link 1"",""url"":""https://www.test.com""},{""label"":""secondary link 2"",""url"":""https://www.test.com""}]",a relation`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { json2csv } from 'json-2-csv';
|
import { json2csv } from 'json-2-csv';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||||
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize';
|
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize';
|
||||||
import { useExportProcessRecordsForCSV } from '@/object-record/object-options-dropdown/hooks/useExportProcessRecordsForCSV';
|
import { useExportProcessRecordsForCSV } from '@/object-record/object-options-dropdown/hooks/useExportProcessRecordsForCSV';
|
||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
@ -10,16 +11,17 @@ import {
|
|||||||
} from '@/object-record/record-index/export/hooks/useExportFetchRecords';
|
} from '@/object-record/record-index/export/hooks/useExportFetchRecords';
|
||||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
|
|
||||||
type GenerateExportOptions = {
|
type GenerateExportOptions = {
|
||||||
columns: ColumnDefinition<FieldMetadata>[];
|
columns: ColumnDefinition<FieldMetadata>[];
|
||||||
rows: object[];
|
rows: Record<string, any>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type GenerateExport = (data: GenerateExportOptions) => string;
|
type GenerateExport = (data: GenerateExportOptions) => string;
|
||||||
@ -62,31 +64,20 @@ export const generateCsv: GenerateExport = ({
|
|||||||
.join(' '),
|
.join(' '),
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldsWithSubFields = rows.find((row) => {
|
const columnType = col.type;
|
||||||
const fieldValue = (row as any)[column.field];
|
if (!isCompositeFieldType(columnType)) return [column];
|
||||||
|
|
||||||
const hasSubFields =
|
const nestedFieldsWithoutTypename = Object.keys(rows[0][column.field])
|
||||||
fieldValue &&
|
.filter((key) => key !== '__typename')
|
||||||
typeof fieldValue === 'object' &&
|
.map((key) => {
|
||||||
!Array.isArray(fieldValue);
|
const subFieldLabel = COMPOSITE_FIELD_SUB_FIELD_LABELS[columnType][key];
|
||||||
|
return {
|
||||||
return hasSubFields;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isDefined(fieldsWithSubFields)) {
|
|
||||||
const nestedFieldsWithoutTypename = Object.keys(
|
|
||||||
(fieldsWithSubFields as any)[column.field],
|
|
||||||
)
|
|
||||||
.filter((key) => key !== '__typename')
|
|
||||||
.map((key) => ({
|
|
||||||
field: `${column.field}.${key}`,
|
field: `${column.field}.${key}`,
|
||||||
title: `${column.title} ${key[0].toUpperCase() + key.slice(1)}`,
|
title: `${column.title} / ${subFieldLabel}`,
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return nestedFieldsWithoutTypename;
|
return nestedFieldsWithoutTypename;
|
||||||
}
|
|
||||||
|
|
||||||
return [column];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return json2csv(rows, {
|
return json2csv(rows, {
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
export const COMPOSITE_FIELD_SUB_FIELD_LABELS: {
|
||||||
|
[key in CompositeFieldType]: Record<string, string>;
|
||||||
|
} = {
|
||||||
|
[FieldMetadataType.CURRENCY]: {
|
||||||
|
amountMicros: 'Amount',
|
||||||
|
currencyCode: 'Currency',
|
||||||
|
},
|
||||||
|
[FieldMetadataType.EMAILS]: {
|
||||||
|
primaryEmail: 'Primary Email',
|
||||||
|
additionalEmails: 'Additional Emails',
|
||||||
|
},
|
||||||
|
[FieldMetadataType.LINKS]: {
|
||||||
|
primaryLinkLabel: 'Link Label',
|
||||||
|
primaryLinkUrl: 'Link URL',
|
||||||
|
secondaryLinks: 'Secondary Links',
|
||||||
|
},
|
||||||
|
[FieldMetadataType.PHONES]: {
|
||||||
|
primaryPhoneNumber: 'Primary Phone Number',
|
||||||
|
primaryPhoneCountryCode: 'Primary Phone Country Code',
|
||||||
|
primaryPhoneCallingCode: 'Primary Phone Calling Code',
|
||||||
|
additionalPhones: 'Additional Phones',
|
||||||
|
},
|
||||||
|
[FieldMetadataType.FULL_NAME]: {
|
||||||
|
firstName: 'First Name',
|
||||||
|
lastName: 'Last Name',
|
||||||
|
},
|
||||||
|
[FieldMetadataType.ADDRESS]: {
|
||||||
|
addressStreet1: 'Address 1',
|
||||||
|
addressStreet2: 'Address 2',
|
||||||
|
addressCity: 'City',
|
||||||
|
addressState: 'State',
|
||||||
|
addressCountry: 'Country',
|
||||||
|
addressPostcode: 'Post Code',
|
||||||
|
addressLat: 'Latitude',
|
||||||
|
addressLng: 'Longitude',
|
||||||
|
},
|
||||||
|
[FieldMetadataType.ACTOR]: {
|
||||||
|
source: 'Source',
|
||||||
|
name: 'Name',
|
||||||
|
workspaceMemberId: 'Workspace Member ID',
|
||||||
|
context: 'Context',
|
||||||
|
},
|
||||||
|
[FieldMetadataType.RICH_TEXT_V2]: {
|
||||||
|
blocknote: 'BlockNote',
|
||||||
|
markdown: 'Markdown',
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -9,6 +9,7 @@ import {
|
|||||||
FieldPhonesValue,
|
FieldPhonesValue,
|
||||||
FieldRichTextV2Value,
|
FieldRichTextV2Value,
|
||||||
} from '@/object-record/record-field/types/FieldMetadata';
|
} from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
|
||||||
import { SettingsFieldTypeConfig } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs';
|
import { SettingsFieldTypeConfig } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs';
|
||||||
import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
|
import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
|
||||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||||
@ -43,8 +44,12 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
|||||||
subFields: ['amountMicros', 'currencyCode'],
|
subFields: ['amountMicros', 'currencyCode'],
|
||||||
filterableSubFields: ['amountMicros', 'currencyCode'],
|
filterableSubFields: ['amountMicros', 'currencyCode'],
|
||||||
labelBySubField: {
|
labelBySubField: {
|
||||||
amountMicros: 'Amount',
|
amountMicros:
|
||||||
currencyCode: 'Currency',
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.CURRENCY]
|
||||||
|
.amountMicros,
|
||||||
|
currencyCode:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.CURRENCY]
|
||||||
|
.currencyCode,
|
||||||
},
|
},
|
||||||
exampleValue: {
|
exampleValue: {
|
||||||
amountMicros: 2000000000,
|
amountMicros: 2000000000,
|
||||||
@ -87,9 +92,15 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
|||||||
'secondaryLinks',
|
'secondaryLinks',
|
||||||
],
|
],
|
||||||
labelBySubField: {
|
labelBySubField: {
|
||||||
primaryLinkUrl: 'Link URL',
|
primaryLinkUrl:
|
||||||
primaryLinkLabel: 'Link Label',
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
|
||||||
secondaryLinks: 'Secondary Links',
|
.primaryLinkUrl,
|
||||||
|
primaryLinkLabel:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
|
||||||
|
.primaryLinkLabel,
|
||||||
|
secondaryLinks:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
|
||||||
|
.secondaryLinks,
|
||||||
},
|
},
|
||||||
} as const satisfies SettingsCompositeFieldTypeConfig<FieldLinksValue>,
|
} as const satisfies SettingsCompositeFieldTypeConfig<FieldLinksValue>,
|
||||||
[FieldMetadataType.PHONES]: {
|
[FieldMetadataType.PHONES]: {
|
||||||
@ -115,10 +126,18 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
|||||||
'additionalPhones',
|
'additionalPhones',
|
||||||
],
|
],
|
||||||
labelBySubField: {
|
labelBySubField: {
|
||||||
primaryPhoneNumber: 'Primary Phone Number',
|
primaryPhoneNumber:
|
||||||
primaryPhoneCountryCode: 'Primary Phone Country Code',
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.PHONES]
|
||||||
primaryPhoneCallingCode: 'Primary Phone Calling Code',
|
.primaryPhoneNumber,
|
||||||
additionalPhones: 'Additional Phones',
|
primaryPhoneCountryCode:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.PHONES]
|
||||||
|
.primaryPhoneCountryCode,
|
||||||
|
primaryPhoneCallingCode:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.PHONES]
|
||||||
|
.primaryPhoneCallingCode,
|
||||||
|
additionalPhones:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.PHONES]
|
||||||
|
.additionalPhones,
|
||||||
},
|
},
|
||||||
category: 'Basic',
|
category: 'Basic',
|
||||||
} as const satisfies SettingsCompositeFieldTypeConfig<FieldPhonesValue>,
|
} as const satisfies SettingsCompositeFieldTypeConfig<FieldPhonesValue>,
|
||||||
@ -130,8 +149,10 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
|||||||
subFields: ['firstName', 'lastName'],
|
subFields: ['firstName', 'lastName'],
|
||||||
filterableSubFields: ['firstName', 'lastName'],
|
filterableSubFields: ['firstName', 'lastName'],
|
||||||
labelBySubField: {
|
labelBySubField: {
|
||||||
firstName: 'First Name',
|
firstName:
|
||||||
lastName: 'Last Name',
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.FULL_NAME].firstName,
|
||||||
|
lastName:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.FULL_NAME].lastName,
|
||||||
},
|
},
|
||||||
} as const satisfies SettingsCompositeFieldTypeConfig<FieldFullNameValue>,
|
} as const satisfies SettingsCompositeFieldTypeConfig<FieldFullNameValue>,
|
||||||
[FieldMetadataType.ADDRESS]: {
|
[FieldMetadataType.ADDRESS]: {
|
||||||
@ -156,14 +177,27 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
|||||||
'addressPostcode',
|
'addressPostcode',
|
||||||
],
|
],
|
||||||
labelBySubField: {
|
labelBySubField: {
|
||||||
addressStreet1: 'Address 1',
|
addressStreet1:
|
||||||
addressStreet2: 'Address 2',
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
|
||||||
addressCity: 'City',
|
.addressStreet1,
|
||||||
addressState: 'State',
|
addressStreet2:
|
||||||
addressCountry: 'Country',
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
|
||||||
addressPostcode: 'Post Code',
|
.addressStreet2,
|
||||||
addressLat: 'Latitude',
|
addressCity:
|
||||||
addressLng: 'Longitude',
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS].addressCity,
|
||||||
|
addressState:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
|
||||||
|
.addressState,
|
||||||
|
addressCountry:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
|
||||||
|
.addressCountry,
|
||||||
|
addressPostcode:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
|
||||||
|
.addressPostcode,
|
||||||
|
addressLat:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS].addressLat,
|
||||||
|
addressLng:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS].addressLng,
|
||||||
},
|
},
|
||||||
exampleValue: {
|
exampleValue: {
|
||||||
addressStreet1: '456 Oak Street',
|
addressStreet1: '456 Oak Street',
|
||||||
@ -184,10 +218,13 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
|||||||
subFields: ['source', 'name'],
|
subFields: ['source', 'name'],
|
||||||
filterableSubFields: ['source', 'name'],
|
filterableSubFields: ['source', 'name'],
|
||||||
labelBySubField: {
|
labelBySubField: {
|
||||||
source: 'Source',
|
source: COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].source,
|
||||||
name: 'Name',
|
name: COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].name,
|
||||||
workspaceMemberId: 'Workspace Member ID',
|
workspaceMemberId:
|
||||||
context: 'Context',
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR]
|
||||||
|
.workspaceMemberId,
|
||||||
|
context:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].context,
|
||||||
},
|
},
|
||||||
exampleValue: {
|
exampleValue: {
|
||||||
source: 'IMPORT',
|
source: 'IMPORT',
|
||||||
@ -202,8 +239,12 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
|||||||
subFields: ['blocknote', 'markdown'],
|
subFields: ['blocknote', 'markdown'],
|
||||||
filterableSubFields: [],
|
filterableSubFields: [],
|
||||||
labelBySubField: {
|
labelBySubField: {
|
||||||
blocknote: 'BlockNote',
|
blocknote:
|
||||||
markdown: 'Markdown',
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.RICH_TEXT_V2]
|
||||||
|
.blocknote,
|
||||||
|
markdown:
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.RICH_TEXT_V2]
|
||||||
|
.markdown,
|
||||||
},
|
},
|
||||||
exampleValue: {
|
exampleValue: {
|
||||||
blocknote: '[{"type":"heading","content":"Hello"}]',
|
blocknote: '[{"type":"heading","content":"Hello"}]',
|
||||||
|
|||||||
Reference in New Issue
Block a user