Skip to content
Open
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
24 changes: 22 additions & 2 deletions app/javascript/components/EditPopup/EditPopup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({});
Expand Down Expand Up @@ -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 (
Expand All @@ -68,7 +80,13 @@ const EditPopup = ({ cardId, onClose, onCardDestroy, onLoadCard, onCardUpdate })
<CircularProgress />
</div>
) : (
<Form errors={errors} onChange={setTask} task={task} />
<Form
errors={errors}
onChange={setTask}
onAttachImage={handleImageAttach}
onRemoveImage={handleImageRemove}
task={task}
/>
)}
</CardContent>
<CardActions className={styles.actions}>
Expand Down Expand Up @@ -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;
20 changes: 18 additions & 2 deletions app/javascript/components/EditPopup/Form/Form.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
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 });

const styles = useStyles();

return (
<form className={styles.root}>
{isNil(TaskPresenter.imageUrl(task)) ? (
<div>
<ImageUpload onUpload={onAttachImage} />
</div>
) : (
<div>
<img className={styles.preview} src={TaskPresenter.imageUrl(task)} alt="Attachment" />
<Button variant="contained" size="small" color="primary" onClick={onRemoveImage}>
Remove image
</Button>
</div>
)}
<TextField
error={has('name', errors)}
helperText={errors.name}
Expand Down Expand Up @@ -60,6 +74,8 @@ const Form = ({ errors, onChange, task }) => {

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),
Expand Down
5 changes: 5 additions & 0 deletions app/javascript/components/EditPopup/Form/useStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ const useStyles = makeStyles(() => ({
display: 'flex',
flexDirection: 'column',
},
preview: {
display: 'block',
maxWidth: 300,
maxHeight: 300,
},
}));

export default useStyles;
96 changes: 96 additions & 0 deletions app/javascript/components/ImageUpload/ImageUpload.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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) => {
changeCropParams(newCropParams);
};

const handleSave = () => {
const { naturalWidth: width, naturalHeight: height } = image;
const actualCropParams = getActualCropParameters(width, height, cropParams);
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 ? (
<>
<div className={styles.crop}>
<ReactCrop
src={fileAsBase64}
crop={cropParams}
onImageLoaded={onImageLoaded}
onComplete={handleCropComplete}
onChange={handleCropChange}
keepSelection
/>
</div>
<Button variant="contained" size="small" color="primary" disabled={isNil(image)} onClick={handleSave}>
Save
</Button>
</>
) : (
<label htmlFor="imageUpload">
<Button variant="contained" size="small" color="primary" component="span">
Add Image
</Button>
<input accept="image/*" id="imageUpload" type="file" onChange={handleLoadFile} hidden />
</label>
);
};

ImageUpload.propTypes = {
onUpload: PropTypes.func.isRequired,
};

export default ImageUpload;
3 changes: 3 additions & 0 deletions app/javascript/components/ImageUpload/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ImageUpload from './ImageUpload';

export default ImageUpload;
10 changes: 10 additions & 0 deletions app/javascript/components/ImageUpload/useStyles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { makeStyles } from '@material-ui/core/styles';

const useStyles = makeStyles(() => ({
crop: {
maxHeight: 300,
maxWidth: 300,
},
}));

export default useStyles;
15 changes: 14 additions & 1 deletion app/javascript/containers/TaskBoard/TaskBoard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -115,6 +126,8 @@ const TaskBoard = () => {
onCardUpdate={handleTaskUpdate}
onClose={handleClose}
cardId={openedTaskId}
onAttachImage={attachTaskImage}
onRemoveImage={removeTaskImage}
/>
)}
</>
Expand Down
13 changes: 12 additions & 1 deletion app/javascript/hooks/store/useTasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -16,6 +25,8 @@ const useTasks = () => {
destroyTask,
createTask,
loadTask,
attachTaskImage,
removeTaskImage,
};
};

Expand Down
1 change: 1 addition & 0 deletions app/javascript/presenters/TaskPresenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default new PropTypesPresenter({
transitions: PropTypes.array,
author: PropTypes.object,
assignee: PropTypes.object,
imageUrl: PropTypes.string,
});

export const STATES = [
Expand Down
10 changes: 10 additions & 0 deletions app/javascript/repositories/TasksRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {});
},
};
Loading