diff --git a/src/App.jsx b/src/App.jsx index 2e066b558..994d11511 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,4 @@ -/* eslint-disable react/no-unused-prop-types */ /* eslint-disable react/forbid-prop-types */ -/* eslint-disable camelcase */ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; @@ -8,7 +6,7 @@ import { FormattedMessage, IntlProvider } from 'react-intl'; import Helmet from 'react-helmet'; import classNames from 'classnames'; import { useApiTokens } from 'hds-react'; -import { useLocation, useParams, useSearchParams } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router-dom'; import messages from './i18n'; import Header from './components/Header/Header'; @@ -29,7 +27,6 @@ import Toast from './components/Toast'; function App({ language, isHighContrast, history, ...props }) { const { user, dispatchSetOidcUser, dispatchEnrichUser } = props; - const [ searchParams ] = useSearchParams(); const { fullscreen } = useParams(); const location = useLocation(); const [locale, setLocale] = useState(language); @@ -49,13 +46,6 @@ function App({ language, isHighContrast, history, ...props }) { }; }, [language]); - useEffect(() => { - const lang = searchParams.get('lang') - if (lang) { - setLocale(lang); - } - }, [searchParams, locale]); - useEffect(() => { if (!user && authenticated) { try { @@ -83,7 +73,7 @@ function App({ language, isHighContrast, history, ...props }) { let header = null; if (!fullscreen && !headless) { - header =
; + header =
; } const mainContainerId = 'main-container'; return ( diff --git a/src/actions/index.js b/src/actions/index.js index dfdc358ae..ef2f5bbe4 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -47,8 +47,8 @@ export function getResponseJSON(response) { } export const requestErrorHandler = ( - dispatch, - localizationKey = undefined + dispatch, + localizationKey = undefined ) => (err) => { Sentry.captureException(err); let payload; @@ -307,10 +307,14 @@ export function fetchAllSectionComments(hearingSlug, sectionId, ordering = '-n_v } export function postSectionComment(hearingSlug, sectionId, commentData = {}) { + // eslint-disable-next-line sonarjs/cognitive-complexity return (dispatch) => { const fetchAction = createAction("postingComment")({ hearingSlug, sectionId }); + dispatch(fetchAction); + const url = (`/v1/hearing/${hearingSlug}/sections/${sectionId}/comments/`); + let params = { content: commentData.text ? commentData.text : "", plugin_data: commentData.pluginData ? commentData.pluginData : null, @@ -321,10 +325,13 @@ export function postSectionComment(hearingSlug, sectionId, commentData = {}) { answers: commentData.answers ? commentData.answers : [], pinned: commentData.pinned ? commentData.pinned : false, map_comment_text: commentData.mapCommentText ? commentData.mapCommentText : "", + organization: commentData.organization ?? undefined }; + if (commentData.authorName) { params = Object.assign(params, { author_name: commentData.authorName }); } + if (commentData.comment) { params = { ...params, comment: commentData.comment }; } @@ -342,7 +349,7 @@ export function postSectionComment(hearingSlug, sectionId, commentData = {}) { createLocalizedAlert("commentReceived"); }).catch((err) => { requestErrorHandler(dispatch, "loginToComment")(err); - }); + }); }; } diff --git a/src/components/BaseCommentForm.jsx b/src/components/BaseCommentForm.jsx index 07cc00a01..4bf5d84f3 100644 --- a/src/components/BaseCommentForm.jsx +++ b/src/components/BaseCommentForm.jsx @@ -68,28 +68,8 @@ const BaseCommentForm = ({ onPostComment, onChangeAnswers, }) => { - const dispatch = useDispatch(); - const defaultState = useMemo(() => ({ - commentText: '', - nickname: defaultNickname, - imageTooBig: false, - images: [], - pinned: false, - showAlert: true, - hideName: false, - geojson: {}, - mapCommentText: '', - commentRequiredError: false, - commentOrAnswerRequiredError: false, - }), [defaultNickname]); - - const [formData, setFormData] = useState({ - ...defaultState, - collapsed: true, - }); - /** * Determines whether the logged in user is admin or not. * The array in users with key adminOrganizations should be of length > 0 @@ -99,6 +79,29 @@ const BaseCommentForm = ({ [loggedIn, user], ); + const defaultState = useMemo( + () => ({ + commentText: '', + nickname: defaultNickname, + imageTooBig: false, + images: [], + pinned: false, + showAlert: true, + hideName: false, + geojson: {}, + mapCommentText: '', + commentRequiredError: false, + commentOrAnswerRequiredError: false, + organization: isUserAdmin ? user.adminOrganizations[0] : undefined, + }), + [defaultNickname, isUserAdmin, user?.adminOrganizations], + ); + + const [formData, setFormData] = useState({ + ...defaultState, + collapsed: true, + }); + const hasQuestions = useMemo(() => hasAnyQuestions(section), [section]); const userAnsweredAllQuestions = useMemo( @@ -125,7 +128,11 @@ const BaseCommentForm = ({ onOverrideCollapse(); } } else { - dispatch(addToast(createLocalizedNotificationPayload(NOTIFICATION_TYPES.error, getSectionCommentingErrorMessage(section)))); + dispatch( + addToast( + createLocalizedNotificationPayload(NOTIFICATION_TYPES.error, getSectionCommentingErrorMessage(section)), + ), + ); } }, [canComment, defaultState, formData, onOverrideCollapse, section, dispatch]); @@ -193,7 +200,7 @@ const BaseCommentForm = ({ const pluginComment = getPluginComment(); let pluginData = getPluginData(); - const { nickname, commentText, geojson, images, pinned, mapCommentText, imageTooBig } = formData; + const { nickname, commentText, geojson, images, pinned, mapCommentText, imageTooBig, organization } = formData; const data = { nickname: nickname === '' ? nicknamePlaceholder : nickname, @@ -203,6 +210,7 @@ const BaseCommentForm = ({ pinned, mapCommentText, label: null, + organization, }; // plugin comment will override comment fields, if provided @@ -239,19 +247,20 @@ const BaseCommentForm = ({ // make sure empty comments are not added when not intended if (isEmptyCommentAllowed(section, hasAnyAnswers(answers)) && !data.commentText.trim()) { - data.setCommentText = config.emptyCommentString; + data.commentText = config.emptyCommentString; } - onPostComment( - data.commentText, - data.nickname, + onPostComment({ + text: data.commentText, + authorName: data.nickname, pluginData, - data.geojson, - data.label, - data.images, - data.pinned, - data.mapCommentText, - ); + geojson: data.geojson, + label: data.label, + images: data.images, + pinned: data.pinned, + mapCommentText: data.mapCommentText, + organization: data.organization ?? undefined, + }); setFormData({ ...formData, @@ -350,34 +359,30 @@ const BaseCommentForm = ({ /** * For admins, there is slightly different form. */ - const renderFormForAdmin = () => { - const organization = isUserAdmin && user.adminOrganizations[0]; - - return ( - <> - } - hideLabel - id='nickname' - placeholder={nicknamePlaceholder} - value={formData.nickname} - onChange={handleNicknameChange} - maxLength={32} - disabled - /> - } - hideLabel - id='organization' - placeholder={intl.formatMessage({ id: 'organization' })} - value={organization || ''} - onChange={() => {}} - maxLength={32} - disabled - /> - - ); - }; + const renderFormForAdmin = () => ( + <> + } + hideLabel + id='nickname' + placeholder={nicknamePlaceholder} + value={formData.nickname} + onChange={handleNicknameChange} + maxLength={32} + disabled + /> + } + hideLabel + id='organization' + placeholder={intl.formatMessage({ id: 'organization' })} + value={formData.organization} + onChange={() => {}} + maxLength={32} + disabled + /> + + ); /** * If an admin type of user is posting comment, the form is slightly different. diff --git a/src/components/DeleteModal.jsx b/src/components/DeleteModal.jsx index f940b6826..907c8bf44 100644 --- a/src/components/DeleteModal.jsx +++ b/src/components/DeleteModal.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import { Button, Dialog } from 'hds-react'; import { FormattedMessage, injectIntl } from 'react-intl'; -const DeleteModal = ({ isOpen, intl, close, onDeleteComment }) => { +const DeleteModal = ({ isOpen, commentSectionId, commentId, refreshUser, intl, close, onDeleteComment }) => { const titleId = 'delete-modal-title'; const descriptionId = 'delete-modal-description'; @@ -30,7 +30,7 @@ const DeleteModal = ({ isOpen, intl, close, onDeleteComment }) => { + ); - const toggleEditor = (event) => { - event.preventDefault(); - if (state.editorOpen) { - setState({ editorOpen: false }); - } else { - setState({ editorOpen: true }); - } - }; - /** * If a user can edit their comment(s) render hyperlinks * @returns {Component|null} diff --git a/src/components/Hearing/Section/SectionContainer.jsx b/src/components/Hearing/Section/SectionContainer.jsx index db6889476..bed2eac51 100644 --- a/src/components/Hearing/Section/SectionContainer.jsx +++ b/src/components/Hearing/Section/SectionContainer.jsx @@ -38,7 +38,12 @@ import { fetchMoreSectionComments, getCommentSubComments, } from '../../../actions'; -import { getHearingWithSlug, getMainSectionComments, getSections, getHearingContacts } from '../../../selectors/hearing'; +import { + getHearingWithSlug, + getMainSectionComments, + getSections, + getHearingContacts, +} from '../../../selectors/hearing'; import getUser from '../../../selectors/user'; import 'react-image-lightbox/style.css'; import { getApiTokenFromStorage, getApiURL, get as apiGet } from '../../../api'; @@ -68,7 +73,7 @@ const SectionContainerComponent = ({ const [mainHearingAttachmentsOpen, setMainHearingAttachmentsOpen] = useState(false); const { hearingSlug, sectionId } = useParams(); - const { search } = useLocation(); + const { search } = useLocation(); const hearing = useSelector((state) => getHearingWithSlug(state, hearingSlug)); const sections = useSelector((state) => getSections(state, hearingSlug)); @@ -76,15 +81,20 @@ const SectionContainerComponent = ({ const contacts = useSelector((state) => getHearingContacts(state, hearingSlug)); const mainSection = sections.find((sec) => sec.type === SectionTypes.MAIN); const section = sections.find((sec) => sec.id === sectionId) || mainSection; + const [data, setData] = useState({ - showDeleteModal: false, commentToDelete: {}, showLightbox: false, mapContainer: null, mapContainerMobile: null, }); - const { showDeleteModal } = data; + const [deleteModal, setDeleteModal] = useState({ + showDeleteModal: false, + commentSectionId: undefined, + commentId: undefined, + refreshUser: false, + }); const getSectionNav = () => { const filterNotClosedSections = sections.filter((sec) => sec.type !== SectionTypes.CLOSURE); @@ -170,16 +180,9 @@ const SectionContainerComponent = ({ editCommentFn(hearingSlug, commentSectionId, commentId, updatedCommentData); }; - const onDeleteComment = () => { - const { commentToDelete } = data; - const { sectionId: commentSectionId, commentId, refreshUser } = commentToDelete; - deleteSectionCommentFn(hearingSlug, commentSectionId, commentId, refreshUser); - }; - - const onPostPluginComment = (text, authorName, pluginData, geojson, label, images) => { - const sectionCommentData = { text, authorName, pluginData, geojson, label, images }; + const onPostPluginComment = (comment) => { const { authCode } = parseQuery(search); - const commentData = { authCode, ...sectionCommentData }; + const commentData = { authCode, ...comment }; postSectionCommentFn(hearingSlug, mainSection.id, commentData); }; @@ -188,17 +191,16 @@ const SectionContainerComponent = ({ postVoteFn(commentId, hearingSlug, commentSectionId); }; - const openDeleteModal = () => { - setData({ ...data, showDeleteModal: true }); + const onDeleteComment = (commentSectionId, commentId, refreshUser) => { + deleteSectionCommentFn(hearingSlug, commentSectionId, commentId, refreshUser); }; - const handleDeleteClick = (commentSectionId, commentId, refreshUser) => { - setData({ ...data, commentToDelete: { sectionId: commentSectionId, commentId, refreshUser } }); - openDeleteModal(); + const openDeleteModal = (commentSectionId, commentId, refreshUser) => { + setDeleteModal({ showDeleteModal: true, commentSectionId, commentId, refreshUser }); }; const closeDeleteModal = () => { - setData({ ...data, showDeleteModal: false, commentToDelete: {} }); + setDeleteModal({ showDeleteModal: false, commentSectionId: null, commentId: null, refreshUser: false }); }; const openLightbox = () => { @@ -421,7 +423,7 @@ const SectionContainerComponent = ({ onPostFlag={onFlagComment} defaultNickname={getNickname(user)} isSectionComments={section} - onDeleteComment={handleDeleteClick} + onDeleteComment={openDeleteModal} onEditComment={onEditComment} fetchAllComments={fetchAllCommentsFn} fetchComments={fetchCommentsForSortableListFn} @@ -569,7 +571,6 @@ const SectionContainerComponent = ({ )} - ); }; @@ -655,7 +656,14 @@ const SectionContainerComponent = ({ > {isMainSection(section) ? renderMainHearing() : renderSubHearing()} - + ); }; @@ -706,4 +714,4 @@ SectionContainerComponent.propTypes = { onPostReply: PropTypes.func, }; -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SectionContainerComponent)); \ No newline at end of file +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SectionContainerComponent)); diff --git a/src/components/Hearing/Section/__tests__/SectionContainer.test.jsx b/src/components/Hearing/Section/__tests__/SectionContainer.test.jsx index 050450502..14f606cd8 100644 --- a/src/components/Hearing/Section/__tests__/SectionContainer.test.jsx +++ b/src/components/Hearing/Section/__tests__/SectionContainer.test.jsx @@ -8,7 +8,7 @@ import { uniqueId } from 'lodash'; import { createMemoryHistory } from 'history'; import SectionContainerComponent from '../SectionContainer'; -import { mockStore as mockData } from '../../../../../test-utils'; +import { mockStore as mockData, mockUser } from '../../../../../test-utils'; import renderWithProviders from '../../../../utils/renderWithProviders'; import * as mockApi from '../../../../api'; @@ -16,13 +16,12 @@ const mockedData = { results: [], }; -jest.spyOn(mockApi, 'get').mockImplementation(() => ( - Promise.resolve( - { - json: () => Promise.resolve(mockedData), - blob: () => Promise.resolve({}), - } - ))); +jest.spyOn(mockApi, 'get').mockImplementation(() => + Promise.resolve({ + json: () => Promise.resolve(mockedData), + blob: () => Promise.resolve({}), + }), +); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -51,12 +50,12 @@ jest.mock('../../../../api', () => { }); const renderComponent = (storeOverrides) => { - const { mockHearingWithSections, mockUser, sectionComments } = mockData; + const { mockHearingWithSections, sectionComments } = mockData; const store = mockStore({ hearing: { [mockHearingWithSections.data.id]: { - ...mockHearingWithSections + ...mockHearingWithSections, }, }, accessibility: { @@ -101,16 +100,16 @@ describe('', () => { }); it('should render correctly when user is admin', () => { - const mockUser = { ...mockData.mockUser, adminOrganizations: [mockData.mockHearingWithSections.data.organization] }; + const mockAdminUser = { + ...mockData.mockUser, + adminOrganizations: [mockData.mockHearingWithSections.data.organization], + }; - renderComponent({ user: { data: mockUser } }); + renderComponent({ user: { data: mockAdminUser } }); }); it('should toggle accordions', async () => { - const mockUser = { ...mockData.mockUser, adminOrganizations: [mockData.mockHearingWithSections.data.organization] }; - renderComponent({ - user: { data: mockUser }, hearing: { [mockData.mockHearingWithSections.data.id]: { data: { @@ -136,9 +135,12 @@ describe('', () => { }); it('should handle report download', async () => { - const mockUser = { ...mockData.mockUser, adminOrganizations: [mockData.mockHearingWithSections.data.organization] }; + const mockAdminUser = { + ...mockData.mockUser, + adminOrganizations: [mockData.mockHearingWithSections.data.organization], + }; - renderComponent({ user: { data: mockUser } }); + renderComponent({ user: { data: mockAdminUser } }); const downloadButton = await screen.findByText(/downloadReport/i); fireEvent.click(downloadButton); diff --git a/src/components/SortableCommentList.jsx b/src/components/SortableCommentList.jsx index 3e8ca07a5..e56802e57 100644 --- a/src/components/SortableCommentList.jsx +++ b/src/components/SortableCommentList.jsx @@ -174,29 +174,16 @@ const SortableCommentListComponent = ({ } }; - /** - * Callback function for posting a comment. - * - * @param {string} text - The comment text. - * @param {string} authorName - The name of the comment author. - * @param {object} pluginData - Additional data related to the comment. - * @param {object} geojson - The geojson data associated with the comment. - * @param {string} label - The label of the comment. - * @param {array} images - An array of images attached to the comment. - * @param {boolean} pinned - Indicates whether the comment is pinned. - * @param {string} mapCommentText - The comment text for the map. - * @returns {Promise} - A promise that resolves when the comment is posted. - */ - const onPostComment = async (text, authorName, pluginData, geojson, label, images, pinned, mapCommentText) => { + const onPostComment = async (comment) => { const { answers } = listState; if (user) { setListState({ ...listState, shouldAnimate: true }); } - const commentData = { text, authorName, pluginData, geojson, label, images, answers, pinned, mapCommentText }; + const commentData = { ...comment, answers }; - if (onPostComment) { + if (onPostCommentFn) { await onPostCommentFn(section.id, commentData); setListState({ ...listState, answers: answersInitialState }); @@ -209,7 +196,7 @@ const SortableCommentListComponent = ({ * @param {string} sectionId - The ID of the section. * @param {object} data - The data of the comment. */ - const handlePostReply = (sectionId, data) => onPostComment(sectionId, data); + const handlePostReply = (sectionId, data) => onPostCommentFn(sectionId, data); /** * Handles the change of answers for a specific question. @@ -245,7 +232,7 @@ const SortableCommentListComponent = ({ ], }); } else if (questionType === 'multiple-choice' && oldAnswer) { - listState({ + setListState({ answers: [ ...listState.answers.filter((answer) => answer.question !== questionId), { diff --git a/src/components/__tests__/BaseCommentForm.jsx b/src/components/__tests__/BaseCommentForm.jsx index 1b2121040..31cf73327 100644 --- a/src/components/__tests__/BaseCommentForm.jsx +++ b/src/components/__tests__/BaseCommentForm.jsx @@ -79,7 +79,17 @@ describe('', () => { fireEvent.click(submitButton); - expect(onPostComment).toHaveBeenCalledWith('Test comment', 'Test Nickname', undefined, {}, null, [], false, ''); + expect(onPostComment).toHaveBeenCalledWith({ + authorName: 'Test Nickname', + geojson: {}, + images: [], + label: null, + mapCommentText: '', + organization: undefined, + pinned: false, + pluginData: undefined, + text: 'Test comment', + }); }); it('handles nickname change', () => { diff --git a/src/components/admin/HearingEditor.jsx b/src/components/admin/HearingEditor.jsx index d168ac3c1..23178c10e 100644 --- a/src/components/admin/HearingEditor.jsx +++ b/src/components/admin/HearingEditor.jsx @@ -1,5 +1,5 @@ /* eslint-disable react/forbid-prop-types */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import { connect, useDispatch } from 'react-redux'; import { isEmpty } from 'lodash'; @@ -72,6 +72,9 @@ const HearingEditor = (props) => { fetchEditorContactPersons(); }, [fetchEditorContactPersons]); + const geoJSONRef = useRef(); + geoJSONRef.current = hearing?.geojson; + const checkIfEmpty = (obj) => !Object.entries(obj).some(([, v]) => Object.entries(v).length > 0); useEffect(() => { @@ -124,11 +127,16 @@ const HearingEditor = (props) => { const onSectionChange = (sectionID, field, value) => dispatch(changeSection(sectionID, field, value)); - const onCreateMapMarker = (value) => dispatch(createMapMarker(value)); - - const onAddMapMarker = (value) => dispatch(addMapMarker(value)); - - const onAddMapMarkersToCollection = (value) => dispatch(addMapMarkerToCollection(value)); + const onAddMapMarker = (value) => { + console.log('geoJSONRef', geoJSONRef.current); + if (isEmpty(geoJSONRef.current) || !geoJSONRef.current) { + dispatch(createMapMarker(value)); + } else if (geoJSONRef.current.type !== 'FeatureCollection') { + dispatch(addMapMarker(value)); + } else { + dispatch(addMapMarkerToCollection(value)); + } + } /** * Add a new attachments to a section. @@ -239,8 +247,6 @@ const HearingEditor = (props) => { labels={labels} language={language} onAddMapMarker={onAddMapMarker} - onAddMapMarkersToCollection={onAddMapMarkersToCollection} - onCreateMapMarker={onCreateMapMarker} onDeleteExistingQuestion={onDeleteExistingQuestion} onDeleteTemporaryQuestion={onDeleteTemporaryQuestion} onHearingChange={onHearingChange} diff --git a/src/components/admin/HearingFormStep3.jsx b/src/components/admin/HearingFormStep3.jsx index cfd8e7b70..503d6db2f 100644 --- a/src/components/admin/HearingFormStep3.jsx +++ b/src/components/admin/HearingFormStep3.jsx @@ -2,10 +2,10 @@ /* eslint-disable no-underscore-dangle */ /* eslint-disable global-require */ /* eslint-disable import/no-unresolved */ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import { injectIntl, FormattedMessage } from 'react-intl'; -import Leaflet from 'leaflet'; +import Leaflet, { featureGroup } from 'leaflet'; import { Button, Fieldset, FileInput } from 'hds-react'; import { isEmpty, includes, keys, isMatch } from 'lodash'; import { connect, useDispatch } from 'react-redux'; @@ -32,14 +32,6 @@ Leaflet.Marker.prototype.options.icon = new Leaflet.Icon({ iconAnchor: [13, 41], }); -function getFirstGeometry(featureCollectionGeoJSON) { - const firstFeature = featureCollectionGeoJSON.features[0]; - if (firstFeature) { - return firstFeature.geometry; - } - return {}; -} - /** * Returns an array of the remaining features * @param {Object[]} currentFeatures @@ -65,8 +57,7 @@ const HearingFormStep3 = (props) => { let map; let featureGroup; const { hearing, language, isHighContrast, visible } = props; // const props - const { onHearingChange, onCreateMapMarker, onAddMapMarker, onAddMapMarkersToCollection, onContinue } = props; // function props - const [isEdited, setIsEdited] = useState(false); + const { onHearingChange, onAddMapMarker, onContinue } = props; // function props const [initialGeoJSON, setInitialGeoJSON] = useState(props.hearing.geojson); const dispatch = useDispatch(); @@ -75,51 +66,12 @@ const HearingFormStep3 = (props) => { Leaflet.drawLocal = getTranslatedTooltips(language); }, [language]); - const onDrawEdited = (event) => { - // TODO: Implement proper onDrawEdited functionality - setIsEdited(true); - onHearingChange('geojson', getFirstGeometry(event.layers.toGeoJSON())); - }; - - const onDrawCreated = (event) => { - // TODO: Implement proper onDrawCreated functionality - if (isEdited) { - /** - * first time an element is created and the map hasn't been edited/elements removed - */ - setIsEdited(true); - if (!hearing.geojson || !hearing.geojson.type) { - /** - * if hearing.geojson is null or doesnt have type -> add a single element - */ - onCreateMapMarker(event.layer.toGeoJSON().geometry); - } else if (hearing.geojson.type !== 'FeatureCollection') { - /** - * if hearing.geojson has a type that isn't FeatureCollection - * -> add element and transform hearing.geojson to FeatureCollection - */ - onAddMapMarker(event.layer.toGeoJSON()); - } else if (hearing.geojson.type === 'FeatureCollection') { - /** - * if hearing.geojson type is FeatureCollection - add element to geojson.features - */ - onAddMapMarkersToCollection(event.layer.toGeoJSON()); - } - } else if (hearing.geojson.coordinates) { - /** - * if geojson has coordinates -> transform hearing.geojson to FeatureCollection and add element - */ - onAddMapMarker(event.layer.toGeoJSON()); - } else { - /** - * hearing.geojson is a FeatureCollection -> add element to geojson.features - */ - onAddMapMarkersToCollection(event.layer.toGeoJSON()); - } - }; + const onDrawCreated = useCallback((event) => { + onAddMapMarker(event.layer.toGeoJSON()); + }, [onAddMapMarker]); // eslint-disable-next-line sonarjs/cognitive-complexity - const onDrawDeleted = (event) => { + const onDrawDeleted = useCallback((event) => { // TODO: Implement proper onDrawDeleted functionality if (event.layers && !isEmpty(event.layers._layers) && hearing.geojson.features) { /** @@ -165,25 +117,20 @@ const HearingFormStep3 = (props) => { if (remainingFeatures.length === 0) { // hearing is a FeatureCollection and all elements have been removed onHearingChange('geojson', {}); - setIsEdited(false); setInitialGeoJSON({}); } else { // hearing is a FeatureCollection that still has elements after removal onHearingChange('geojson', { type: hearing.geojson.type, features: remainingFeatures }); if (currentStateFeatures) { - setIsEdited(true); setInitialGeoJSON({ type: hearing.geojson.type, features: remainingStateFeatures }); - } else { - setIsEdited(true); } } } else { // hearing.geojson is a single element that has been removed onHearingChange('geojson', {}); - setIsEdited(false); setInitialGeoJSON({}); } - }; + }, [hearing.geojson, initialGeoJSON, onHearingChange]); const readTextFile = (file, callback) => { try { @@ -217,7 +164,7 @@ const HearingFormStep3 = (props) => { } onHearingChange('geojson', featureCollection.features[0].geometry); const parsedFile = parseCollection(featureCollection); - onCreateMapMarker(parsedFile); + onAddMapMarker(parsedFile); setInitialGeoJSON(parsedFile); } else { dispatch(addToast(createLocalizedNotificationPayload(NOTIFICATION_TYPES.error, MESSAGE_INCORRECT_FILE))); @@ -252,9 +199,11 @@ const HearingFormStep3 = (props) => { } }, [visible, map]); - const refCallBack = (el) => { - map = el; - }; + function refCallback(instance) { + if (instance) { + map = instance; + } + } if (typeof window === 'undefined') return null; @@ -262,7 +211,7 @@ const HearingFormStep3 = (props) => {
}> { )} /> { - featureGroup = group; - }} + ref={featureGroup} > { const { answers, @@ -51,6 +52,15 @@ const MapQuestionnaire = ({ user, } = data; + /** + * Determines whether the logged in user is admin or not. + * The array in users with key adminOrganizations should be of length > 0 + */ + const isUserAdmin = useMemo( + () => loggedIn && user && Array.isArray(user.adminOrganizations) && user.adminOrganizations.length > 0, + [loggedIn, user], + ); + const [formData, setFormData] = useState({ collapsed: true, commentOrAnswerRequiredError: false, @@ -71,6 +81,7 @@ const MapQuestionnaire = ({ submitting: false, showAlert: true, userDataChanged: false, + organization: isUserAdmin ? user.adminOrganizations[0] : undefined, }); const [messageListener, setMessageListener] = useState(null); @@ -122,7 +133,7 @@ const MapQuestionnaire = ({ const pluginComment = getPluginComment(); let pluginData = getPluginData(); - const { nickname, commentText, geojson, images, pinned, mapCommentText, imageTooBig } = formData; + const { nickname, commentText, geojson, images, pinned, mapCommentText, imageTooBig, organization } = formData; const submitData = { nickname: nickname === '' ? nicknamePlaceholder : nickname, @@ -132,6 +143,7 @@ const MapQuestionnaire = ({ pinned, mapCommentText, label: null, + organization, }; // plugin comment will override comment fields, if provided @@ -171,16 +183,17 @@ const MapQuestionnaire = ({ submitData.setCommentText = config.emptyCommentString; } - onPostComment( - submitData.commentText, - submitData.nickname, + onPostComment({ + text: submitData.commentText, + authorName: submitData.nickname, pluginData, - submitData.geojson, - submitData.label, - submitData.images, - submitData.pinned, - submitData.mapCommentText, - ); + geojson: submitData.geojson, + label: submitData.label, + images: submitData.images, + pinned: submitData.pinned, + mapCommentText: submitData.mapCommentText, + organization: submitData.organization ?? undefined, + }); setFormData((prevState) => ({ ...prevState, diff --git a/src/components/plugins/legacy/mapdon-hkr.jsx b/src/components/plugins/legacy/mapdon-hkr.jsx index 1b184668a..5fec59676 100644 --- a/src/components/plugins/legacy/mapdon-hkr.jsx +++ b/src/components/plugins/legacy/mapdon-hkr.jsx @@ -125,7 +125,6 @@ class MapdonHKRPlugin extends BaseCommentForm { } MapdonHKRPlugin.propTypes = { - onPostComment: PropTypes.func, data: PropTypes.string, }; diff --git a/src/components/plugins/legacy/mapdon-ksv.jsx b/src/components/plugins/legacy/mapdon-ksv.jsx index 7cc3b7ed3..3bf1674b5 100644 --- a/src/components/plugins/legacy/mapdon-ksv.jsx +++ b/src/components/plugins/legacy/mapdon-ksv.jsx @@ -170,7 +170,6 @@ class MapdonKSVPlugin extends BaseCommentForm { } MapdonKSVPlugin.propTypes = { - onPostComment: PropTypes.func, data: PropTypes.string, pluginPurpose: PropTypes.string, comments: PropTypes.array, diff --git a/src/middleware/hearingEditor.js b/src/middleware/hearingEditor.js index 344811e34..1e691004e 100644 --- a/src/middleware/hearingEditor.js +++ b/src/middleware/hearingEditor.js @@ -58,7 +58,6 @@ export const normalizeReceiveEditorContactPersons = export const normalizeSavedHearing = ({ dispatch }) => (next) => (action) => { const NORMALIZE_ACTIONS = [EditorActions.POST_HEARING_SUCCESS, EditorActions.SAVE_HEARING_SUCCESS]; - if (NORMALIZE_ACTIONS.includes(action.type)) { const hearing = get(action, 'payload.hearing'); dispatch(updateHearingAfterSave(fillFrontIdsAndNormalizeHearing(hearing))); diff --git a/src/reducers/hearingEditor/hearing.js b/src/reducers/hearingEditor/hearing.js index 86782230e..3841542c6 100644 --- a/src/reducers/hearingEditor/hearing.js +++ b/src/reducers/hearingEditor/hearing.js @@ -25,7 +25,7 @@ const data = handleActions( [EditorActions.EDIT_HEARING]: (state, { payload: { field, value } }) => ({ ...state, [field]: value }), [EditorActions.CREATE_MAP_MARKER]: (state, { payload: { value } }) => ({ ...state, - geojson: value, + geojson: value.geometry, }), [EditorActions.ADD_MAP_MARKER]: (state, { payload: { value } }) => { const foo = state.geojson; diff --git a/src/utils/map.js b/src/utils/map.js index 86413d2c3..28920b376 100644 --- a/src/utils/map.js +++ b/src/utils/map.js @@ -4,9 +4,9 @@ import { Polygon, GeoJSON, Marker, Polyline } from 'react-leaflet'; import 'proj4'; // import required for side effect import 'proj4leaflet'; // import required for side effect -import * as leafletMarkerIconUrl from '../../assets/images/leaflet/marker-icon.png'; -import * as leafletMarkerRetinaIconUrl from '../../assets/images/leaflet/marker-icon-2x.png'; -import * as leafletMarkerShadowUrl from '../../assets/images/leaflet/marker-shadow.png'; +import leafletMarkerIconUrl from '../../assets/images/leaflet/marker-icon.png'; +import leafletMarkerRetinaIconUrl from '../../assets/images/leaflet/marker-icon-2x.png'; +import leafletMarkerShadowUrl from '../../assets/images/leaflet/marker-shadow.png'; export function EPSG3067() { const crsName = 'EPSG:3067'; diff --git a/src/utils/section.js b/src/utils/section.js index 361591ad0..5915d51d2 100644 --- a/src/utils/section.js +++ b/src/utils/section.js @@ -41,7 +41,7 @@ export function isCommentRequired(hasQuestions, isReply, userAnsweredAllQuestion * @returns {boolean} true when at least one question is answered and false if not */ export function hasAnyAnswers(answers) { - return answers.some(questionAnswers => questionAnswers.answers && questionAnswers.answers.length > 0); + return answers && answers.some(questionAnswers => questionAnswers.answers && questionAnswers.answers.length > 0); } /** diff --git a/src/views/FullscreenHearing/FullscreenHearingContainer.jsx b/src/views/FullscreenHearing/FullscreenHearingContainer.jsx index 33987ee25..a91b924fa 100644 --- a/src/views/FullscreenHearing/FullscreenHearingContainer.jsx +++ b/src/views/FullscreenHearing/FullscreenHearingContainer.jsx @@ -13,7 +13,12 @@ import { getHearingWithSlug, getMainSection, getMainSectionComments } from '../. import LoadSpinner from '../../components/LoadSpinner'; import getAttr from '../../utils/getAttr'; import { parseQuery } from '../../utils/urlQuery'; -import { fetchHearing as fetchHearingAction, postSectionComment, postVote, fetchAllSectionComments } from '../../actions'; +import { + fetchHearing as fetchHearingAction, + postSectionComment, + postVote, + fetchAllSectionComments, +} from '../../actions'; import Link from '../../components/LinkWithLang'; import Icon from '../../utils/Icon'; import getUser from '../../selectors/user'; @@ -28,8 +33,8 @@ const FullscreenHearingContainerComponent = (ownProps) => { const user = useSelector((state) => getUser(state)); const language = useSelector((state) => state.language); const fetchAllComments = (hearingSlug, sectionId, ordering) => { - dispatch(fetchAllSectionComments(hearingSlug, sectionId, ordering)) - } + dispatch(fetchAllSectionComments(hearingSlug, sectionId, ordering)); + }; useEffect(() => { if (isEmpty(hearing)) { @@ -38,13 +43,12 @@ const FullscreenHearingContainerComponent = (ownProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const onPostComment = (text, authorName, pluginData, geojson, label, images) => { - const sectionCommentData = { text, authorName, pluginData, geojson, label, images }; + const onPostComment = (comment) => { // eslint-disable-next-line no-shadow const { mainSection } = ownProps; const { hearingSlug } = params; const { authCode } = parseQuery(location.search); - const commentData = { authCode, ...sectionCommentData }; + const commentData = { authCode, ...comment }; return dispatch(postSectionComment(hearingSlug, mainSection.id, commentData)); }; diff --git a/src/views/Hearing/__tests__/HearingContainer.test.jsx b/src/views/Hearing/__tests__/HearingContainer.test.jsx index 5a10397e1..279e0c6dc 100644 --- a/src/views/Hearing/__tests__/HearingContainer.test.jsx +++ b/src/views/Hearing/__tests__/HearingContainer.test.jsx @@ -89,7 +89,9 @@ describe('', () => { isSaving: false, }, }, - user, + user: { + data: user, + }, sectionComments: [], accessibility: { isHighContrast: false, diff --git a/test-utils.js b/test-utils.js index b8e219ba6..ce87e8fd8 100644 --- a/test-utils.js +++ b/test-utils.js @@ -6,7 +6,7 @@ import createStore from './src/createStore'; import messages from './src/i18n'; -export const mockUser = { id: "fff", displayName: "Mock von User" }; +export const mockUser = { id: "fff", displayName: "Mock von User", adminOrganizations: [] }; export function createTestStore(state) { commonInit();