fix IndexFieldMetadata availability in IndexMetadata/ObjectMetadata in front (#12886)
Context : - IndexFieldMetadata was no longer available on 'objects' gql query ([since this PR](https://github.com/twentyhq/twenty/pull/12785)). Then, unicity checks on import do not work anymore. Fix : - Add a dataloader logic in indexFieldMetadata - Add extra check in unicity hook on import
This commit is contained in:
@ -30,7 +30,7 @@ const documents = {
|
||||
"\n mutation UpdateOneObjectMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateObjectPayload!\n ) {\n updateOneObject(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSearchable\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n isLabelSyncedWithName\n }\n }\n": types.UpdateOneObjectMetadataItemDocument,
|
||||
"\n mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {\n deleteOneObject(input: { id: $idToDelete }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSearchable\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n isLabelSyncedWithName\n }\n }\n": types.DeleteOneObjectMetadataItemDocument,
|
||||
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
||||
"\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
||||
"\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadataList {\n id\n fieldMetadataId\n createdAt\n updatedAt\n order\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
||||
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
||||
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
||||
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
||||
@ -128,7 +128,7 @@ export function graphql(source: "\n mutation DeleteOneFieldMetadataItem($idToDe
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadataList {\n id\n fieldMetadataId\n createdAt\n updatedAt\n order\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadataList {\n id\n fieldMetadataId\n createdAt\n updatedAt\n order\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -778,6 +778,7 @@ export type Index = {
|
||||
__typename?: 'Index';
|
||||
createdAt: Scalars['DateTime'];
|
||||
id: Scalars['UUID'];
|
||||
indexFieldMetadataList: Array<IndexField>;
|
||||
indexFieldMetadatas: IndexIndexFieldMetadatasConnection;
|
||||
indexType: IndexType;
|
||||
indexWhereClause?: Maybe<Scalars['String']>;
|
||||
|
||||
@ -33,6 +33,13 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
|
||||
indexWhereClause
|
||||
indexType
|
||||
isUnique
|
||||
indexFieldMetadataList {
|
||||
id
|
||||
fieldMetadataId
|
||||
createdAt
|
||||
updatedAt
|
||||
order
|
||||
}
|
||||
}
|
||||
fieldsList {
|
||||
id
|
||||
|
||||
@ -3,7 +3,10 @@ import { Index as GeneratedIndex } from '~/generated-metadata/graphql';
|
||||
|
||||
export type IndexMetadataItem = Omit<
|
||||
GeneratedIndex,
|
||||
'__typename' | 'indexFieldMetadatas' | 'objectMetadata'
|
||||
| '__typename'
|
||||
| 'indexFieldMetadatas'
|
||||
| 'objectMetadata'
|
||||
| 'indexFieldMetadataList'
|
||||
> & {
|
||||
__typename?: string;
|
||||
indexFieldMetadatas: IndexFieldMetadataItem[];
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { IndexFieldMetadataItem } from '@/object-metadata/types/IndexFieldMetadataItem';
|
||||
import { IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
|
||||
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
||||
import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
|
||||
@ -26,7 +27,12 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
|
||||
(index) =>
|
||||
({
|
||||
...index,
|
||||
indexFieldMetadatas: [],
|
||||
indexFieldMetadatas: index.indexFieldMetadataList.map(
|
||||
(indexFieldMetadata) =>
|
||||
({
|
||||
...indexFieldMetadata,
|
||||
}) satisfies IndexFieldMetadataItem,
|
||||
),
|
||||
}) satisfies IndexMetadataItem,
|
||||
),
|
||||
} satisfies ObjectMetadataItem;
|
||||
|
||||
@ -81,9 +81,9 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
|
||||
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
|
||||
|
||||
const testData: ImportedStructuredRow<string>[] = [
|
||||
{ 'Link URL (domainName)': 'duplicaTe.com', id: '1' },
|
||||
{ 'Link URL (domainName)': 'duplicate.com ', id: '2' },
|
||||
{ 'Link URL (domainName)': 'other.com', id: '3' },
|
||||
{ 'Link URL (domainName)': 'duplicaTe.com' },
|
||||
{ 'Link URL (domainName)': 'duplicate.com ' },
|
||||
{ 'Link URL (domainName)': 'other.com' },
|
||||
];
|
||||
|
||||
const addErrorMock = jest.fn();
|
||||
|
||||
@ -1,12 +1,24 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||
import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
|
||||
import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
|
||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||
import {
|
||||
ImportedStructuredRow,
|
||||
SpreadsheetImportRowHook,
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
isDefined,
|
||||
lowercaseUrlAndRemoveTrailingSlash,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
type Column = {
|
||||
columnName: string;
|
||||
fieldType: FieldMetadataType;
|
||||
};
|
||||
|
||||
export const spreadsheetImportGetUnicityRowHook = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
@ -15,8 +27,8 @@ export const spreadsheetImportGetUnicityRowHook = (
|
||||
(indexMetadata) => indexMetadata.isUnique,
|
||||
);
|
||||
|
||||
const uniqueConstraintFields = [
|
||||
['id'],
|
||||
const uniqueConstraintsWithColumnNames: Column[][] = [
|
||||
[{ columnName: 'id', fieldType: FieldMetadataType.UUID }],
|
||||
...uniqueConstraints.map((indexMetadata) =>
|
||||
indexMetadata.indexFieldMetadatas.flatMap((indexField) => {
|
||||
const field = objectMetadataItem.fields.find(
|
||||
@ -35,12 +47,13 @@ export const spreadsheetImportGetUnicityRowHook = (
|
||||
(subField) => subField.isIncludedInUniqueConstraint,
|
||||
);
|
||||
|
||||
return uniqueSubFields.map((subField) =>
|
||||
getSubFieldOptionKey(field, subField.subFieldName),
|
||||
);
|
||||
return uniqueSubFields.map((subField) => ({
|
||||
columnName: getSubFieldOptionKey(field, subField.subFieldName),
|
||||
fieldType: field.type,
|
||||
}));
|
||||
}
|
||||
|
||||
return [field.name];
|
||||
return [{ columnName: field.name, fieldType: field.type }];
|
||||
}),
|
||||
),
|
||||
];
|
||||
@ -50,9 +63,13 @@ export const spreadsheetImportGetUnicityRowHook = (
|
||||
return row;
|
||||
}
|
||||
|
||||
uniqueConstraintFields.forEach((uniqueConstraint) => {
|
||||
uniqueConstraintsWithColumnNames.forEach((uniqueConstraint) => {
|
||||
const rowUniqueValues = getUniqueValues(row, uniqueConstraint);
|
||||
|
||||
if (!isNonEmptyString(rowUniqueValues)) {
|
||||
return row;
|
||||
}
|
||||
|
||||
const duplicateRows = table.filter(
|
||||
(r) => getUniqueValues(r, uniqueConstraint) === rowUniqueValues,
|
||||
);
|
||||
@ -61,10 +78,10 @@ export const spreadsheetImportGetUnicityRowHook = (
|
||||
return row;
|
||||
}
|
||||
|
||||
uniqueConstraint.forEach((field) => {
|
||||
if (isDefined(row[field])) {
|
||||
addError(field, {
|
||||
message: `This ${field} value already exists in your import data`,
|
||||
uniqueConstraint.forEach(({ columnName }) => {
|
||||
if (isDefined(row[columnName])) {
|
||||
addError(columnName, {
|
||||
message: t`This ${columnName} value already exists in your import data`,
|
||||
level: 'error',
|
||||
});
|
||||
}
|
||||
@ -79,9 +96,24 @@ export const spreadsheetImportGetUnicityRowHook = (
|
||||
|
||||
const getUniqueValues = (
|
||||
row: ImportedStructuredRow<string>,
|
||||
uniqueConstraint: string[],
|
||||
uniqueConstraint: Column[],
|
||||
) => {
|
||||
return uniqueConstraint
|
||||
.map((field) => row?.[field]?.toString().trim().toLowerCase())
|
||||
.map(({ columnName, fieldType }) => {
|
||||
// need to ensure the primary link url is processed before import as on server side
|
||||
if (
|
||||
fieldType === FieldMetadataType.LINKS &&
|
||||
columnName.includes(
|
||||
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
|
||||
.primaryLinkUrl,
|
||||
)
|
||||
) {
|
||||
return lowercaseUrlAndRemoveTrailingSlash(
|
||||
row?.[columnName]?.toString().trim() || '',
|
||||
);
|
||||
}
|
||||
|
||||
return row?.[columnName]?.toString().trim().toLowerCase();
|
||||
})
|
||||
.join('');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user