From 769dc691686873f30d95a7eee243e71bd360f700 Mon Sep 17 00:00:00 2001 From: mikkojamG Date: Mon, 3 Feb 2025 10:23:51 +0200 Subject: [PATCH 1/6] fix: hearingFormStep2 main image allow jpg KER-422 --- src/components/admin/SectionForm.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/admin/SectionForm.jsx b/src/components/admin/SectionForm.jsx index 81a25f4e1..a45354ca7 100644 --- a/src/components/admin/SectionForm.jsx +++ b/src/components/admin/SectionForm.jsx @@ -311,7 +311,7 @@ const SectionForm = ({ name='sectionImage' dragAndDrop label={} - accept='.jpeg,.png,.webp,.gif' + accept='.jpeg,.jpg,.png,.webp,.gif' helperText={} language={language} onChange={onImageChange} From 53cecb3b299febe04f97afca14482172e342ae8b Mon Sep 17 00:00:00 2001 From: mikkojamG Date: Mon, 3 Feb 2025 10:29:52 +0200 Subject: [PATCH 2/6] fix: hearingFormStep2 main image allow 1MB KER-422 --- src/components/admin/SectionForm.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/admin/SectionForm.jsx b/src/components/admin/SectionForm.jsx index a45354ca7..bb65915c3 100644 --- a/src/components/admin/SectionForm.jsx +++ b/src/components/admin/SectionForm.jsx @@ -58,7 +58,7 @@ const fetchFiles = async (data, fileType, language) => { * MAX_IMAGE_SIZE given in MB * MAX_FILE_SIZE given in MB */ -const MAX_IMAGE_SIZE = 0.9; +const MAX_IMAGE_SIZE = 1; const MAX_FILE_SIZE = 70; const SectionForm = ({ From 233abd08351fc641ec67236887aa2d5d531a3688 Mon Sep 17 00:00:00 2001 From: mikkojamG Date: Mon, 3 Feb 2025 11:04:55 +0200 Subject: [PATCH 3/6] feat: imageModal replace dropzone KER-422 --- .../RichTextEditor/Image/ImageModal.jsx | 128 ++++++++---------- src/components/admin/SectionForm.jsx | 20 +-- src/utils/images/compressFile.js | 5 + src/utils/images/fileToDataUri.js | 16 +++ 4 files changed, 78 insertions(+), 91 deletions(-) create mode 100644 src/utils/images/compressFile.js create mode 100644 src/utils/images/fileToDataUri.js diff --git a/src/components/RichTextEditor/Image/ImageModal.jsx b/src/components/RichTextEditor/Image/ImageModal.jsx index 4fdb91386..ef52e60c4 100644 --- a/src/components/RichTextEditor/Image/ImageModal.jsx +++ b/src/components/RichTextEditor/Image/ImageModal.jsx @@ -1,27 +1,22 @@ /* eslint-disable react/forbid-prop-types */ import React, { useState } from 'react'; -import { FormattedMessage, injectIntl } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import PropTypes from 'prop-types'; -import { ControlLabel, HelpBlock, Image } from 'react-bootstrap'; -import { Button, Dialog } from 'hds-react'; -import Dropzone from 'react-dropzone'; -import FormControl from 'react-bootstrap/lib/FormControl'; -import { useDispatch } from 'react-redux'; +import { ControlLabel } from 'react-bootstrap'; +import { Button, Card, Dialog, FileInput, TextInput } from 'hds-react'; import getMessage from '../../../utils/getMessage'; import { isFormValid } from '../../../utils/iframeUtils'; -import Icon from '../../../utils/Icon'; -import { createLocalizedNotificationPayload, NOTIFICATION_TYPES } from '../../../utils/notify'; -import { addToast } from '../../../actions/toast'; +import compressFile from '../../../utils/images/compressFile'; +import fileToDataUri from '../../../utils/images/fileToDataUri'; /** * MAX_IMAGE_SIZE given in bytes */ -const MAX_IMAGE_SIZE = 999999; +const MAX_IMAGE_SIZE = 1; -const ImageModal = (props) => { - - const dispatch = useDispatch(); +const ImageModal = ({ isOpen, onClose, onSubmit }) => { + const intl = useIntl(); const [showFormErrorMsg, setShowFormErrorMsg] = useState(false); const [fileReaderResult, setFileReaderResult] = useState(false); @@ -31,35 +26,25 @@ const ImageModal = (props) => { setShowFormErrorMsg(false); setFileReaderResult(false); setImageAltText(''); - } + }; - const onFileDrop = (files) => { - if (files[0].size > MAX_IMAGE_SIZE) { - dispatch(addToast(createLocalizedNotificationPayload(NOTIFICATION_TYPES.error, 'imageSizeError'))); - return; - } - const file = files[0]; // Only one file is supported for now. - const fileReader = new FileReader(); - fileReader.addEventListener( - 'load', - () => { - setFileReaderResult(fileReader.result); - }, - false, - ); - fileReader.readAsDataURL(file); - } - - const getImagePreview = () => { - if (fileReaderResult) { - return ; + const onFileChange = async (files) => { + try { + const file = files[0]; + + const compressed = await compressFile(file, MAX_IMAGE_SIZE, 'image/webp'); + const blob = await fileToDataUri(compressed); + + setFileReaderResult(blob); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); } - return false; - } + }; const setImageAltTextFn = (event) => { setImageAltText(event.target.value); - } + }; const validateForm = () => { const inputErrors = { @@ -67,19 +52,16 @@ const ImageModal = (props) => { }; return isFormValid(inputErrors); - } + }; const confirmImage = () => { if (validateForm()) { - props.onSubmit(fileReaderResult, imageAltText); + onSubmit(fileReaderResult, imageAltText); resetState(); } else { setShowFormErrorMsg(true); } - } - - const { isOpen, intl, onClose } = props; - const dropZoneClass = fileReaderResult ? 'dropzone preview' : 'dropzone'; + }; const titleId = 'image-modal-title'; const descriptionId = 'image-modal-description'; @@ -100,33 +82,34 @@ const ImageModal = (props) => { - - { - ({getRootProps, getInputProps}) => ( - <> - {getImagePreview()} -
-
- - - - - -
-
- - ) - } - -
- - - - - - - + {fileReaderResult && ( + + + + )} + } + helperText={} + language={intl.locale} + onChange={onFileChange} + /> + + } className='sectionImageCaptionInput' value={imageAltText} onChange={setImageAltTextFn} @@ -148,13 +131,12 @@ const ImageModal = (props) => { ); -} +}; ImageModal.propTypes = { isOpen: PropTypes.bool, - intl: PropTypes.object, onClose: PropTypes.func, onSubmit: PropTypes.func, }; -export default injectIntl(ImageModal); +export default ImageModal; diff --git a/src/components/admin/SectionForm.jsx b/src/components/admin/SectionForm.jsx index bb65915c3..068ca5b51 100644 --- a/src/components/admin/SectionForm.jsx +++ b/src/components/admin/SectionForm.jsx @@ -6,12 +6,13 @@ import PropTypes from 'prop-types'; import { FormattedMessage, useIntl } from 'react-intl'; import { get, isEmpty } from 'lodash'; import { Button, Card, Checkbox, FileInput, LoadingSpinner, Select } from 'hds-react'; -import imageCompression from 'browser-image-compression'; import { QuestionForm } from './QuestionForm'; import MultiLanguageTextField, { TextFieldTypes } from '../forms/MultiLanguageTextField'; import { sectionShape } from '../../types'; import { isSpecialSectionType } from '../../utils/section'; +import compressFile from '../../utils/images/compressFile'; +import fileToDataUri from '../../utils/images/fileToDataUri'; const getFileTitle = (title, language) => { if (title?.[language] && typeof title[language] !== 'undefined') { @@ -138,23 +139,6 @@ const SectionForm = ({ } }; - const fileToDataUri = (file) => - new Promise((resolve, reject) => { - const fileReader = new FileReader(); - - fileReader.onload = (event) => { - resolve(event.target.result); - }; - - fileReader.onerror = (error) => { - reject(error); - }; - - fileReader.readAsDataURL(file); - }); - - const compressFile = async (file, maxSizeMB, fileType) => imageCompression(file, { maxSizeMB, fileType }); - const onImageChange = async (files) => { try { const file = files[0]; diff --git a/src/utils/images/compressFile.js b/src/utils/images/compressFile.js new file mode 100644 index 000000000..5b9c27e07 --- /dev/null +++ b/src/utils/images/compressFile.js @@ -0,0 +1,5 @@ +import imageCompression from 'browser-image-compression'; + +const compressFile = async (file, maxSizeMB, fileType) => imageCompression(file, { maxSizeMB, fileType }); + +export default compressFile; diff --git a/src/utils/images/fileToDataUri.js b/src/utils/images/fileToDataUri.js new file mode 100644 index 000000000..07c35b637 --- /dev/null +++ b/src/utils/images/fileToDataUri.js @@ -0,0 +1,16 @@ +const fileToDataUri = (file) => + new Promise((resolve, reject) => { + const fileReader = new FileReader(); + + fileReader.onload = (event) => { + resolve(event.target.result); + }; + + fileReader.onerror = (error) => { + reject(error); + }; + + fileReader.readAsDataURL(file); + }); + +export default fileToDataUri; From 47bfe4ad81e37f86dcbd30a685c52c07138f1d28 Mon Sep 17 00:00:00 2001 From: mikkojamG Date: Mon, 3 Feb 2025 11:35:34 +0200 Subject: [PATCH 4/6] fix: phase component fix text input onChange KER-422 --- src/components/admin/Phase.jsx | 35 ++++++++++++--- src/components/forms/FormControlOnChange.jsx | 47 -------------------- 2 files changed, 29 insertions(+), 53 deletions(-) delete mode 100644 src/components/forms/FormControlOnChange.jsx diff --git a/src/components/admin/Phase.jsx b/src/components/admin/Phase.jsx index f7e405e52..befcd5581 100644 --- a/src/components/admin/Phase.jsx +++ b/src/components/admin/Phase.jsx @@ -1,5 +1,5 @@ /* eslint-disable react/forbid-prop-types */ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage, useIntl } from 'react-intl'; import { Button, Checkbox, IconTrash, TextInput } from 'hds-react'; @@ -8,12 +8,25 @@ import { useDispatch } from 'react-redux'; import { createNotificationPayload } from '../../utils/notify'; import { addToast } from '../../actions/toast'; -const Phase = (props) => { - const { phaseInfo, indexNumber, onDelete, onChange, onActive, languages, errors } = props; - +const Phase = ({ phaseInfo, indexNumber, onDelete, onChange, onActive, languages, errors }) => { const dispatch = useDispatch(); const intl = useIntl(); + const durationsInitial = languages.reduce((acc, current) => { + acc[current] = phaseInfo.schedule[current]; + + return acc; + }, {}); + + const descriptionsInitial = languages.reduce((acc, current) => { + acc[current] = phaseInfo.description[current]; + + return acc; + }, {}); + + const [phaseDurations, setPhaseDurations] = useState(durationsInitial); + const [phaseDescriptions, setPhaseDescriptions] = useState(descriptionsInitial); + const handleRadioOnChange = (event) => { if (event.target.checked) { onActive(phaseInfo.id || phaseInfo.frontId); @@ -73,7 +86,12 @@ const Phase = (props) => { name={`phase-duration-${indexNumber + 1}`} label={} maxLength={50} - value={phaseInfo.schedule[usedLanguage]} + value={phaseDurations[usedLanguage]} + onChange={(event) => { + const { value } = event.target; + + setPhaseDurations({ ...phaseDurations, [usedLanguage]: value }); + }} onBlur={(event) => onChange(phaseInfo.id || phaseInfo.frontId, 'schedule', usedLanguage, event.target.value) } @@ -85,7 +103,12 @@ const Phase = (props) => { name={`phase-description-${indexNumber + 1}`} label={} maxLength={100} - value={phaseInfo.description[usedLanguage]} + value={phaseDescriptions[usedLanguage]} + onChange={(event) => { + const { value } = event.target; + + setPhaseDescriptions({ ...phaseDescriptions, [usedLanguage]: value }); + }} onBlur={(event) => onChange(phaseInfo.id || phaseInfo.frontId, 'description', usedLanguage, event.target.value) } diff --git a/src/components/forms/FormControlOnChange.jsx b/src/components/forms/FormControlOnChange.jsx deleted file mode 100644 index 765a782c2..000000000 --- a/src/components/forms/FormControlOnChange.jsx +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-disable camelcase */ -import React from 'react'; -import PropTypes from 'prop-types'; -import FormControl from 'react-bootstrap/lib/FormControl'; - -class FormControlOnChange extends React.Component { - constructor(props) { - super(props); - this.state = { - value: this.props.defaultValue || '', - }; - } - - UNSAFE_componentWillReceiveProps(newProps) { - this.setState({ - value: newProps.defaultValue || '', - }); - } - - onChange = (event) => { - this.setState({ - value: event.target.value, - }); - }; - - render() { - const { type, onBlur, maxLength } = this.props; - return ( - - ); - } -} - -FormControlOnChange.propTypes = { - defaultValue: PropTypes.string, - type: PropTypes.string, - onBlur: PropTypes.func, - maxLength: PropTypes.string, -}; - -export default FormControlOnChange; From 0f8e57926e923194fc6fa99f6292453ff483e4d8 Mon Sep 17 00:00:00 2001 From: mikkojamG Date: Mon, 3 Feb 2025 11:55:40 +0200 Subject: [PATCH 5/6] chore: fileToDataUri tests KER-422 --- .../images/__tests__/fileToDataUri.test.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/utils/images/__tests__/fileToDataUri.test.js diff --git a/src/utils/images/__tests__/fileToDataUri.test.js b/src/utils/images/__tests__/fileToDataUri.test.js new file mode 100644 index 000000000..45e558e25 --- /dev/null +++ b/src/utils/images/__tests__/fileToDataUri.test.js @@ -0,0 +1,24 @@ +/* eslint-disable func-names */ +import fileToDataUri from '../fileToDataUri'; + +describe('fileToDataUri', () => { + it('should resolve to a data URI string when file reading is successful', async () => { + const mockFile = new Blob(['file content'], { type: 'text/plain' }); + const expectedDataUri = 'data:text/plain;base64,ZmlsZSBjb250ZW50'; + + const result = await fileToDataUri(mockFile); + + expect(result).toBe(expectedDataUri); + }); + + it('should reject with an error when file reading fails', async () => { + const mockFile = new Blob(['file content'], { type: 'text/plain' }); + const mockError = new Error('File reading failed'); + + jest.spyOn(FileReader.prototype, 'readAsDataURL').mockImplementation(function () { + this.onerror(mockError); + }); + + await expect(fileToDataUri(mockFile)).rejects.toThrow('File reading failed'); + }); +}); From a88fc4d8d026f84e03f81b379220add929be3100 Mon Sep 17 00:00:00 2001 From: mikkojamG Date: Mon, 3 Feb 2025 12:58:42 +0200 Subject: [PATCH 6/6] chore: imageModal on error addToast KER-422 --- src/components/RichTextEditor/Image/ImageModal.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/RichTextEditor/Image/ImageModal.jsx b/src/components/RichTextEditor/Image/ImageModal.jsx index ef52e60c4..e728967fa 100644 --- a/src/components/RichTextEditor/Image/ImageModal.jsx +++ b/src/components/RichTextEditor/Image/ImageModal.jsx @@ -4,11 +4,14 @@ import { FormattedMessage, useIntl } from 'react-intl'; import PropTypes from 'prop-types'; import { ControlLabel } from 'react-bootstrap'; import { Button, Card, Dialog, FileInput, TextInput } from 'hds-react'; +import { useDispatch } from 'react-redux'; import getMessage from '../../../utils/getMessage'; import { isFormValid } from '../../../utils/iframeUtils'; import compressFile from '../../../utils/images/compressFile'; import fileToDataUri from '../../../utils/images/fileToDataUri'; +import { addToast } from '../../../actions/toast'; +import { createLocalizedNotificationPayload, NOTIFICATION_TYPES } from '../../../utils/notify'; /** * MAX_IMAGE_SIZE given in bytes @@ -16,6 +19,7 @@ import fileToDataUri from '../../../utils/images/fileToDataUri'; const MAX_IMAGE_SIZE = 1; const ImageModal = ({ isOpen, onClose, onSubmit }) => { + const dispatch = useDispatch(); const intl = useIntl(); const [showFormErrorMsg, setShowFormErrorMsg] = useState(false); @@ -39,6 +43,8 @@ const ImageModal = ({ isOpen, onClose, onSubmit }) => { } catch (error) { // eslint-disable-next-line no-console console.error(error); + + dispatch(addToast(createLocalizedNotificationPayload(NOTIFICATION_TYPES.error, 'imageFileUploadError'))); } };