From b8109c73f540c258091a5fecd6c248ce162100dd Mon Sep 17 00:00:00 2001 From: graph1589 Date: Mon, 31 Jan 2022 08:22:07 +0300 Subject: [PATCH 1/7] added necessary packages --- package.json | 2 ++ yarn.lock | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 80054c4..8afc2a8 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,13 @@ "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "humps": "^2.0.1", "material-design-lite": "^1.3.0", + "object-to-formdata": "^4.4.1", "prop-types": "^15.7.2", "qs": "^6.10.1", "ramda": "^0.27.1", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-image-crop": "^9.0.5", "react-is": "^17.0.2", "react-redux": "^7.2.6", "react-select": "^5.2.1", diff --git a/yarn.lock b/yarn.lock index 3b72ec2..a981de4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2346,7 +2346,7 @@ cliui@^5.0.0: strip-ansi "^5.2.0" wrap-ansi "^5.1.0" -clsx@^1.0.4: +clsx@^1.0.4, clsx@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== @@ -5507,6 +5507,11 @@ object-keys@^1.0.12, object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== +object-to-formdata@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/object-to-formdata/-/object-to-formdata-4.4.1.tgz#70e599e6887c8caa5b7fca741fb72ae9f60ea784" + integrity sha512-hHqo2JID8muY6hSeaEmze8BRN7bYu/BZvh2a+okxMHrYOkU29kxqlkn1ITAqsH7Mx9eRSWlJ3cC6sV3sx5F+6Q== + object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" @@ -6792,6 +6797,13 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-image-crop@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-9.0.5.tgz#a6dfe5411156f1dd1e435b128424ccf175a86948" + integrity sha512-J5lbsBMI36GsbkRHXNEo95OjwpxfkUBrEl9Oxb7z5mDC7iamySXYKhkv9QoidkfmI9e8Vj7q3SgNCVfuZHQMQw== + dependencies: + clsx "^1.1.1" + react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" From 4998ee315fec7fae4b10ee2bd8d8c3f80e414b92 Mon Sep 17 00:00:00 2001 From: graph1589 Date: Mon, 31 Jan 2022 08:28:35 +0300 Subject: [PATCH 2/7] added ImageUpload component --- .../components/ImageUpload/ImageUpload.jsx | 100 ++++++++++++++++++ .../components/ImageUpload/index.js | 3 + .../components/ImageUpload/useStyles.js | 10 ++ 3 files changed, 113 insertions(+) create mode 100644 app/javascript/components/ImageUpload/ImageUpload.jsx create mode 100644 app/javascript/components/ImageUpload/index.js create mode 100644 app/javascript/components/ImageUpload/useStyles.js diff --git a/app/javascript/components/ImageUpload/ImageUpload.jsx b/app/javascript/components/ImageUpload/ImageUpload.jsx new file mode 100644 index 0000000..2317add --- /dev/null +++ b/app/javascript/components/ImageUpload/ImageUpload.jsx @@ -0,0 +1,100 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import ReactCrop, { makeAspectCrop } from 'react-image-crop'; +import { isNil, path } from 'ramda'; + +import Button from '@material-ui/core/Button'; + +import useStyles from './useStyles'; +import 'react-image-crop/dist/ReactCrop.css'; + +const DEFAULT_CROP_PARAMS = { + x: 0, + y: 0, + width: 300, + height: 300, +}; + +const ImageUpload = ({ onUpload }) => { + const styles = useStyles(); + + const [fileAsBase64, changeFileAsBase64] = useState(null); + const [cropParams, changeCropParams] = useState(DEFAULT_CROP_PARAMS); + const [file, changeFile] = useState(null); + const [image, changeImage] = useState(null); + + const handleCropComplete = (newCrop, newPercentageCrop) => { + changeCropParams(newPercentageCrop); + }; + + const onImageLoaded = (loadedImage) => { + const newCropParams = makeAspectCrop(DEFAULT_CROP_PARAMS, loadedImage.width, loadedImage.height); + changeCropParams(newCropParams); + changeImage(loadedImage); + }; + + const getActualCropParameters = (width, height, params) => ({ + cropX: (params.x * width) / 100, + cropY: (params.y * height) / 100, + cropWidth: (params.width * width) / 100, + cropHeight: (params.height * height) / 100, + }); + + const handleCropChange = (_, newCropParams) => { + console.log('newCropParams:'); + console.log(newCropParams); + changeCropParams(newCropParams); + }; + + const handleSave = () => { + const { naturalWidth: width, naturalHeight: height } = image; + const actualCropParams = getActualCropParameters(width, height, cropParams); + console.log('actualCropParams:'); + console.log(actualCropParams); + onUpload({ attachment: { ...actualCropParams, image: file } }); + }; + + const handleImageRead = (newImage) => changeFileAsBase64(path(['target', 'result'], newImage)); + + const handleLoadFile = (e) => { + e.preventDefault(); + + const [acceptedFile] = e.target.files; + + const fileReader = new FileReader(); + fileReader.onload = handleImageRead; + fileReader.readAsDataURL(acceptedFile); + changeFile(acceptedFile); + }; + + return fileAsBase64 ? ( + <> +
+ +
+ + + ) : ( + + ); +}; + +ImageUpload.propTypes = { + onUpload: PropTypes.func.isRequired, +}; + +export default ImageUpload; diff --git a/app/javascript/components/ImageUpload/index.js b/app/javascript/components/ImageUpload/index.js new file mode 100644 index 0000000..b3a99c0 --- /dev/null +++ b/app/javascript/components/ImageUpload/index.js @@ -0,0 +1,3 @@ +import ImageUpload from './ImageUpload'; + +export default ImageUpload; diff --git a/app/javascript/components/ImageUpload/useStyles.js b/app/javascript/components/ImageUpload/useStyles.js new file mode 100644 index 0000000..2d7758a --- /dev/null +++ b/app/javascript/components/ImageUpload/useStyles.js @@ -0,0 +1,10 @@ +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles(() => ({ + crop: { + maxHeight: 300, + maxWidth: 300, + }, +})); + +export default useStyles; From c704400fe93984fd128cdf5b565ae670305c5805 Mon Sep 17 00:00:00 2001 From: graph1589 Date: Mon, 31 Jan 2022 08:31:36 +0300 Subject: [PATCH 3/7] updated routes --- app/javascript/routes/ApiRoutes.js | 108 +++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/app/javascript/routes/ApiRoutes.js b/app/javascript/routes/ApiRoutes.js index 3ecb40b..380371d 100644 --- a/app/javascript/routes/ApiRoutes.js +++ b/app/javascript/routes/ApiRoutes.js @@ -558,6 +558,15 @@ export const apiV1UserPath = __jsr.r({"id":{"r":true},"format":{"d":"json"}}, [2 */ export const apiV1UsersPath = __jsr.r({"format":{"d":"json"}}, [2,[7,"/"],[2,[6,"api"],[2,[7,"/"],[2,[6,"v1"],[2,[7,"/"],[2,[6,"users"],[1,[2,[8,"."],[3,"format"]]]]]]]]]); +/** + * Generates rails route to + * /api/v1/tasks/:id/attach_image(.:format) + * @param {any} id + * @param {object | undefined} options + * @returns {string} route path + */ +export const attachImageApiV1TaskPath = __jsr.r({"id":{"r":true},"format":{"d":"json"}}, [2,[7,"/"],[2,[6,"api"],[2,[7,"/"],[2,[6,"v1"],[2,[7,"/"],[2,[6,"tasks"],[2,[7,"/"],[2,[3,"id"],[2,[7,"/"],[2,[6,"attach_image"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]); + /** * Generates rails route to * /board(.:format) @@ -583,6 +592,22 @@ export const developersPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"developer */ export const editAdminUserPath = __jsr.r({"id":{"r":true},"format":{}}, [2,[7,"/"],[2,[6,"admin"],[2,[7,"/"],[2,[6,"users"],[2,[7,"/"],[2,[3,"id"],[2,[7,"/"],[2,[6,"edit"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]); +/** + * Generates rails route to + * /password/edit(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const editPasswordPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"password"],[2,[7,"/"],[2,[6,"edit"],[1,[2,[8,"."],[3,"format"]]]]]]]); + +/** + * Generates rails route to + * /password_reset/edit(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const editPasswordResetPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"password_reset"],[2,[7,"/"],[2,[6,"edit"],[1,[2,[8,"."],[3,"format"]]]]]]]); + /** * Generates rails route to * /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format) @@ -592,6 +617,48 @@ export const editAdminUserPath = __jsr.r({"id":{"r":true},"format":{}}, [2,[7,"/ */ export const editRailsConductorInboundEmailPath = __jsr.r({"id":{"r":true},"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"conductor"],[2,[7,"/"],[2,[6,"action_mailbox"],[2,[7,"/"],[2,[6,"inbound_emails"],[2,[7,"/"],[2,[3,"id"],[2,[7,"/"],[2,[6,"edit"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]]]); +/** + * Generates rails route to + * /letter_opener + * @param {object | undefined} options + * @returns {string} route path + */ +export const letterOpenerWebPath = __jsr.r({}, [2,[7,"/"],[6,"letter_opener"]]); + +/** + * Generates rails route to + * /letter_opener/ + * @param {object | undefined} options + * @returns {string} route path + */ +export const letterOpenerWebLettersPath = __jsr.r({}, [2,[2,[7,"/"],[6,"letter_opener"]],[7,"/"]]); + +/** + * Generates rails route to + * /letter_opener/clear(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const letterOpenerWebClearLettersPath = __jsr.r({"format":{}}, [2,[2,[2,[7,"/"],[6,"letter_opener"]],[7,"/"]],[2,[6,"clear"],[1,[2,[8,"."],[3,"format"]]]]]); + +/** + * Generates rails route to + * /letter_opener/:id(/:style)(.:format) + * @param {any} id + * @param {object | undefined} options + * @returns {string} route path + */ +export const letterOpenerWebLetterPath = __jsr.r({"id":{"r":true},"style":{},"format":{}}, [2,[2,[2,[7,"/"],[6,"letter_opener"]],[7,"/"]],[2,[3,"id"],[2,[1,[2,[7,"/"],[3,"style"]]],[1,[2,[8,"."],[3,"format"]]]]]]); + +/** + * Generates rails route to + * /letter_opener/:id/delete(.:format) + * @param {any} id + * @param {object | undefined} options + * @returns {string} route path + */ +export const letterOpenerWebDeleteLetterPath = __jsr.r({"id":{"r":true},"format":{}}, [2,[2,[2,[7,"/"],[6,"letter_opener"]],[7,"/"]],[2,[3,"id"],[2,[7,"/"],[2,[6,"delete"],[1,[2,[8,"."],[3,"format"]]]]]]]); + /** * Generates rails route to * /admin/users/new(.:format) @@ -608,6 +675,14 @@ export const newAdminUserPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"admin"] */ export const newDeveloperPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"developers"],[2,[7,"/"],[2,[6,"new"],[1,[2,[8,"."],[3,"format"]]]]]]]); +/** + * Generates rails route to + * /password_reset/new(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const newPasswordResetPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"password_reset"],[2,[7,"/"],[2,[6,"new"],[1,[2,[8,"."],[3,"format"]]]]]]]); + /** * Generates rails route to * /rails/conductor/action_mailbox/inbound_emails/new(.:format) @@ -632,6 +707,22 @@ export const newRailsConductorInboundEmailSourcePath = __jsr.r({"format":{}}, [2 */ export const newSessionPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"session"],[2,[7,"/"],[2,[6,"new"],[1,[2,[8,"."],[3,"format"]]]]]]]); +/** + * Generates rails route to + * /password(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const passwordPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"password"],[1,[2,[8,"."],[3,"format"]]]]]); + +/** + * Generates rails route to + * /password_reset(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const passwordResetPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"password_reset"],[1,[2,[8,"."],[3,"format"]]]]]); + /** * Generates rails route to * /rails/active_storage/representations/redirect/:signed_blob_id/:variation_key/*filename(.:format) @@ -806,6 +897,15 @@ export const railsServiceBlobPath = __jsr.r({"signed_id":{"r":true},"filename":{ */ export const railsServiceBlobProxyPath = __jsr.r({"signed_id":{"r":true},"filename":{"r":true},"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"active_storage"],[2,[7,"/"],[2,[6,"blobs"],[2,[7,"/"],[2,[6,"proxy"],[2,[7,"/"],[2,[3,"signed_id"],[2,[7,"/"],[2,[5,[3,"filename"]],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]]]); +/** + * Generates rails route to + * /api/v1/tasks/:id/remove_image(.:format) + * @param {any} id + * @param {object | undefined} options + * @returns {string} route path + */ +export const removeImageApiV1TaskPath = __jsr.r({"id":{"r":true},"format":{"d":"json"}}, [2,[7,"/"],[2,[6,"api"],[2,[7,"/"],[2,[6,"v1"],[2,[7,"/"],[2,[6,"tasks"],[2,[7,"/"],[2,[3,"id"],[2,[7,"/"],[2,[6,"remove_image"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]); + /** * Generates rails route to * / @@ -822,6 +922,14 @@ export const rootPath = __jsr.r({}, [7,"/"]); */ export const sessionPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"session"],[1,[2,[8,"."],[3,"format"]]]]]); +/** + * Generates rails route to + * /admin/sidekiq + * @param {object | undefined} options + * @returns {string} route path + */ +export const sidekiqWebPath = __jsr.r({}, [2,[7,"/"],[2,[6,"admin"],[2,[7,"/"],[6,"sidekiq"]]]]); + /** * Generates rails route to * /rails/active_storage/disk/:encoded_token(.:format) From 8fccd1f1cdf81db2365b1166806d2cffec97f6c3 Mon Sep 17 00:00:00 2001 From: graph1589 Date: Mon, 31 Jan 2022 08:33:17 +0300 Subject: [PATCH 4/7] updated api methods --- app/javascript/utils/fetchHelper.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/javascript/utils/fetchHelper.js b/app/javascript/utils/fetchHelper.js index 31ea1b9..0aaf1e1 100644 --- a/app/javascript/utils/fetchHelper.js +++ b/app/javascript/utils/fetchHelper.js @@ -1,3 +1,4 @@ +import { serialize } from 'object-to-formdata'; import axios from 'axios'; import qs from 'qs'; @@ -60,4 +61,22 @@ export default { delete(url) { return axios.delete(url).then(camelize); }, + + putFormData(url, json) { + const body = decamelize(json); + const formData = serialize({ + attachment: { + ...body.attachment, + image: json.attachment.image, + }, + }); + + return axios + .put(url, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + .then(camelize); + }, }; From a4e5c893d4a1870e98ffa0a97b3b8294d75846ce Mon Sep 17 00:00:00 2001 From: graph1589 Date: Mon, 31 Jan 2022 08:34:53 +0300 Subject: [PATCH 5/7] updated compnents and containers --- .../components/EditPopup/EditPopup.js | 24 +++++++++++++++++-- .../components/EditPopup/Form/Form.js | 20 ++++++++++++++-- .../components/EditPopup/Form/useStyles.js | 5 ++++ .../components/ImageUpload/ImageUpload.jsx | 4 ---- .../containers/TaskBoard/TaskBoard.js | 15 +++++++++++- 5 files changed, 59 insertions(+), 9 deletions(-) diff --git a/app/javascript/components/EditPopup/EditPopup.js b/app/javascript/components/EditPopup/EditPopup.js index e15319f..b58a6e8 100644 --- a/app/javascript/components/EditPopup/EditPopup.js +++ b/app/javascript/components/EditPopup/EditPopup.js @@ -15,8 +15,9 @@ import Modal from '@material-ui/core/Modal'; import Form from 'components/EditPopup/Form'; import useStyles from './useStyles'; +import TaskPresenter from '../../presenters/TaskPresenter'; -const EditPopup = ({ cardId, onClose, onCardDestroy, onLoadCard, onCardUpdate }) => { +const EditPopup = ({ cardId, onClose, onCardDestroy, onLoadCard, onCardUpdate, onAttachImage, onRemoveImage }) => { const [task, setTask] = useState(null); const [isSaving, setSaving] = useState(false); const [errors, setErrors] = useState({}); @@ -49,6 +50,17 @@ const EditPopup = ({ cardId, onClose, onCardDestroy, onLoadCard, onCardUpdate }) }); }; + const handleImageAttach = (params) => { + const id = TaskPresenter.id(task); + onCardUpdate(task); + onAttachImage(id, params); + }; + + const handleImageRemove = () => { + onCardUpdate(task); + onRemoveImage(TaskPresenter.id(task)); + }; + const isLoading = isNil(task); return ( @@ -68,7 +80,13 @@ const EditPopup = ({ cardId, onClose, onCardDestroy, onLoadCard, onCardUpdate }) ) : ( -
+ )} @@ -102,6 +120,8 @@ EditPopup.propTypes = { onCardDestroy: PropTypes.func.isRequired, onLoadCard: PropTypes.func.isRequired, onCardUpdate: PropTypes.func.isRequired, + onAttachImage: PropTypes.func.isRequired, + onRemoveImage: PropTypes.func.isRequired, }; export default EditPopup; diff --git a/app/javascript/components/EditPopup/Form/Form.js b/app/javascript/components/EditPopup/Form/Form.js index 355f680..c425a63 100644 --- a/app/javascript/components/EditPopup/Form/Form.js +++ b/app/javascript/components/EditPopup/Form/Form.js @@ -1,15 +1,17 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { has } from 'ramda'; +import { has, isNil } from 'ramda'; import TextField from '@material-ui/core/TextField'; +import Button from '@material-ui/core/Button'; import UserSelect from 'components/UserSelect'; import TaskPresenter from 'presenters/TaskPresenter'; +import ImageUpload from 'components/ImageUpload'; import useStyles from './useStyles'; -const Form = ({ errors, onChange, task }) => { +const Form = ({ errors, onChange, onRemoveImage, onAttachImage, task }) => { const handleChangeTextField = (fieldName) => (event) => onChange({ ...task, [fieldName]: event.target.value }); const handleChangeSelect = (fieldName) => (user) => onChange({ ...task, [fieldName]: user }); @@ -17,6 +19,18 @@ const Form = ({ errors, onChange, task }) => { return ( + {isNil(TaskPresenter.imageUrl(task)) ? ( +
+ +
+ ) : ( +
+ Attachment + +
+ )} { Form.propTypes = { onChange: PropTypes.func.isRequired, + onAttachImage: PropTypes.func.isRequired, + onRemoveImage: PropTypes.func.isRequired, task: TaskPresenter.shape().isRequired, errors: PropTypes.shape({ name: PropTypes.arrayOf(PropTypes.string), diff --git a/app/javascript/components/EditPopup/Form/useStyles.js b/app/javascript/components/EditPopup/Form/useStyles.js index 36abbcc..d78d31e 100644 --- a/app/javascript/components/EditPopup/Form/useStyles.js +++ b/app/javascript/components/EditPopup/Form/useStyles.js @@ -5,6 +5,11 @@ const useStyles = makeStyles(() => ({ display: 'flex', flexDirection: 'column', }, + preview: { + display: 'block', + maxWidth: 300, + maxHeight: 300, + }, })); export default useStyles; diff --git a/app/javascript/components/ImageUpload/ImageUpload.jsx b/app/javascript/components/ImageUpload/ImageUpload.jsx index 2317add..d8e3ef2 100644 --- a/app/javascript/components/ImageUpload/ImageUpload.jsx +++ b/app/javascript/components/ImageUpload/ImageUpload.jsx @@ -41,16 +41,12 @@ const ImageUpload = ({ onUpload }) => { }); const handleCropChange = (_, newCropParams) => { - console.log('newCropParams:'); - console.log(newCropParams); changeCropParams(newCropParams); }; const handleSave = () => { const { naturalWidth: width, naturalHeight: height } = image; const actualCropParams = getActualCropParameters(width, height, cropParams); - console.log('actualCropParams:'); - console.log(actualCropParams); onUpload({ attachment: { ...actualCropParams, image: file } }); }; diff --git a/app/javascript/containers/TaskBoard/TaskBoard.js b/app/javascript/containers/TaskBoard/TaskBoard.js index 951e3ba..2ca7dd3 100644 --- a/app/javascript/containers/TaskBoard/TaskBoard.js +++ b/app/javascript/containers/TaskBoard/TaskBoard.js @@ -22,7 +22,18 @@ const MODES = { }; const TaskBoard = () => { - const { board, loadBoard, loadColumn, loadColumnMore, updateTask, destroyTask, createTask, loadTask } = useTasks(); + const { + board, + loadBoard, + loadColumn, + loadColumnMore, + updateTask, + destroyTask, + createTask, + loadTask, + attachTaskImage, + removeTaskImage, + } = useTasks(); const [mode, setMode] = useState(MODES.NONE); const [openedTaskId, setOpenedTaskId] = useState(null); @@ -115,6 +126,8 @@ const TaskBoard = () => { onCardUpdate={handleTaskUpdate} onClose={handleClose} cardId={openedTaskId} + onAttachImage={attachTaskImage} + onRemoveImage={removeTaskImage} /> )} From db730cf3195128f48d07d794134cdf6754b536ba Mon Sep 17 00:00:00 2001 From: graph1589 Date: Mon, 31 Jan 2022 08:36:40 +0300 Subject: [PATCH 6/7] updated store and presenter --- app/javascript/hooks/store/useTasks.js | 13 ++++++++++++- app/javascript/presenters/TaskPresenter.js | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/javascript/hooks/store/useTasks.js b/app/javascript/hooks/store/useTasks.js index b28cb9b..96c4d38 100644 --- a/app/javascript/hooks/store/useTasks.js +++ b/app/javascript/hooks/store/useTasks.js @@ -4,7 +4,16 @@ import { STATES } from 'presenters/TaskPresenter'; const useTasks = () => { const board = useSelector((state) => state.TasksSlice.board); - const { loadColumn, loadColumnMore, updateTask, destroyTask, createTask, loadTask } = useTasksActions(); + const { + loadColumn, + loadColumnMore, + updateTask, + destroyTask, + createTask, + loadTask, + attachTaskImage, + removeTaskImage, + } = useTasksActions(); const loadBoard = () => Promise.all(STATES.map(({ key }) => loadColumn(key))); return { @@ -16,6 +25,8 @@ const useTasks = () => { destroyTask, createTask, loadTask, + attachTaskImage, + removeTaskImage, }; }; diff --git a/app/javascript/presenters/TaskPresenter.js b/app/javascript/presenters/TaskPresenter.js index 4a8d16f..bc46c46 100644 --- a/app/javascript/presenters/TaskPresenter.js +++ b/app/javascript/presenters/TaskPresenter.js @@ -10,6 +10,7 @@ export default new PropTypesPresenter({ transitions: PropTypes.array, author: PropTypes.object, assignee: PropTypes.object, + imageUrl: PropTypes.string, }); export const STATES = [ From 916a4d142338f3352d8791c5ec6760ec7373cf41 Mon Sep 17 00:00:00 2001 From: graph1589 Date: Mon, 31 Jan 2022 08:37:28 +0300 Subject: [PATCH 7/7] updated slice and repository --- app/javascript/repositories/TasksRepository.js | 10 ++++++++++ app/javascript/slices/TasksSlice.js | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/app/javascript/repositories/TasksRepository.js b/app/javascript/repositories/TasksRepository.js index f4d251a..381c7a3 100644 --- a/app/javascript/repositories/TasksRepository.js +++ b/app/javascript/repositories/TasksRepository.js @@ -26,4 +26,14 @@ export default { const path = routes.apiV1TaskPath(id); return FetchHelper.delete(path); }, + + attachImage(id, params) { + const path = routes.attachImageApiV1TaskPath(id); + return FetchHelper.putFormData(path, params); + }, + + removeImage(id) { + const path = routes.removeImageApiV1TaskPath(id); + return FetchHelper.put(path, {}); + }, }; diff --git a/app/javascript/slices/TasksSlice.js b/app/javascript/slices/TasksSlice.js index 9c114af..f617093 100644 --- a/app/javascript/slices/TasksSlice.js +++ b/app/javascript/slices/TasksSlice.js @@ -81,6 +81,10 @@ export const useTasksActions = () => { const loadTask = (id) => TasksRepository.show(id).then(({ data: { task } }) => task); + const attachTaskImage = (id, json) => TasksRepository.attachImage(id, json); + + const removeTaskImage = (id) => TasksRepository.removeImage(id); + return { loadBoard, loadColumn, @@ -89,5 +93,7 @@ export const useTasksActions = () => { destroyTask, createTask, loadTask, + attachTaskImage, + removeTaskImage, }; };