Skip to content

Commit

Permalink
feat: add image editor
Browse files Browse the repository at this point in the history
  • Loading branch information
andre-code committed Jan 3, 2023
1 parent 8645ef7 commit d645243
Show file tree
Hide file tree
Showing 7 changed files with 2,489 additions and 250 deletions.
2,420 changes: 2,251 additions & 169 deletions client/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@renku/ckeditor5-build-renku": "0.0.6",
"@sentry/react": "^6.16.1",
"@sentry/tracing": "^6.16.1",
"@types/react-avatar-editor": "^13.0.0",
"ajv": "^6.12.6",
"bootstrap": "^5.0.2",
"cookieconsent": "^3.1.1",
Expand All @@ -36,6 +37,7 @@
"query-string": "^6.14.1",
"react": "^17.0.2",
"react-autosuggest": "^10.1.0",
"react-avatar-editor": "^13.0.0",
"react-bootstrap-icons": "^1.8.4",
"react-clipboard.js": "^2.0.16",
"react-collapse": "^5.0.0",
Expand Down Expand Up @@ -95,7 +97,7 @@
"@storybook/addon-links": "^6.4.19",
"@storybook/addon-storysource": "^6.4.19",
"@storybook/node-logger": "^6.4.19",
"@storybook/preset-create-react-app": "^4.1.2",
"@storybook/preset-create-react-app": "^3.2.0",
"@storybook/react": "^6.4.19",
"@storybook/testing-library": "^0.0.9",
"@types/dropzone": "^5.7.4",
Expand Down
4 changes: 2 additions & 2 deletions client/src/dataset/Dataset.present.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function DisplayFiles(props) {
}

function DisplayProjects(props) {
if (props.projects === undefined) return null;
if (props?.projects === undefined) return null;
return <Card key="datasetProjectDetails" className="border-rk-light mb-4">
<CardHeader className="bg-white p-3 ps-4">Projects using this dataset</CardHeader>
<CardBody className="p-4 pt-3 pb-3 lh-lg pb-2">
Expand All @@ -122,7 +122,7 @@ function DisplayProjects(props) {
</tr>
</thead>
<tbody>
{props.projects.map((project, index) =>
{props.projects?.map((project, index) =>
<tr data-cy="project-using-dataset" key={project.name + index}>
<td className="text-break">
<Link to={`${props.projectsUrl}/${project.path}`}>
Expand Down
39 changes: 39 additions & 0 deletions client/src/styles/bootstrap_ext/_button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,42 @@ $borderRadius: 1000px;
padding-left: 20px;
}
}
.edit-image-btn {
position: absolute;
left: 15px;
background-color: #d9d9d9de;
color: black;
border: none;
border-radius: 8px;

&:hover {
background-color: rgba(166, 166, 166, 0.87);
color: white;
}
&:focus {
background-color: rgba(166, 166, 166, 0.87);
color: white;
}
}

.editor-control-btn {
border-radius: 4px;
background-color: #D9D9D9;
border: none;
padding: 2px 8px;
color: var(--bs-dark);

&:hover {
background-color: #d9d9d9de;
color: var(--bs-dark);
}
&:focus {
background-color: #d9d9d9de;
color: var(--bs-dark);
}
&:disabled {
background-color: rgba(217, 217, 217, 0.81);
color: var(--bs-dark);
}
}

141 changes: 64 additions & 77 deletions client/src/utils/components/formgenerator/fields/ImageInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,35 +23,15 @@
* Presentational components for presenting images.
*/

import React, { useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { Col, Row } from "reactstrap";
import { Button, ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle, FormGroup, Input } from "reactstrap";
import { InputGroup } from "reactstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCaretLeft, faCaretRight } from "@fortawesome/free-solid-svg-icons";

import { ImageFieldPropertyName as Prop } from "./stockimages";
import { formatBytes } from "../../../helpers/HelperFunctions";
import { ErrorLabel, InputHintLabel, InputLabel } from "../../formlabels/FormLabels";
/**
* Update the value of the function
* @param {string} name Field name
* @param {integer} current The current value
* @param {+1/-1} direction The direction to rotate towards
* @param {array} options The full list of options
* @param {function} setInputs The setValue function
*/
function rotateValue(name, current, direction, options, setInputs) {
const length = options.length;
let newValue = current + direction;
if (newValue >= length) newValue = -1;
if (newValue < -1) newValue = length - 1;
const artificialEvent = {
target: { name, value: { options, selected: newValue } },
isPersistent: () => false
};
setInputs(artificialEvent);
}
import ImageEditor from "../../imageEditor/ImageEditor";

function userInputOption(options) {
let userInput = options.find(o => o[Prop.STOCK] === false);
Expand All @@ -62,65 +42,64 @@ function userInputOption(options) {
return [userInput, options];
}

function ImagePreview(
{ name, value, selected, disabled, setInputs, maxSize, setSizeAlert, options, originalImageInput }) {
const [editMode, setEditMode] = useState(false);
const [imageEditionState, setImageEditionState] = useState({
scale: 1,
positions: { x: 0, y: 0 }
});

useEffect(() => {
// reset imageEditionState if the image change
setImageEditionState({
scale: 1,
positions: { x: 0, y: 0 }
});
}, [originalImageInput]);

function ImagePreviewControls({ value, options, rotate, disabled, currentImageIndex }) {
if ((options.length < 1) || disabled)
return <div className="d-flex justify-content-around p-0"></div>;

const lastImageIndex = options.length - 1;
const rightControl = currentImageIndex !== lastImageIndex ? (
<div>
<Button color="link" onClick={() => { rotate(1); }}>
<FontAwesomeIcon icon={faCaretRight} />
</Button>
</div>
) : null;

const leftControl = currentImageIndex !== 0 ? (
<div>
<Button color="link" onClick={() => { rotate(-1); }}>
<FontAwesomeIcon icon={faCaretLeft} />
</Button>
</div>
) : null;
return <div className="d-flex justify-content-around p-0">
{leftControl}
<div className="pt-2" style={{ fontSize: "smaller" }}>{value}</div>
{rightControl}
</div>;
}

function ImagePreview({ name, value, selected, disabled, setInputs }) {
const options = value.options.length > 0 ? value.options.filter(o => o.URL && o.URL.length > 0) : [];
const selectedIndex = value.selected;
const imageSize = { width: 160, height: 135, borderRadius: "8px" };
const imageSize = { width: 133, height: 77, borderRadius: "8px" };
const imageStyle = { ...imageSize, objectFit: "cover" };
const imagePreviewStyle = { ...imageStyle, backgroundColor: "#C4C4C4", borderRadius: "8px" };
const displayValue = selected[Prop.NAME] ?? "Current Image";
const isImageSelected = (selectedIndex > -1 && selected[Prop.URL]);
const isNewFileUploaded = selected[Prop.URL] && !selected[Prop.FILE];

const onChangeImage = (fileModified) => {
if (fileModified) {
setEditMode(false);
reviewFile(fileModified, maxSize, setSizeAlert, options, setInputs, name);
}
};

const image = (selectedIndex > -1 && selected[Prop.URL]) ?
const image = isImageSelected ?
<img src={selected[Prop.URL]} alt={displayValue} style={imageStyle} /> :
(<div style={imagePreviewStyle}
className="d-flex justify-content-center align-items-center text-white">
<div>No Image Yet</div>
</div>);

const rotate = (direction) => rotateValue(name, value.selected, direction, options, setInputs);

const imageControls = options.length > 1 ?
<ImagePreviewControls
value={displayValue}
options={options}
disabled={disabled}
currentImageIndex={selectedIndex}
rotate={rotate}
/>
: null;
return (<div className="m-auto" style={imageSize}>
<div className="d-flex justify-content-around card">
<div style={imageSize}>{image}</div>
const editButton = !isNewFileUploaded && isImageSelected && !editMode ?
<Button className="edit-image-btn fs-small" onClick={() => setEditMode(true)}>Edit Image</Button> : null;
const imageEditor = editMode && !disabled ?
<ImageEditor
file={originalImageInput}
onSave={onChangeImage}
imageEditionState={imageEditionState}
setImageEditionState={setImageEditionState} /> : null;
const imageView = !editMode ? (
<div style={imageSize}>
<div className="d-flex justify-content-around card">
<div style={imageSize}>{image}</div>
{editButton}
</div>
</div>
{imageControls}
) : null;

return (<div className="m-auto">
{imageView}
{imageEditor}
</div>);
}

Expand All @@ -145,7 +124,7 @@ function onUrlInputChange(name, options, setInputs, e) {
setInputs(artificialEvent);
}

function onFileInputChange(name, options, maxSize, setInputs, setSizeAlert, e) {
function onFileInputChange(name, options, maxSize, setInputs, setSizeAlert, setOriginalImageInput, e) {
e.preventDefault();
e.persist();
const files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
Expand All @@ -154,6 +133,10 @@ function onFileInputChange(name, options, maxSize, setInputs, setSizeAlert, e) {
return;
}
const file = files[0];
reviewFile(file, maxSize, setSizeAlert, options, setInputs, name, setOriginalImageInput);
}

function reviewFile(file, maxSize, setSizeAlert, options, setInputs, name, setOriginalImageInput) {
if (file == null) return;
if (file.size > maxSize) {
setSizeAlert(`Please select an image that is at most ${formatBytes(maxSize)}`);
Expand All @@ -172,11 +155,12 @@ function onFileInputChange(name, options, maxSize, setInputs, setSizeAlert, e) {
isPersistent: () => false
};
setInputs(artificialEvent);
if (setOriginalImageInput)
setOriginalImageInput(file);
};
if (file)
reader.readAsDataURL(file);
setSizeAlert(null);

}


Expand Down Expand Up @@ -208,9 +192,8 @@ function ImageContentInputMode({ name, modes, mode, setMode, onClick, color }) {
}

function ImageContentInput({ name, value, placeholder, modes, setInputs,
help, maxSize, format, disabled, options, readOnly, color }) {
help, maxSize, format, disabled, options, readOnly, color, sizeAlert, setSizeAlert, setOriginalImageInput }) {
const [mode, setMode] = useState(modes[0]);
const [sizeAlert, setSizeAlert] = useState(null);
const fileInput = useRef(null);

const widgetId = name;
Expand Down Expand Up @@ -249,8 +232,8 @@ function ImageContentInput({ name, value, placeholder, modes, setInputs,
<InputHintLabel text={helpValue} />
<input id={hiddenInputId} name={hiddenInputId} style={{ display: "none" }}
type="file" accept={format}
onChange={(e) => onFileInputChange(name, options, maxSize, setInputs, setSizeAlert, e)}
onDrop={(e) => onFileInputChange(name, options, maxSize, setInputs, setSizeAlert, e)}
onChange={(e) => onFileInputChange(name, options, maxSize, setInputs, setSizeAlert, setOriginalImageInput, e)}
onDrop={(e) => onFileInputChange(name, options, maxSize, setInputs, setSizeAlert, setOriginalImageInput, e)}
ref={fileInput} />
{sizeAlertLabel}
</FormGroup>;
Expand Down Expand Up @@ -291,6 +274,8 @@ function ImageInput(
optional,
submitting,
}) {
const [sizeAlert, setSizeAlert] = useState(null);
const [originalImageInput, setOriginalImageInput] = useState(null);
const options = value.options;
const selectedIndex = value.selected;
const selected = (selectedIndex > -1) ?
Expand All @@ -303,7 +288,9 @@ function ImageInput(
<div className="flex-grow-1">
<ImageContentInput name={name} value={selected}
setInputs={setInputs} help={help} maxSize={maxSize} readOnly={submitting}
disabled={disabled} options={options} modes={allowedModes} format={format} />
disabled={disabled} options={options} modes={allowedModes} format={format}
sizeAlert={sizeAlert} setSizeAlert={setSizeAlert} setOriginalImageInput={setOriginalImageInput}
/>
{alert && <ErrorLabel text={alert} />}
</div>
);
Expand All @@ -319,8 +306,8 @@ function ImageInput(
<div className="pe-2">
<ImagePreview
name={name}
value={value} selected={selected}
disabled={disabled} setInputs={setInputs} />
value={value} selected={selected} maxSize={maxSize} originalImageInput={originalImageInput}
disabled={disabled} setInputs={setInputs} setSizeAlert={setSizeAlert} options={options} />
<InputHintLabel text={previewHelp} />
</div>
</div>
Expand Down
Loading

0 comments on commit d645243

Please sign in to comment.