diff --git a/src/components/Feedback/FeedbackModal.spec.tsx b/src/components/Feedback/FeedbackModal.spec.tsx index 7b6c47d27a61f87cfd5587f4d14bf6b53dab3b47..f5f66fcb4adae72eb58864a37767ac66e2dad9ec 100644 --- a/src/components/Feedback/FeedbackModal.spec.tsx +++ b/src/components/Feedback/FeedbackModal.spec.tsx @@ -1,10 +1,16 @@ import React from 'react' import * as reactRedux from 'react-redux' -import { shallow } from 'enzyme' +import { mount } from 'enzyme' import FeedbackModal from 'components/Feedback/FeedbackModal' import Button from '@material-ui/core/Button' import { userChallengeExplo1OnGoing } from '../../../tests/__mocks__/userChallengeData.mock' +import { Provider } from 'react-redux' +import { + createMockStore, + mockInitialEcolyoState, +} from '../../../tests/__mocks__/store' +import { act } from '@testing-library/react' const mockSendMail = jest.fn() jest.mock('services/mail.service', () => { @@ -31,22 +37,42 @@ const mockUseSelector = jest.spyOn(reactRedux, 'useSelector') const mockUseDispatch = jest.spyOn(reactRedux, 'useDispatch') describe('FeedbackModal component', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let store: any + beforeEach(() => { + store = createMockStore(mockInitialEcolyoState) + }) it('should render the component', () => { mockUseDispatch.mockReturnValue(jest.fn()) mockUseSelector.mockReturnValue(userChallengeExplo1OnGoing) - const component = shallow( - <FeedbackModal open={true} handleCloseClick={handleFeedbackModalClose} /> + const component = mount( + <Provider store={store}> + <FeedbackModal + open={true} + handleCloseClick={handleFeedbackModalClose} + /> + </Provider> ).getElement() expect(component).toMatchSnapshot() }) }) describe('FeedbackModal functionnalities', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let store: any + beforeEach(() => { + store = createMockStore(mockInitialEcolyoState) + }) it('should should send an email to the support', () => { mockUseDispatch.mockReturnValue(jest.fn()) mockUseSelector.mockReturnValue(userChallengeExplo1OnGoing) - const wrapper = shallow( - <FeedbackModal open={true} handleCloseClick={handleFeedbackModalClose} /> + const wrapper = mount( + <Provider store={store}> + <FeedbackModal + open={true} + handleCloseClick={handleFeedbackModalClose} + /> + </Provider> ) const mockPlatform = 'platform' @@ -58,8 +84,14 @@ describe('FeedbackModal functionnalities', () => { const expectedMailData = { mode: 'from', - to: [{ name: 'Support', email: 'ecolyo@grandlyon.com' }], + to: [ + { + name: 'Support', + email: 'ecolyo@grandlyon.com', + }, + ], subject: '[Ecolyo] - Feedbacks - feedback.type_bug', + attachments: [], parts: [ { type: 'text/plain', @@ -90,9 +122,11 @@ describe('FeedbackModal functionnalities', () => { .find('div.fb-selector-item') .first() .simulate('click') - wrapper - .find('#idFeedbackDescription') - .simulate('change', { target: { value: 'La description' } }) + wrapper.find('#idFeedbackDescription').simulate('change', { + target: { + value: 'La description', + }, + }) wrapper.find(Button).simulate('click') expect(mockSendMail).toHaveBeenCalledWith( @@ -100,19 +134,57 @@ describe('FeedbackModal functionnalities', () => { expectedMailData ) }) - it('should close the modal and reset the inputs', () => { + it('should close the modal and reset the inputs', async () => { mockUseDispatch.mockReturnValue(jest.fn()) mockUseSelector.mockReturnValue(userChallengeExplo1OnGoing) - const wrapper = shallow( - <FeedbackModal open={true} handleCloseClick={handleFeedbackModalClose} /> + const wrapper = mount( + <Provider store={store}> + <FeedbackModal + open={true} + handleCloseClick={handleFeedbackModalClose} + /> + </Provider> ) + await act(async () => { + await new Promise(resolve => setTimeout(resolve)) + wrapper.update() + }) + wrapper.find('#idFeedbackDescription').simulate('change', { + target: { + value: 'La description', + }, + }) wrapper - .find('#idFeedbackDescription') - .simulate('change', { target: { value: 'La description' } }) - wrapper.find('.modal-paper-close-button').simulate('click') + .find('.modal-paper-close-button') + .first() + .simulate('click') expect(handleFeedbackModalClose).toHaveBeenCalledTimes(1) setTimeout(() => { - expect(wrapper.find('#idFeedbackDescription').prop('value')).toBeNull() + expect(wrapper.find('#idFeedbackDescription').prop('value')).toBe('') }) }) + it('should upload the image', async () => { + const wrapper = mount( + <Provider store={store}> + <FeedbackModal + open={true} + handleCloseClick={handleFeedbackModalClose} + /> + </Provider> + ) + await act(async () => { + await new Promise(resolve => setTimeout(resolve)) + wrapper.update() + }) + const file = new File([new ArrayBuffer(1)], 'file.jpg') + const readAsDataURLSpy = jest.spyOn(FileReader.prototype, 'readAsDataURL') + wrapper.find('#folder').simulate('change', { target: { files: [file] } }) + expect(readAsDataURLSpy).toBeCalledWith(file) + expect( + wrapper + .find('.removeUploaded') + .first() + .simulate('click') + ) + }) }) diff --git a/src/components/Feedback/FeedbackModal.tsx b/src/components/Feedback/FeedbackModal.tsx index 3beae7efbad208cb17f14c2fcdd9a7a6111e9762..9855ee3f17d574114f5036222bc5a995193011d9 100644 --- a/src/components/Feedback/FeedbackModal.tsx +++ b/src/components/Feedback/FeedbackModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { ChangeEvent, useState } from 'react' import { IuseI18n, useI18n } from 'cozy-ui/transpiled/react/I18n' import Icon from 'cozy-ui/transpiled/react/Icon' import { Client, useClient } from 'cozy-client' @@ -9,7 +9,6 @@ import IconButton from '@material-ui/core/IconButton' import Button from '@material-ui/core/Button' import Dialog from '@material-ui/core/Dialog' import StyledIconBorderedButton from 'components/CommonKit/IconButton/StyledIconBorderedButton' - import BugOnIcon from 'assets/icons/visu/feedback/bug-on.svg' import BugOffIcon from 'assets/icons/visu/feedback/bug-off.svg' import IdeaOnIcon from 'assets/icons/visu/feedback/idea-on.svg' @@ -43,9 +42,10 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ const [sending, setSending] = useState<boolean>(false) const [sent, setSent] = useState<boolean>(false) const [error, setError] = useState<string>('') + const [textFile, setTextFile] = useState<string>() + const [uploadedFile, setuploadedFile] = useState<File | null>(null) const [, setValidExploration] = useExploration() - const resetInputs = () => { setType('bug') setDescription('') @@ -90,6 +90,10 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ to: [{ name: 'Support', email: FEEDBACK_EMAIL }], subject: '[Ecolyo] - Feedbacks - ' + t('feedback.type_' + type), parts: [{ type: 'text/plain', body: mailContent }], + attachments: + uploadedFile && textFile + ? [{ filename: uploadedFile.name, content: textFile }] + : [], } try { const mailService = new MailService() @@ -102,6 +106,7 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ } setSending(false) setSent(true) + setuploadedFile(null) } const validResult = () => { @@ -163,6 +168,28 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ </div> ) } + const readFileAsDataURL = async (file: File): Promise<string> => { + return new Promise(resolve => { + const reader = new FileReader() + reader.onloadend = () => { + if (reader.result) { + resolve(reader.result as string) + } + } + reader.readAsDataURL(file) + }) + } + const getDocument = async ( + e: ChangeEvent<HTMLInputElement> + ): Promise<void> => { + const { files } = e.target + const file = files ? files[0] : null + if (file) { + setuploadedFile(file) + const base64File: string = await readFileAsDataURL(file) + setTextFile(base64File.split(',')[1]) + } + } return ( <Dialog @@ -233,6 +260,37 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ {selectorItem('idea')} {selectorItem('other')} </fieldset> + + {!uploadedFile ? ( + <> + <input + type="file" + id="folder" + accept="image/*" + onChange={(e): Promise<void> => getDocument(e)} + className="input-file" + hidden + /> + <label htmlFor="folder" className="upload-label"> + {t('feedback.upload')} + </label> + </> + ) : ( + <> + <span className="fb-label text-16-bold"> + {t('feedback.imageLabel')} + </span> + <div className="fileName"> + <span>{uploadedFile.name}</span> + <IconButton + onClick={() => setuploadedFile(null)} + className="removeUploaded" + > + <Icon icon={CloseIcon} size={12} /> + </IconButton> + </div> + </> + )} <label htmlFor="idFeedbackDescription" className="fb-label text-16-bold" diff --git a/src/components/Feedback/__snapshots__/FeedbackModal.spec.tsx.snap b/src/components/Feedback/__snapshots__/FeedbackModal.spec.tsx.snap index e0e94c316e6b119ab2d479b49ee49f3eaca22783..2fd286dd67fe400f211e0d6e01b0fe4bd275a457 100644 --- a/src/components/Feedback/__snapshots__/FeedbackModal.spec.tsx.snap +++ b/src/components/Feedback/__snapshots__/FeedbackModal.spec.tsx.snap @@ -1,143 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`FeedbackModal component should render the component 1`] = ` -<ForwardRef(WithStyles) - aria-labelledby="accessibility-title" - classes={ +<Provider + store={ Object { - "paper": "modal-paper yellow-border", - "root": "modal-root", + "clearActions": [Function], + "dispatch": [Function], + "getActions": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], } } - disableBackdropClick={true} - disableEscapeKeyDown={true} - onClose={[Function]} - open={true} > - <div - id="accessibility-title" - > - feedback.accessibility.window_title - </div> - <ForwardRef(WithStyles) - aria-label="feedback.accessibility.button_close" - className="modal-paper-close-button" - onClick={[Function]} - > - <Icon - icon="test-file-stub" - size={16} - spin={false} - /> - </ForwardRef(WithStyles)> - <div - className="fb-root" - > - <React.Fragment> - <div - className="fb-header text-18-bold" - id="title" - > - feedback.title - </div> - <form - className="fb-content" - > - <label - className="fb-label text-16-bold" - htmlFor="feedbackType" - > - feedback.type - </label> - <fieldset - className="fb-selector" - id="feedbackType" - > - <div - className="fb-selector-item" - > - <StyledIconBorderedButton - aria-label="feedback.accessibility.select_type_bug" - autoFocus={true} - icon="test-file-stub" - onClick={[Function]} - selected={true} - size={36} - > - <div - className="fb-selector-item-selectedlabel text-10-bold" - > - feedback.type_bug - </div> - </StyledIconBorderedButton> - </div> - <div - className="fb-selector-item" - > - <StyledIconBorderedButton - aria-label="feedback.accessibility.select_type_idea" - autoFocus={false} - icon="test-file-stub" - onClick={[Function]} - selected={false} - size={36} - > - <div - className="fb-selector-item-label text-10-normal" - > - feedback.type_idea - </div> - </StyledIconBorderedButton> - </div> - <div - className="fb-selector-item" - > - <StyledIconBorderedButton - aria-label="feedback.accessibility.select_type_other" - autoFocus={false} - icon="test-file-stub" - onClick={[Function]} - selected={false} - size={36} - > - <div - className="fb-selector-item-label text-10-normal" - > - feedback.type_other - </div> - </StyledIconBorderedButton> - </div> - </fieldset> - <label - className="fb-label text-16-bold" - htmlFor="idFeedbackDescription" - > - feedback.description - </label> - <textarea - className="fb-form fb-textarea" - id="idFeedbackDescription" - name="description" - onChange={[Function]} - placeholder="feedback.description_placeholder" - value="" - /> - <ForwardRef(WithStyles) - aria-label="feedback.accessibility.button_send" - classes={ - Object { - "label": "text-16-bold", - "root": "btn-highlight", - } - } - disabled={false} - onClick={[Function]} - type="submit" - > - feedback.send - </ForwardRef(WithStyles)> - </form> - </React.Fragment> - </div> -</ForwardRef(WithStyles)> + <FeedbackModal + handleCloseClick={[MockFunction]} + open={true} + /> +</Provider> `; diff --git a/src/components/Feedback/feedbackModal.scss b/src/components/Feedback/feedbackModal.scss index 46234e06b41c6a505e0e9c9825d74b8a6e5357cf..e79334006bc530b8b19da8c2c82ae8d8fc4db4d9 100644 --- a/src/components/Feedback/feedbackModal.scss +++ b/src/components/Feedback/feedbackModal.scss @@ -1,4 +1,5 @@ @import 'src/styles/base/color'; +@import '../../styles/base/mixins'; .fb-root { overflow-y: auto; @@ -12,7 +13,7 @@ } .fb-content { - padding: 1rem 0.5rem 0.5rem 0.5rem; + padding: 1rem 0.5rem 1.5rem 0.5rem; display: flex; flex-direction: column; .fb-content-success { @@ -83,7 +84,28 @@ } } } - +.upload-label { + appearance: none; + @include button($gold-shadow, #000000, none, $multi-color-radial-gradient) { + } + color: $dark-2; + padding: 0.5rem; + text-align: center; + font-weight: bold; + margin-bottom: 0.5rem; + max-width: 180px; + margin: 0.8rem auto; +} +.fileName { + display: flex; + justify-content: space-between; + padding: 0 0.5rem; + color: $grey-bright; + border: solid 1px $grey-dark; + border-radius: 3px; + margin: 0.5rem 0; + align-items: center; +} #accessibility-title { display: none; -} \ No newline at end of file +} diff --git a/src/locales/fr.json b/src/locales/fr.json index ef5c6ad9777e4262ede64d207f51c45387e0fed1..cbd24b321e3b6b8a25a50a1503a7d6743eb40c3c 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -372,6 +372,8 @@ "warning": "Attention !", "error_empty_description": "Le champ de description est vide.", "error_sending": "Erreur lors de l'envoi, veuillez essayer ultérieurement.", + "upload": "Joindre une image", + "imageLabel": "Image :", "accessibility": { "window_title": "Fenêtre de partage de retours", "select_type_bug": "Sélectionner le motif bug",