Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/ReportFormSummary/V2SchemaFormSummary/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';

import { FORM_ELEMENT_TYPES, ROOT_CANVAS_ID } from '../../utils/form-schemas/constants';
import getHumanizedFieldValue from '../../utils/form-schemas/getHumanizedFieldValue';
import normalizeChoiceListValues from '../../utils/form-schemas/normalizeChoiceListValues';
import { selectCoordinatesRepresentation } from '../../selectors/location';
import transformSchemaToFormElements from '../../utils/form-schemas/transformSchemaToFormElements';

Expand Down Expand Up @@ -55,8 +56,10 @@ const SectionSummary = ({ formData, formElements, section }) => <div className={
const V2SchemaFormSummary = ({ eventSchema, formData }) => {
const formElements = useMemo(() => transformSchemaToFormElements(eventSchema), [eventSchema]);

const normalizedFormData = useMemo(() => normalizeChoiceListValues(formData, formElements), [formData, formElements]);

return formElements[ROOT_CANVAS_ID]?.details.sections.map((sectionId) => <SectionSummary
formData={formData}
formData={normalizedFormData}
formElements={formElements}
section={formElements[sectionId]}
key={sectionId}
Expand Down
53 changes: 53 additions & 0 deletions src/ReportFormSummary/V2SchemaFormSummary/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,57 @@ describe('ReportFormSummary - V2SchemaFormSummary', () => {
expect(screen.getByText('Text Field')).toBeInTheDocument();
expect(screen.getByText('Hello')).toBeInTheDocument();
});

test('shows the display names of a legacy choice list stored as { name, value } objects', () => {
const eventSchema = {
json: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties: {
team_members: {
items: {
anyOf: [{
enum: ['kumoi_njapit', 'sam_kumum'],
'x-enumExtra': {
kumoi_njapit: { display: 'Kumoi Njapit' },
sam_kumum: { display: 'Sam Kumum' },
},
}],
},
title: 'Team Member',
type: 'array',
},
},
required: [],
type: 'object',
unevaluatedProperties: false,
},
ui: {
fields: { team_members: { inputType: 'LIST', parent: 'section-1', type: 'CHOICE_LIST' } },
headers: {},
order: ['section-1'],
sections: {
'section-1': {
columns: 1,
isActive: true,
label: 'Details',
leftColumn: [{ name: 'team_members', type: 'field' }],
rightColumn: [],
},
},
},
};

renderV2SchemaFormSummary({
eventSchema,
formData: {
team_members: [
{ name: 'Kumoi Njapit', value: 'kumoi_njapit' },
{ name: 'Sam Kumum', value: 'sam_kumum' },
],
},
});

expect(screen.getByText('Team Member')).toBeInTheDocument();
expect(screen.getByText('Kumoi Njapit, Sam Kumum')).toBeInTheDocument();
});
});
12 changes: 11 additions & 1 deletion src/ReportManager/ReportDetailView/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ import { generateErrorMessageForRequest } from '../../utils/request';
import { extractObjectDifference } from '../../utils/objects';
import { fetchEventTypeSchema } from '../../ducks/event-schemas';
import { fetchPatrol } from '../../ducks/patrols';
import normalizeChoiceListValues from '../../utils/form-schemas/normalizeChoiceListValues';
import { selectEventSchema } from '../../selectors/event-schemas';
import { selectEventTypeById, selectEventTypeByValue } from '../../selectors/event-types';
import { setLocallyEditedEvent, unsetLocallyEditedEvent } from '../../ducks/locally-edited-event';
import { SidebarScrollContext } from '../../SidebarScrollContext';
import { TAB_KEYS } from '../../constants';
import { TrackerContext } from '../../utils/analytics';
import transformSchemaToFormElements from '../../utils/form-schemas/transformSchemaToFormElements';
import useNavigate from '../../hooks/useNavigate';
import { usePreviewFeature } from '../../hooks';
import { uuid } from '../../utils/string';
Expand Down Expand Up @@ -172,6 +174,13 @@ const ReportDetailView = ({
? selectEventSchema(state, reportForm.event_type, reportForm.id)
: null);

const formElements = useMemo(
() => eventType?.version === 2 && eventSchema?.json && !eventSchema?.error
? transformSchemaToFormElements(eventSchema)
: null,
[eventSchema, eventType?.version]
);

const {
onCancelAddedReport,
onSaveError: onSaveErrorCallback,
Expand Down Expand Up @@ -329,8 +338,8 @@ const ReportDetailView = ({
} else {
reportToSubmit = {
...reportChanges,
event_details: normalizeChoiceListValues(reportForm.event_details, formElements),
id: reportForm.id,
event_details: reportForm.event_details,
location: originalReport.location,
};

Expand Down Expand Up @@ -382,6 +391,7 @@ const ReportDetailView = ({
attachmentsToAdd,
communityInputValue,
dispatch,
formElements,
isAddedReport,
isCommunity,
isNewReport,
Expand Down
79 changes: 78 additions & 1 deletion src/ReportManager/ReportDetailView/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { addEventToIncident, createEvent, fetchEvent } from '../../ducks/events'
import { activePatrol } from '../../__test-helpers/fixtures/patrols';
import { createMapMock } from '../../__test-helpers/mocks';
import { eventSchemas } from '../../__test-helpers/fixtures/event-schemas';
import { eventTypes } from '../../__test-helpers/fixtures/event-types';
import { eventTypes, snareV2 } from '../../__test-helpers/fixtures/event-types';
import { executeSaveActions, generateSaveActionsForReportLikeObject } from '../../utils/save';
import { TrackerContext } from '../../utils/analytics';
import { fetchEventTypeSchema } from '../../ducks/event-schemas';
Expand Down Expand Up @@ -112,6 +112,65 @@ describe('ReportManager - ReportDetailView', () => {
user: { first_name: 'First', last_name: 'Last' },
}],
};
const setUpLegacyChoiceListEvent = () => {
state.data.eventTypes = [...eventTypes, snareV2];
state.data.eventSchemas = {
...eventSchemas,
[snareV2.value]: {
792: {
json: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties: {
team_members: {
items: {
anyOf: [{
enum: ['kumoi_njapit', 'sam_kumum'],
'x-enumExtra': {
kumoi_njapit: { display: 'Kumoi Njapit' },
sam_kumum: { display: 'Sam Kumum' },
},
}],
},
title: 'Team Member',
type: 'array',
},
},
required: [],
type: 'object',
unevaluatedProperties: false,
},
ui: {
fields: { team_members: { inputType: 'LIST', parent: 'section-1', type: 'CHOICE_LIST' } },
headers: {},
order: ['section-1'],
sections: {
'section-1': {
columns: 1,
isActive: true,
label: 'Details',
leftColumn: [{ name: 'team_members', type: 'field' }],
rightColumn: [],
},
},
},
},
},
};
state.data.eventStore = {
...state.data.eventStore,
792: {
...mockReport,
event_type: snareV2.value,
event_details: {
team_members: [
{ name: 'Kumoi Njapit', value: 'kumoi_njapit' },
{ name: 'Sam Kumum', value: 'sam_kumum' },
],
},
id: '792',
},
};
};
let AddItemButtonMock,
addEventToIncidentMock,
createEventMock,
Expand Down Expand Up @@ -502,6 +561,24 @@ describe('ReportManager - ReportDetailView', () => {
});
});

test('renders and saves the option values of a legacy V2 choice list stored as { name, value } objects', async () => {
setUpLegacyChoiceListEvent();

renderWithWrapper(<ReportDetailView isNewReport={false} reportId="792" />);

expect(await screen.findByRole('checkbox', { name: 'Kumoi Njapit' })).toBeChecked();
expect(await screen.findByRole('checkbox', { name: 'Sam Kumum' })).toBeChecked();

await userEvent.click(await screen.findByText('Save'));

await waitFor(() => {
expect(generateSaveActionsForReportLikeObject).toHaveBeenCalledTimes(1);
});
expect(generateSaveActionsForReportLikeObject.mock.calls[0][0].event_details).toEqual({
team_members: ['kumoi_njapit', 'sam_kumum'],
});
});

test('still blocks saving when a cleared legacy dropdown is required by the schema', async () => {
const accidentSchema = eventSchemas.accident_rep.base;
state.data.eventSchemas = {
Expand Down
25 changes: 14 additions & 11 deletions src/SchemaForm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { clearUserContent } from '../ducks/user-content';
import evaluateSectionConditions from './utils/evaluateSectionConditions';
import { FORM_ELEMENT_TYPES, ROOT_CANVAS_ID } from '../utils/form-schemas/constants';
import getDefaultFormData from './utils/getDefaultFormData';
import normalizeChoiceListValues from '../utils/form-schemas/normalizeChoiceListValues';
import normalizeDateTimeFieldValue from './utils/normalizeDateTimeFieldValue';
import transformSchemaToFormElements from '../utils/form-schemas/transformSchemaToFormElements';
import useMapLocationMarkers from './utils/useMapLocationMarkers';
Expand Down Expand Up @@ -82,19 +83,21 @@ const SchemaForm = ({

const formElements = useMemo(() => transformSchemaToFormElements(schema), [schema]);

const normalizedFormData = useMemo(() => normalizeChoiceListValues(formData, formElements), [formData, formElements]);

const runSchemaValidations = useSchemaValidations(schema);
const runUploadValidations = useUploadValidations(formElements);

const visibleSectionIds = useMemo(
() => getVisibleSectionIds(formElements, formData),
[formData, formElements]
() => getVisibleSectionIds(formElements, normalizedFormData),
[formElements, normalizedFormData]
);

const onSubmit = (event) => {
event.preventDefault();

const schemaErrors = runSchemaValidations(formData) || {};
const uploadErrors = runUploadValidations(formData);
const schemaErrors = runSchemaValidations(normalizedFormData) || {};
const uploadErrors = runUploadValidations(normalizedFormData);
const fieldErrors = merge({}, schemaErrors, uploadErrors);
if (Object.keys(fieldErrors).length > 0) {
const erroneousFields = Object.keys(fieldErrors);
Expand All @@ -116,7 +119,7 @@ const SchemaForm = ({
const onSectionFieldChange = (fieldId, value) => {
// Section children's ids and names are the same.
const fieldName = formElements[fieldId].details.value;
const newFormData = { ...formData, [fieldName]: value };
const newFormData = { ...normalizedFormData, [fieldName]: value };

// Conditional sections can depend on fields in other conditional sections.
// Remove hidden fields from the form data in a loop until all sections
Expand Down Expand Up @@ -232,15 +235,15 @@ const SchemaForm = ({
]);
const initialData = getDefaultFormData(visibleFieldIds, formElements);

if (!isEqual(initialData, formData)) {
if (!isEqual(initialData, normalizedFormData)) {
onFormDataChange(initialData);
}
}

// eslint-disable-next-line react-hooks/set-state-in-effect
setShouldCalculateInitialData(false);
}
}, [formData, formElements, onFormDataChange, shouldPopulateDefaultData, shouldCalculateInitialData, visibleSectionIds]);
}, [formElements, normalizedFormData, onFormDataChange, shouldPopulateDefaultData, shouldCalculateInitialData, visibleSectionIds]);

useEffect(() => {
// Update the location markers when there is a change in the form data.
Expand All @@ -266,10 +269,10 @@ const SchemaForm = ({
});
};

addLocationMarkersFromFormDataRecursively(formData);
addLocationMarkersFromFormDataRecursively(normalizedFormData);

setLocationMarkers(locationMarkers);
}, [formData, formElements, setLocationMarkers]);
}, [formElements, normalizedFormData, setLocationMarkers]);

useEffect(() => () => dispatch(clearUserContent()), [dispatch]);

Expand All @@ -290,15 +293,15 @@ const SchemaForm = ({
details={formElements[sectionId].details}
fieldErrors={fieldErrors}
focusLocationMarker={focusLocationMarker}
formData={formData}
formData={normalizedFormData}
formElements={formElements}
hidden={!visibleSectionIds.includes(sectionId)}
id={sectionId}
key={sectionId}
onFieldChange={onSectionFieldChange}
onFieldErrorsChange={(newFieldErrors) => setFieldErrors(newFieldErrors)}
renderFormElement={renderFormElement}
setDefaultFormData={(defaultFormData) => onFormDataChange({ ...defaultFormData, ...formData })}
setDefaultFormData={(defaultFormData) => onFormDataChange({ ...defaultFormData, ...normalizedFormData })}
/>)}

{renderSubmitButton()}
Expand Down
54 changes: 54 additions & 0 deletions src/utils/form-schemas/normalizeChoiceListValues/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { isPlainObject } from 'lodash-es';

import { FORM_ELEMENT_TYPES } from '../constants';

const isLegacyChoiceValue = (value) => isPlainObject(value)
&& typeof value.name === 'string'
&& ['boolean', 'number', 'string'].includes(typeof value.value);

const normalizeChoiceValue = (value) => isLegacyChoiceValue(value) ? value.value : value;

const normalizeArrayItems = (array, normalizeItem) => {
const normalizedArray = array.map(normalizeItem);

return normalizedArray.some((item, index) => item !== array[index]) ? normalizedArray : array;
};

const normalizeObjectValues = (object, normalizeValue) => {
const normalizedEntries = Object.entries(object).map(([key, value]) => [key, normalizeValue(value, key)]);

return normalizedEntries.some(([key, value]) => value !== object[key])
? Object.fromEntries(normalizedEntries)
: object;
};

const normalizeFieldValues = (formData, formElements, parentCollectionFieldId = null) => {
if (!isPlainObject(formData)) {
return formData;
}

return normalizeObjectValues(formData, (value, fieldName) => {
const fieldId = parentCollectionFieldId ? `${parentCollectionFieldId}.${fieldName}` : fieldName;

switch (formElements[fieldId]?.type) {
case FORM_ELEMENT_TYPES.CHOICE_LIST:
return Array.isArray(value) ? normalizeArrayItems(value, normalizeChoiceValue) : normalizeChoiceValue(value);

case FORM_ELEMENT_TYPES.COLLECTION:
return Array.isArray(value)
? normalizeArrayItems(value, (item) => normalizeFieldValues(item, formElements, fieldId))
: value;

default:
return value;
}
});
};

// Replaces the values of the choice list fields described by formElements from
// { name, value } format to their value.
const normalizeChoiceListValues = (formData, formElements) => isPlainObject(formElements)
? normalizeFieldValues(formData, formElements)
: formData;

export default normalizeChoiceListValues;
Loading