diff --git a/packages/location-management/src/components/LocationForm/CustomSelect/index.tsx b/packages/location-management/src/components/LocationForm/CustomSelect/index.tsx index 08296bb6a..409fa1ef5 100644 --- a/packages/location-management/src/components/LocationForm/CustomSelect/index.tsx +++ b/packages/location-management/src/components/LocationForm/CustomSelect/index.tsx @@ -7,17 +7,46 @@ import { SelectProps } from 'antd/lib/select'; import { OptionData } from 'rc-select/lib/interface/'; type RawValueType = string | number | (string | number)[]; +export type GetOptions = (data: T[]) => OptionData[]; +export type GetSelectedFullData = ( + data: T[], + getOptions: GetOptions, + value: SelectProps['value'] +) => T[]; + +/** + * default method to get the fullData object once use selects an option in the select dropdown, + * once the user selects, you only get the id of the selected object, this function will be called + * to get the full object. + * + * @param data - the full data objects + * @param getOptions - function used to get the options tos how on the dropdown + * @param value - selected value (an array for multi select otherwise a string) + */ +export function getSelectedFullData( + data: T[], + getOptions: GetOptions, + value: SelectProps['value'] +) { + const selected = data.filter((dt) => { + const option = getOptions([dt])[0]; + return (Array.isArray(value) && value.includes(option.value)) || value === option.value; + }); + return selected; +} /** props for custom select component */ export interface CustomSelectProps extends SelectProps { loadData: (stateSetter: Dispatch>) => Promise; - getOptions: (data: T[]) => OptionData[]; + getOptions: GetOptions; fullDataCallback?: (data: T[]) => void; + getSelectedFullData: GetSelectedFullData; } const defaultServiceTypeProps = { loadData: () => Promise.resolve(), getOptions: () => [], + getSelectedFullData, }; /** custom select, gets options from the api @@ -27,7 +56,14 @@ const defaultServiceTypeProps = { function CustomSelect(props: CustomSelectProps) { const [loading, setLoading] = useState(true); const [data, setData] = useState([]); - const { loadData, getOptions, value, fullDataCallback, ...restProps } = props; + const { + loadData, + getOptions, + value, + fullDataCallback, + getSelectedFullData, + ...restProps + } = props; useEffect(() => { loadData(setData) @@ -39,12 +75,9 @@ function CustomSelect(props: CustomSelectProps) { }, []); useEffect(() => { - const selected = data.filter((dt) => { - const option = getOptions([dt])[0]; - return (Array.isArray(value) && value.includes(option.value)) || value === option.value; - }); + const selected = getSelectedFullData(data, getOptions, value); fullDataCallback?.(selected); - }, [data, fullDataCallback, getOptions, value]); + }, [data, fullDataCallback, getOptions, getSelectedFullData, value]); const selectOptions = getOptions(data); diff --git a/packages/location-management/src/components/LocationForm/index.tsx b/packages/location-management/src/components/LocationForm/index.tsx index deb704dea..f6475fe95 100644 --- a/packages/location-management/src/components/LocationForm/index.tsx +++ b/packages/location-management/src/components/LocationForm/index.tsx @@ -7,6 +7,7 @@ import { defaultFormField, generateLocationUnit, getLocationTagOptions, + getSelectedLocTagObj, getServiceTypeOptions, handleGeoFieldsChangeFactory, LocationFormFields, @@ -357,6 +358,7 @@ const LocationForm = (props: LocationFormProps) => { }} getOptions={getLocationTagOptions} fullDataCallback={setLocationTags} + getSelectedFullData={getSelectedLocTagObj} /> diff --git a/packages/location-management/src/components/LocationForm/tests/fixtures.ts b/packages/location-management/src/components/LocationForm/tests/fixtures.ts index 3cc08f4d9..205ab47ec 100644 --- a/packages/location-management/src/components/LocationForm/tests/fixtures.ts +++ b/packages/location-management/src/components/LocationForm/tests/fixtures.ts @@ -159,6 +159,34 @@ export const generatedLocation4 = { geometry: { type: 'Point', coordinates: [19.56, 34.56] }, }; +export const generatedLocation4Dot1 = { + properties: { + geographicLevel: 1, + parentId: '03176924-6b3c-4b74-bccd-32afcceebabd', + name: 'MENABE', + name_en: 'MENABE', + status: 'Active', + version: 0, + }, + id: '38a0a19b-f91e-4044-a8db-a4b62490bf27', + syncStatus: 'Synced', + type: 'Feature', + locationTags: [{ id: 2, active: true, name: 'Region', description: 'Region Location Tag' }], + geometry: { + type: 'Polygon', + coordinates: [ + [ + [17.4298095703125, 29.897805610155874], + [17.215576171875, 29.750070930806785], + [17.4957275390625, 29.3965337391284], + [17.9901123046875, 29.54000879252545], + [18.006591796874996, 29.79298413547051], + [17.4298095703125, 29.897805610155874], + ], + ], + }, +}; + export const expectedFormFields = { externalId: '', extraFields: [], @@ -323,6 +351,27 @@ export const locationTags = [ { id: 12, active: true, name: 'Test', description: '' }, ]; +export const duplicateLocationTags = [ + { id: 1, active: true, name: 'Country', description: 'Country Location Tag' }, + { id: 3, active: true, name: 'District', description: 'District Location Tag' }, + { id: 4, active: true, name: 'Commune', description: 'Commune Location Tag' }, + { id: 5, active: true, name: 'Service Point', description: 'Service Point' }, + { id: 6, active: false, name: 'Location name', description: '' }, + { id: 1, active: true, name: 'Country', description: 'Country Location Tag' }, + { id: 3, active: true, name: 'District', description: 'District Location Tag' }, + { id: 4, active: true, name: 'Commune', description: 'Commune Location Tag' }, + { id: 5, active: true, name: 'Service Point', description: 'Service Point' }, + { id: 6, active: false, name: 'Location name', description: '' }, + { id: 1, active: true, name: 'Country', description: 'Country Location Tag' }, + { id: 3, active: true, name: 'District', description: 'District Location Tag' }, + { id: 4, active: true, name: 'Commune', description: 'Commune Location Tag' }, + { id: 5, active: true, name: 'Service Point', description: 'Service Point' }, + { id: 6, active: false, name: 'Location name', description: '' }, + { id: 2, active: true, name: 'Region', description: 'Region Location Tag' }, + { id: 2, active: true, name: 'Region', description: 'Region Location Tag' }, + { id: 2, active: true, name: 'Region', description: 'Region Location Tag' }, +]; + export const locationSettings = [ { key: 'sample_key', diff --git a/packages/location-management/src/components/LocationForm/tests/index.test.tsx b/packages/location-management/src/components/LocationForm/tests/index.test.tsx index 74fbedb48..0ed7f8702 100644 --- a/packages/location-management/src/components/LocationForm/tests/index.test.tsx +++ b/packages/location-management/src/components/LocationForm/tests/index.test.tsx @@ -11,9 +11,11 @@ import { authenticateUser } from '@onaio/session-reducer'; import { Form } from 'antd'; import { createdLocation1, + duplicateLocationTags, fetchCalls1, generatedLocation2, generatedLocation4, + generatedLocation4Dot1, location2, location4, locationSettings, @@ -683,4 +685,64 @@ describe('LocationForm', () => { ]); wrapper.unmount(); }); + + it('#595 Duplicate location Tags failing upload', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + fetch + .once(JSON.stringify([location2])) + .once(JSON.stringify(serviceTypeSettings)) + .once(JSON.stringify(duplicateLocationTags)) + .once(JSON.stringify(locationSettings)) + .once(JSON.stringify(rawOpenSRPHierarchy1)); + + const initialValues = getLocationFormFields(location4); + + const locationFormProps = { + initialValues, + }; + + const wrapper = mount( + + + , + + { attachTo: container } + ); + + await act(async () => { + await new Promise((resolve) => setImmediate(resolve)); + wrapper.update(); + }); + + fetch.mockReset(); + + wrapper.find('form').simulate('submit'); + + await act(async () => { + await new Promise((resolve) => setImmediate(resolve)); + wrapper.update(); + }); + + /** payload does not contain duplicate entries in locationTags field*/ + expect(generatedLocation4Dot1.locationTags).toHaveLength(1); + expect(fetch.mock.calls).toEqual([ + [ + 'https://opensrp-stage.smartregister.org/opensrp/rest/location?is_jurisdiction=true', + { + 'Cache-Control': 'no-cache', + Pragma: 'no-cache', + body: JSON.stringify(generatedLocation4Dot1), + headers: { + accept: 'application/json', + authorization: 'Bearer sometoken', + 'content-type': 'application/json;charset=UTF-8', + }, + method: 'PUT', + }, + ], + ]); + wrapper.unmount(); + }); }); diff --git a/packages/location-management/src/components/LocationForm/utils.tsx b/packages/location-management/src/components/LocationForm/utils.ts similarity index 93% rename from packages/location-management/src/components/LocationForm/utils.tsx rename to packages/location-management/src/components/LocationForm/utils.ts index 7973ca3d2..56891f300 100644 --- a/packages/location-management/src/components/LocationForm/utils.tsx +++ b/packages/location-management/src/components/LocationForm/utils.ts @@ -12,6 +12,8 @@ import { v4 } from 'uuid'; import { Geometry, Point } from 'geojson'; import lang, { Lang } from '../../lang'; import { FormInstance } from 'antd/lib/form/hooks/useForm'; +import { GetSelectedFullData } from './CustomSelect'; +import { uniqBy } from 'lodash'; export enum FormInstances { CORE = 'core', @@ -359,6 +361,29 @@ export const getLocationTagOptions = (tags: LocationUnitTag[]) => { }); }; +/** + * method to get the full Location tag object once user selects an option in the select dropdown, + * once the user selects, you only get the id of the selected object, this function will be called + * to get the full location Tag. + * + * @param data - the full data objects + * @param getOptions - function used to get the options tos how on the dropdown + * @param value - selected value (an array for multi select otherwise a string) + */ +export const getSelectedLocTagObj: GetSelectedFullData = ( + data, + getOptions, + value +) => { + // #595 - remove duplicate data(those that have the same id) + const uniqData = uniqBy(data, (obj) => obj.id); + const selected = uniqData.filter((dt) => { + const option = getOptions([dt])[0]; + return (Array.isArray(value) && value.includes(option.value)) || value === option.value; + }); + return selected; +}; + /** * generates tree select options *