diff --git a/client/.flowconfig b/client/.flowconfig new file mode 100644 index 000000000..1fed44533 --- /dev/null +++ b/client/.flowconfig @@ -0,0 +1,11 @@ +[ignore] + +[include] + +[libs] + +[lints] + +[options] + +[strict] diff --git a/client/package.json b/client/package.json index f5dd2ccb1..9de51b5ae 100644 --- a/client/package.json +++ b/client/package.json @@ -40,6 +40,7 @@ "eslint-plugin-react": "7.18.0", "eslint-plugin-react-hooks": "^1.6.1", "file-loader": "4.3.0", + "flow-bin": "^0.123.0", "formik": "^2.1.4", "fs-extra": "^8.1.0", "html-webpack-plugin": "4.0.0-beta.11", @@ -54,6 +55,7 @@ "moment": "^2.24.0", "node-sass": "^4.13.1", "optimize-css-assets-webpack-plugin": "5.0.3", + "p-progress": "^0.4.2", "pnp-webpack-plugin": "1.6.0", "postcss-flexbugs-fixes": "4.1.0", "postcss-loader": "3.0.0", @@ -65,6 +67,7 @@ "react-body-classname": "^1.3.1", "react-dev-utils": "^10.2.0", "react-dom": "^16.12.0", + "react-dropzone": "^11.0.1", "react-grid-system": "^6.2.3", "react-hook-form": "^4.9.4", "react-intl": "^3.12.0", @@ -97,7 +100,8 @@ "scripts": { "start": "PORT=8000 node scripts/start.js", "build": "node scripts/build.js", - "test": "node scripts/test.js" + "test": "node scripts/test.js", + "flow": "flow" }, "eslintConfig": { "extends": "react-app" @@ -115,6 +119,7 @@ ] }, "devDependencies": { + "@babel/preset-flow": "^7.9.0", "http-proxy-middleware": "^1.0.0", "redux-devtools": "^3.5.0" }, diff --git a/client/src/components/Dragzone.js b/client/src/components/Dragzone.js new file mode 100644 index 000000000..d5331ef5f --- /dev/null +++ b/client/src/components/Dragzone.js @@ -0,0 +1,77 @@ +import React, { useState, useMemo, useCallback, useEffect } from 'react' +import { useDropzone } from 'react-dropzone' +import { pullAt } from 'lodash'; +import classNames from 'classnames'; +import Icon from 'components/Icon'; + +// const initialFile: { +// file: ?File, +// preview: string, +// metadata: ?object, +// uploaded: boolean, +// }; + +export default function Dropzone({ + text = 'Drag/Drop files here or click here', + onDrop, + initialFiles = [], + onDeleteFile, + hint, + className, +}) { + const [files, setFiles] = useState([]); + + useEffect(() => { + setFiles([ ...initialFiles ]); + }, [initialFiles]); + + const {getRootProps, getInputProps} = useDropzone({ + accept: 'image/*', + onDrop: (acceptedFiles) => { + const _files = acceptedFiles.map((file) => ({ + file, + preview: URL.createObjectURL(file), + uploaded: false, + })); + setFiles(_files); + } + }); + + const handleRemove = useCallback((index) => { + const deletedFile = files.splice(index, 1); + setFiles([...files]); + onDeleteFile && onDeleteFile(deletedFile); + }, [files, onDeleteFile]); + + const thumbs = files.map((file, index) => ( +
+
+ +
+ )); + + useEffect(() => () => { + files.forEach(file => URL.revokeObjectURL(file.preview)); + }, [files, onDrop]); + + useEffect(() => { + onDrop && onDrop(files); + }, [files, onDrop]); + + return ( +
+ {(hint) &&
{ hint }
} + +
+ +

{ text }

+
+ +
+ { thumbs } +
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/Items/ItemForm.js b/client/src/components/Items/ItemForm.js index f9fa1ec46..38b109450 100644 --- a/client/src/components/Items/ItemForm.js +++ b/client/src/components/Items/ItemForm.js @@ -23,15 +23,34 @@ import Icon from 'components/Icon'; import ItemCategoryConnect from 'connectors/ItemsCategory.connect'; import MoneyInputGroup from 'components/MoneyInputGroup'; import {useHistory} from 'react-router-dom'; +import Dragzone from 'components/Dragzone'; +import MediaConnect from 'connectors/Media.connect'; +import useMedia from 'hooks/useMedia'; const ItemForm = ({ requestSubmitItem, + accounts, categories, + + requestSubmitMedia, + requestDeleteMedia, }) => { const [selectedAccounts, setSelectedAccounts] = useState({}); const history = useHistory(); + const { + files, + setFiles, + saveMedia, + deletedFiles, + setDeletedFiles, + deleteMedia, + } = useMedia({ + saveCallback: requestSubmitMedia, + deleteCallback: requestDeleteMedia, + }) + const ItemTypeDisplay = useMemo(() => ([ { value: null, label: 'Select Item Type' }, { value: 'service', label: 'Service' }, @@ -85,15 +104,27 @@ const ItemForm = ({ ...initialValues, }, onSubmit: (values, { setSubmitting }) => { - requestSubmitItem(values).then((response) => { - AppToaster.show({ - message: 'The_Items_has_been_Submit' + const saveItem = (mediaIds) => { + const formValues = { ...values, media_ids: mediaIds }; + + return requestSubmitItem(formValues).then((response) => { + AppToaster.show({ + message: 'The_Items_has_been_submit' + }); + setSubmitting(false); + history.push('/dashboard/items'); + }) + .catch((error) => { + setSubmitting(false); }); - setSubmitting(false); - history.push('/dashboard/items'); - }) - .catch((error) => { - setSubmitting(false); + }; + + Promise.all([ + saveMedia(), + deleteMedia(), + ]).then(([savedMediaResponses]) => { + const mediaIds = savedMediaResponses.map(res => res.data.media.id); + return saveItem(mediaIds); }); } }); @@ -139,95 +170,126 @@ const ItemForm = ({ setFieldValue(fieldKey, value); }; + const initialAttachmentFiles = useMemo(() => { + return []; + }, []); + + const handleDropFiles = useCallback((_files) => { + setFiles(_files.filter((file) => file.uploaded === false)); + }, []); + + const handleDeleteFile = useCallback((_deletedFiles) => { + _deletedFiles.forEach((deletedFile) => { + if (deletedFile.uploaded && deletedFile.metadata.id) { + setDeletedFiles([ + ...deletedFiles, deletedFile.metadata.id, + ]); + } + }); + }, [setDeletedFiles, deletedFiles]); + return (
- } - inline={true} - > - - + + + } + inline={true} + > + + - } - inline={true} - > - - + } + inline={true} + > + + - } - inline={true} - > - - + } + inline={true} + > + + - } - className={classNames( - 'form-group--select-list', - 'form-group--category', - Classes.FILL, - )} - > - - + } + className={classNames( + 'form-group--select-list', + 'form-group--category', + Classes.FILL, + )} + > + + - - - + + + + + + + + +
@@ -258,8 +320,7 @@ const ItemForm = ({ intent={(errors.sell_account_id && touched.sell_account_id) && Intent.DANGER} helperText={} className={classNames( - 'form-group--sell-account', - 'form-group--select-list', + 'form-group--sell-account', 'form-group--select-list', Classes.FILL)} >