diff --git a/src/actions/hearingEditor.js b/src/actions/hearingEditor.js
index 1e33a6376..6c0b07daf 100644
--- a/src/actions/hearingEditor.js
+++ b/src/actions/hearingEditor.js
@@ -106,7 +106,7 @@ export const deleteSectionAttachment = (sectionId, attachment) => (dispatch) =>
.then(() => dispatch(createAction(EditorActions.DELETE_ATTACHMENT)({ sectionId, attachment })));
};
-export const changeProject = (projectId, projectLists) => createAction(EditorActions.CHANGE_PROJECT)(projectId, projectLists);
+export const changeProject = (hearingSlug, projectId, projectLists) => createAction(EditorActions.CHANGE_PROJECT)(hearingSlug, projectId, projectLists);
export const updateProjectLanguage = (languages) => createAction(EditorActions.UPDATE_PROJECT_LANGUAGE)({ languages });
diff --git a/src/components/admin/HearingFormStep5.jsx b/src/components/admin/HearingFormStep5.jsx
index eacfcb069..aac0bfe23 100644
--- a/src/components/admin/HearingFormStep5.jsx
+++ b/src/components/admin/HearingFormStep5.jsx
@@ -1,17 +1,14 @@
/* eslint-disable react/forbid-prop-types */
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { v1 as uuid } from 'uuid';
import { connect, useDispatch } from 'react-redux';
import { isEmpty } from 'lodash';
-import { Button, Notification, Select, TextInput } from 'hds-react';
-import { injectIntl, FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
+import { Select } from 'hds-react';
+import { injectIntl, FormattedMessage, useIntl } from 'react-intl';
-import Icon from '../../utils/Icon';
import { createNotificationPayload, NOTIFICATION_TYPES } from '../../utils/notify';
import * as ProjectsSelector from '../../selectors/projectLists';
-import Phase from './Phase';
import { hearingShape } from '../../types';
import {
changeProjectName,
@@ -22,17 +19,41 @@ import {
changePhase,
} from '../../actions/hearingEditor';
import { addToast } from '../../actions/toast';
+import Project from './Project';
-const HearingFormStep5 = ({ errors, hearing, hearingLanguages, language, projects, intl }) => {
+const HearingFormStep5 = ({ errors, hearing, hearingLanguages, language, projects }) => {
const dispatch = useDispatch();
+ const intl = useIntl();
- const onChangeProject = (selected) =>
+ const defaultProjectOptions = [
+ { value: uuid(), label: intl.formatMessage({ id: 'noProject' }) },
+ { value: '', label: intl.formatMessage({ id: 'defaultProject' }) },
+ ];
+
+ const projectsOptions = projects.map((project) => ({
+ value: project.id,
+ label: `${
+ project.title[language] || project.title.fi || project.title.en || project.title.sv || 'Default project'
+ }`,
+ }));
+
+ const options = [...defaultProjectOptions, ...projectsOptions];
+
+ const [selectedProject, setSelectedProject] = useState(hearing.project);
+
+ useEffect(() => {
+ setSelectedProject(hearing.project);
+ }, [hearing.project]);
+
+ const onChangeProject = (selected) => {
dispatch(
changeProject({
+ hearingSlug: hearing.slug,
projectId: selected.value,
projectLists: projects,
}),
);
+ };
const addPhase = () => {
if (!isEmpty(hearingLanguages)) {
@@ -51,100 +72,31 @@ const HearingFormStep5 = ({ errors, hearing, hearingLanguages, language, project
const onActivePhase = (phaseId) => dispatch(activePhase(phaseId));
- const renderProject = (selectedProject) => {
- const phasesLength = hearing.project ? hearing.project.phases.length : null;
- const errorStyle = !errors.project_phase_active && phasesLength === 0 ? 'has-error' : null;
-
- return (
-
- {selectedProject &&
- hearingLanguages.map((usedLanguage) => (
-
-
- ({usedLanguage})
- >
- }
- maxLength={100}
- value={selectedProject.title[usedLanguage]}
- onBlur={(event) => onChangeProjectName(usedLanguage, event.target.value)}
- invalid={!!errors.project_title}
- errorText={errors.project_title}
- style={{ marginBottom: 'var(--spacing-s)' }}
- required
- />
-
- ))}
-
- {selectedProject &&
- selectedProject.phases.map((phase, index) => {
- const key = index;
-
- return (
-
- );
- })}
-
- {selectedProject && (
-
-
-
- )}
- {!!errors.project_phase_active && phasesLength === 0 && (
-
- {errors.project_phase_active}
-
- )}
-
- );
- };
-
- const selectedProject = hearing.project;
-
- const defaultProjectOptions = [
- { value: uuid(), label: intl.formatMessage({ id: 'noProject' }) },
- { value: '', label: intl.formatMessage({ id: 'defaultProject' }) },
- ];
-
- const projectsOptions = projects.map((project) => ({
- value: project.id,
- label: `${
- project.title[language] || project.title.fi || project.title.en || project.title.sv || 'Default project'
- }`,
- }));
-
- const options = [...defaultProjectOptions, ...projectsOptions];
-
- const projectsInitialValue = selectedProject?.id ? selectedProject.id : options[0];
+ const projectValue = options.find((option) => option.value === hearing.project?.id);
return (
}
options={options}
onChange={onChangeProject}
- defaultValue={projectsInitialValue}
+ value={projectValue}
/>
- {renderProject(selectedProject)}
+
);
};
@@ -155,13 +107,10 @@ HearingFormStep5.propTypes = {
language: PropTypes.string,
hearing: hearingShape,
hearingLanguages: PropTypes.arrayOf(PropTypes.string),
- intl: PropTypes.object,
};
const mapStateToProps = (state) => ({
projects: ProjectsSelector.getProjects(state),
});
-const WrappedHearingFormStep5 = connect(mapStateToProps)(injectIntl(HearingFormStep5));
-
-export default WrappedHearingFormStep5;
+export default connect(mapStateToProps)(injectIntl(HearingFormStep5));
diff --git a/src/components/admin/Phase.jsx b/src/components/admin/Phase.jsx
index befcd5581..f1729e9a2 100644
--- a/src/components/admin/Phase.jsx
+++ b/src/components/admin/Phase.jsx
@@ -1,9 +1,10 @@
/* eslint-disable react/forbid-prop-types */
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, useIntl } from 'react-intl';
import { Button, Checkbox, IconTrash, TextInput } from 'hds-react';
import { useDispatch } from 'react-redux';
+import { isEmpty } from 'lodash';
import { createNotificationPayload } from '../../utils/notify';
import { addToast } from '../../actions/toast';
@@ -12,22 +13,38 @@ const Phase = ({ phaseInfo, indexNumber, onDelete, onChange, onActive, languages
const dispatch = useDispatch();
const intl = useIntl();
- const durationsInitial = languages.reduce((acc, current) => {
- acc[current] = phaseInfo.schedule[current];
+ const getValuePerLanguage = (selector) =>
+ languages.reduce((acc, current) => {
+ acc[current] = selector[current];
- return acc;
- }, {});
+ return acc;
+ }, {});
- const descriptionsInitial = languages.reduce((acc, current) => {
- acc[current] = phaseInfo.description[current];
-
- return acc;
- }, {});
+ const titlesInitial = getValuePerLanguage(phaseInfo.title);
+ const durationsInitial = getValuePerLanguage(phaseInfo.schedule);
+ const descriptionsInitial = getValuePerLanguage(phaseInfo.description);
+ const [phaseTitles, setPhaseTitles] = useState(titlesInitial);
const [phaseDurations, setPhaseDurations] = useState(durationsInitial);
const [phaseDescriptions, setPhaseDescriptions] = useState(descriptionsInitial);
+ const [phaseIsActive, setPhaseIsActive] = useState(phaseInfo.is_active);
+
+ useEffect(() => {
+ setPhaseTitles(!isEmpty(getValuePerLanguage(phaseInfo.title)) ? getValuePerLanguage(phaseInfo.title) : undefined);
+ setPhaseDurations(
+ !isEmpty(getValuePerLanguage(phaseInfo.schedule)) ? getValuePerLanguage(phaseInfo.schedule) : undefined,
+ );
+ setPhaseDescriptions(
+ !isEmpty(getValuePerLanguage(phaseInfo.description)) ? getValuePerLanguage(phaseInfo.description) : undefined,
+ );
+
+ setPhaseIsActive(phaseInfo.is_active ? phaseInfo.is_active : undefined);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [phaseInfo]);
const handleRadioOnChange = (event) => {
+ setPhaseIsActive(event.target.checked);
+
if (event.target.checked) {
onActive(phaseInfo.id || phaseInfo.frontId);
} else {
@@ -35,6 +52,10 @@ const Phase = ({ phaseInfo, indexNumber, onDelete, onChange, onActive, languages
}
};
+ if (!phaseInfo) {
+ return null;
+ }
+
return (
{languages.map((usedLanguage, index) => (
@@ -49,11 +70,14 @@ const Phase = ({ phaseInfo, indexNumber, onDelete, onChange, onActive, languages
{indexNumber + 1} ({usedLanguage})
>
}
- value={phaseInfo.title[usedLanguage]}
+ value={phaseTitles ? phaseTitles[usedLanguage] : ''}
maxLength={100}
- onBlur={(event) =>
- onChange(phaseInfo.id || phaseInfo.frontId, 'title', usedLanguage, event.target.value)
- }
+ onChange={(event) => {
+ const { value } = event.target;
+
+ setPhaseTitles((prevState) => ({ ...prevState, [usedLanguage]: value }));
+ onChange(phaseInfo.id || phaseInfo.frontId, 'title', usedLanguage, value);
+ }}
invalid={!!errors.project_phase_title}
errorText={errors.project_phase_title}
required
@@ -90,11 +114,9 @@ const Phase = ({ phaseInfo, indexNumber, onDelete, onChange, onActive, languages
onChange={(event) => {
const { value } = event.target;
- setPhaseDurations({ ...phaseDurations, [usedLanguage]: value });
+ setPhaseDurations((prevState) => ({ ...prevState, [usedLanguage]: value }));
+ onChange(phaseInfo.id || phaseInfo.frontId, 'schedule', usedLanguage, value);
}}
- onBlur={(event) =>
- onChange(phaseInfo.id || phaseInfo.frontId, 'schedule', usedLanguage, event.target.value)
- }
/>
@@ -107,11 +129,9 @@ const Phase = ({ phaseInfo, indexNumber, onDelete, onChange, onActive, languages
onChange={(event) => {
const { value } = event.target;
- setPhaseDescriptions({ ...phaseDescriptions, [usedLanguage]: value });
+ setPhaseDescriptions((prevState) => ({ ...prevState, [usedLanguage]: value }));
+ onChange(phaseInfo.id || phaseInfo.frontId, 'description', usedLanguage, value);
}}
- onBlur={(event) =>
- onChange(phaseInfo.id || phaseInfo.frontId, 'description', usedLanguage, event.target.value)
- }
/>
@@ -122,7 +142,7 @@ const Phase = ({ phaseInfo, indexNumber, onDelete, onChange, onActive, languages
name={`phase-active-${indexNumber + 1}`}
label={intl.formatMessage({ id: 'phaseActive' })}
onChange={handleRadioOnChange}
- checked={phaseInfo.is_active}
+ checked={phaseIsActive}
errorText={errors.project_phase_active}
/>
)}
diff --git a/src/components/admin/Project.jsx b/src/components/admin/Project.jsx
new file mode 100644
index 000000000..7c121bc87
--- /dev/null
+++ b/src/components/admin/Project.jsx
@@ -0,0 +1,129 @@
+import React, { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import { Button, Notification, TextInput } from 'hds-react';
+import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import { isEmpty } from 'lodash';
+
+import Phase from './Phase';
+import Icon from '../../utils/Icon';
+
+const Project = ({
+ project,
+ errors,
+ hearingLanguages,
+ onChangeProjectName,
+ onChangePhase,
+ deletePhase,
+ onActivePhase,
+ addPhase,
+}) => {
+ const getProjectTitles = (selector) =>
+ hearingLanguages.reduce((acc, current) => {
+ if (!isEmpty(selector?.title)) {
+ acc[current] = selector?.title[current];
+ }
+
+ return acc;
+ }, {});
+
+ const projectTitlesInitial = getProjectTitles(project);
+
+ const [selectedTitles, setSelectedTitles] = useState(projectTitlesInitial);
+
+ useEffect(() => {
+ setSelectedTitles(!isEmpty(getProjectTitles(project)) ? getProjectTitles(project) : undefined);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [project]);
+
+ if (!project) {
+ return null;
+ }
+
+ const phasesLength = project.phases ? project.phases.length : null;
+ const errorStyle = !errors.project_phase_active && phasesLength === 0 ? 'has-error' : null;
+
+ return (
+
+ {hearingLanguages.map((usedLanguage) => (
+
+
+ ({usedLanguage})
+ >
+ }
+ maxLength={100}
+ value={selectedTitles ? selectedTitles[usedLanguage] : ''}
+ onChange={(event) => {
+ const { value } = event.target;
+
+ setSelectedTitles((prevState) => ({ ...prevState, [usedLanguage]: value }));
+ onChangeProjectName(usedLanguage, value);
+ }}
+ invalid={!!errors.project_title}
+ errorText={errors.project_title}
+ style={{ marginBottom: 'var(--spacing-s)' }}
+ required
+ />
+
+ ))}
+
+ {project.phases?.map((phase, index) => {
+ const key = index;
+
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+ {!!errors.project_phase_active && phasesLength === 0 && (
+
+ {errors.project_phase_active}
+
+ )}
+
+ );
+};
+
+Project.propTypes = {
+ project: PropTypes.shape({
+ // eslint-disable-next-line react/forbid-prop-types
+ phases: PropTypes.arrayOf(PropTypes.object),
+ title: PropTypes.shape({
+ en: PropTypes.string,
+ fi: PropTypes.string,
+ sv: PropTypes.string,
+ }),
+ }).isRequired,
+ errors: PropTypes.shape({
+ project_phase_active: PropTypes.string,
+ project_title: PropTypes.string,
+ }).isRequired,
+ hearingLanguages: PropTypes.arrayOf(PropTypes.string).isRequired,
+ onChangeProjectName: PropTypes.func.isRequired,
+ onChangePhase: PropTypes.func.isRequired,
+ deletePhase: PropTypes.func.isRequired,
+ onActivePhase: PropTypes.func.isRequired,
+ addPhase: PropTypes.func.isRequired,
+};
+
+export default Project;
diff --git a/src/components/admin/__tests__/HearingFormStep5.test.jsx b/src/components/admin/__tests__/HearingFormStep5.test.jsx
index 6d5b9e448..4caa2d8de 100644
--- a/src/components/admin/__tests__/HearingFormStep5.test.jsx
+++ b/src/components/admin/__tests__/HearingFormStep5.test.jsx
@@ -1,3 +1,4 @@
+/* eslint-disable sonarjs/no-duplicate-string */
import React from 'react';
import configureStore from 'redux-mock-store';
import { thunk } from 'redux-thunk';
@@ -6,7 +7,6 @@ import userEvent from '@testing-library/user-event';
import HearingFormStep5 from '../HearingFormStep5';
import renderWithProviders from '../../../utils/renderWithProviders';
-import { getIntlAsProp } from '../../../../test-utils';
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
@@ -18,9 +18,9 @@ const storeInitialState = {
{
id: '123',
title: {
- en: 'en',
- fi: 'fi',
- sv: 'sv',
+ en: 'test',
+ fi: 'test',
+ sv: 'test',
},
phases: [
{
@@ -28,9 +28,39 @@ const storeInitialState = {
has_hearings: true,
hearings: ['test'],
title: {
- en: 'en',
- fi: 'fi',
- sv: 'sv',
+ en: 'In English',
+ fi: 'Suomeksi',
+ sv: 'På Svenska',
+ },
+ description: {
+ en: 'test',
+ fi: 'test',
+ sv: 'test',
+ },
+ schedule: {
+ en: '2024',
+ fi: '2024',
+ sv: '2024',
+ },
+ },
+ ],
+ },
+ {
+ id: '456',
+ title: {
+ en: 'test2',
+ fi: 'test2',
+ sv: 'test2',
+ },
+ phases: [
+ {
+ id: '3456',
+ has_hearings: true,
+ hearings: ['test'],
+ title: {
+ en: 'In English',
+ fi: 'Suomeksi',
+ sv: 'På Svenska',
},
description: {
en: 'test',
@@ -52,13 +82,32 @@ const storeInitialState = {
const renderComponent = (propOverrides, storeOverride) => {
const props = {
errors: {},
+ projects: [
+ {
+ id: '123',
+ title: {
+ en: 'test',
+ fi: 'test',
+ sv: 'test',
+ },
+ },
+ {
+ id: '456',
+ title: {
+ en: 'test2',
+ fi: 'test2',
+ sv: 'test2',
+ },
+ },
+ ],
+ language: 'en',
hearing: {
project: {
- id: '123',
+ id: 'project123',
title: {
- en: 'en',
- fi: 'fi',
- sv: 'sv',
+ en: 'Project In English',
+ fi: 'Project Suomeksi',
+ sv: 'Project På Svenska',
},
phases: [
{
@@ -66,9 +115,9 @@ const renderComponent = (propOverrides, storeOverride) => {
has_hearings: true,
hearings: ['test'],
title: {
- en: 'en',
- fi: 'fi',
- sv: 'sv',
+ en: 'Phase In English',
+ fi: 'Phase Suomeksi',
+ sv: 'Phase På Svenska',
},
description: {
en: 'test',
@@ -85,19 +134,6 @@ const renderComponent = (propOverrides, storeOverride) => {
},
},
hearingLanguages: ['en', 'fi', 'sv'],
- language: 'en',
- projects: [
- {
- id: '123',
- title: {
- en: 'test',
- fi: 'test',
- sv: 'test',
- },
- },
- ],
- intl: getIntlAsProp(),
- dispatch: jest.fn(),
...propOverrides,
};
@@ -121,7 +157,7 @@ describe('', () => {
await user.click(input);
- const option = await screen.findByText('en');
+ const option = await screen.findByText('test2');
await user.click(option);
@@ -129,7 +165,7 @@ describe('', () => {
const expected = [
{
type: 'changeProject',
- payload: { projectId: '123', projectLists: expect.anything() },
+ payload: { projectId: '456', projectLists: expect.anything() },
},
];
diff --git a/src/components/admin/__tests__/Phase.test.jsx b/src/components/admin/__tests__/Phase.test.jsx
index 81708a94f..b16e1c872 100644
--- a/src/components/admin/__tests__/Phase.test.jsx
+++ b/src/components/admin/__tests__/Phase.test.jsx
@@ -46,7 +46,7 @@ describe('', () => {
renderComponent({ onChange });
- fireEvent.blur(screen.getAllByLabelText(/phase 1 /i)[0], { target: { value: 'New Title' } });
+ fireEvent.change(screen.getAllByLabelText(/phase 1 /i)[0], { target: { value: 'New Title' } });
expect(onChange).toHaveBeenCalledWith('1', 'title', 'en', 'New Title');
});
diff --git a/src/reducers/hearingEditor/hearing.js b/src/reducers/hearingEditor/hearing.js
index 86782230e..0b12c8315 100644
--- a/src/reducers/hearingEditor/hearing.js
+++ b/src/reducers/hearingEditor/hearing.js
@@ -100,11 +100,21 @@ const data = handleActions(
})
}
}, state),
- [EditorActions.CHANGE_PROJECT]: (state, { payload: { projectId, projectLists } }) => {
+ [EditorActions.CHANGE_PROJECT]: (state, { payload: { hearingSlug, projectId, projectLists } }) => {
let updatedProject;
- if (projectId === '') updatedProject = initNewProject();
- else updatedProject = projectLists.find(project => project.id === projectId) || null;
- return { ...state, project: updatedProject };
+ if (projectId === '') {
+ updatedProject = initNewProject()
+ } else {
+ updatedProject = projectLists.find(project => project.id === projectId) || null
+ }
+
+ const mutatePhases = updatedProject?.phases.map((phase) => ({ ...phase, is_active: phase.hearings.includes(hearingSlug) }))
+
+ if (mutatePhases) {
+ return { ...state, project: { ...updatedProject, phases: mutatePhases } };
+ }
+
+ return { ...state, project: updatedProject }
},
[EditorActions.CHANGE_PROJECT_NAME]: (state, { payload: { fieldname, value } }) =>
updeep({