diff --git a/public/locales/en-US/details-view.json b/public/locales/en-US/details-view.json index fbb696c97..f444ac5ce 100644 --- a/public/locales/en-US/details-view.json +++ b/public/locales/en-US/details-view.json @@ -72,6 +72,9 @@ }, "reportFormSummary": { "reportTypeLabel": "Event Type", - "reportedByLabel": "Reported By" + "reportedByLabel": "Reported By", + "v2SchemaFormSummary": { + "collectionHumanizedValue": "{{collectionLength}} items" + } } } \ No newline at end of file diff --git a/public/locales/es/details-view.json b/public/locales/es/details-view.json index ec46e3c7d..b7ed9e21a 100644 --- a/public/locales/es/details-view.json +++ b/public/locales/es/details-view.json @@ -58,6 +58,9 @@ }, "reportFormSummary": { "reportTypeLabel": "Tipo de evento", - "reportedByLabel": "Reportado por" + "reportedByLabel": "Reportado por", + "v2SchemaFormSummary": { + "collectionHumanizedValue": "{{collectionLength}} elementos" + } } } \ No newline at end of file diff --git a/public/locales/fr/details-view.json b/public/locales/fr/details-view.json index cda4882e2..632665e28 100644 --- a/public/locales/fr/details-view.json +++ b/public/locales/fr/details-view.json @@ -58,6 +58,9 @@ }, "reportFormSummary": { "reportTypeLabel": "Type d'Évenement", - "reportedByLabel": "Complété Par" + "reportedByLabel": "Complété Par", + "v2SchemaFormSummary": { + "collectionHumanizedValue": "{{collectionLength}} éléments" + } } } \ No newline at end of file diff --git a/public/locales/ne-NP/details-view.json b/public/locales/ne-NP/details-view.json index 06bfa026a..5725f72f6 100644 --- a/public/locales/ne-NP/details-view.json +++ b/public/locales/ne-NP/details-view.json @@ -71,7 +71,10 @@ }, "reportFormSummary": { "reportTypeLabel": "घटनाका प्रकार", - "reportedByLabel": "द्वारा रिपोर्ट गरिएको" + "reportedByLabel": "द्वारा रिपोर्ट गरिएको", + "v2SchemaFormSummary": { + "collectionHumanizedValue": "{{collectionLength}} वस्तुहरू" + } }, "addReportButtonLabel": "नयाँ घटना बनाउनुहोस्" } \ No newline at end of file diff --git a/public/locales/pt/details-view.json b/public/locales/pt/details-view.json index 4efab3140..7b6fea3b6 100644 --- a/public/locales/pt/details-view.json +++ b/public/locales/pt/details-view.json @@ -59,5 +59,8 @@ "reportFormSummary": { "reportTypeLabel": "Tipo de evento", "reportedByLabel": "Reportado por" + }, + "v2SchemaFormSummary": { + "collectionHumanizedValue": "{{collectionLength}} itens" } } \ No newline at end of file diff --git a/public/locales/sw/details-view.json b/public/locales/sw/details-view.json index e3a03150b..d6fc616c2 100644 --- a/public/locales/sw/details-view.json +++ b/public/locales/sw/details-view.json @@ -72,6 +72,9 @@ }, "reportFormSummary": { "reportTypeLabel": "Aina ya Tukio", - "reportedByLabel": "Imeorodheshwa na" + "reportedByLabel": "Imeorodheshwa na", + "v2SchemaFormSummary": { + "collectionHumanizedValue": "{{collectionLength}} vitu" + } } } \ No newline at end of file diff --git a/src/DetailViewComponents/ActivitySection/ContainedReportListItem/index.js b/src/DetailViewComponents/ActivitySection/ContainedReportListItem/index.js index a56a7cdd9..3ea1765fb 100644 --- a/src/DetailViewComponents/ActivitySection/ContainedReportListItem/index.js +++ b/src/DetailViewComponents/ActivitySection/ContainedReportListItem/index.js @@ -9,9 +9,6 @@ import { ReactComponent as ArrowIntoIcon } from '../../../common/images/icons/ar import { ReactComponent as ArrowUpSimpleIcon } from '../../../common/images/icons/arrow-up-simple.svg'; import { fetchEvent } from '../../../ducks/events'; -import { fetchEventTypeSchema } from '../../../ducks/event-schemas'; -import { selectEventSchema } from '../../../selectors/event-schemas'; -import { selectEventTypeByValue } from '../../../selectors/event-types'; import { TAB_KEYS } from '../../../constants'; import useNavigate from '../../../hooks/useNavigate'; @@ -31,10 +28,6 @@ const ContainedReportListItem = ({ cardsExpanded, onCollapse, onExpand, report } const { t } = useTranslation('details-view', { keyPrefix: 'containedReportListItem' }); const reportFromEventStore = useSelector((state) => state.data.eventStore[report.id]); - const eventSchema = useSelector((state) => reportFromEventStore - ? selectEventSchema(state, reportFromEventStore.event_type, reportFromEventStore.id) - : null); - const eventType = useSelector((state) => selectEventTypeByValue(state, report.event_type)); const isOpen = useMemo(() => cardsExpanded.includes(report), [cardsExpanded, report]); @@ -46,12 +39,6 @@ const ContainedReportListItem = ({ cardsExpanded, onCollapse, onExpand, report } } }, [dispatch, report.id, reportFromEventStore]); - useEffect(() => { - if (!!eventType && !eventSchema) { - dispatch(fetchEventTypeSchema(report.event_type, report.id)); - } - }, [dispatch, eventSchema, eventType, report.event_type, report.id]); - return
  • - {!!reportFromEventStore && !!eventSchema - ? + {!!reportFromEventStore + ? :
    } diff --git a/src/DetailViewComponents/ActivitySection/ContainedReportListItem/index.test.js b/src/DetailViewComponents/ActivitySection/ContainedReportListItem/index.test.js index b07f88d94..1c6c52cc7 100644 --- a/src/DetailViewComponents/ActivitySection/ContainedReportListItem/index.test.js +++ b/src/DetailViewComponents/ActivitySection/ContainedReportListItem/index.test.js @@ -16,7 +16,7 @@ import ContainedReportListItem from '.'; const server = setupServer( http.get(`${EVENT_API_URL}:eventId`, () => HttpResponse.json( { data: { ...report } })), - http.get(`${EVENT_TYPE_SCHEMA_API_URL}:name`, () => HttpResponse.json( { data: { results: {} } })) + http.get(`${EVENT_TYPE_SCHEMA_API_URL}/:name`, () => HttpResponse.json( { data: { results: {} } })) ); beforeAll(() => server.listen()); diff --git a/src/ReportFormSummary/V1SchemaFormSummary/index.js b/src/ReportFormSummary/V1SchemaFormSummary/index.js new file mode 100644 index 000000000..2aa07a45a --- /dev/null +++ b/src/ReportFormSummary/V1SchemaFormSummary/index.js @@ -0,0 +1,56 @@ +import React, { useMemo } from 'react'; +import Form from '@rjsf/bootstrap-4'; + +import { formValidator } from '../../utils/events'; + +import { + AddButton, + ArrayFieldItemTemplate, + ArrayFieldTemplate, + BaseInputTemplate, + ExternalLinkField, + MoveDownButton, + MoveUpButton, + ObjectFieldTemplate, + RemoveButton, +} from '../../SchemaFields'; + +import * as styles from './styles.module.scss'; + +// For V1 schemas, we basically hack a rjsf Form so they render all the fields for us and we styled them so they don't +// look like fields. +const V1SchemaFormSummary = ({ eventSchema, report }) => { + const { schema, uiSchema } = eventSchema; + + const filteredSchema = useMemo(() => { + const { properties = {} } = schema ?? {}; + const eventDetailsKeys = Object.keys(report?.event_details ?? {}); + + return { + ...schema, + properties: Object.entries(properties).reduce((acc, [key, value]) => { + return eventDetailsKeys.includes(key) ? { ...acc, [key]: value } : acc; + }, {}) + }; + }, [report, schema]); + + return
    ; +}; + +export default V1SchemaFormSummary; diff --git a/src/ReportFormSummary/V1SchemaFormSummary/styles.module.scss b/src/ReportFormSummary/V1SchemaFormSummary/styles.module.scss new file mode 100644 index 000000000..4524b4b4a --- /dev/null +++ b/src/ReportFormSummary/V1SchemaFormSummary/styles.module.scss @@ -0,0 +1,113 @@ +@use '../../common/styles/vars/colors'; + +.form { + [class*=react-datepicker-wrapper] { + div { + border: none; + padding: 0; + height: auto; + background: transparent; + } + } + + [class*=row] { + padding: 0; + + > * { + padding: 0; + } + } + + [class*=col-] { + padding: 0; + } + + label { + color: colors.$secondary-medium-gray; + font-size: 0.875rem; + margin: 0.75rem 0 0; + } + + button, + input, + [class*=control] { + background-color: transparent !important; + border: transparent; + font-size: 1rem; + height: 1.5rem !important; + margin: 0; + padding: 0; + } + + [class*=control] { + min-height: 1.5rem; + } + + [class*=selectWidget] { + div { + height: 1.5rem; + display: block; + padding: 0; + } + + [class*=singleValue] { + color: black; + margin: 0; + } + } + + legend { + border-bottom: 1px solid colors.$light-gray-border; + font-size: 1rem; + font-weight: 500; + margin: 1.5rem 0 0.5rem; + } + + h5 { + font-size: 1rem; + } + + hr { + margin-bottom: 0; + } + + button[class*=moveButton], + button[type=submit], + svg, + [class*=addButton], + [class*=triangle], + [class*=IndicatorsContainer] { + display: none !important; + } + + [class*=checkboxesWidget] { + border: none; + max-height: none; + overflow-y: hidden; + + [class*=checkbox] { + margin-bottom: 0; + padding-left: 0; + + label { + color: black; + font-size: 1rem; + margin: 0; + opacity: 1; + + &:before { + content: "\002D"; + margin-right: 0.25rem; + } + } + + input { + display: none; + } + + &:has(input:not(:checked)) { + display: none; + } + } + } +} diff --git a/src/ReportFormSummary/V2SchemaFormSummary/index.js b/src/ReportFormSummary/V2SchemaFormSummary/index.js new file mode 100644 index 000000000..8a10c67c7 --- /dev/null +++ b/src/ReportFormSummary/V2SchemaFormSummary/index.js @@ -0,0 +1,75 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; + +import getHumanizedFieldValue from '../../utils/v2-event-schemas/getHumanizedFieldValue'; +import makeFieldsFromSchema from '../../utils/v2-event-schemas/makeFieldsFromSchema'; +import { FORM_ELEMENT_TYPES, ROOT_CANVAS_ID } from '../../utils/v2-event-schemas/constants'; + +import * as styles from './styles.module.scss'; + +const FieldSummary = ({ field, formData, id }) => { + const { i18n, t } = useTranslation('details-view', { keyPrefix: 'reportFormSummary.v2SchemaFormSummary' }); + + const gpsFormat = useSelector((state) => state.view.userPreferences.gpsFormat); + + return <> +

    {field.details.label}

    + +

    {getHumanizedFieldValue( + field, + formData[id], + '-', + i18n.language, + gpsFormat, + t + )}

    + ; +}; + +const SectionSummary = ({ details, fields, formData }) =>
    +
    + + {details.label &&

    {details.label}

    } + +
    +
    + {details.leftColumn.map((fieldId) => fields[fieldId].type === FORM_ELEMENT_TYPES.HEADER + ?

    + {fields[fieldId].details.label} +

    + : )} +
    + + {details.columns === 2 &&
    + {details.rightColumn.map((fieldId) => )} +
    } +
    +
    ; + +// For V2 schemas, we have a customized rendering of the details using utility functions like makeFieldsFromSchema and +// getHumanizedFieldValue following the schema definition. +const V2SchemaFormSummary = ({ eventSchema, formData }) => { + const fields = useMemo(() => makeFieldsFromSchema(eventSchema), [eventSchema]); + + return fields[ROOT_CANVAS_ID]?.details.fields.map((sectionId) => ); +}; + +export default V2SchemaFormSummary; diff --git a/src/ReportFormSummary/V2SchemaFormSummary/styles.module.scss b/src/ReportFormSummary/V2SchemaFormSummary/styles.module.scss new file mode 100644 index 000000000..c2b2de8c3 --- /dev/null +++ b/src/ReportFormSummary/V2SchemaFormSummary/styles.module.scss @@ -0,0 +1,62 @@ +@use '../../common/styles/vars/colors'; + +.section { + .separator { + margin: 0.75rem -1rem; + } + + .sectionLabel { + font-size: 1.125rem; + font-weight: 500; + margin: 0; + } + + .columns { + display: flex; + + .column { + &.fullWidth { + width: 100%; + } + + &.halfWidthLeft { + padding-right: 0.5rem; + width: 50%; + } + + &.halfWidthRight { + padding-left: 0.5rem; + width: 50%; + } + + .header-large, + .header-medium, + .header-small { + font-weight: 500; + margin: 1rem 0 0.25rem; + } + + .header-large { + font-size: 1.125rem; + } + + .header-medium { + font-size: 1rem; + } + + .header-small { + font-size: 0.875rem; + } + + .fieldLabel { + color: colors.$secondary-medium-gray; + font-size: 0.875rem; + margin: 0.75rem 0 0; + } + + .fieldValue { + margin: 0; + } + } + } +} diff --git a/src/ReportFormSummary/index.js b/src/ReportFormSummary/index.js index 70de21336..fc13e8007 100644 --- a/src/ReportFormSummary/index.js +++ b/src/ReportFormSummary/index.js @@ -1,39 +1,45 @@ -import React, { memo, useMemo } from 'react'; -import Form from '@rjsf/bootstrap-4'; +import React, { memo, useEffect } from 'react'; +import MoonLoader from 'react-spinners/MoonLoader'; +import { useDispatch, useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; +import { fetchEventTypeSchema } from '../ducks/event-schemas'; +import { selectEventSchema } from '../selectors/event-schemas'; +import { selectEventTypeByValue } from '../selectors/event-types'; import useReport from '../hooks/useReport'; -import { - AddButton, - ArrayFieldItemTemplate, - ArrayFieldTemplate, - BaseInputTemplate, - ExternalLinkField, - MoveDownButton, - MoveUpButton, - ObjectFieldTemplate, - RemoveButton, -} from '../SchemaFields'; -import { formValidator } from '../utils/events'; +import V1SchemaFormSummary from './V1SchemaFormSummary'; +import V2SchemaFormSummary from './V2SchemaFormSummary'; import * as styles from './styles.module.scss'; -const ReportFormSummary = ({ className, report, schema, uiSchema }) => { +const LOADER_COLOR = '#006cd9'; // Bright blue +const LOADER_SIZE = 30; + +const EventFormSummary = ({ report }) => { + const dispatch = useDispatch(); const { t } = useTranslation('details-view', { keyPrefix: 'reportFormSummary' }); + + const eventSchema = useSelector((state) => report + ? selectEventSchema(state, report.event_type, report.id) + : null); + const eventType = useSelector((state) => selectEventTypeByValue(state, report.event_type)); + const { eventTypeTitle } = useReport(report); - const filteredSchema = useMemo(() => { - const { properties = {} } = schema ?? {}; - const eventDetailsKeys = Object.keys(report?.event_details ?? {}); - return { - ...schema, - properties: Object.entries(properties).reduce((acc, [key, value]) => { - return eventDetailsKeys.includes(key) ? { ...acc, [key]: value } : acc; - }, {}) - }; - }, [report, schema]); - return
    + useEffect(() => { + if (!!eventType && !eventSchema) { + dispatch(fetchEventTypeSchema(report.event_type, report.id)); + } + }, [dispatch, eventSchema, eventType, report.event_type, report.id]); + + if (!eventSchema) { + return
    + +
    ; + } + + return
    - { - report.reported_by?.name && -
    - - {report.reported_by?.name} -
    - } + {report.reported_by?.name &&
    + + {report.reported_by?.name} +
    }
    - {schema && } + + {eventType.version === 2 && }
    ; }; -export default memo(ReportFormSummary); +export default memo(EventFormSummary); diff --git a/src/ReportFormSummary/index.test.js b/src/ReportFormSummary/index.test.js index 8499df1d9..3691182af 100644 --- a/src/ReportFormSummary/index.test.js +++ b/src/ReportFormSummary/index.test.js @@ -1,79 +1,132 @@ import React from 'react'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { EVENT_TYPE_SCHEMA_API_URL } from '../ducks/event-schemas'; +import { GPS_FORMATS } from '../utils/location'; import ReportFormSummary from './index'; import { report as mockedReport } from '../__test-helpers/fixtures/reports'; -import { eventSchemas } from '../__test-helpers/fixtures/event-schemas'; +import { eventSchemas, snareSchemaV2 } from '../__test-helpers/fixtures/event-schemas'; import { Provider } from 'react-redux'; import { mockStore } from '../__test-helpers/MockStore'; import { eventTypes } from '../__test-helpers/fixtures/event-types'; -import { eventTypeTitleForEvent } from '../utils/events'; import { render, screen } from '../test-utils'; +const server = setupServer( + http.get( + `${EVENT_TYPE_SCHEMA_API_URL}/:name`, + () => HttpResponse.json( { data: eventSchemas.wildlife_sighting_rep['a78576a5-3c5b-40df-b374-12db53fbfdd6'] }) + ) +); -describe('ReportFormSummary', () => { +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); - const { schema, uiSchema } = eventSchemas.wildlife_sighting_rep['a78576a5-3c5b-40df-b374-12db53fbfdd6']; - const event_details = { - wildlifesightingrep_species: 'cheetah', - wildlifesightingrep_numberanimals: 2, - wildlifesightingrep_collared: 'yes', - }; - const reported_by = { name: 'Ranger' }; - const report = { ...mockedReport, event_details, reported_by }; - const initialProps = { report, schema, uiSchema }; - const store = mockStore({ - data: { eventTypes } +describe('ReportFormSummary', () => { + let store; + beforeEach(() => { + store = { + data: { + eventSchemas: { + light_rep: { + 'd45cb504-4612-41fe-9ea5-f1b423ac3ba4': eventSchemas.wildlife_sighting_rep['a78576a5-3c5b-40df-b374-12db53fbfdd6'] + }, + }, + eventTypes, + }, + view: { + userPreferences: { + gpsFormat: GPS_FORMATS.DEG, + }, + }, + }; }); - const renderReportFormSummary = (props = initialProps) => render( - - + const renderReportFormSummary = (props) => render( + + ); - test('render given summary form', async () => { + test('renders a loader while the schema is being loaded', () => { + store.data.eventSchemas = {}; + renderReportFormSummary(); + + expect(screen.getByTestId('reportFormSummary-loader')).toBeVisible(); + }); + + test('render a form summary for a v1 schema', async () => { renderReportFormSummary(); - const reportTitle = eventTypeTitleForEvent(report, eventTypes); - expect( screen.getByText(reportTitle) ).toBeInTheDocument(); - expect( screen.getByText(reported_by.name) ).toBeInTheDocument(); + expect(screen.getByText('Light')).toBeInTheDocument(); + expect(screen.getByText('Ranger')).toBeInTheDocument(); const numberAnimalsInput = screen.getByRole('spinbutton'); - expect( numberAnimalsInput ).toHaveValue(event_details.wildlifesightingrep_numberanimals); - expect( numberAnimalsInput ).toHaveAttribute('disabled'); + expect(numberAnimalsInput).toHaveValue(2); + expect(numberAnimalsInput).toHaveAttribute('disabled'); const species = screen.getByRole('combobox', { name: 'Species' } ); - expect( species ).toHaveValue('2');// cheetah - expect( species ).toHaveAttribute('disabled'); + expect(species).toHaveValue('2'); + expect(species).toHaveAttribute('disabled'); const animals = screen.getByRole('combobox', { name: 'Are Animals Collared' } ); - expect( animals ).toHaveValue('0'); // yes - expect( animals ).toHaveAttribute('disabled'); + expect(animals).toHaveValue('0'); + expect(animals).toHaveAttribute('disabled'); }); - test('Hide blank fields of a report summary', async () => { + test('hides the blank fields of a form summary for a v1 schema', async () => { const roleOptions = { name: 'Species' }; const { rerender } = renderReportFormSummary(); - expect( screen.queryByRole('combobox', roleOptions ) ).toBeInTheDocument(); - - const eventDetails = { ...event_details }; - delete eventDetails.wildlifesightingrep_species; - const props = { - ...initialProps, - report: { - ...report, - event_details: eventDetails - } - }; - - rerender( - + expect(screen.queryByRole('combobox', roleOptions )).toBeInTheDocument(); + + rerender( + ); - expect( screen.queryByRole('combobox', roleOptions ) ).not.toBeInTheDocument(); + expect(screen.queryByRole('combobox', roleOptions)).not.toBeInTheDocument(); }); + test('render a form summary for a v2 schema', async () => { + store.data.eventTypes = [{ value: 'snare_v2_rep', version: 2 }]; + store.data.eventSchemas.snare_v2_rep = { + 'd45cb504-4612-41fe-9ea5-f1b423ac3ba4': snareSchemaV2, + }; + renderReportFormSummary({ + report: { + ...mockedReport, + event_details: { + number_of_snares_found: 3, + }, + event_type: 'snare_v2_rep', + reported_by: { name: 'Ranger' }, + } + }); + expect(screen.getByText('Ranger')).toBeVisible(); + expect(screen.getByText('Number of Snares Found')).toBeVisible(); + expect(screen.getByText('3')).toBeVisible(); + }); }); diff --git a/src/ReportFormSummary/styles.module.scss b/src/ReportFormSummary/styles.module.scss index 40219a3d2..843606d34 100644 --- a/src/ReportFormSummary/styles.module.scss +++ b/src/ReportFormSummary/styles.module.scss @@ -1,17 +1,13 @@ -@import '../common/styles/vars/colors'; -@import '../common/styles/layout'; +@use '../common/styles/vars/colors'; +@use '../common/styles/layout'; -.reportFormSummary { - - [class*=react-datepicker-wrapper] { - div { - border: none; - padding: 0; - height: auto; - background: transparent; - } - } +.loaderWrapper { + display: flex; + justify-content: center; + width: 100%; +} +.reportFormSummary { .nonSchemaFields { display: flex; flex-direction: column; @@ -23,119 +19,18 @@ width: 100%; label { - margin: 0; + color: colors.$secondary-medium-gray; + font-size: 0.875rem; + margin: 0.75rem 0 0; } - @media (min-width: $md-layout-width-min) { + @media (min-width: layout.$md-layout-width-min) { width: 50%; } } - @media (min-width: $md-layout-width-min) { + @media (min-width: layout.$md-layout-width-min) { flex-direction: row; } } - - label { - color: $secondary-medium-gray; - font-size: 0.875rem; - margin: 0.75rem 0 0; - } - - .form { - [class*=row] { - padding: 0; - - > * { - padding: 0; - } - } - - [class*=col-] { - padding: 0; - } - - button, - input, - [class*=control] { - background-color: transparent !important; - border: transparent; - font-size: 1rem; - height: 1.5rem !important; - margin: 0; - padding: 0; - } - - [class*=control] { - min-height: 1.5rem; - } - - [class*=selectWidget] { - div { - height: 1.5rem; - display: block; - padding: 0; - } - - [class*=singleValue] { - color: black; - margin: 0; - } - } - - legend { - border-bottom: 1px solid $light-gray-border; - font-size: 1rem; - font-weight: 500; - margin: 1.5rem 0 0.5rem; - } - - h5 { - font-size: 1rem; - } - - hr { - margin-bottom: 0; - } - - button[class*=moveButton], - button[type=submit], - svg, - [class*=addButton], - [class*=triangle], - [class*=IndicatorsContainer] { - display: none !important; - } - - [class*=checkboxesWidget] { - border: none; - max-height: none; - overflow-y: hidden; - - [class*=checkbox] { - margin-bottom: 0; - padding-left: 0; - - label { - color: black; - font-size: 1rem; - margin: 0; - opacity: 1; - - &:before { - content: "\002D"; - margin-right: 0.25rem; - } - } - - input { - display: none; - } - - &:has(input:not(:checked)) { - display: none; - } - } - } - } } diff --git a/src/ReportManager/DetailsSection/SchemaForm/constants.js b/src/ReportManager/DetailsSection/SchemaForm/constants.js index 03eb6c1c6..441a32389 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/constants.js +++ b/src/ReportManager/DetailsSection/SchemaForm/constants.js @@ -1,42 +1 @@ -export const CHOICE_LIST_ELEMENT_CHOICE_TYPES = { - EXISTING_CHOICE_LIST: 'EXISTING_CHOICE_LIST', - MY_DATA: 'MY_DATA', -}; - -export const CHOICE_LIST_ELEMENT_INPUT_TYPES = { - DROPDOWN: 'DROPDOWN', - LIST: 'LIST', -}; - -export const DATE_TIME_ELEMENT_INPUT_TYPES = { - DATE_TIME: 'DATE_TIME', - DATE: 'DATE', - TIME: 'TIME', -}; - -export const FORM_ELEMENT_TYPES = { - ATTACHMENT: 'ATTACHMENT', - CHOICE_LIST: 'CHOICE_LIST', - COLLECTION: 'COLLECTION', - DATE_TIME: 'DATE_TIME', - HEADER: 'HEADER', - LOCATION: 'LOCATION', - NUMERIC: 'NUMERIC', - SECTION: 'SECTION', - TEXT: 'TEXT', -}; - -export const HEADER_ELEMENT_SIZES = { - LARGE: 'LARGE', - MEDIUM: 'MEDIUM', - SMALL: 'SMALL', -}; - export const JUMP_TO_LOCATION_BUTTON_ZOOM = 20; - -export const ROOT_CANVAS_ID = 'root'; - -export const TEXT_ELEMENT_INPUT_TYPES = { - SHORT: 'SHORT_TEXT', - LONG: 'LONG_TEXT', -}; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/ChoiceList/index.js b/src/ReportManager/DetailsSection/SchemaForm/fields/ChoiceList/index.js index 7484343cc..9d824f695 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/ChoiceList/index.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/ChoiceList/index.js @@ -1,6 +1,6 @@ import React, { memo } from 'react'; -import { CHOICE_LIST_ELEMENT_INPUT_TYPES } from '../../constants'; +import { CHOICE_LIST_ELEMENT_INPUT_TYPES } from '../../../../../utils/v2-event-schemas/constants'; import Dropdown from './Dropdown'; import List from './List'; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/ChoiceList/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/fields/ChoiceList/index.test.js index b4b3eae6b..21934adb3 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/ChoiceList/index.test.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/ChoiceList/index.test.js @@ -1,9 +1,8 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; -import { CHOICE_LIST_ELEMENT_INPUT_TYPES } from '../../constants'; - import { render, screen } from '../../../../../test-utils'; +import { CHOICE_LIST_ELEMENT_INPUT_TYPES } from '../../../../../utils/v2-event-schemas/constants'; import ChoiceList from './'; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormPreview/index.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormPreview/index.js index be97a4d8d..016f5d623 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormPreview/index.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormPreview/index.js @@ -4,8 +4,9 @@ import { useTranslation } from 'react-i18next'; import { ReactComponent as MarkerFeedIcon } from '../../../../../../../../common/images/icons/marker-feed.svg'; -import { FORM_ELEMENT_TYPES, JUMP_TO_LOCATION_BUTTON_ZOOM } from '../../../../../constants'; -import { getHumanizedValue } from '../utils'; +import { FORM_ELEMENT_TYPES } from '../../../../../../../../utils/v2-event-schemas/constants'; +import { JUMP_TO_LOCATION_BUTTON_ZOOM } from '../../../../../constants'; +import getHumanizedFieldValue from '../../../../../../../../utils/v2-event-schemas/getHumanizedFieldValue'; import useJumpToLocation from '../../../../../../../../hooks/useJumpToLocation'; import * as styles from './styles.module.scss'; @@ -39,7 +40,7 @@ const FormPreview = ({

    - {getHumanizedValue(fields[fieldId], formData[fieldId], '-', i18n.language, gpsFormat, t)} + {getHumanizedFieldValue(fields[fieldId], formData[fieldId], '-', i18n.language, gpsFormat, t)}

    diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormPreview/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormPreview/index.test.js index 6a91642ec..7da9cf0fc 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormPreview/index.test.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormPreview/index.test.js @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import userEvent from '@testing-library/user-event'; import { fireEvent, render, screen } from '../../../../../../../../test-utils'; -import { FORM_ELEMENT_TYPES } from '../../../../../constants'; +import { FORM_ELEMENT_TYPES } from '../../../../../../../../utils/v2-event-schemas/constants'; import { GPS_FORMATS } from '../../../../../../../../utils/location'; import { mockStore } from '../../../../../../../../__test-helpers/MockStore'; import useJumpToLocation from '../../../../../../../../hooks/useJumpToLocation'; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/index.test.js index 1c84f1281..4b3c44948 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/index.test.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/index.test.js @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import userEvent from '@testing-library/user-event'; import { render, screen, within } from '../../../../../../../test-utils'; -import { FORM_ELEMENT_TYPES } from '../../../../constants'; +import { FORM_ELEMENT_TYPES } from '../../../../../../../utils/v2-event-schemas/constants'; import { GPS_FORMATS } from '../../../../../../../utils/location'; import { mockStore } from '../../../../../../../__test-helpers/MockStore'; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/utils/index.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/utils/index.js index 576a1e051..0c8b4e7d9 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/utils/index.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/utils/index.js @@ -1,67 +1,6 @@ -import { format, isValid, parseISO } from 'date-fns'; - -import { calcGpsDisplayString } from '../../../../../../../../utils/location'; -import { DATE_TIME_ELEMENT_INPUT_TYPES, FORM_ELEMENT_TYPES } from '../../../../../constants'; -import { shouldUse12HourFormat } from '../../../../../../../../utils/datetime'; - -const getChoiceListOptionLabel = (value, field) => field.details.options - .find((option) => option.const === value)?.title; - -// Utility to calculate a human readable version of the field values. For example, render a date-time like -// 2020/01/01 12:00 PM instead of 2020-01-01T12:00:00Z. -export const getHumanizedValue = (field, value, defaultHumanizedValue, language, gpsFormat, t) => { - if (!value) { - return defaultHumanizedValue; - } - - const use12HourFormat = shouldUse12HourFormat(language); - - switch (field.type) { - case FORM_ELEMENT_TYPES.COLLECTION: - return t('collectionHumanizedValue', { collectionLength: value.length }); - - case FORM_ELEMENT_TYPES.CHOICE_LIST: - if (field.details.multiple) { - const humanizedValues = value.map((val) => { - return getChoiceListOptionLabel(val, field); - }); - return humanizedValues.join(', '); - } - return getChoiceListOptionLabel(value, field); - - case FORM_ELEMENT_TYPES.DATE_TIME: - let parsedDate; - let formatStr; - switch (field.details.inputType) { - case DATE_TIME_ELEMENT_INPUT_TYPES.DATE: - parsedDate = parseISO(value); - formatStr = 'yyyy/MM/dd'; - break; - - case DATE_TIME_ELEMENT_INPUT_TYPES.DATE_TIME: - parsedDate = parseISO(value); - formatStr = use12HourFormat ? 'yyyy/MM/dd hh:mm a' : 'yyyy/MM/dd HH:mm'; - break; - - case DATE_TIME_ELEMENT_INPUT_TYPES.TIME: - parsedDate = parseISO(`2000-01-01T${value}`); - formatStr = use12HourFormat ? 'hh:mm a' : 'HH:mm'; - break; - - default: - return defaultHumanizedValue; - } - return isValid(parsedDate) ? format(parsedDate, formatStr) : defaultHumanizedValue; - - case FORM_ELEMENT_TYPES.LOCATION: - return calcGpsDisplayString(value.latitude, value.longitude, gpsFormat); - - default: - return value; - }; -}; +import getHumanizedFieldValue from '../../../../../../../../utils/v2-event-schemas/getHumanizedFieldValue'; export const getItemTitle = (formData, identifier, defaultTitle, identifierField, language, gpsFormat, t) => !identifier || !formData[identifier] ? defaultTitle - : getHumanizedValue(identifierField, formData[identifier], defaultTitle, language, gpsFormat, t); + : getHumanizedFieldValue(identifierField, formData[identifier], defaultTitle, language, gpsFormat, t); diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/utils/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/utils/index.test.js index f802ba8e4..0b94ea0b2 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/utils/index.test.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/utils/index.test.js @@ -1,133 +1,9 @@ -import { format, parseISO } from 'date-fns'; - -import { choicesListOptions } from '../../../../../fixtures'; -import { DATE_TIME_ELEMENT_INPUT_TYPES, FORM_ELEMENT_TYPES } from '../../../../../constants'; +import { FORM_ELEMENT_TYPES } from '../../../../../../../../utils/v2-event-schemas/constants'; import { GPS_FORMATS } from '../../../../../../../../utils/location'; -import { getHumanizedValue, getItemTitle } from './'; +import { getItemTitle } from './'; describe('ReportManager - DetailsSection - SchemaForm - fields - Collection - SortableList - Item - utils', () => { - describe('getHumanizedValue', () => { - const t = (_, { collectionLength }) => `${collectionLength} items`; - - test('returns the default value if no value is provided', () => { - expect(getHumanizedValue({ type: FORM_ELEMENT_TYPES.TEXT }, undefined, 'default', 'en-US', GPS_FORMATS.DEG, t)).toBe('default'); - }); - - test('returns the length of a collection', () => { - expect(getHumanizedValue( - { type: FORM_ELEMENT_TYPES.COLLECTION }, - [{}, {}], - 'default', - 'en-US', - GPS_FORMATS.DEG, - t - )).toBe('2 items'); - }); - - test('returns the choice list values separated by a comma if it supports multiple values', () => { - expect(getHumanizedValue( - { - details: { - multiple: true, - options: choicesListOptions - }, - type: FORM_ELEMENT_TYPES.CHOICE_LIST - }, - ['17e67b22-0e4a-4fcb-aeee-903b51a7a2e0', '223ab492-0ea7-4ff2-b8b8-cb6504c943b6'], - 'default', - 'en-US', - GPS_FORMATS.DEG, - t - )).toBe('Desert Bighorn Sheep, Ranger Cruz'); - }); - - test('returns the choice list value it supports a single value', () => { - expect(getHumanizedValue( - { - details: { - multiple: false, - options: choicesListOptions - }, - type: FORM_ELEMENT_TYPES.CHOICE_LIST - }, - '0d553bb7-5c4f-43d7-9b82-a561a668ae64', - 'default', - 'en-US', - GPS_FORMATS.DEG, - t - )).toBe('EarthRanger System'); - }); - - test('returns a readable date value', () => { - expect(getHumanizedValue( - { details: { inputType: DATE_TIME_ELEMENT_INPUT_TYPES.DATE }, type: FORM_ELEMENT_TYPES.DATE_TIME }, - '2020-01-01', - 'default', - 'en-US', - GPS_FORMATS.DEG, - t - )).toBe('2020/01/01'); - }); - - test('returns a readable date time value', () => { - const utcValue = '2020-01-01T06:30:00Z'; - expect(getHumanizedValue( - { details: { inputType: DATE_TIME_ELEMENT_INPUT_TYPES.DATE_TIME }, type: FORM_ELEMENT_TYPES.DATE_TIME }, - utcValue, - 'default', - 'en-US', - GPS_FORMATS.DEG, - t - )).toBe(format(parseISO(utcValue), 'yyyy/MM/dd hh:mm a')); - }); - - test('returns a readable time value', () => { - const utcValue = '06:30:00Z'; - expect(getHumanizedValue( - { details: { inputType: DATE_TIME_ELEMENT_INPUT_TYPES.TIME }, type: FORM_ELEMENT_TYPES.DATE_TIME }, - utcValue, - 'default', - 'en-US', - GPS_FORMATS.DEG, - t - )).toBe(format(parseISO(`2000-01-01T${utcValue}`), 'hh:mm a')); - }); - - test('returns the default value if a date-time element is invalid', () => { - expect(getHumanizedValue( - { details: { inputType: DATE_TIME_ELEMENT_INPUT_TYPES.DATE }, type: FORM_ELEMENT_TYPES.DATE_TIME }, - 'invalid', - 'default', - 'en-US', - GPS_FORMATS.DEG, - t - )).toBe('default'); - }); - - test('returns the coordinates from a location in the provided GPS format', () => { - expect(getHumanizedValue( - { type: FORM_ELEMENT_TYPES.LOCATION }, - { latitude: 10.1234, longitude: 30.987 }, - 'default', - 'en-US', - GPS_FORMATS.DEG, - t - )).toBe('10.123400°, 30.987000°'); - }); - - test('returns the plain value for other element types', () => { - expect(getHumanizedValue( - { type: FORM_ELEMENT_TYPES.TEXT }, - 'Value', - 'default', - 'en-US', - GPS_FORMATS.DEG, - t - )).toBe('Value'); - }); - }); - describe('getItemTitle', () => { const t = (_, { collectionLength }) => `${collectionLength} items`; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/SortableItem/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/SortableItem/index.test.js index f5e8278c2..37e733447 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/SortableItem/index.test.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/SortableItem/index.test.js @@ -6,7 +6,7 @@ import { useSortable } from '@dnd-kit/sortable'; import { render, screen } from '../../../../../../../test-utils'; import { GPS_FORMATS } from '../../../../../../../utils/location'; import { mockStore } from '../../../../../../../__test-helpers/MockStore'; -import { FORM_ELEMENT_TYPES } from '../../../../constants'; +import { FORM_ELEMENT_TYPES } from '../../../../../../../utils/v2-event-schemas/constants'; import SortableItem from './'; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/index.test.js index 0ef9b48ae..ea99f22a5 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/index.test.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/index.test.js @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import userEvent from '@testing-library/user-event'; import { render, screen, within } from '../../../../../test-utils'; -import { FORM_ELEMENT_TYPES } from '../../constants'; +import { FORM_ELEMENT_TYPES } from '../../../../../utils/v2-event-schemas/constants'; import { GPS_FORMATS } from '../../../../../utils/location'; import { mockStore } from '../../../../../__test-helpers/MockStore'; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/DateTime/index.js b/src/ReportManager/DetailsSection/SchemaForm/fields/DateTime/index.js index af757a1b0..603e01acd 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/DateTime/index.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/DateTime/index.js @@ -1,7 +1,7 @@ import React, { memo, useEffect, useState } from 'react'; import { format, isValid, parseISO } from 'date-fns'; -import { DATE_TIME_ELEMENT_INPUT_TYPES } from '../../constants'; +import { DATE_TIME_ELEMENT_INPUT_TYPES } from '../../../../../utils/v2-event-schemas/constants'; import DatePicker, { isValidDate, EMPTY_DATE_VALUE } from '../../../../../DatePicker'; import DateTimePicker, { EMPTY_DATE_TIME_VALUE } from '../../../../../DateTimePicker'; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/DateTime/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/fields/DateTime/index.test.js index da627a503..05af9db95 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/DateTime/index.test.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/DateTime/index.test.js @@ -3,7 +3,7 @@ import { format, parseISO } from 'date-fns'; import userEvent from '@testing-library/user-event'; import { render, screen } from '../../../../../test-utils'; -import { DATE_TIME_ELEMENT_INPUT_TYPES } from '../../constants'; +import { DATE_TIME_ELEMENT_INPUT_TYPES } from '../../../../../utils/v2-event-schemas/constants'; import DateTime from './'; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Header/index.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Header/index.js index 243a578b8..936cbcd98 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Header/index.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Header/index.js @@ -1,6 +1,6 @@ import React, { memo } from 'react'; -import { HEADER_ELEMENT_SIZES } from '../../constants'; +import { HEADER_ELEMENT_SIZES } from '../../../../../utils/v2-event-schemas/constants'; import * as styles from './styles.module.scss'; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Header/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Header/index.test.js index 8f15fefa2..c38f09b11 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Header/index.test.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Header/index.test.js @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen } from '../../../../../test-utils'; -import { HEADER_ELEMENT_SIZES } from '../../constants'; +import { HEADER_ELEMENT_SIZES } from '../../../../../utils/v2-event-schemas/constants'; import Header from './'; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Text/index.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Text/index.js index ce8584047..cad18e303 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Text/index.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Text/index.js @@ -1,6 +1,6 @@ import React, { memo, useEffect, useRef } from 'react'; -import { TEXT_ELEMENT_INPUT_TYPES } from '../../constants'; +import { TEXT_ELEMENT_INPUT_TYPES } from '../../../../../utils/v2-event-schemas/constants'; import * as styles from './styles.module.scss'; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Text/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Text/index.test.js index 9e3edc388..eb39f7c6c 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Text/index.test.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Text/index.test.js @@ -2,7 +2,7 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; import { render, screen } from '../../../../../test-utils'; -import { TEXT_ELEMENT_INPUT_TYPES } from '../../constants'; +import { TEXT_ELEMENT_INPUT_TYPES } from '../../../../../utils/v2-event-schemas/constants'; import Text from './'; diff --git a/src/ReportManager/DetailsSection/SchemaForm/index.js b/src/ReportManager/DetailsSection/SchemaForm/index.js index 0b4472ca7..1a5467092 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/index.js +++ b/src/ReportManager/DetailsSection/SchemaForm/index.js @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { FORM_ELEMENT_TYPES, ROOT_CANVAS_ID } from './constants'; -import makeFieldsFromSchema from './utils/makeFieldsFromSchema'; +import { FORM_ELEMENT_TYPES, ROOT_CANVAS_ID } from '../../../utils/v2-event-schemas/constants'; +import makeFieldsFromSchema from '../../../utils/v2-event-schemas/makeFieldsFromSchema'; import useMapLocationMarkers from './utils/useMapLocationMarkers'; import useSchemaValidations from './utils/useSchemaValidations'; diff --git a/src/i18n.js b/src/i18n.js index 85a6f99d6..58a96b37d 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -50,12 +50,12 @@ i18n backendOptions: [{ expirationTime: 24 * 60 * 60 * 1000 * 7, versions: { - es: 'v1.11', - 'en-US': 'v1.11', - fr: 'v1.11', - 'ne-NP': 'v1.11', - pt: 'v1.11', - sw: 'v1.11' + es: 'v1.12', + 'en-US': 'v1.12', + fr: 'v1.12', + 'ne-NP': 'v1.12', + pt: 'v1.12', + sw: 'v1.12' } }] } diff --git a/src/utils/v2-event-schemas/constants.js b/src/utils/v2-event-schemas/constants.js new file mode 100644 index 000000000..58885bf63 --- /dev/null +++ b/src/utils/v2-event-schemas/constants.js @@ -0,0 +1,40 @@ +export const CHOICE_LIST_ELEMENT_CHOICE_TYPES = { + EXISTING_CHOICE_LIST: 'EXISTING_CHOICE_LIST', + MY_DATA: 'MY_DATA', +}; + +export const CHOICE_LIST_ELEMENT_INPUT_TYPES = { + DROPDOWN: 'DROPDOWN', + LIST: 'LIST', +}; + +export const DATE_TIME_ELEMENT_INPUT_TYPES = { + DATE_TIME: 'DATE_TIME', + DATE: 'DATE', + TIME: 'TIME', +}; + +export const FORM_ELEMENT_TYPES = { + ATTACHMENT: 'ATTACHMENT', + CHOICE_LIST: 'CHOICE_LIST', + COLLECTION: 'COLLECTION', + DATE_TIME: 'DATE_TIME', + HEADER: 'HEADER', + LOCATION: 'LOCATION', + NUMERIC: 'NUMERIC', + SECTION: 'SECTION', + TEXT: 'TEXT', +}; + +export const HEADER_ELEMENT_SIZES = { + LARGE: 'LARGE', + MEDIUM: 'MEDIUM', + SMALL: 'SMALL', +}; + +export const ROOT_CANVAS_ID = 'root'; + +export const TEXT_ELEMENT_INPUT_TYPES = { + SHORT: 'SHORT_TEXT', + LONG: 'LONG_TEXT', +}; diff --git a/src/ReportManager/DetailsSection/SchemaForm/fixtures.js b/src/utils/v2-event-schemas/fixtures.js similarity index 99% rename from src/ReportManager/DetailsSection/SchemaForm/fixtures.js rename to src/utils/v2-event-schemas/fixtures.js index 1b0fc59a4..c713e88a5 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fixtures.js +++ b/src/utils/v2-event-schemas/fixtures.js @@ -63,4 +63,4 @@ export const choicesListOptions = [ const: '3bc7c8df-3461-47f2-8196-7b0a45405a13', title: 'Subject X' } -]; \ No newline at end of file +]; diff --git a/src/utils/v2-event-schemas/getHumanizedFieldValue/index.js b/src/utils/v2-event-schemas/getHumanizedFieldValue/index.js new file mode 100644 index 000000000..d54493977 --- /dev/null +++ b/src/utils/v2-event-schemas/getHumanizedFieldValue/index.js @@ -0,0 +1,64 @@ +import { format, isValid, parseISO } from 'date-fns'; + +import { calcGpsDisplayString } from '../../location'; +import { DATE_TIME_ELEMENT_INPUT_TYPES, FORM_ELEMENT_TYPES } from '../constants'; +import { shouldUse12HourFormat } from '../../datetime'; + +const getChoiceListOptionLabel = (value, field) => field.details.options + .find((option) => option.const === value)?.title; + +// Utility to calculate a human readable version of the field values. For example, render a date-time like +// 2020/01/01 12:00 PM instead of 2020-01-01T12:00:00Z. +const getHumanizedFieldValue = (field, value, defaultHumanizedValue, language, gpsFormat, t) => { + if (!value) { + return defaultHumanizedValue; + } + + const use12HourFormat = shouldUse12HourFormat(language); + + switch (field.type) { + case FORM_ELEMENT_TYPES.COLLECTION: + return t('collectionHumanizedValue', { collectionLength: value.length }); + + case FORM_ELEMENT_TYPES.CHOICE_LIST: + if (field.details.multiple) { + const humanizedValues = value.map((val) => { + return getChoiceListOptionLabel(val, field); + }); + return humanizedValues.join(', '); + } + return getChoiceListOptionLabel(value, field); + + case FORM_ELEMENT_TYPES.DATE_TIME: + let parsedDate; + let formatStr; + switch (field.details.inputType) { + case DATE_TIME_ELEMENT_INPUT_TYPES.DATE: + parsedDate = parseISO(value); + formatStr = 'yyyy/MM/dd'; + break; + + case DATE_TIME_ELEMENT_INPUT_TYPES.DATE_TIME: + parsedDate = parseISO(value); + formatStr = use12HourFormat ? 'yyyy/MM/dd hh:mm a' : 'yyyy/MM/dd HH:mm'; + break; + + case DATE_TIME_ELEMENT_INPUT_TYPES.TIME: + parsedDate = parseISO(`2000-01-01T${value}`); + formatStr = use12HourFormat ? 'hh:mm a' : 'HH:mm'; + break; + + default: + return defaultHumanizedValue; + } + return isValid(parsedDate) ? format(parsedDate, formatStr) : defaultHumanizedValue; + + case FORM_ELEMENT_TYPES.LOCATION: + return calcGpsDisplayString(value.latitude, value.longitude, gpsFormat); + + default: + return value; + }; +}; + +export default getHumanizedFieldValue; diff --git a/src/utils/v2-event-schemas/getHumanizedFieldValue/index.test.js b/src/utils/v2-event-schemas/getHumanizedFieldValue/index.test.js new file mode 100644 index 000000000..7f3063cb7 --- /dev/null +++ b/src/utils/v2-event-schemas/getHumanizedFieldValue/index.test.js @@ -0,0 +1,127 @@ +import { format, parseISO } from 'date-fns'; + +import { choicesListOptions } from '../fixtures'; +import { DATE_TIME_ELEMENT_INPUT_TYPES, FORM_ELEMENT_TYPES } from '../constants'; +import getHumanizedFieldValue from '.'; +import { GPS_FORMATS } from '../../location'; + +describe('getHumanizedFieldValue', () => { + const t = (_, { collectionLength }) => `${collectionLength} items`; + + test('returns the default value if no value is provided', () => { + expect(getHumanizedFieldValue({ type: FORM_ELEMENT_TYPES.TEXT }, undefined, 'default', 'en-US', GPS_FORMATS.DEG, t)).toBe('default'); + }); + + test('returns the length of a collection', () => { + expect(getHumanizedFieldValue( + { type: FORM_ELEMENT_TYPES.COLLECTION }, + [{}, {}], + 'default', + 'en-US', + GPS_FORMATS.DEG, + t + )).toBe('2 items'); + }); + + test('returns the choice list values separated by a comma if it supports multiple values', () => { + expect(getHumanizedFieldValue( + { + details: { + multiple: true, + options: choicesListOptions + }, + type: FORM_ELEMENT_TYPES.CHOICE_LIST + }, + ['17e67b22-0e4a-4fcb-aeee-903b51a7a2e0', '223ab492-0ea7-4ff2-b8b8-cb6504c943b6'], + 'default', + 'en-US', + GPS_FORMATS.DEG, + t + )).toBe('Desert Bighorn Sheep, Ranger Cruz'); + }); + + test('returns the choice list value it supports a single value', () => { + expect(getHumanizedFieldValue( + { + details: { + multiple: false, + options: choicesListOptions + }, + type: FORM_ELEMENT_TYPES.CHOICE_LIST + }, + '0d553bb7-5c4f-43d7-9b82-a561a668ae64', + 'default', + 'en-US', + GPS_FORMATS.DEG, + t + )).toBe('EarthRanger System'); + }); + + test('returns a readable date value', () => { + expect(getHumanizedFieldValue( + { details: { inputType: DATE_TIME_ELEMENT_INPUT_TYPES.DATE }, type: FORM_ELEMENT_TYPES.DATE_TIME }, + '2020-01-01', + 'default', + 'en-US', + GPS_FORMATS.DEG, + t + )).toBe('2020/01/01'); + }); + + test('returns a readable date time value', () => { + const utcValue = '2020-01-01T06:30:00Z'; + expect(getHumanizedFieldValue( + { details: { inputType: DATE_TIME_ELEMENT_INPUT_TYPES.DATE_TIME }, type: FORM_ELEMENT_TYPES.DATE_TIME }, + utcValue, + 'default', + 'en-US', + GPS_FORMATS.DEG, + t + )).toBe(format(parseISO(utcValue), 'yyyy/MM/dd hh:mm a')); + }); + + test('returns a readable time value', () => { + const utcValue = '06:30:00Z'; + expect(getHumanizedFieldValue( + { details: { inputType: DATE_TIME_ELEMENT_INPUT_TYPES.TIME }, type: FORM_ELEMENT_TYPES.DATE_TIME }, + utcValue, + 'default', + 'en-US', + GPS_FORMATS.DEG, + t + )).toBe(format(parseISO(`2000-01-01T${utcValue}`), 'hh:mm a')); + }); + + test('returns the default value if a date-time element is invalid', () => { + expect(getHumanizedFieldValue( + { details: { inputType: DATE_TIME_ELEMENT_INPUT_TYPES.DATE }, type: FORM_ELEMENT_TYPES.DATE_TIME }, + 'invalid', + 'default', + 'en-US', + GPS_FORMATS.DEG, + t + )).toBe('default'); + }); + + test('returns the coordinates from a location in the provided GPS format', () => { + expect(getHumanizedFieldValue( + { type: FORM_ELEMENT_TYPES.LOCATION }, + { latitude: 10.1234, longitude: 30.987 }, + 'default', + 'en-US', + GPS_FORMATS.DEG, + t + )).toBe('10.123400°, 30.987000°'); + }); + + test('returns the plain value for other element types', () => { + expect(getHumanizedFieldValue( + { type: FORM_ELEMENT_TYPES.TEXT }, + 'Value', + 'default', + 'en-US', + GPS_FORMATS.DEG, + t + )).toBe('Value'); + }); +}); \ No newline at end of file diff --git a/src/ReportManager/DetailsSection/SchemaForm/utils/makeFieldsFromSchema/index.js b/src/utils/v2-event-schemas/makeFieldsFromSchema/index.js similarity index 99% rename from src/ReportManager/DetailsSection/SchemaForm/utils/makeFieldsFromSchema/index.js rename to src/utils/v2-event-schemas/makeFieldsFromSchema/index.js index 3689a4230..224d9d4ce 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/utils/makeFieldsFromSchema/index.js +++ b/src/utils/v2-event-schemas/makeFieldsFromSchema/index.js @@ -1,4 +1,4 @@ -import { DATE_TIME_ELEMENT_INPUT_TYPES, FORM_ELEMENT_TYPES, ROOT_CANVAS_ID } from '../../constants'; +import { DATE_TIME_ELEMENT_INPUT_TYPES, FORM_ELEMENT_TYPES, ROOT_CANVAS_ID } from '../constants'; const SECTION_CHILD_TYPES = { FIELD: 'field', HEADER: 'header' }; diff --git a/src/ReportManager/DetailsSection/SchemaForm/utils/makeFieldsFromSchema/index.test.js b/src/utils/v2-event-schemas/makeFieldsFromSchema/index.test.js similarity index 99% rename from src/ReportManager/DetailsSection/SchemaForm/utils/makeFieldsFromSchema/index.test.js rename to src/utils/v2-event-schemas/makeFieldsFromSchema/index.test.js index 807bef944..5d1729a35 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/utils/makeFieldsFromSchema/index.test.js +++ b/src/utils/v2-event-schemas/makeFieldsFromSchema/index.test.js @@ -6,9 +6,8 @@ import { HEADER_ELEMENT_SIZES, ROOT_CANVAS_ID, TEXT_ELEMENT_INPUT_TYPES, -} from '../../constants'; - -import { choicesListOptions } from '../../fixtures'; +} from '../constants'; +import { choicesListOptions } from '../fixtures'; describe('ReportManager - DetailsSection - SchemaForm - Utils - makeFieldsFromSchema', () => { it('creates section fields from the schema', () => {