diff --git a/public/locales/en-US/reports.json b/public/locales/en-US/reports.json index 5c22a9a26..076fc90ef 100644 --- a/public/locales/en-US/reports.json +++ b/public/locales/en-US/reports.json @@ -173,7 +173,8 @@ "doneButton": "Done" }, "formPreview": { - "collectionHumanizedValue": "{{collectionLength}} items" + "collectionHumanizedValue": "{{collectionLength}} items", + "jumpToLocationButtonLabel": "Jump to {{field}} location" }, "chevronButtonLabel": { "closed": "Open the {{itemTitle}} form preview", diff --git a/public/locales/es/reports.json b/public/locales/es/reports.json index dc6e95303..a2d10e250 100644 --- a/public/locales/es/reports.json +++ b/public/locales/es/reports.json @@ -162,7 +162,8 @@ "doneButton": "Hecho" }, "formPreview": { - "collectionHumanizedValue": "{{collectionLength}} elementos" + "collectionHumanizedValue": "{{collectionLength}} elementos", + "jumpToLocationButtonLabel": "Ir a la ubicación de {{field}}" }, "chevronButtonLabel": { "closed": "Abrir la vista previa del formulario {{itemTitle}}", diff --git a/public/locales/fr/reports.json b/public/locales/fr/reports.json index ee4d4f3a6..d47a3c224 100644 --- a/public/locales/fr/reports.json +++ b/public/locales/fr/reports.json @@ -162,7 +162,8 @@ "doneButton": "Terminé" }, "formPreview": { - "collectionHumanizedValue": "{{collectionLength}} éléments" + "collectionHumanizedValue": "{{collectionLength}} éléments", + "jumpToLocationButtonLabel": "Aller à l'emplacement de {{field}}" }, "chevronButtonLabel": { "closed": "Ouvrir l'aperçu du formulaire {{itemTitle}}", diff --git a/public/locales/ne-NP/reports.json b/public/locales/ne-NP/reports.json index 466e23ec2..5300589b4 100644 --- a/public/locales/ne-NP/reports.json +++ b/public/locales/ne-NP/reports.json @@ -170,8 +170,9 @@ "doneButton": "सम्पन्न" }, "formPreview": { - "collectionHumanizedValue": "{{collectionLength}} वस्तुहरू" - }, + "collectionHumanizedValue": "{{collectionLength}} वस्तुहरू", + "jumpToLocationButtonLabel": "{{field}} स्थानमा जानुहोस्" + }, "chevronButtonLabel": { "closed": "{{itemTitle}} फारमको पूर्वावलोकन खोल्नुहोस्", "open": "{{itemTitle}} फारमको पूर्वावलोकन बन्द गर्नुहोस्" diff --git a/public/locales/pt/reports.json b/public/locales/pt/reports.json index 067c7a3cb..5a549223d 100644 --- a/public/locales/pt/reports.json +++ b/public/locales/pt/reports.json @@ -162,7 +162,8 @@ "doneButton": "Concluído" }, "formPreview": { - "collectionHumanizedValue": "{{collectionLength}} itens" + "collectionHumanizedValue": "{{collectionLength}} itens", + "jumpToLocationButtonLabel": "Ir para a localização de {{field}}" }, "chevronButtonLabel": { "closed": "Abrir a visualização do formulário {{itemTitle}}", diff --git a/public/locales/sw/reports.json b/public/locales/sw/reports.json index 9cbd32ebe..ecff28ff2 100644 --- a/public/locales/sw/reports.json +++ b/public/locales/sw/reports.json @@ -173,7 +173,8 @@ "doneButton": "Imekamilika" }, "formPreview": { - "collectionHumanizedValue": "{{collectionLength}} vitu" + "collectionHumanizedValue": "{{collectionLength}} vitu", + "jumpToLocationButtonLabel": "Ruka kwenye eneo la {{field}}" }, "chevronButtonLabel": { "closed": "Fungua muhtasari wa fomu ya {{itemTitle}}", diff --git a/src/CursorGpsDisplay/MenuPopover/index.js b/src/CursorGpsDisplay/MenuPopover/index.js index 5195e8da2..47265525b 100644 --- a/src/CursorGpsDisplay/MenuPopover/index.js +++ b/src/CursorGpsDisplay/MenuPopover/index.js @@ -59,6 +59,12 @@ const MenuPopover = ({ buttonRef, className, onClose, ...otherProps }, ref) => { } }; + const onGpsInputButtonClick = (event) => { + event.stopPropagation(); + + onJumpToCoordinates(); + }; + useEffect(() => { // Select the GPS input on mount so user can type away or navigate. gpsInputRef.current.select(); @@ -115,7 +121,7 @@ const MenuPopover = ({ buttonRef, className, onClose, ...otherProps }, ref) => { aria-label={t('gpsInputButtonLabel')} className={styles.gpsInputButton} disabled={!gpsInputValue} - onClick={() => onJumpToCoordinates()} + onClick={onGpsInputButtonClick} ref={gpsInputButtonRef} title={t('gpsInputButtonLabel')} type="button" diff --git a/src/LocationPicker/MenuPopover/index.js b/src/LocationPicker/MenuPopover/index.js index 29dbac309..d56297e8a 100644 --- a/src/LocationPicker/MenuPopover/index.js +++ b/src/LocationPicker/MenuPopover/index.js @@ -22,6 +22,7 @@ const eventReportTracker = trackEventFactory(EVENT_REPORT_CATEGORY); const MenuPopover = ({ className, id, + onBlur, onChange, onClose, setLocationButtonRef, @@ -104,14 +105,34 @@ const MenuPopover = ({ }, []); useEffect(() => { - const onMouseDown = (event) => !wrapperRef.current.contains(event.target) - && !setLocationButtonRef.current.contains(event.target) - && onClose(); + const onMouseDown = (event) => { + if (!wrapperRef.current.contains(event.target) && !setLocationButtonRef.current.contains(event.target)) { + onClose(event); + + if (!target.current.contains(event.target)) { + // Clicking away from our picker when the menu is open doesn't trigger the wrapper's blur event, so we need + // to trigger the onBlur callback manually. + const blurEvent = new FocusEvent('blur', { + bubbles: true, + cancelable: false, + relatedTarget: event.target, + }); + + Object.defineProperties(blurEvent, { + target: { + value: target.current, + }, + }); + + onBlur(blurEvent); + } + } + }; document.addEventListener('mousedown', onMouseDown); return () => document.removeEventListener('mousedown', onMouseDown); - }, [onClose, setLocationButtonRef]); + }, [onBlur, onClose, setLocationButtonRef, target]); return ({ })); describe('LocationPicker - MenuPopover', () => { + const onBlur = jest.fn(); const onChange = jest.fn(); const onClose = jest.fn(); const setLocationButtonRefFocus = jest.fn(); @@ -48,6 +49,7 @@ describe('LocationPicker - MenuPopover', () => { { style={{}} target={{ current: { + contains: () => false, offsetWidth: 100, }, }} @@ -166,7 +169,47 @@ describe('LocationPicker - MenuPopover', () => { expect(setLocationButtonRefFocus).toHaveBeenCalledTimes(1); }); - test('closes the menu if the user clicks outside', () => { + test('closes the menu if the user clicks outside and triggers the blur callback if the click was outside of the picker', () => { + render(<> +
+ + + + false, + focus: setLocationButtonRefFocus, + }, + }} + style={{}} + target={{ + current: { + contains: () => false, + offsetWidth: 100, + }, + }} + value={null} + /> + + + ); + + expect(onClose).not.toHaveBeenCalled(); + expect(onBlur).not.toHaveBeenCalled(); + + userEvent.click(screen.getByTestId('outside')); + + expect(onClose).toHaveBeenCalledTimes(1); + expect(onBlur).toHaveBeenCalledTimes(1); + }); + + test('closes the menu if the user clicks outside but does not trigger the blur callback if the click was inside the picker', () => { render(<>
@@ -175,6 +218,7 @@ describe('LocationPicker - MenuPopover', () => { { style={{}} target={{ current: { + contains: () => true, offsetWidth: 100, }, }} @@ -200,5 +245,6 @@ describe('LocationPicker - MenuPopover', () => { userEvent.click(screen.getByTestId('outside')); expect(onClose).toHaveBeenCalledTimes(1); + expect(onBlur).not.toHaveBeenCalled(); }); }); diff --git a/src/LocationPicker/index.js b/src/LocationPicker/index.js index caf099766..52fcda2ec 100644 --- a/src/LocationPicker/index.js +++ b/src/LocationPicker/index.js @@ -18,6 +18,7 @@ const LocationPicker = ({ disabled = false, id, inputProps = {}, + jumpToLocationButtonZoom = undefined, name = '', onBlur = null, onChange, @@ -71,6 +72,7 @@ const LocationPicker = ({ className={`${styles.input} ${readOnly ? styles.readOnly : ''}`} disabled={disabled} id={id} + onFocus={() => setLocationButtonRef.current.focus()} placeholder={placeholder || t('defaultPlaceholder')} readOnly required={required} @@ -97,7 +99,7 @@ const LocationPicker = ({ aria-label={t('jumpToLocationButtonLabel')} className={styles.jumpToLocationButton} disabled={!value || disabled} - onClick={() => jumpToLocation([value.longitude, value.latitude])} + onClick={() => jumpToLocation([value.longitude, value.latitude], jumpToLocationButtonZoom)} title={t('jumpToLocationButtonLabel')} type="button" > @@ -116,6 +118,7 @@ const LocationPicker = ({ setIsMenuPopoverOpen(false)} setLocationButtonRef={setLocationButtonRef} target={innerRef} diff --git a/src/LocationPicker/index.test.js b/src/LocationPicker/index.test.js index f86e20384..5f9ed088e 100644 --- a/src/LocationPicker/index.test.js +++ b/src/LocationPicker/index.test.js @@ -163,6 +163,14 @@ describe('LocationPicker', () => { expect(screen.getByLabelText('Location')).toBeRequired(); }); + test('forwards the focusing of the input to the set location button', () => { + renderLocationPicker(); + + fireEvent.focus(screen.getByLabelText('Location')); + + expect(screen.getByLabelText('Open the location picker menu to set a value')).toHaveFocus(); + }); + test('shows a display value in the input', () => { renderLocationPicker({ value: { @@ -221,7 +229,24 @@ describe('LocationPicker', () => { userEvent.click(screen.getByLabelText('Jump to location')); expect(jumpToLocationMock).toHaveBeenCalledTimes(1); - expect(jumpToLocationMock).toHaveBeenCalledWith([10, 15]); + expect(jumpToLocationMock).toHaveBeenCalledWith([10, 15], undefined); + }); + + test('jumps to the location with a custom zoom', () => { + renderLocationPicker({ + jumpToLocationButtonZoom: 20, + value: { + latitude: 15, + longitude: 10, + }, + }); + + expect(jumpToLocationMock).not.toHaveBeenCalled(); + + userEvent.click(screen.getByLabelText('Jump to location')); + + expect(jumpToLocationMock).toHaveBeenCalledTimes(1); + expect(jumpToLocationMock).toHaveBeenCalledWith([10, 15], 20); }); test('opens the menu popover', () => { diff --git a/src/LocationPicker/styles.module.scss b/src/LocationPicker/styles.module.scss index d424d4bf4..b9c0a2744 100644 --- a/src/LocationPicker/styles.module.scss +++ b/src/LocationPicker/styles.module.scss @@ -50,7 +50,7 @@ background-color: colors.$disabled-field-gray; } - &:focus-visible:not(:disabled) { + &:focus:not(:disabled) { border: 2px solid colors.$bright-blue; } diff --git a/src/ReportManager/DetailsSection/SchemaForm/constants.js b/src/ReportManager/DetailsSection/SchemaForm/constants.js index 58885bf63..03eb6c1c6 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/constants.js +++ b/src/ReportManager/DetailsSection/SchemaForm/constants.js @@ -32,6 +32,8 @@ export const HEADER_ELEMENT_SIZES = { SMALL: 'SMALL', }; +export const JUMP_TO_LOCATION_BUTTON_ZOOM = 20; + export const ROOT_CANVAS_ID = 'root'; export const TEXT_ELEMENT_INPUT_TYPES = { diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormModal/index.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormModal/index.js index af21e6c8d..ca216f7f5 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormModal/index.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormModal/index.js @@ -12,6 +12,7 @@ const FormModal = ({ breadcrumbs, columns, errors, + focusLocationMarker, formData, isOpen, itemName, @@ -74,6 +75,7 @@ const FormModal = ({ formData[fieldId], onFieldChange, errors?.[fieldId], + focusLocationMarker, [...breadcrumbs, { display: title, id: fieldId }] ))}
@@ -87,6 +89,7 @@ const FormModal = ({ formData[fieldId], onFieldChange, errors?.[fieldId], + focusLocationMarker, [...breadcrumbs, { display: title, id: fieldId }] ))}
} diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormModal/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormModal/index.test.js index d7432202d..04bf30155 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormModal/index.test.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormModal/index.test.js @@ -8,6 +8,7 @@ import { mockStore } from '../../../../../../../../__test-helpers/MockStore'; import FormModal from './'; describe('ReportManager - DetailsSection - SchemaForm - fields - Collection - SortableList - Item - FormModal', () => { + const focusLocationMarker = jest.fn(); const onCancel = jest.fn(); const onDeleteItem = jest.fn(); const onDone = jest.fn(); @@ -31,6 +32,7 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - Collection - So breadcrumbs={[{ id: '1', display: 'Item 1' }, { id: '2', display: 'Item 2' }]} columns={1} errors={{}} + focusLocationMarker={focusLocationMarker} formData={{ 'field-1': 'Value 1', 'field-2': 'Value 2' }} isOpen itemName="Item" @@ -115,6 +117,7 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - Collection - So 'Value 1', onFieldChange, undefined, + focusLocationMarker, [{ id: '1', display: 'Item 1' }, { id: '2', display: 'Item 2' }, { id: 'field-1', display: 'Item 3' }] ); expect(renderField).toHaveBeenCalledWith( @@ -122,6 +125,7 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - Collection - So 'Value 2', onFieldChange, undefined, + focusLocationMarker, [{ id: '1', display: 'Item 1' }, { id: '2', display: 'Item 2' }, { id: 'field-2', display: 'Item 3' }] ); }); 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 0c75925c3..f02a0941d 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 @@ -2,11 +2,24 @@ import React from 'react'; import { useSelector } from 'react-redux'; 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 useJumpToLocation from '../../../../../../../../hooks/useJumpToLocation'; import styles from './styles.module.scss'; -const FormPreview = ({ errors, fieldIds, fields, formData, isDragOverlay }) => { +const FormPreview = ({ + blurLocationMarker, + errors, + fieldIds, + fields, + focusLocationMarker, + formData, + isDragOverlay, +}) => { + const jumpToLocation = useJumpToLocation(); const { t, i18n } = useTranslation('reports', { keyPrefix: 'reportManager.detailsSection.schemaForm.fields.collection.sortableList.item.formPreview', }); @@ -19,15 +32,33 @@ const FormPreview = ({ errors, fieldIds, fields, formData, isDragOverlay }) => { className={`${styles.formPreview} ${isDragOverlay ? styles.dragOverlay : ''} ${hasError ? styles.error : ''}`} data-testid="schema-form-collection-item-form-preview" > - {fieldIds.map((fieldId) =>
-

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

- -

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

-
)} + {fieldIds.map((fieldId) =>
  • +
    +

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

    + +

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

    +
    + + {fields[fieldId].type === FORM_ELEMENT_TYPES.LOCATION && formData[fieldId] && } +
  • )} ; }; 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 aab069520..19654183e 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 @@ -1,16 +1,26 @@ import React from 'react'; import { Provider } from 'react-redux'; +import userEvent from '@testing-library/user-event'; -import { render, screen } from '../../../../../../../../test-utils'; +import { fireEvent, render, screen } from '../../../../../../../../test-utils'; import { FORM_ELEMENT_TYPES } from '../../../../../constants'; import { GPS_FORMATS } from '../../../../../../../../utils/location'; import { mockStore } from '../../../../../../../../__test-helpers/MockStore'; +import useJumpToLocation from '../../../../../../../../hooks/useJumpToLocation'; import FormPreview from './'; +jest.mock('../../../../../../../../hooks/useJumpToLocation', () => jest.fn()); + describe('ReportManager - DetailsSection - SchemaForm - fields - Collection - SortableList - Item - FormPreview', () => { - let store; + const blurLocationMarker = jest.fn(); + const focusLocationMarker = jest.fn(); + + let jumpToLocationMock, store; beforeEach(() => { + jumpToLocationMock = jest.fn(); + useJumpToLocation.mockImplementation(() => jumpToLocationMock); + store = { view: { userPreferences: { @@ -23,6 +33,7 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - Collection - So const renderFormPreview = (props, overrideStore) => render( { + renderFormPreview({ + fieldIds: ['field-1', 'field-2'], + fields: { + 'field-1': { + details: { + label: 'Field 1', + }, + type: FORM_ELEMENT_TYPES.TEXT, + }, + 'field-2': { + details: { + label: 'Field 2', + }, + type: FORM_ELEMENT_TYPES.LOCATION, + }, + }, + formData: { 'field-1': 'Value 1', 'field-2': { latitude: 10, longitude: 10 } }, + }); + + expect(screen.getByLabelText('Jump to Field 2 location')).toBeVisible(); + }); + + test('does not show a jump to location button if fields are not of type location', () => { + renderFormPreview(); + + expect(screen.queryByLabelText('Jump to Field 2 location')).toBeNull(); + }); + + test('does not show a jump to location button for location fields without values', () => { + renderFormPreview({ + fieldIds: ['field-1', 'field-2'], + fields: { + 'field-1': { + details: { + label: 'Field 1', + }, + type: FORM_ELEMENT_TYPES.TEXT, + }, + 'field-2': { + details: { + label: 'Field 2', + }, + type: FORM_ELEMENT_TYPES.LOCATION, + }, + }, + formData: { 'field-1': 'Value 1' }, + }); + + expect(screen.queryByLabelText('Jump to Field 2 location')).toBeNull(); + }); + + test('jumps to the location of a location field when clicking the button and focuses its marker', () => { + renderFormPreview({ + fieldIds: ['field-1', 'field-2'], + fields: { + 'field-1': { + details: { + label: 'Field 1', + }, + type: FORM_ELEMENT_TYPES.TEXT, + }, + 'field-2': { + details: { + label: 'Field 2', + }, + type: FORM_ELEMENT_TYPES.LOCATION, + }, + }, + formData: { 'field-1': 'Value 1', 'field-2': { latitude: 10, longitude: 10 } }, + }); + + expect(jumpToLocationMock).not.toHaveBeenCalled(); + expect(focusLocationMarker).not.toHaveBeenCalled(); + + userEvent.click(screen.getByLabelText('Jump to Field 2 location')); + + expect(jumpToLocationMock).toHaveBeenCalledTimes(1); + expect(jumpToLocationMock).toHaveBeenCalledWith([10, 10], 20); + expect(focusLocationMarker).toHaveBeenCalledTimes(1); + expect(focusLocationMarker).toHaveBeenCalledWith('field-2'); + }); + + test('does neither jump to the location of a location field when clicking the button nor focuses its marker if its a drag overlay', () => { + renderFormPreview({ + fieldIds: ['field-1', 'field-2'], + fields: { + 'field-1': { + details: { + label: 'Field 1', + }, + type: FORM_ELEMENT_TYPES.TEXT, + }, + 'field-2': { + details: { + label: 'Field 2', + }, + type: FORM_ELEMENT_TYPES.LOCATION, + }, + }, + formData: { 'field-1': 'Value 1', 'field-2': { latitude: 10, longitude: 10 } }, + isDragOverlay: true, + }); + + userEvent.click(screen.getByLabelText('Jump to Field 2 location')); + + expect(jumpToLocationMock).not.toHaveBeenCalled(); + expect(focusLocationMarker).not.toHaveBeenCalled(); + }); + + test('blurs the location marker when the jump to location button is blurred', () => { + renderFormPreview({ + fieldIds: ['field-1', 'field-2'], + fields: { + 'field-1': { + details: { + label: 'Field 1', + }, + type: FORM_ELEMENT_TYPES.TEXT, + }, + 'field-2': { + details: { + label: 'Field 2', + }, + type: FORM_ELEMENT_TYPES.LOCATION, + }, + }, + formData: { 'field-1': 'Value 1', 'field-2': { latitude: 10, longitude: 10 } }, + }); + + userEvent.click(screen.getByLabelText('Jump to Field 2 location')); + + expect(blurLocationMarker).not.toHaveBeenCalled(); + + fireEvent.blur(screen.getByLabelText('Jump to Field 2 location')); + + expect(blurLocationMarker).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormPreview/styles.module.scss b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormPreview/styles.module.scss index 12ed59814..62d213b07 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormPreview/styles.module.scss +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/FormPreview/styles.module.scss @@ -17,22 +17,54 @@ border-color: colors.$bright-red; } - .summaryLabel { - font-size: 0.875rem; - color: colors.$secondary-medium-gray; - margin: 0; + .fieldSummary { + align-items: center; + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; - &.error { - color: colors.$bright-red; + .label { + font-size: 0.875rem; + color: colors.$secondary-medium-gray; + margin: 0; + + &.error { + color: colors.$bright-red; + } } - } + + .value { + color: black; + margin: 0; + + &.error { + color: colors.$bright-red; + } + } + + .jumpToLocationButton { + background: none; + border: none; + border-radius: 0.1875rem; + color: colors.$secondary-medium-gray; + outline: none; + + &:hover { + background: colors.$light-gray-background; + } + + &:focus-visible { + border: 2px solid colors.$bright-blue; + color: colors.$bright-blue; + } - .summaryValue { - color: black; - margin-bottom: 0.5rem; + &.dragOverlay { + cursor: grabbing; - &.error { - color: colors.$bright-red; + &:hover { + background: unset; + } + } } } } diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/index.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/index.js index 44a9b4356..bc02641e8 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/index.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/SortableList/Item/index.js @@ -17,12 +17,15 @@ import FormPreview from './FormPreview'; import styles from './styles.module.scss'; const Item = ({ + blurLocationMarker = null, breadcrumbs = null, collectionDetails, errors, fields, + focusLocationMarker = null, formData, id, + index = null, isDragging = false, isDragOverlay = false, isFormModalOpen = false, @@ -118,7 +121,15 @@ const Item = ({ + (isDragging ? ` ${styles.isDragging}` : '') + (isDragOverlay ? ` ${styles.dragOverlay}` : '') + (hasError ? ` ${styles.error}` : ''); - return
  • + return
  • { + const blurLocationMarker = jest.fn(); + const focusLocationMarker = jest.fn(); const onChange = jest.fn(); const onDelete = jest.fn(); const renderField = jest.fn(); @@ -24,6 +26,7 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - Collection - So itemName: 'Collection 1', leftColumn: ['field-1', 'field-2'], rightColumn: [], + value: 'collection-1', }; store = { @@ -41,6 +44,7 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - Collection - So const renderItem = (props, overrideStore) => render( { + renderItem(); + + expect(screen.getByTestId('schema-form-collection-item')).toHaveAttribute('id', 'collection-1.0'); + }); + + test('does not assign an id to the item if the position index is not provided', () => { + renderItem({ index: undefined }); + + expect(screen.getByTestId('schema-form-collection-item')).not.toHaveAttribute('id'); + }); + test('opens the form preview when the user clicks the title', () => { renderItem(); @@ -273,6 +291,7 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - Collection - So rerender( { }; const SortableList = ({ + blurLocationMarker, breadcrumbs, collectionDetails, fields, + focusLocationMarker, items, onItemChange, onItemDelete, @@ -140,12 +142,15 @@ const SortableList = ({ > item.id)} strategy={verticalListSortingStrategy}> {items.map((item, index) => (markerId) => + focusLocationMarker(`${id}.${itemIndex}.${markerId}`); + return
    : !!value[index]) diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/index.test.js index bae4d0d14..f644951e5 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/index.test.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Collection/index.test.js @@ -10,6 +10,8 @@ import { mockStore } from '../../../../../__test-helpers/MockStore'; import Collection from './'; describe('ReportManager - DetailsSection - SchemaForm - fields - Collection', () => { + const blurLocationMarker = jest.fn(); + const focusLocationMarker = jest.fn(); const onFieldChange = jest.fn(); const renderField = jest.fn(); @@ -44,6 +46,7 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - Collection', () const renderCollectionField = (props, overrideStore) => render( { + renderField.mockImplementation((_id, _value, _onChange, _error, focusLocationMarker) => { + focusLocationMarker('location-1'); + + return null; + }); + renderCollectionField({ value: [{}] }); + + userEvent.click(screen.getByLabelText('Edit Item 1')); + + expect(focusLocationMarker).toHaveBeenCalled(); + expect(focusLocationMarker).toHaveBeenCalledWith('collection-1.0.location-1'); + }); + test('opens and closes the form preview of an item', () => { renderCollectionField({ value: [{}] }); @@ -281,6 +299,7 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - Collection', () rerender( () => blurLocationMarker(), [blurLocationMarker]); + return
    @@ -27,7 +33,10 @@ const Location = ({ 'aria-invalid': hasError, 'aria-required': details.isRequired, }} + jumpToLocationButtonZoom={JUMP_TO_LOCATION_BUTTON_ZOOM} + onBlur={() => blurLocationMarker()} onChange={(newLocation) => onFieldChange(id, newLocation || undefined)} + onFocus={() => focusLocationMarker(id)} value={value} /> diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Location/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Location/index.test.js index b4d8547a9..72274d097 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Location/index.test.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Location/index.test.js @@ -8,7 +8,11 @@ import { mockStore } from '../../../../../__test-helpers/MockStore'; import Location from './'; +jest.mock('../../../../../hooks/useJumpToLocation', () => () => () => {}); + describe('ReportManager - DetailsSection - SchemaForm - fields - Location', () => { + const blurLocationMarker = jest.fn(); + const focusLocationMarker = jest.fn(); const onFieldChange = jest.fn(); let details, store; @@ -34,8 +38,10 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - Location', () = const renderLocationField = (props, overrideStore) => render( { + renderLocationField({ + value: { + latitude: 10, + longitude: 10, + }, + }); + + expect(focusLocationMarker).not.toHaveBeenCalled(); + + await userEvent.click(screen.getByLabelText('Jump to location')); + + expect(focusLocationMarker).toHaveBeenCalledTimes(1); + expect(focusLocationMarker).toHaveBeenCalledWith('location-1'); + }); + test('updates the form data when the user does changes to the input', async () => { details.defaultInput = ''; renderLocationField(); @@ -122,4 +144,36 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - Location', () = expect(onFieldChange).toHaveBeenCalledTimes(2); expect(onFieldChange).toHaveBeenCalledWith('location-1', { latitude: 10, longitude: 10 }); }); + + test('blurs the location marker when the user blurs the location picker', async () => { + renderLocationField({ + value: { + latitude: 10, + longitude: 10, + }, + }); + + await userEvent.click(screen.getByLabelText('Jump to location')); + + expect(blurLocationMarker).not.toHaveBeenCalled(); + + await userEvent.click(screen.getByText('Location 1 Description')); + + expect(blurLocationMarker).toHaveBeenCalledTimes(1); + }); + + test('blurs the location marker when component unmounts', async () => { + const { unmount } = renderLocationField({ + value: { + latitude: 10, + longitude: 10, + }, + }); + + expect(blurLocationMarker).not.toHaveBeenCalled(); + + unmount(); + + expect(blurLocationMarker).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Section/index.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Section/index.js index 0f5ff762f..fc0a3366e 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Section/index.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Section/index.js @@ -4,7 +4,16 @@ import styles from './styles.module.scss'; // Sections are just visual elements that are not present in the form data structure. Thus, fields contained by // sections are in the root objects of form data and field errors. -const Section = ({ details, fieldErrors, formData, id, onFieldChange, onFieldErrorsChange, renderField }) => { +const Section = ({ + details, + fieldErrors, + focusLocationMarker, + formData, + id, + onFieldChange, + onFieldErrorsChange, + renderField, +}) => { const onColumnFieldChange = (fieldId, value, error) => { onFieldChange(fieldId, value); onFieldErrorsChange({ ...fieldErrors, [fieldId]: error }); @@ -25,7 +34,8 @@ const Section = ({ details, fieldErrors, formData, id, onFieldChange, onFieldErr fieldId, formData[fieldId], onColumnFieldChange, - fieldErrors[fieldId] + fieldErrors[fieldId], + focusLocationMarker ))}
    @@ -37,7 +47,8 @@ const Section = ({ details, fieldErrors, formData, id, onFieldChange, onFieldErr fieldId, formData[fieldId], onColumnFieldChange, - fieldErrors[fieldId] + fieldErrors[fieldId], + focusLocationMarker ))}
    }
    diff --git a/src/ReportManager/DetailsSection/SchemaForm/fields/Section/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/fields/Section/index.test.js index 56cea844e..ce8919555 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/fields/Section/index.test.js +++ b/src/ReportManager/DetailsSection/SchemaForm/fields/Section/index.test.js @@ -6,6 +6,7 @@ import { render, screen } from '../../../../../test-utils'; import Section from './'; describe('ReportManager - DetailsSection - SchemaForm - fields - Section', () => { + const focusLocationMarker = jest.fn(); const onFieldChange = jest.fn(); const onFieldErrorsChange = jest.fn(); const renderField = jest.fn(); @@ -23,6 +24,7 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - Section', () => const renderSectionField = (props) => render(
    expect(renderField.mock.calls[0][0]).toBe('text-1'); expect(renderField.mock.calls[0][1]).toBe('Value 1'); expect(renderField.mock.calls[0][3]).toBe(undefined); + expect(renderField.mock.calls[0][4]).toBe(focusLocationMarker); }); test('applies changes in values and errors from the children', () => { diff --git a/src/ReportManager/DetailsSection/SchemaForm/index.js b/src/ReportManager/DetailsSection/SchemaForm/index.js index 5bd674c88..0a20951c5 100644 --- a/src/ReportManager/DetailsSection/SchemaForm/index.js +++ b/src/ReportManager/DetailsSection/SchemaForm/index.js @@ -2,6 +2,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 useLocationMarkersLayer from './utils/useLocationMarkersLayer'; import useSchemaValidations from './utils/useSchemaValidations'; import Collection from './fields/Collection'; @@ -16,13 +17,13 @@ import Text from './fields/Text'; export const FIELDS = { [FORM_ELEMENT_TYPES.CHOICE_LIST]: ChoiceList, [FORM_ELEMENT_TYPES.DATE_TIME]: DateTime, - [FORM_ELEMENT_TYPES.LOCATION]: Location, [FORM_ELEMENT_TYPES.NUMERIC]: Numeric, [FORM_ELEMENT_TYPES.TEXT]: Text, }; const SchemaForm = ({ autofillDefaultInputs, + eventLocation, initialFormData, onFormDataChange, onFormSubmit, @@ -31,6 +32,25 @@ const SchemaForm = ({ }) => { const runValidations = useSchemaValidations(schema); + const onLocationMarkerClick = useCallback((markerId) => { + const locationField = document.getElementById(markerId); + if (locationField) { + locationField.focus(); + } else { + // If the location field of the clicked marker is not defined, it will be contained by a collection item, so we + // try to calculate the collection item id to focus it. + const markerIdPathParts = markerId.split('.'); + const collectionItemId = `${markerIdPathParts[0]}.${markerIdPathParts[1]}`; + document.getElementById(collectionItemId)?.focus(); + } + }, []); + + const { + blurLocationMarker, + focusLocationMarker, + updateLocationMarkers + } = useLocationMarkersLayer(eventLocation, onLocationMarkerClick); + // This ref works as a flag to trigger a useEffect and call onFormDataChange asynchronously when there are changes in // the form data, so we can keep the onSectionFieldChange dependency array empty. const shouldSendFormDataChangeRef = useRef(false); @@ -57,7 +77,6 @@ const SchemaForm = ({ // collection). const idOfFirstErroneousField = Object.keys(fieldErrors)[0]; const elementWithError = document.getElementById(idOfFirstErroneousField); - elementWithError?.scrollIntoView?.(); elementWithError?.focus(); } else { onFormSubmit(); @@ -65,18 +84,21 @@ const SchemaForm = ({ }; // This method is designed to render fields inside sections and collections. In order to support recursion we let the - // parents handle the propagation of values, change callbacks, errors, breadcrumbs (only for collections), etc... - const renderField = (id, value, onChange, error, breadcrumbs = []) => { + // parents handle the propagation of values, change callbacks, errors, focusing of location markers and breadcrumbs + // (only for collections). + const renderField = (id, value, onChange, error, focusLocationMarker, breadcrumbs = []) => { switch (fields[id].type) { case FORM_ELEMENT_TYPES.HEADER: return
    ; case FORM_ELEMENT_TYPES.COLLECTION: return ; + case FORM_ELEMENT_TYPES.LOCATION: + return ; + default: const Field = FIELDS[fields[id].type]; return { + // Update the location markers whenever there is a change. + const locationMarkers = {}; + const addLocationMarkersFromFormDataRecursively = (formData, idPrefix = '') => { + Object.entries(formData).forEach(([fieldId, fieldValue]) => { + if (fields[fieldId]?.type === FORM_ELEMENT_TYPES.LOCATION && fieldValue) { + // If the field is a location with a value, add it to the location markers. + locationMarkers[`${idPrefix}${fieldId}`] = fieldValue; + } else if (fields[fieldId]?.type === FORM_ELEMENT_TYPES.COLLECTION) { + // If the field is a collection, add the location markers for each of its items recursively with a prefix to + // differentiate the same fields in different collection items. + fieldValue.forEach((itemFormData, index) => addLocationMarkersFromFormDataRecursively( + itemFormData, + `${idPrefix}${fieldId}.${index}.` + )); + } + }); + }; + addLocationMarkersFromFormDataRecursively(formData); + + updateLocationMarkers(locationMarkers); + }, [fields, formData, updateLocationMarkers]); + return
    {fields[ROOT_CANVAS_ID]?.details.fields.map((sectionId) =>
    { + const map = useContext(MapContext); + + // State variables to hold the markers and to track which one to focus, if any. + const [focused, setFocused] = useState(null); + const [markers, setMarkers] = useState({}); + + // GeoJSON feature collection with the location of each marker as a point. We store the marker id as a feature + // property instead of as the feature id because Mapbox doesn't support string feature ids. + const markerPointsFeatureCollection = useMemo(() => featureCollection( + Object.entries(markers).map(([markerId, markerLocation]) => point( + [markerLocation.longitude, markerLocation.latitude], + { id: markerId }, + )) + ), [markers]); + + // GeoJSON feature collection with line strings connecting each marker to the event location. + const markerConnectingLinesFeatureCollection = useMemo(() => { + if (eventLocation?.latitude && eventLocation?.longitude) { + return featureCollection( + Object.values(markers).map((markerLocation) => lineString([ + [markerLocation.longitude, markerLocation.latitude], + [eventLocation.longitude, eventLocation.latitude], + ])) + ); + } + return null; + }, [eventLocation?.latitude, eventLocation?.longitude, markers]); + + // Map sources for the marker points and connecting lines. + useMapSources([{ data: markerPointsFeatureCollection, id: MARKERS_SOURCE_ID }]); + useMapSources([{ data: markerConnectingLinesFeatureCollection, id: MARKER_CONNECTING_LINES_SOURCE_ID }]); + + // Layer for the markers. + useMapLayers([{ + id: MARKERS_LAYER_ID, + layout: { + 'icon-allow-overlap': true, + // Use the blue location dot if focused, otherwise use the gray one. + 'icon-image': [ + 'case', + ['==', ['get', 'id'], focused], 'location-dot-blue', + 'location-dot-gray', + ], + 'icon-offset': [0, -29], + 'icon-size': 0.5, + }, + paint: { 'icon-color': 'white' }, + sourceId: MARKERS_SOURCE_ID, + type: 'symbol', + }]); + + // Layer for the black connecting lines. + useMapLayers([{ + id: MARKER_CONNECTING_LINES_LAYER_ID, + options: { before: MARKERS_LAYER_ID }, + paint: { 'line-color': 'black', 'line-width': 1 }, + sourceId: MARKER_CONNECTING_LINES_SOURCE_ID, + type: 'line', + }]); + + // Layer for the white outline of the connecting lines. + useMapLayers([{ + id: MARKER_CONNECTING_OUTLINES_LAYER_ID, + options: { before: MARKER_CONNECTING_LINES_LAYER_ID }, + paint: { 'line-color': 'white', 'line-width': 5 }, + sourceId: MARKER_CONNECTING_LINES_SOURCE_ID, + type: 'line', + }]); + + // Listener to trigger the marker click callback with the id of the clicked marker. + const onMarkerClick = useCallback( + (event) => onMarkerClickCallback(event.features[0].properties.id), + [onMarkerClickCallback] + ); + + // Listeners to add a hover effect to the markers. + const onMarkerMouseEnter = useCallback(() => map.getCanvas().style.cursor = 'pointer', [map]); + const onMarkerMouseLeave = useCallback(() => map.getCanvas().style.cursor = '', [map]); + + useMapEventBinding('click', onMarkerClick, MARKERS_LAYER_ID); + useMapEventBinding('mouseenter', onMarkerMouseEnter, MARKERS_LAYER_ID); + useMapEventBinding('mouseleave', onMarkerMouseLeave, MARKERS_LAYER_ID); + + // Add location dot images to the map if they are not there yet. + useEffect(() => { + if (map) { + if (!map.hasImage('location-dot-blue')) { + addMapImage({ src: LocationDotBluePNG, id: 'location-dot-blue' }); + } + if (!map.hasImage('location-dot-gray')) { + addMapImage({ src: LocationDotGrayPNG, id: 'location-dot-gray' }); + } + } + }, [map]); + + // Exposed methods to update, focus and blur the markers. + const updateLocationMarkers = useCallback((markers) => setMarkers(markers), []); + const focusLocationMarker = useCallback((id) => setFocused(id), []); + const blurLocationMarker = useCallback(() => setFocused(null), []); + + return { blurLocationMarker, focusLocationMarker, updateLocationMarkers }; +}; + +export default useLocationMarkersLayer; diff --git a/src/ReportManager/DetailsSection/SchemaForm/utils/useLocationMarkersLayer/index.test.js b/src/ReportManager/DetailsSection/SchemaForm/utils/useLocationMarkersLayer/index.test.js new file mode 100644 index 000000000..1f11c1ba2 --- /dev/null +++ b/src/ReportManager/DetailsSection/SchemaForm/utils/useLocationMarkersLayer/index.test.js @@ -0,0 +1,232 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { useMapEventBinding } from '../../../../../hooks'; +import useMapLayers from '../../../../../hooks/useMapLayers'; +import useMapSources from '../../../../../hooks/useMapSources'; + +import useLocationMarkersLayer from '.'; + +jest.mock('../../../../../hooks', () => ({ + useMapEventBinding: jest.fn(), +})); + +jest.mock('../../../../../hooks/useMapLayers', () => jest.fn()); + +jest.mock('../../../../../hooks/useMapSources', () => jest.fn()); + +describe('ReportManager - DetailsSection - SchemaForm - Utils - useLocationMarkersLayer', () => { + const onMarkerClickCallback = jest.fn(); + + it('triggers the onMarkerClickCallback when a marker is clicked', () => { + useMapEventBinding.mockImplementation((eventType, handlerFn) => { + if (eventType === 'click') { + handlerFn({ features: [{ properties: { id: 'clicked-marker' } }] }); + } + }); + + renderHook(() => + useLocationMarkersLayer( + { latitude: 10, longitude: 10 }, + onMarkerClickCallback + ) + ); + + expect(onMarkerClickCallback).toHaveBeenCalledTimes(1); + expect(onMarkerClickCallback).toHaveBeenCalledWith('clicked-marker'); + }); + + it('updates the markers in the map', () => { + const { result } = renderHook(() => + useLocationMarkersLayer( + { latitude: 10, longitude: 10 }, + onMarkerClickCallback + ) + ); + + const { updateLocationMarkers } = result.current; + + expect(useMapSources).toHaveBeenCalledTimes(2); + expect(useMapSources).toHaveBeenCalledWith([ + { + data: { features: [], type: 'FeatureCollection' }, + id: 'event-location-markers-source', + }, + ]); + expect(useMapSources).toHaveBeenCalledWith([ + { + data: { features: [], type: 'FeatureCollection' }, + id: 'event-location-markers-source-lines', + }, + ]); + + updateLocationMarkers({ + 'location-1': { + latitude: 15, + longitude: 15, + }, + 'location-2': { + latitude: 20, + longitude: 20, + }, + }); + + expect(useMapSources).toHaveBeenCalledTimes(4); + expect(useMapSources).toHaveBeenCalledWith([ + { + data: { + features: [ + { + geometry: { + coordinates: [15, 15], + type: 'Point', + }, + properties: { + id: 'location-1', + }, + type: 'Feature', + }, + { + geometry: { + coordinates: [20, 20], + type: 'Point', + }, + properties: { + id: 'location-2', + }, + type: 'Feature', + }, + ], + type: 'FeatureCollection', + }, + id: 'event-location-markers-source', + }, + ]); + expect(useMapSources).toHaveBeenCalledWith([ + { + data: { + features: [ + { + geometry: { + coordinates: [ + [15, 15], + [10, 10], + ], + type: 'LineString', + }, + properties: {}, + type: 'Feature', + }, + { + geometry: { + coordinates: [ + [20, 20], + [10, 10], + ], + type: 'LineString', + }, + properties: {}, + type: 'Feature', + }, + ], + type: 'FeatureCollection', + }, + id: 'event-location-markers-source-lines', + }, + ]); + }); + + it('focuses a marker', () => { + const { result } = renderHook(() => + useLocationMarkersLayer( + { latitude: 10, longitude: 10 }, + onMarkerClickCallback + ) + ); + + const { focusLocationMarker, updateLocationMarkers } = result.current; + + updateLocationMarkers({ + 'location-1': { + latitude: 15, + longitude: 15, + }, + 'location-2': { + latitude: 20, + longitude: 20, + }, + }); + + expect(useMapLayers).toHaveBeenCalledTimes(6); + + focusLocationMarker('location-1'); + + expect(useMapLayers).toHaveBeenCalledTimes(9); + expect(useMapLayers).toHaveBeenCalledWith([ + { + id: 'event-location-markers-layer', + layout: { + 'icon-allow-overlap': true, + 'icon-image': [ + 'case', + ['==', ['get', 'id'], 'location-1'], + 'location-dot-blue', + 'location-dot-gray', + ], + 'icon-offset': [0, -29], + 'icon-size': 0.5, + }, + paint: { 'icon-color': 'white' }, + sourceId: 'event-location-markers-source', + type: 'symbol', + }, + ]); + }); + + it('blurs a marker', () => { + const { result } = renderHook(() => + useLocationMarkersLayer( + { latitude: 10, longitude: 10 }, + onMarkerClickCallback + ) + ); + + const { blurLocationMarker, focusLocationMarker, updateLocationMarkers } = result.current; + + updateLocationMarkers({ + 'location-1': { + latitude: 15, + longitude: 15, + }, + 'location-2': { + latitude: 20, + longitude: 20, + }, + }); + + focusLocationMarker('location-1'); + expect(useMapLayers).toHaveBeenCalledTimes(9); + + blurLocationMarker(); + + expect(useMapLayers).toHaveBeenCalledTimes(12); + expect(useMapLayers.mock.calls[9][0]).toEqual([ + { + id: 'event-location-markers-layer', + layout: { + 'icon-allow-overlap': true, + 'icon-image': [ + 'case', + ['==', ['get', 'id'], null], + 'location-dot-blue', + 'location-dot-gray', + ], + 'icon-offset': [0, -29], + 'icon-size': 0.5, + }, + paint: { 'icon-color': 'white' }, + sourceId: 'event-location-markers-source', + type: 'symbol', + }, + ]); + }); +}); diff --git a/src/ReportManager/DetailsSection/index.js b/src/ReportManager/DetailsSection/index.js index 61d85b0e1..3971fd095 100644 --- a/src/ReportManager/DetailsSection/index.js +++ b/src/ReportManager/DetailsSection/index.js @@ -291,6 +291,7 @@ const DetailsSection = ({ {(eventType?.version === 2 || efbFormSchemaSupportEnabled) && eventSchemaOverride &&