Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 58 additions & 70 deletions src/components/RichTextEditor/Image/ImageModal.jsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
/* 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 { 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 Icon from '../../../utils/Icon';
import { createLocalizedNotificationPayload, NOTIFICATION_TYPES } from '../../../utils/notify';
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
*/
const MAX_IMAGE_SIZE = 999999;

const ImageModal = (props) => {
const MAX_IMAGE_SIZE = 1;

const ImageModal = ({ isOpen, onClose, onSubmit }) => {
const dispatch = useDispatch();
const intl = useIntl();

const [showFormErrorMsg, setShowFormErrorMsg] = useState(false);
const [fileReaderResult, setFileReaderResult] = useState(false);
Expand All @@ -31,55 +30,44 @@ 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 <Image className='preview' src={fileReaderResult} responsive />;
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);

dispatch(addToast(createLocalizedNotificationPayload(NOTIFICATION_TYPES.error, 'imageFileUploadError')));
}
return false;
}
};

const setImageAltTextFn = (event) => {
setImageAltText(event.target.value);
}
};

const validateForm = () => {
const inputErrors = {
fileReaderResult: fileReaderResult ? '' : getMessage('validationCantBeEmpty'),
};

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';
Expand All @@ -100,33 +88,34 @@ const ImageModal = (props) => {
<ControlLabel>
<FormattedMessage id='sectionImage' />
</ControlLabel>
<Dropzone accept='image/*' multiple={false} onDrop={onFileDrop}>
{
({getRootProps, getInputProps}) => (
<>
{getImagePreview()}
<div className={dropZoneClass}>
<div {...getRootProps()}>
<input {...getInputProps()} />
<span className='text'>
<FormattedMessage id='selectOrDropImage' />
<Icon className='icon' name='upload' />
</span>
</div>
</div>
</>
)
}

</Dropzone>
<HelpBlock>
<FormattedMessage id='sectionImageHelpText' />
</HelpBlock>
<ControlLabel>
<FormattedMessage id='sectionImageCaption' />
</ControlLabel>
<FormControl
type='text'
<div style={{ marginBottom: 'var(--spacing-s)' }}>
{fileReaderResult && (
<Card
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 'var(--spacing-s)',
}}
>
<img style={{ maxWidth: '100%', height: 'auto' }} src={fileReaderResult} alt='' />
</Card>
)}
<FileInput
id='image-modal-add-image'
name='image-modal-add-image'
accept='.jpeg,.jpg,.png,.webp,.gif'
dragAndDrop
label={<FormattedMessage id='selectOrDropImage' />}
helperText={<FormattedMessage id='sectionImageHelpText' />}
language={intl.locale}
onChange={onFileChange}
/>
</div>
<TextInput
id='sectionImageCaptionInput'
name='sectionImageCaptionInput'
label={<FormattedMessage id='sectionImageCaption' />}
className='sectionImageCaptionInput'
value={imageAltText}
onChange={setImageAltTextFn}
Expand All @@ -148,13 +137,12 @@ const ImageModal = (props) => {
</Dialog.ActionButtons>
</Dialog>
);
}
};

ImageModal.propTypes = {
isOpen: PropTypes.bool,
intl: PropTypes.object,
onClose: PropTypes.func,
onSubmit: PropTypes.func,
};

export default injectIntl(ImageModal);
export default ImageModal;
35 changes: 29 additions & 6 deletions src/components/admin/Phase.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -73,7 +86,12 @@ const Phase = (props) => {
name={`phase-duration-${indexNumber + 1}`}
label={<FormattedMessage id='phaseDuration' />}
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)
}
Expand All @@ -85,7 +103,12 @@ const Phase = (props) => {
name={`phase-description-${indexNumber + 1}`}
label={<FormattedMessage id='phaseDescription' />}
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)
}
Expand Down
24 changes: 4 additions & 20 deletions src/components/admin/SectionForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -58,7 +59,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 = ({
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -311,7 +295,7 @@ const SectionForm = ({
name='sectionImage'
dragAndDrop
label={<FormattedMessage id='sectionImage' />}
accept='.jpeg,.png,.webp,.gif'
accept='.jpeg,.jpg,.png,.webp,.gif'
helperText={<FormattedMessage id='sectionImageHelpText' />}
language={language}
onChange={onImageChange}
Expand Down
47 changes: 0 additions & 47 deletions src/components/forms/FormControlOnChange.jsx

This file was deleted.

24 changes: 24 additions & 0 deletions src/utils/images/__tests__/fileToDataUri.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
5 changes: 5 additions & 0 deletions src/utils/images/compressFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import imageCompression from 'browser-image-compression';

const compressFile = async (file, maxSizeMB, fileType) => imageCompression(file, { maxSizeMB, fileType });

export default compressFile;
Loading
Loading