From 2075d6dc639a676f4bc45586abb38b48a62361a4 Mon Sep 17 00:00:00 2001 From: Andrii Baran <59182007+Anddrrew@users.noreply.github.com> Date: Tue, 20 Feb 2024 12:19:26 +0200 Subject: [PATCH] fix(core): Fix renaming of product with readonly custom field (#2684) --- .../src/data/providers/base-data.service.ts | 6 +- .../is-entity-create-or-update-mutation.ts | 51 ++++++++ .../remove-readonly-custom-fields.spec.ts | 35 ++++++ .../utils/remove-readonly-custom-fields.ts | 118 ++++-------------- 4 files changed, 113 insertions(+), 97 deletions(-) create mode 100644 packages/admin-ui/src/lib/core/src/data/utils/is-entity-create-or-update-mutation.ts diff --git a/packages/admin-ui/src/lib/core/src/data/providers/base-data.service.ts b/packages/admin-ui/src/lib/core/src/data/providers/base-data.service.ts index c945359aa5..d46803fa41 100644 --- a/packages/admin-ui/src/lib/core/src/data/providers/base-data.service.ts +++ b/packages/admin-ui/src/lib/core/src/data/providers/base-data.service.ts @@ -11,11 +11,9 @@ import { CustomFields } from '../../common/generated-types'; import { QueryResult } from '../query-result'; import { ServerConfigService } from '../server-config'; import { addCustomFields } from '../utils/add-custom-fields'; -import { - isEntityCreateOrUpdateMutation, - removeReadonlyCustomFields, -} from '../utils/remove-readonly-custom-fields'; +import { removeReadonlyCustomFields } from '../utils/remove-readonly-custom-fields'; import { transformRelationCustomFieldInputs } from '../utils/transform-relation-custom-field-inputs'; +import { isEntityCreateOrUpdateMutation } from '../utils/is-entity-create-or-update-mutation'; @Injectable() export class BaseDataService { diff --git a/packages/admin-ui/src/lib/core/src/data/utils/is-entity-create-or-update-mutation.ts b/packages/admin-ui/src/lib/core/src/data/utils/is-entity-create-or-update-mutation.ts new file mode 100644 index 0000000000..c8206748cd --- /dev/null +++ b/packages/admin-ui/src/lib/core/src/data/utils/is-entity-create-or-update-mutation.ts @@ -0,0 +1,51 @@ +import { DocumentNode, getOperationAST, NamedTypeNode, TypeNode } from 'graphql'; + +const CREATE_ENTITY_REGEX = /Create([A-Za-z]+)Input/; +const UPDATE_ENTITY_REGEX = /Update([A-Za-z]+)Input/; + +/** + * Checks the current documentNode for an operation with a variable named "CreateInput" or "UpdateInput" + * and if a match is found, returns the name. + */ +export function isEntityCreateOrUpdateMutation(documentNode: DocumentNode): string | undefined { + const operationDef = getOperationAST(documentNode, null); + if (operationDef && operationDef.variableDefinitions) { + for (const variableDef of operationDef.variableDefinitions) { + const namedType = extractInputType(variableDef.type); + const inputTypeName = namedType.name.value; + + // special cases which don't follow the usual pattern + if (inputTypeName === 'UpdateActiveAdministratorInput') { + return 'Administrator'; + } + if (inputTypeName === 'ModifyOrderInput') { + return 'Order'; + } + if ( + inputTypeName === 'AddItemToDraftOrderInput' || + inputTypeName === 'AdjustDraftOrderLineInput' + ) { + return 'OrderLine'; + } + + const createMatch = inputTypeName.match(CREATE_ENTITY_REGEX); + if (createMatch) { + return createMatch[1]; + } + const updateMatch = inputTypeName.match(UPDATE_ENTITY_REGEX); + if (updateMatch) { + return updateMatch[1]; + } + } + } +} + +function extractInputType(type: TypeNode): NamedTypeNode { + if (type.kind === 'NonNullType') { + return extractInputType(type.type); + } + if (type.kind === 'ListType') { + return extractInputType(type.type); + } + return type; +} diff --git a/packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.spec.ts b/packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.spec.ts index dfdcbf53ba..ac6a46500a 100644 --- a/packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.spec.ts +++ b/packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.spec.ts @@ -45,6 +45,23 @@ describe('removeReadonlyCustomFields', () => { } as any); }); + it('readonly field and customFields is undefined', () => { + const config: CustomFieldConfig[] = [{ name: 'alias', type: 'string', readonly: true, list: false }]; + + const entity = { + id: 1, + name: 'test', + customFields: undefined, + }; + + const result = removeReadonlyCustomFields(entity, config); + expect(result).toEqual({ + id: 1, + name: 'test', + customFields: undefined, + } as any); + }); + it('readonly field in translation', () => { const config: CustomFieldConfig[] = [ { name: 'alias', type: 'localeString', readonly: true, list: false }, @@ -63,6 +80,24 @@ describe('removeReadonlyCustomFields', () => { } as any); }); + it('readonly field and customFields is undefined in translation', () => { + const config: CustomFieldConfig[] = [ + { name: 'alias', type: 'localeString', readonly: true, list: false }, + ]; + const entity = { + id: 1, + name: 'test', + translations: [{ id: 1, languageCode: LanguageCode.en, customFields: undefined }], + }; + + const result = removeReadonlyCustomFields(entity, config); + expect(result).toEqual({ + id: 1, + name: 'test', + translations: [{ id: 1, languageCode: LanguageCode.en, customFields: undefined }], + } as any); + }); + it('wrapped in an input object', () => { const config: CustomFieldConfig[] = [ { name: 'weight', type: 'int', list: false }, diff --git a/packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.ts b/packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.ts index 7c5ecfd508..1ee102d9a1 100644 --- a/packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.ts +++ b/packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.ts @@ -1,121 +1,53 @@ -import { simpleDeepClone } from '@vendure/common/lib/simple-deep-clone'; -import { DocumentNode, getOperationAST, NamedTypeNode, TypeNode } from 'graphql'; - import { CustomFieldConfig } from '../../common/generated-types'; -const CREATE_ENTITY_REGEX = /Create([A-Za-z]+)Input/; -const UPDATE_ENTITY_REGEX = /Update([A-Za-z]+)Input/; - type InputWithOptionalCustomFields = Record & { customFields?: Record; }; -type InputWithCustomFields = Record & { - customFields: Record; -}; type EntityInput = InputWithOptionalCustomFields & { translations?: InputWithOptionalCustomFields[]; }; -/** - * Checks the current documentNode for an operation with a variable named "CreateInput" or "UpdateInput" - * and if a match is found, returns the name. - */ -export function isEntityCreateOrUpdateMutation(documentNode: DocumentNode): string | undefined { - const operationDef = getOperationAST(documentNode, null); - if (operationDef && operationDef.variableDefinitions) { - for (const variableDef of operationDef.variableDefinitions) { - const namedType = extractInputType(variableDef.type); - const inputTypeName = namedType.name.value; - - // special cases which don't follow the usual pattern - if (inputTypeName === 'UpdateActiveAdministratorInput') { - return 'Administrator'; - } - if (inputTypeName === 'ModifyOrderInput') { - return 'Order'; - } - if ( - inputTypeName === 'AddItemToDraftOrderInput' || - inputTypeName === 'AdjustDraftOrderLineInput' - ) { - return 'OrderLine'; - } +type Variable = EntityInput | EntityInput[]; - const createMatch = inputTypeName.match(CREATE_ENTITY_REGEX); - if (createMatch) { - return createMatch[1]; - } - const updateMatch = inputTypeName.match(UPDATE_ENTITY_REGEX); - if (updateMatch) { - return updateMatch[1]; - } - } - } -} - -function extractInputType(type: TypeNode): NamedTypeNode { - if (type.kind === 'NonNullType') { - return extractInputType(type.type); - } - if (type.kind === 'ListType') { - return extractInputType(type.type); - } - return type; -} +type WrappedVariable = { + input: Variable; +}; /** * Removes any `readonly` custom fields from an entity (including its translations). * To be used before submitting the entity for a create or update request. */ export function removeReadonlyCustomFields( - variables: { input?: EntityInput | EntityInput[] } | EntityInput | EntityInput[], + variables: Variable | WrappedVariable | WrappedVariable[], customFieldConfig: CustomFieldConfig[], -): { input?: EntityInput | EntityInput[] } | EntityInput | EntityInput[] { - if (!Array.isArray(variables)) { +) { + if (Array.isArray(variables)) { + return variables.map(variable => removeReadonlyCustomFields(variable, customFieldConfig)); + } + + if ('input' in variables && variables.input) { if (Array.isArray(variables.input)) { - for (const input of variables.input) { - removeReadonly(input, customFieldConfig); - } + variables.input = variables.input.map(variable => removeReadonly(variable, customFieldConfig)); } else { - removeReadonly(variables.input, customFieldConfig); - } - } else { - for (const input of variables) { - removeReadonly(input, customFieldConfig); + variables.input = removeReadonly(variables.input, customFieldConfig); } + return variables; } + return removeReadonly(variables, customFieldConfig); } -function removeReadonly(input: InputWithOptionalCustomFields, customFieldConfig: CustomFieldConfig[]) { - for (const field of customFieldConfig) { - if (field.readonly) { - if (field.type === 'localeString') { - if (hasTranslations(input)) { - for (const translation of input.translations) { - if ( - hasCustomFields(translation) && - translation.customFields[field.name] !== undefined - ) { - delete translation.customFields[field.name]; - } - } - } - } else { - if (hasCustomFields(input) && input.customFields[field.name] !== undefined) { - delete input.customFields[field.name]; - } - } - } - } - return input; -} +function removeReadonly(input: EntityInput, customFieldConfig: CustomFieldConfig[]) { + const readonlyConfigs = customFieldConfig.filter(({ readonly }) => readonly); -function hasCustomFields(input: any): input is InputWithCustomFields { - return input != null && input.hasOwnProperty('customFields'); -} + readonlyConfigs.forEach(({ name }) => { + input.translations?.forEach(translation => { + delete translation.customFields?.[name]; + }); + + delete input.customFields?.[name]; + }); -function hasTranslations(input: any): input is { translations: InputWithOptionalCustomFields[] } { - return input != null && input.hasOwnProperty('translations'); + return input; }