diff --git a/src/pages/dataElements/Edit.tsx b/src/pages/dataElements/Edit.tsx index 1051df0c..553b11d9 100644 --- a/src/pages/dataElements/Edit.tsx +++ b/src/pages/dataElements/Edit.tsx @@ -116,7 +116,7 @@ function usePatchDirtyFields() { const jsonPatchPayload = createJsonPatchOperations({ values, dirtyFields, - dataElement, + originalValue: dataElement, }) // We want the promise so we know when submitting is done. The promise diff --git a/src/pages/dataElements/edit/createJsonPatchOperations.spec.ts b/src/pages/dataElements/edit/createJsonPatchOperations.spec.ts new file mode 100644 index 00000000..4a28b18e --- /dev/null +++ b/src/pages/dataElements/edit/createJsonPatchOperations.spec.ts @@ -0,0 +1,127 @@ +import { + sanitizeDirtyValueKeys, + createJsonPatchOperations, +} from './createJsonPatchOperations' + +describe('createJsonPatchOperations', () => { + describe('sanitizeDirtyValueKeys', () => { + it('should return the dirty values array as is', () => { + const actual = sanitizeDirtyValueKeys(['foo', 'bar']) + const expected = ['foo', 'bar'] + expect(actual).toEqual(expected) + }) + + it('should remove all attribute values changes and add a single "attributeValues"', () => { + const actual = sanitizeDirtyValueKeys([ + 'foo', + 'bar', + 'attributeValues[0].value', + 'attributeValues[1].value', + ]) + const expected = ['foo', 'bar', 'attributeValues'] + expect(actual).toEqual(expected) + }) + + it('should remove style.icon and style.color changes and add a single "style"', () => { + const actual = sanitizeDirtyValueKeys([ + 'foo', + 'bar', + 'style.color', + 'style.icon', + ]) + const expected = ['foo', 'bar', 'style'] + expect(actual).toEqual(expected) + }) + }) + + describe('createJsonPatchOperations', () => { + it('should return an empty array if no dirty fields', () => { + const actual = createJsonPatchOperations({ + dirtyFields: {}, + originalValue: { + id: 'foo', + attributeValues: [], + }, + values: { + attributeValues: [], + }, + }) + expect(actual).toEqual([]) + }) + + it('should return a json-patch payload for a single field', () => { + const actual = createJsonPatchOperations({ + dirtyFields: { + name: true, + }, + originalValue: { + id: 'foo', + name: 'bar', + attributeValues: [], + }, + values: { + name: 'baz', + attributeValues: [], + }, + }) + const expected = [ + { + op: 'replace', + path: '/name', + value: 'baz', + }, + ] + expect(actual).toEqual(expected) + }) + it('should return a json-patch payload with add if value does not exist in originalValue', () => { + const actual = createJsonPatchOperations({ + dirtyFields: { + name: true, + }, + originalValue: { + id: 'foo', + attributeValues: [], + }, + values: { + name: 'baz', + attributeValues: [], + }, + }) + const expected = [ + { + op: 'add', + path: '/name', + value: 'baz', + }, + ] + expect(actual).toEqual(expected) + }) + + it('should handle attributeValues', () => { + const actual = createJsonPatchOperations({ + dirtyFields: { + attributeValues: true, + }, + originalValue: { + id: 'foo', + name: 'bar', + attributeValues: [], + }, + values: { + name: 'baz', + attributeValues: [ + { value: 'INPUT', attribute: { id: 'foo' } }, + ], + }, + }) + const expected = [ + { + op: 'replace', + path: '/attributeValues', + value: [{ value: 'INPUT', attribute: { id: 'foo' } }], + }, + ] + expect(actual).toEqual(expected) + }) + }) +}) diff --git a/src/pages/dataElements/edit/createJsonPatchOperations.ts b/src/pages/dataElements/edit/createJsonPatchOperations.ts index 7a830bce..4afafd29 100644 --- a/src/pages/dataElements/edit/createJsonPatchOperations.ts +++ b/src/pages/dataElements/edit/createJsonPatchOperations.ts @@ -1,64 +1,69 @@ import get from 'lodash/fp/get' import { JsonPatchOperation } from '../../../types' -import { DataElement } from '../../../types/generated' -import type { FormValues } from '../form' +import { Attribute, AttributeValue } from './../../../types/generated/models' -interface FormatFormValuesArgs { - dataElement: DataElement - dirtyFields: { [name: string]: boolean } - values: FormValues +type PatchAttributeFields = { + id: Attribute['id'] } -const sanitizeDirtyValueKeys = (keys: string[]) => { - // these are removed from the dirtyKeys - // attributeValues is an array in the form, thus fields will be attributeValues[0] etc - // style.code should post to style, not style.code, because it's a complex object - const keyStartsWithToRemove = ['attributeValues', 'style'] as const - const shouldInclude = Object.fromEntries( - keys.map((key) => [key, false]) - ) as Record<(typeof keyStartsWithToRemove)[number], boolean> +type PatchAttributeValue = { + attribute: PatchAttributeFields + value: AttributeValue['value'] +} - const keysWithout = keys.filter( - (key) => - !keyStartsWithToRemove.some((keyToRemove) => { - const shouldRemove = key.startsWith(keyToRemove) - if (shouldRemove) { - shouldInclude[keyToRemove] = true - } - return shouldRemove - }) - ) +type ModelWithAttributeValues = { + attributeValues: PatchAttributeValue[] +} - // no difference - if (keysWithout.length === keys.length) { - return keys - } +interface FormatFormValuesArgs { + originalValue: unknown + dirtyFields: { [key in keyof FormValues]?: boolean } + values: FormValues +} - const keysToInclude = Object.entries(shouldInclude) - .filter(([, val]) => val) - .map(([key]) => key) +// these are removed from the dirtyKeys +// attributeValues is an array in the form, thus the key will be attributeValues[0] etc +// remove these, and replace with 'attributeValues' +// style.code should post to style, not style.code, because it's a complex object +const complexKeys = ['attributeValues', 'style'] as const +export const sanitizeDirtyValueKeys = (dirtyKeys: string[]) => { + const complexChanges = complexKeys.filter((complexKey) => + dirtyKeys.some((dirtyKey) => dirtyKey.startsWith(complexKey)) + ) + + const dirtyKeysWithoutComplexKeys = dirtyKeys.filter( + (dirtyKey) => + !complexChanges.some((complexKey) => + dirtyKey.startsWith(complexKey) + ) + ) - return keysWithout.concat(keysToInclude) + return dirtyKeysWithoutComplexKeys.concat(complexChanges) } -export function createJsonPatchOperations({ +export function createJsonPatchOperations< + FormValues extends ModelWithAttributeValues +>({ dirtyFields, - dataElement, + originalValue, values: unsanitizedValues, -}: FormatFormValuesArgs): JsonPatchOperation[] { +}: FormatFormValuesArgs): JsonPatchOperation[] { // Remove attribute values without a value const values = { ...unsanitizedValues, - attributeValues: unsanitizedValues.attributeValues.filter( - ({ value }) => !!value - ), + attributeValues: unsanitizedValues.attributeValues + .filter(({ value }) => !!value) + .map((value) => ({ + value: value.value, + attribute: { id: value.attribute.id }, + })), } const dirtyFieldsKeys = Object.keys(dirtyFields) const adjustedDirtyFieldsKeys = sanitizeDirtyValueKeys(dirtyFieldsKeys) return adjustedDirtyFieldsKeys.map((name) => ({ - op: get(name, dataElement) ? 'replace' : 'add', + op: get(name, originalValue) ? 'replace' : 'add', path: `/${name.replace(/[.]/g, '/')}`, value: get(name, values) || '', }))