Skip to content

Commit

Permalink
refactor(json-patch): refactor createJsonPathOperations, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Birkbjo committed Nov 22, 2023
1 parent 624dd4f commit d2a84dc
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 40 deletions.
2 changes: 1 addition & 1 deletion src/pages/dataElements/Edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
127 changes: 127 additions & 0 deletions src/pages/dataElements/edit/createJsonPatchOperations.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
83 changes: 44 additions & 39 deletions src/pages/dataElements/edit/createJsonPatchOperations.ts
Original file line number Diff line number Diff line change
@@ -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<FormValues extends ModelWithAttributeValues> {
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<FormValues>): 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) || '',
}))
Expand Down

0 comments on commit d2a84dc

Please sign in to comment.