1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 20:36:40 +07:00

added comment form drop zone

This commit is contained in:
Fedor Katurov 2020-11-09 18:44:36 +07:00
parent 63b9781977
commit 62d4e03206
8 changed files with 142 additions and 93 deletions

View file

@ -20,6 +20,7 @@ import { getRandomPhrase } from '~/constants/phrases';
import { ERROR_LITERAL } from '~/constants/errors'; import { ERROR_LITERAL } from '~/constants/errors';
import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches'; import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches';
import { CommentFormAttachButtons } from '~/components/comment/CommentFormButtons'; import { CommentFormAttachButtons } from '~/components/comment/CommentFormButtons';
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
const mapStateToProps = (state: IState) => ({ const mapStateToProps = (state: IState) => ({
node: selectNode(state), node: selectNode(state),
@ -50,8 +51,12 @@ const CommentFormUnconnected: FC<IProps> = memo(
uploadUploadFiles, uploadUploadFiles,
nodeCancelCommentEdit, nodeCancelCommentEdit,
}) => { }) => {
const comment = useMemo(() => comment_data[id], [comment_data, id]);
const onUpload = useCallback( const onUpload = useCallback(
(files: File[]) => { (files: File[]) => {
console.log(files);
const items: IFileWithUUID[] = files.map( const items: IFileWithUUID[] = files.map(
(file: File): IFileWithUUID => ({ (file: File): IFileWithUUID => ({
file, file,
@ -64,28 +69,25 @@ const CommentFormUnconnected: FC<IProps> = memo(
const temps = items.map(file => file.temp_id); const temps = items.map(file => file.temp_id);
nodeSetCommentData( nodeSetCommentData(id, assocPath(['temp_ids'], [...comment.temp_ids, ...temps], comment));
id,
assocPath(['temp_ids'], [...comment_data[id].temp_ids, ...temps], comment_data[id])
);
uploadUploadFiles(items); uploadUploadFiles(items);
}, },
[uploadUploadFiles, comment_data, id, nodeSetCommentData] [uploadUploadFiles, comment, id, nodeSetCommentData]
); );
const onInput = useCallback<InputHandler>( const onInput = useCallback<InputHandler>(
text => { text => {
nodeSetCommentData(id, assocPath(['text'], text, comment_data[id])); nodeSetCommentData(id, assocPath(['text'], text, comment));
}, },
[nodeSetCommentData, comment_data, id] [nodeSetCommentData, comment, id]
); );
useEffect(() => { useEffect(() => {
const temp_ids = (comment_data && comment_data[id] && comment_data[id].temp_ids) || []; const temp_ids = (comment && comment.temp_ids) || [];
const added_files = temp_ids const added_files = temp_ids
.map(temp_uuid => statuses[temp_uuid] && statuses[temp_uuid].uuid) .map(temp_uuid => statuses[temp_uuid] && statuses[temp_uuid].uuid)
.map(el => !!el && files[el]) .map(el => !!el && files[el])
.filter(el => !!el && !comment_data[id].files.some(file => file && file.id === el.id)); .filter(el => !!el && !comment.files.some(file => file && file.id === el.id));
const filtered_temps = temp_ids.filter( const filtered_temps = temp_ids.filter(
temp_id => temp_id =>
@ -95,25 +97,23 @@ const CommentFormUnconnected: FC<IProps> = memo(
if (added_files.length) { if (added_files.length) {
nodeSetCommentData(id, { nodeSetCommentData(id, {
...comment_data[id], ...comment,
temp_ids: filtered_temps, temp_ids: filtered_temps,
files: [...comment_data[id].files, ...added_files], files: [...comment.files, ...added_files],
}); });
} }
}, [statuses, files]); }, [statuses, files]);
const comment = useMemo(() => comment_data[id], [comment_data, id]); const isUploadingNow = useMemo(() => comment.temp_ids.length > 0, [comment.temp_ids]);
const is_uploading_files = useMemo(() => comment.temp_ids.length > 0, [comment.temp_ids]);
const onSubmit = useCallback( const onSubmit = useCallback(
event => { event => {
if (event) event.preventDefault(); if (event) event.preventDefault();
if (is_uploading_files || is_sending_comment) return; if (isUploadingNow || is_sending_comment) return;
nodePostComment(id, is_before); nodePostComment(id, is_before);
}, },
[nodePostComment, id, is_before, is_uploading_files, is_sending_comment] [nodePostComment, id, is_before, isUploadingNow, is_sending_comment]
); );
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>( const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
@ -172,56 +172,59 @@ const CommentFormUnconnected: FC<IProps> = memo(
); );
return ( return (
<form onSubmit={onSubmit} className={styles.wrap}> <CommentFormDropzone onUpload={onUpload}>
<div className={styles.input}> <form onSubmit={onSubmit} className={styles.wrap}>
<Textarea <div className={styles.input}>
value={comment.text} <Textarea
handler={onInput} value={comment.text}
onKeyDown={onKeyDown} handler={onInput}
disabled={is_sending_comment} onKeyDown={onKeyDown}
placeholder={placeholder} disabled={is_sending_comment}
minRows={2} placeholder={placeholder}
minRows={2}
/>
{comment.error && (
<div className={styles.error} onClick={clearError}>
{ERROR_LITERAL[comment.error] || comment.error}
</div>
)}
</div>
<CommentFormAttaches
images={images}
audios={audios}
locked_audios={locked_audios}
locked_images={locked_images}
comment={comment}
setComment={setData}
onUpload={onUpload}
/> />
{comment.error && ( <Group horizontal className={styles.buttons}>
<div className={styles.error} onClick={clearError}> <CommentFormAttachButtons onUpload={onUpload} />
{ERROR_LITERAL[comment.error] || comment.error}
</div>
)}
</div>
<CommentFormAttaches <Filler />
images={images}
audios={audios}
locked_audios={locked_audios}
locked_images={locked_images}
comment={comment}
setComment={setData}
/>
<Group horizontal className={styles.buttons}> {(is_sending_comment || isUploadingNow) && <LoaderCircle size={20} />}
<CommentFormAttachButtons onUpload={onUpload} />
<Filler /> {id !== 0 && (
<Button size="small" color="link" type="button" onClick={onCancelEdit}>
Отмена
</Button>
)}
{(is_sending_comment || is_uploading_files) && <LoaderCircle size={20} />} <Button
size="small"
{id !== 0 && ( color="gray"
<Button size="small" color="link" type="button" onClick={onCancelEdit}> iconRight={id === 0 ? 'enter' : 'check'}
Отмена disabled={is_sending_comment || isUploadingNow}
>
{id === 0 ? 'Сказать' : 'Сохранить'}
</Button> </Button>
)} </Group>
</form>
<Button </CommentFormDropzone>
size="small"
color="gray"
iconRight={id === 0 ? 'enter' : 'check'}
disabled={is_sending_comment || is_uploading_files}
>
{id === 0 ? 'Сказать' : 'Сохранить'}
</Button>
</Group>
</form>
); );
} }
); );

View file

@ -7,6 +7,8 @@ import { IUploadStatus } from '~/redux/uploads/reducer';
import { SortEnd } from 'react-sortable-hoc'; import { SortEnd } from 'react-sortable-hoc';
import assocPath from 'ramda/es/assocPath'; import assocPath from 'ramda/es/assocPath';
import { moveArrItem } from '~/utils/fn'; import { moveArrItem } from '~/utils/fn';
import { useDropZone } from '~/utils/hooks';
import { COMMENT_FILE_TYPES } from '~/redux/uploads/constants';
interface IProps { interface IProps {
images: IFile[]; images: IFile[];
@ -15,6 +17,7 @@ interface IProps {
locked_audios: IUploadStatus[]; locked_audios: IUploadStatus[];
comment: IComment; comment: IComment;
setComment: (data: IComment) => void; setComment: (data: IComment) => void;
onUpload: (files: File[]) => void;
} }
const CommentFormAttaches: FC<IProps> = ({ const CommentFormAttaches: FC<IProps> = ({
@ -24,7 +27,10 @@ const CommentFormAttaches: FC<IProps> = ({
locked_audios, locked_audios,
comment, comment,
setComment, setComment,
onUpload,
}) => { }) => {
const onDrop = useDropZone(onUpload, COMMENT_FILE_TYPES);
const hasImageAttaches = images.length > 0 || locked_images.length > 0; const hasImageAttaches = images.length > 0 || locked_images.length > 0;
const hasAudioAttaches = audios.length > 0 || locked_audios.length > 0; const hasAudioAttaches = audios.length > 0 || locked_audios.length > 0;
const hasAttaches = hasImageAttaches || hasAudioAttaches; const hasAttaches = hasImageAttaches || hasAudioAttaches;
@ -99,7 +105,7 @@ const CommentFormAttaches: FC<IProps> = ({
return ( return (
hasAttaches && ( hasAttaches && (
<div className={styles.attaches}> <div className={styles.attaches} onDropCapture={onDrop}>
{hasImageAttaches && ( {hasImageAttaches && (
<SortableImageGrid <SortableImageGrid
onDelete={onFileDelete} onDelete={onFileDelete}

View file

@ -1,21 +1,19 @@
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback } from 'react';
import { ButtonGroup } from '~/components/input/ButtonGroup'; import { ButtonGroup } from '~/components/input/ButtonGroup';
import { Button } from '~/components/input/Button'; import { Button } from '~/components/input/Button';
import { FILE_MIMES, UPLOAD_TYPES } from '~/redux/uploads/constants'; import { COMMENT_FILE_TYPES } from '~/redux/uploads/constants';
interface IProps { interface IProps {
onUpload: (files: File[]) => void; onUpload: (files: File[]) => void;
} }
const ALLOWED_TYPES = [...FILE_MIMES[UPLOAD_TYPES.IMAGE], ...FILE_MIMES[UPLOAD_TYPES.AUDIO]];
const CommentFormAttachButtons: FC<IProps> = ({ onUpload }) => { const CommentFormAttachButtons: FC<IProps> = ({ onUpload }) => {
const onInputChange = useCallback( const onInputChange = useCallback(
event => { event => {
event.preventDefault(); event.preventDefault();
const files = Array.from(event.target?.files as File[]).filter((file: File) => const files = Array.from(event.target?.files as File[]).filter((file: File) =>
ALLOWED_TYPES.includes(file.type) COMMENT_FILE_TYPES.includes(file.type)
); );
if (!files || !files.length) return; if (!files || !files.length) return;

View file

@ -0,0 +1,14 @@
import React, { FC } from 'react';
import { COMMENT_FILE_TYPES } from '~/redux/uploads/constants';
import { useDropZone } from '~/utils/hooks';
interface IProps {
onUpload: (files: File[]) => void;
}
const CommentFormDropzone: FC<IProps> = ({ children, onUpload }) => {
const onDrop = useDropZone(onUpload, COMMENT_FILE_TYPES);
return <div onDropCapture={onDrop}>{children}</div>;
};
export { CommentFormDropzone };

View file

@ -19,8 +19,6 @@ const SortableAudioGrid = SortableContainer(
onDelete: (file_id: IFile['id']) => void; onDelete: (file_id: IFile['id']) => void;
onTitleChange: (file_id: IFile['id'], title: IFile['metadata']['title']) => void; onTitleChange: (file_id: IFile['id'], title: IFile['metadata']['title']) => void;
}) => { }) => {
console.log(locked);
return ( return (
<div className={styles.grid}> <div className={styles.grid}>
{items {items

View file

@ -1,4 +1,4 @@
import React from 'react'; import React, { useCallback } from 'react';
import { SortableContainer } from 'react-sortable-hoc'; import { SortableContainer } from 'react-sortable-hoc';
import { ImageUpload } from '~/components/upload/ImageUpload'; import { ImageUpload } from '~/components/upload/ImageUpload';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
@ -19,26 +19,31 @@ const SortableImageGrid = SortableContainer(
locked: IUploadStatus[]; locked: IUploadStatus[];
onDelete: (file_id: IFile['id']) => void; onDelete: (file_id: IFile['id']) => void;
size?: number; size?: number;
}) => ( }) => {
<div const preventEvent = useCallback(event => event.preventDefault(), []);
className={styles.grid}
style={{ gridTemplateColumns: `repeat(auto-fill, minmax(${size}px, 1fr))` }} return (
> <div
{items className={styles.grid}
.filter(file => file && file.id) style={{ gridTemplateColumns: `repeat(auto-fill, minmax(${size}px, 1fr))` }}
.map((file, index) => ( onDropCapture={preventEvent}
<SortableImageGridItem key={file.id} index={index} collection={0}> >
<ImageUpload id={file.id} thumb={getURL(file, PRESETS.cover)} onDrop={onDelete} /> {items
.filter(file => file && file.id)
.map((file, index) => (
<SortableImageGridItem key={file.id} index={index} collection={0}>
<ImageUpload id={file.id} thumb={getURL(file, PRESETS.cover)} onDrop={onDelete} />
</SortableImageGridItem>
))}
{locked.map((item, index) => (
<SortableImageGridItem key={item.temp_id} index={index} collection={1} disabled>
<ImageUpload thumb={item.preview} progress={item.progress} is_uploading />
</SortableImageGridItem> </SortableImageGridItem>
))} ))}
</div>
{locked.map((item, index) => ( );
<SortableImageGridItem key={item.temp_id} index={index} collection={1} disabled> }
<ImageUpload thumb={item.preview} progress={item.progress} is_uploading />
</SortableImageGridItem>
))}
</div>
)
); );
export { SortableImageGrid }; export { SortableImageGrid };

View file

@ -69,3 +69,8 @@ export const FILE_MIMES = {
[UPLOAD_TYPES.AUDIO]: ['audio/mpeg3', 'audio/mpeg', 'audio/mp3'], [UPLOAD_TYPES.AUDIO]: ['audio/mpeg3', 'audio/mpeg', 'audio/mp3'],
[UPLOAD_TYPES.OTHER]: [], [UPLOAD_TYPES.OTHER]: [],
}; };
export const COMMENT_FILE_TYPES = [
...FILE_MIMES[UPLOAD_TYPES.IMAGE],
...FILE_MIMES[UPLOAD_TYPES.AUDIO],
];

View file

@ -1,19 +1,18 @@
import { import { useCallback, useEffect } from 'react';
useCallback, useEffect, useRef, useState
} from 'react';
export const useCloseOnEscape = (onRequestClose: () => void, ignore_inputs = false) => { export const useCloseOnEscape = (onRequestClose: () => void, ignore_inputs = false) => {
const onEscape = useCallback( const onEscape = useCallback(
(event) => { event => {
if (event.key !== 'Escape') return; if (event.key !== 'Escape') return;
if ( if (
ignore_inputs ignore_inputs &&
&& (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA')
) return; )
return;
onRequestClose(); onRequestClose();
}, },
[ignore_inputs, onRequestClose], [ignore_inputs, onRequestClose]
); );
useEffect(() => { useEffect(() => {
@ -34,3 +33,24 @@ export const useDelayedReady = (setReady: (val: boolean) => void, delay: number
}; };
}, [delay, setReady]); }, [delay, setReady]);
}; };
/**
* useDropZone returns onDrop handler to upload files
* @param onUpload -- upload callback
* @param allowedTypes -- list of allowed types
*/
export const useDropZone = (onUpload: (file: File[]) => void, allowedTypes?: string[]) => {
return useCallback(
event => {
event.preventDefault();
const files: File[] = Array.from((event.dataTransfer?.files as File[]) || []).filter(
(file: File) => file?.type && (!allowedTypes || allowedTypes.includes(file.type))
);
if (!files || !files.length) return;
onUpload(files);
},
[onUpload]
);
};