mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 20:59:44 +02:00
Add dropzone for attachment, paste attachment from clipboard
This commit is contained in:
parent
693602698b
commit
d264382fda
24 changed files with 576 additions and 253 deletions
23
client/package-lock.json
generated
23
client/package-lock.json
generated
|
@ -2198,6 +2198,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
|
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
|
||||||
},
|
},
|
||||||
|
"attr-accept": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-sLzVM3zCCmmDtDNhI0i96k6PUztkotSOXqE4kDGQt/6iDi5M+H0srjeF+QC6jN581l4X/Zq3Zu/tgcErEssavg=="
|
||||||
|
},
|
||||||
"autoprefixer": {
|
"autoprefixer": {
|
||||||
"version": "9.7.5",
|
"version": "9.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.5.tgz",
|
||||||
|
@ -5566,6 +5571,14 @@
|
||||||
"schema-utils": "^2.5.0"
|
"schema-utils": "^2.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"file-selector": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^1.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"file-uri-to-path": {
|
"file-uri-to-path": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
@ -11207,6 +11220,16 @@
|
||||||
"scheduler": "^0.19.1"
|
"scheduler": "^0.19.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-dropzone": {
|
||||||
|
"version": "11.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.0.1.tgz",
|
||||||
|
"integrity": "sha512-x/6wqRHaR8jsrNiu/boVMIPYuoxb83Vyfv77hO7/3ZRn8Pr+KH5onsCsB8MLBa3zdJl410C5FXPUINbu16XIzw==",
|
||||||
|
"requires": {
|
||||||
|
"attr-accept": "^2.0.0",
|
||||||
|
"file-selector": "^0.1.12",
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-error-overlay": {
|
"react-error-overlay": {
|
||||||
"version": "6.0.7",
|
"version": "6.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
"react-beautiful-dnd": "^13.0.0",
|
"react-beautiful-dnd": "^13.0.0",
|
||||||
"react-datepicker": "^2.14.1",
|
"react-datepicker": "^2.14.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
|
"react-dropzone": "^11.0.1",
|
||||||
"react-i18next": "^11.3.4",
|
"react-i18next": "^11.3.4",
|
||||||
"react-input-mask": "^2.0.4",
|
"react-input-mask": "^2.0.4",
|
||||||
"react-markdown": "^4.3.1",
|
"react-markdown": "^4.3.1",
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 103 KiB |
BIN
client/public/logo192.png
Normal file → Executable file
BIN
client/public/logo192.png
Normal file → Executable file
Binary file not shown.
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.6 KiB |
BIN
client/public/logo512.png
Normal file → Executable file
BIN
client/public/logo512.png
Normal file → Executable file
Binary file not shown.
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.2 KiB |
23
client/src/components/CardModal/AddAttachment.jsx
Normal file
23
client/src/components/CardModal/AddAttachment.jsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { FilePicker } from '../../lib/custom-ui';
|
||||||
|
|
||||||
|
const AddAttachment = React.memo(({ children, onCreate }) => {
|
||||||
|
const handleFileSelect = useCallback(
|
||||||
|
(file) => {
|
||||||
|
onCreate({
|
||||||
|
file,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onCreate],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <FilePicker onSelect={handleFileSelect}>{children}</FilePicker>;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAttachment.propTypes = {
|
||||||
|
children: PropTypes.element.isRequired,
|
||||||
|
onCreate: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddAttachment;
|
|
@ -0,0 +1,108 @@
|
||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { closePopup } from '../../../lib/popup';
|
||||||
|
|
||||||
|
import { useModal } from '../../../hooks';
|
||||||
|
import AddTextFileModal from './AddTextFileModal';
|
||||||
|
|
||||||
|
import styles from './AddAttachmentZone.module.css';
|
||||||
|
|
||||||
|
const AddAttachmentZone = React.memo(({ children, onCreate }) => {
|
||||||
|
const [t] = useTranslation();
|
||||||
|
const [modal, openModal, handleModalClose] = useModal();
|
||||||
|
|
||||||
|
const submit = useCallback(
|
||||||
|
(file) => {
|
||||||
|
onCreate({
|
||||||
|
file,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onCreate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDropAccepted = useCallback(
|
||||||
|
(files) => {
|
||||||
|
submit(files[0]);
|
||||||
|
},
|
||||||
|
[submit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
|
multiple: false,
|
||||||
|
noClick: true,
|
||||||
|
noKeyboard: true,
|
||||||
|
onDropAccepted: handleDropAccepted,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFileCreate = useCallback(
|
||||||
|
(file) => {
|
||||||
|
submit(file);
|
||||||
|
},
|
||||||
|
[submit],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePaste = (event) => {
|
||||||
|
const item = event.clipboardData && event.clipboardData.items[0];
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.kind === 'file') {
|
||||||
|
submit(item.getAsFile());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
['input', 'textarea'].includes(event.target.tagName.toLowerCase()) &&
|
||||||
|
event.target === document.activeElement
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
closePopup();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
item.getAsString((content) => {
|
||||||
|
openModal({
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('paste', handlePaste);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('paste', handlePaste);
|
||||||
|
};
|
||||||
|
}, [openModal, submit]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||||
|
<div {...getRootProps()}>
|
||||||
|
{isDragActive && <div className={styles.dropzone}>{t('common.dropFileToUpload')}</div>}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{modal && (
|
||||||
|
<AddTextFileModal
|
||||||
|
content={modal.content}
|
||||||
|
onCreate={handleFileCreate}
|
||||||
|
onClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAttachmentZone.propTypes = {
|
||||||
|
children: PropTypes.element.isRequired,
|
||||||
|
onCreate: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddAttachmentZone;
|
|
@ -0,0 +1,13 @@
|
||||||
|
.dropzone {
|
||||||
|
background: white;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 30px;
|
||||||
|
opacity: 0.7;
|
||||||
|
padding: 200px 50px;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button, Form, Header, Modal } from 'semantic-ui-react';
|
||||||
|
import { Input } from '../../../lib/custom-ui';
|
||||||
|
|
||||||
|
import { useForm } from '../../../hooks';
|
||||||
|
|
||||||
|
import styles from './AddTextFileModal.module.css';
|
||||||
|
|
||||||
|
const AddTextFileModal = React.memo(({ content, onCreate, onClose }) => {
|
||||||
|
const [t] = useTranslation();
|
||||||
|
|
||||||
|
const [data, handleFieldChange] = useForm(() => ({
|
||||||
|
name: '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const nameField = useRef(null);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
const cleanData = {
|
||||||
|
...data,
|
||||||
|
name: data.name.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!cleanData.name) {
|
||||||
|
nameField.current.select();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = new File([content], `${cleanData.name}.txt`, {
|
||||||
|
type: 'plain/text',
|
||||||
|
});
|
||||||
|
|
||||||
|
onCreate(file);
|
||||||
|
onClose();
|
||||||
|
}, [content, onCreate, onClose, data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
nameField.current.select();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open basic centered closeIcon size="tiny" onClose={onClose}>
|
||||||
|
<Modal.Content>
|
||||||
|
<Header inverted size="huge">
|
||||||
|
{t('common.createTextFile', {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</Header>
|
||||||
|
<p>{t('common.enterFilename')}</p>
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<Input
|
||||||
|
fluid
|
||||||
|
inverted
|
||||||
|
ref={nameField}
|
||||||
|
name="name"
|
||||||
|
value={data.name}
|
||||||
|
label=".txt"
|
||||||
|
labelPosition="right"
|
||||||
|
className={styles.field}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
inverted
|
||||||
|
color="green"
|
||||||
|
icon="checkmark"
|
||||||
|
content={t('action.createFile')}
|
||||||
|
floated="right"
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddTextFileModal.propTypes = {
|
||||||
|
content: PropTypes.string.isRequired,
|
||||||
|
onCreate: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddTextFileModal;
|
|
@ -0,0 +1,3 @@
|
||||||
|
.field {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import AddAttachmentZone from './AddAttachmentZone';
|
||||||
|
|
||||||
|
export default AddAttachmentZone;
|
|
@ -38,8 +38,11 @@
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
line-height: 80px;
|
line-height: 80px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 20px 0 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,14 @@ import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Grid, Icon, Modal } from 'semantic-ui-react';
|
import { Button, Grid, Icon, Modal } from 'semantic-ui-react';
|
||||||
import { FilePicker, Markdown } from '../../lib/custom-ui';
|
import { Markdown } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import NameField from './NameField';
|
import NameField from './NameField';
|
||||||
import EditDescription from './EditDescription';
|
import EditDescription from './EditDescription';
|
||||||
import Tasks from './Tasks';
|
import Tasks from './Tasks';
|
||||||
import Attachments from './Attachments';
|
import Attachments from './Attachments';
|
||||||
|
import AddAttachment from './AddAttachment';
|
||||||
|
import AddAttachmentZone from './AddAttachmentZone';
|
||||||
import Actions from './Actions';
|
import Actions from './Actions';
|
||||||
import User from '../User';
|
import User from '../User';
|
||||||
import Label from '../Label';
|
import Label from '../Label';
|
||||||
|
@ -107,15 +109,6 @@ const CardModal = React.memo(
|
||||||
[onUpdate],
|
[onUpdate],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAttachmentFileSelect = useCallback(
|
|
||||||
(file) => {
|
|
||||||
onAttachmentCreate({
|
|
||||||
file,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[onAttachmentCreate],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleToggleSubscribeClick = useCallback(() => {
|
const handleToggleSubscribeClick = useCallback(() => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
isSubscribed: !isSubscribed,
|
isSubscribed: !isSubscribed,
|
||||||
|
@ -127,252 +120,258 @@ const CardModal = React.memo(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open closeIcon size="small" centered={false} onClose={onClose}>
|
<Modal open closeIcon size="small" centered={false} onClose={onClose}>
|
||||||
<Grid className={styles.grid}>
|
<AddAttachmentZone onCreate={onAttachmentCreate}>
|
||||||
<Grid.Row className={styles.headerPadding}>
|
<Grid className={styles.grid}>
|
||||||
<Grid.Column width={16} className={styles.headerPadding}>
|
<Grid.Row className={styles.headerPadding}>
|
||||||
<div className={styles.headerWrapper}>
|
<Grid.Column width={16} className={styles.headerPadding}>
|
||||||
<Icon name="list alternate outline" className={styles.moduleIcon} />
|
<div className={styles.headerWrapper}>
|
||||||
<div className={styles.headerTitle}>
|
<Icon name="list alternate outline" className={styles.moduleIcon} />
|
||||||
<NameField defaultValue={name} onUpdate={handleNameUpdate} />
|
<div className={styles.headerTitle}>
|
||||||
|
<NameField defaultValue={name} onUpdate={handleNameUpdate} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Grid.Column>
|
||||||
</Grid.Column>
|
</Grid.Row>
|
||||||
</Grid.Row>
|
<Grid.Row className={styles.modalPadding}>
|
||||||
<Grid.Row className={styles.modalPadding}>
|
<Grid.Column width={12} className={styles.contentPadding}>
|
||||||
<Grid.Column width={12} className={styles.contentPadding}>
|
{(users.length > 0 || labels.length > 0 || dueDate || timer) && (
|
||||||
{(users.length > 0 || labels.length > 0 || dueDate || timer) && (
|
<div className={styles.moduleWrapper}>
|
||||||
<div className={styles.moduleWrapper}>
|
{users.length > 0 && (
|
||||||
{users.length > 0 && (
|
<div className={styles.attachments}>
|
||||||
<div className={styles.attachments}>
|
<div className={styles.text}>
|
||||||
<div className={styles.text}>
|
{t('common.members', {
|
||||||
{t('common.members', {
|
context: 'title',
|
||||||
context: 'title',
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
{users.map((user) => (
|
||||||
{users.map((user) => (
|
<span key={user.id} className={styles.attachment}>
|
||||||
<span key={user.id} className={styles.attachment}>
|
<ProjectMembershipsPopup
|
||||||
<ProjectMembershipsPopup
|
items={allProjectMemberships}
|
||||||
items={allProjectMemberships}
|
currentUserIds={userIds}
|
||||||
currentUserIds={userIds}
|
onUserSelect={onUserAdd}
|
||||||
onUserSelect={onUserAdd}
|
onUserDeselect={onUserRemove}
|
||||||
onUserDeselect={onUserRemove}
|
>
|
||||||
>
|
<User name={user.name} avatarUrl={user.avatarUrl} />
|
||||||
<User name={user.name} avatarUrl={user.avatarUrl} />
|
</ProjectMembershipsPopup>
|
||||||
</ProjectMembershipsPopup>
|
</span>
|
||||||
</span>
|
))}
|
||||||
))}
|
<ProjectMembershipsPopup
|
||||||
<ProjectMembershipsPopup
|
items={allProjectMemberships}
|
||||||
items={allProjectMemberships}
|
currentUserIds={userIds}
|
||||||
currentUserIds={userIds}
|
onUserSelect={onUserAdd}
|
||||||
onUserSelect={onUserAdd}
|
onUserDeselect={onUserRemove}
|
||||||
onUserDeselect={onUserRemove}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={classNames(styles.attachment, styles.dueDate)}
|
|
||||||
>
|
>
|
||||||
<Icon name="add" size="small" className={styles.addAttachment} />
|
<button
|
||||||
</button>
|
type="button"
|
||||||
</ProjectMembershipsPopup>
|
className={classNames(styles.attachment, styles.dueDate)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{labels.length > 0 && (
|
|
||||||
<div className={styles.attachments}>
|
|
||||||
<div className={styles.text}>
|
|
||||||
{t('common.labels', {
|
|
||||||
context: 'title',
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{labels.map((label) => (
|
|
||||||
<span key={label.id} className={styles.attachment}>
|
|
||||||
<LabelsPopup
|
|
||||||
key={label.id}
|
|
||||||
items={allLabels}
|
|
||||||
currentIds={labelIds}
|
|
||||||
onSelect={onLabelAdd}
|
|
||||||
onDeselect={onLabelRemove}
|
|
||||||
onCreate={onLabelCreate}
|
|
||||||
onUpdate={onLabelUpdate}
|
|
||||||
onDelete={onLabelDelete}
|
|
||||||
>
|
>
|
||||||
<Label name={label.name} color={label.color} />
|
<Icon name="add" size="small" className={styles.addAttachment} />
|
||||||
</LabelsPopup>
|
</button>
|
||||||
</span>
|
</ProjectMembershipsPopup>
|
||||||
))}
|
|
||||||
<LabelsPopup
|
|
||||||
items={allLabels}
|
|
||||||
currentIds={labelIds}
|
|
||||||
onSelect={onLabelAdd}
|
|
||||||
onDeselect={onLabelRemove}
|
|
||||||
onCreate={onLabelCreate}
|
|
||||||
onUpdate={onLabelUpdate}
|
|
||||||
onDelete={onLabelDelete}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={classNames(styles.attachment, styles.dueDate)}
|
|
||||||
>
|
|
||||||
<Icon name="add" size="small" className={styles.addAttachment} />
|
|
||||||
</button>
|
|
||||||
</LabelsPopup>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{dueDate && (
|
|
||||||
<div className={styles.attachments}>
|
|
||||||
<div className={styles.text}>
|
|
||||||
{t('common.dueDate', {
|
|
||||||
context: 'title',
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.attachment}>
|
|
||||||
<EditDueDatePopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}>
|
|
||||||
<DueDate value={dueDate} />
|
|
||||||
</EditDueDatePopup>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{timer && (
|
|
||||||
<div className={styles.attachments}>
|
|
||||||
<div className={styles.text}>
|
|
||||||
{t('common.timer', {
|
|
||||||
context: 'title',
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<span className={styles.attachment}>
|
|
||||||
<EditTimerPopup defaultValue={timer} onUpdate={handleTimerUpdate}>
|
|
||||||
<Timer startedAt={timer.startedAt} total={timer.total} />
|
|
||||||
</EditTimerPopup>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.contentModule}>
|
|
||||||
<div className={styles.moduleWrapper}>
|
|
||||||
<Icon name="align justify" className={styles.moduleIcon} />
|
|
||||||
<div className={styles.moduleHeader}>{t('common.description')}</div>
|
|
||||||
<EditDescription defaultValue={description} onUpdate={handleDescriptionUpdate}>
|
|
||||||
{description ? (
|
|
||||||
<button type="button" className={styles.descriptionText}>
|
|
||||||
<Markdown linkStopPropagation source={description} linkTarget="_blank" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button type="button" className={styles.descriptionButton}>
|
|
||||||
<span className={styles.descriptionButtonText}>
|
|
||||||
{t('action.addMoreDetailedDescription')}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</EditDescription>
|
{labels.length > 0 && (
|
||||||
</div>
|
<div className={styles.attachments}>
|
||||||
</div>
|
<div className={styles.text}>
|
||||||
<div className={styles.contentModule}>
|
{t('common.labels', {
|
||||||
<div className={styles.moduleWrapper}>
|
context: 'title',
|
||||||
<Icon name="check square outline" className={styles.moduleIcon} />
|
})}
|
||||||
<div className={styles.moduleHeader}>{t('common.tasks')}</div>
|
</div>
|
||||||
<Tasks
|
{labels.map((label) => (
|
||||||
items={tasks}
|
<span key={label.id} className={styles.attachment}>
|
||||||
onCreate={onTaskCreate}
|
<LabelsPopup
|
||||||
onUpdate={onTaskUpdate}
|
key={label.id}
|
||||||
onDelete={onTaskDelete}
|
items={allLabels}
|
||||||
/>
|
currentIds={labelIds}
|
||||||
</div>
|
onSelect={onLabelAdd}
|
||||||
</div>
|
onDeselect={onLabelRemove}
|
||||||
{attachments.length > 0 && (
|
onCreate={onLabelCreate}
|
||||||
|
onUpdate={onLabelUpdate}
|
||||||
|
onDelete={onLabelDelete}
|
||||||
|
>
|
||||||
|
<Label name={label.name} color={label.color} />
|
||||||
|
</LabelsPopup>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
<LabelsPopup
|
||||||
|
items={allLabels}
|
||||||
|
currentIds={labelIds}
|
||||||
|
onSelect={onLabelAdd}
|
||||||
|
onDeselect={onLabelRemove}
|
||||||
|
onCreate={onLabelCreate}
|
||||||
|
onUpdate={onLabelUpdate}
|
||||||
|
onDelete={onLabelDelete}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames(styles.attachment, styles.dueDate)}
|
||||||
|
>
|
||||||
|
<Icon name="add" size="small" className={styles.addAttachment} />
|
||||||
|
</button>
|
||||||
|
</LabelsPopup>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{dueDate && (
|
||||||
|
<div className={styles.attachments}>
|
||||||
|
<div className={styles.text}>
|
||||||
|
{t('common.dueDate', {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<span className={styles.attachment}>
|
||||||
|
<EditDueDatePopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}>
|
||||||
|
<DueDate value={dueDate} />
|
||||||
|
</EditDueDatePopup>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{timer && (
|
||||||
|
<div className={styles.attachments}>
|
||||||
|
<div className={styles.text}>
|
||||||
|
{t('common.timer', {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<span className={styles.attachment}>
|
||||||
|
<EditTimerPopup defaultValue={timer} onUpdate={handleTimerUpdate}>
|
||||||
|
<Timer startedAt={timer.startedAt} total={timer.total} />
|
||||||
|
</EditTimerPopup>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className={styles.contentModule}>
|
<div className={styles.contentModule}>
|
||||||
<div className={styles.moduleWrapper}>
|
<div className={styles.moduleWrapper}>
|
||||||
<Icon name="attach" className={styles.moduleIcon} />
|
<Icon name="align justify" className={styles.moduleIcon} />
|
||||||
<div className={styles.moduleHeader}>{t('common.attachments')}</div>
|
<div className={styles.moduleHeader}>{t('common.description')}</div>
|
||||||
<Attachments
|
<EditDescription defaultValue={description} onUpdate={handleDescriptionUpdate}>
|
||||||
items={attachments}
|
{description ? (
|
||||||
onUpdate={onAttachmentUpdate}
|
<button type="button" className={styles.descriptionText}>
|
||||||
onDelete={onAttachmentDelete}
|
<Markdown linkStopPropagation source={description} linkTarget="_blank" />
|
||||||
onCoverUpdate={handleCoverUpdate}
|
</button>
|
||||||
|
) : (
|
||||||
|
<button type="button" className={styles.descriptionButton}>
|
||||||
|
<span className={styles.descriptionButtonText}>
|
||||||
|
{t('action.addMoreDetailedDescription')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</EditDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.contentModule}>
|
||||||
|
<div className={styles.moduleWrapper}>
|
||||||
|
<Icon name="check square outline" className={styles.moduleIcon} />
|
||||||
|
<div className={styles.moduleHeader}>{t('common.tasks')}</div>
|
||||||
|
<Tasks
|
||||||
|
items={tasks}
|
||||||
|
onCreate={onTaskCreate}
|
||||||
|
onUpdate={onTaskUpdate}
|
||||||
|
onDelete={onTaskDelete}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{attachments.length > 0 && (
|
||||||
<Actions
|
<div className={styles.contentModule}>
|
||||||
items={actions}
|
<div className={styles.moduleWrapper}>
|
||||||
isFetching={isActionsFetching}
|
<Icon name="attach" className={styles.moduleIcon} />
|
||||||
isAllFetched={isAllActionsFetched}
|
<div className={styles.moduleHeader}>{t('common.attachments')}</div>
|
||||||
isEditable={isEditable}
|
<Attachments
|
||||||
onFetch={onActionsFetch}
|
items={attachments}
|
||||||
onCommentCreate={onCommentActionCreate}
|
onUpdate={onAttachmentUpdate}
|
||||||
onCommentUpdate={onCommentActionUpdate}
|
onDelete={onAttachmentDelete}
|
||||||
onCommentDelete={onCommentActionDelete}
|
onCoverUpdate={handleCoverUpdate}
|
||||||
/>
|
/>
|
||||||
</Grid.Column>
|
</div>
|
||||||
<Grid.Column width={4} className={styles.sidebarPadding}>
|
</div>
|
||||||
<div className={styles.actions}>
|
)}
|
||||||
<span className={styles.actionsTitle}>{t('action.addToCard')}</span>
|
<Actions
|
||||||
<ProjectMembershipsPopup
|
items={actions}
|
||||||
items={allProjectMemberships}
|
isFetching={isActionsFetching}
|
||||||
currentUserIds={userIds}
|
isAllFetched={isAllActionsFetched}
|
||||||
onUserSelect={onUserAdd}
|
isEditable={isEditable}
|
||||||
onUserDeselect={onUserRemove}
|
onFetch={onActionsFetch}
|
||||||
>
|
onCommentCreate={onCommentActionCreate}
|
||||||
<Button fluid className={styles.actionButton}>
|
onCommentUpdate={onCommentActionUpdate}
|
||||||
<Icon name="user outline" className={styles.actionIcon} />
|
onCommentDelete={onCommentActionDelete}
|
||||||
{t('common.members')}
|
/>
|
||||||
|
</Grid.Column>
|
||||||
|
<Grid.Column width={4} className={styles.sidebarPadding}>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<span className={styles.actionsTitle}>{t('action.addToCard')}</span>
|
||||||
|
<ProjectMembershipsPopup
|
||||||
|
items={allProjectMemberships}
|
||||||
|
currentUserIds={userIds}
|
||||||
|
onUserSelect={onUserAdd}
|
||||||
|
onUserDeselect={onUserRemove}
|
||||||
|
>
|
||||||
|
<Button fluid className={styles.actionButton}>
|
||||||
|
<Icon name="user outline" className={styles.actionIcon} />
|
||||||
|
{t('common.members')}
|
||||||
|
</Button>
|
||||||
|
</ProjectMembershipsPopup>
|
||||||
|
<LabelsPopup
|
||||||
|
items={allLabels}
|
||||||
|
currentIds={labelIds}
|
||||||
|
onSelect={onLabelAdd}
|
||||||
|
onDeselect={onLabelRemove}
|
||||||
|
onCreate={onLabelCreate}
|
||||||
|
onUpdate={onLabelUpdate}
|
||||||
|
onDelete={onLabelDelete}
|
||||||
|
>
|
||||||
|
<Button fluid className={styles.actionButton}>
|
||||||
|
<Icon name="bookmark outline" className={styles.actionIcon} />
|
||||||
|
{t('common.labels')}
|
||||||
|
</Button>
|
||||||
|
</LabelsPopup>
|
||||||
|
<EditDueDatePopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}>
|
||||||
|
<Button fluid className={styles.actionButton}>
|
||||||
|
<Icon name="calendar check outline" className={styles.actionIcon} />
|
||||||
|
{t('common.dueDate')}
|
||||||
|
</Button>
|
||||||
|
</EditDueDatePopup>
|
||||||
|
<EditTimerPopup defaultValue={timer} onUpdate={handleTimerUpdate}>
|
||||||
|
<Button fluid className={styles.actionButton}>
|
||||||
|
<Icon name="clock outline" className={styles.actionIcon} />
|
||||||
|
{t('common.timer')}
|
||||||
|
</Button>
|
||||||
|
</EditTimerPopup>
|
||||||
|
<AddAttachment onCreate={onAttachmentCreate}>
|
||||||
|
<Button fluid className={styles.actionButton}>
|
||||||
|
<Icon name="attach" className={styles.actionIcon} />
|
||||||
|
{t('common.attachment')}
|
||||||
|
</Button>
|
||||||
|
</AddAttachment>
|
||||||
|
</div>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<span className={styles.actionsTitle}>{t('common.actions')}</span>
|
||||||
|
<Button
|
||||||
|
fluid
|
||||||
|
className={styles.actionButton}
|
||||||
|
onClick={handleToggleSubscribeClick}
|
||||||
|
>
|
||||||
|
<Icon name="paper plane outline" className={styles.actionIcon} />
|
||||||
|
{isSubscribed ? t('action.unsubscribe') : t('action.subscribe')}
|
||||||
</Button>
|
</Button>
|
||||||
</ProjectMembershipsPopup>
|
<DeletePopup
|
||||||
<LabelsPopup
|
title={t('common.deleteCard', {
|
||||||
items={allLabels}
|
context: 'title',
|
||||||
currentIds={labelIds}
|
})}
|
||||||
onSelect={onLabelAdd}
|
content={t('common.areYouSureYouWantToDeleteThisCard')}
|
||||||
onDeselect={onLabelRemove}
|
buttonContent={t('action.deleteCard')}
|
||||||
onCreate={onLabelCreate}
|
onConfirm={onDelete}
|
||||||
onUpdate={onLabelUpdate}
|
>
|
||||||
onDelete={onLabelDelete}
|
<Button fluid className={styles.actionButton}>
|
||||||
>
|
<Icon name="trash alternate outline" className={styles.actionIcon} />
|
||||||
<Button fluid className={styles.actionButton}>
|
{t('action.delete')}
|
||||||
<Icon name="bookmark outline" className={styles.actionIcon} />
|
</Button>
|
||||||
{t('common.labels')}
|
</DeletePopup>
|
||||||
</Button>
|
</div>
|
||||||
</LabelsPopup>
|
</Grid.Column>
|
||||||
<EditDueDatePopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}>
|
</Grid.Row>
|
||||||
<Button fluid className={styles.actionButton}>
|
</Grid>
|
||||||
<Icon name="calendar check outline" className={styles.actionIcon} />
|
</AddAttachmentZone>
|
||||||
{t('common.dueDate')}
|
|
||||||
</Button>
|
|
||||||
</EditDueDatePopup>
|
|
||||||
<EditTimerPopup defaultValue={timer} onUpdate={handleTimerUpdate}>
|
|
||||||
<Button fluid className={styles.actionButton}>
|
|
||||||
<Icon name="clock outline" className={styles.actionIcon} />
|
|
||||||
{t('common.timer')}
|
|
||||||
</Button>
|
|
||||||
</EditTimerPopup>
|
|
||||||
<FilePicker onSelect={handleAttachmentFileSelect}>
|
|
||||||
<Button fluid className={styles.actionButton}>
|
|
||||||
<Icon name="attach" className={styles.actionIcon} />
|
|
||||||
{t('common.attachment')}
|
|
||||||
</Button>
|
|
||||||
</FilePicker>
|
|
||||||
</div>
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<span className={styles.actionsTitle}>{t('common.actions')}</span>
|
|
||||||
<Button fluid className={styles.actionButton} onClick={handleToggleSubscribeClick}>
|
|
||||||
<Icon name="paper plane outline" className={styles.actionIcon} />
|
|
||||||
{isSubscribed ? t('action.unsubscribe') : t('action.subscribe')}
|
|
||||||
</Button>
|
|
||||||
<DeletePopup
|
|
||||||
title={t('common.deleteCard', {
|
|
||||||
context: 'title',
|
|
||||||
})}
|
|
||||||
content={t('common.areYouSureYouWantToDeleteThisCard')}
|
|
||||||
buttonContent={t('action.deleteCard')}
|
|
||||||
onConfirm={onDelete}
|
|
||||||
>
|
|
||||||
<Button fluid className={styles.actionButton}>
|
|
||||||
<Icon name="trash alternate outline" className={styles.actionIcon} />
|
|
||||||
{t('action.delete')}
|
|
||||||
</Button>
|
|
||||||
</DeletePopup>
|
|
||||||
</div>
|
|
||||||
</Grid.Column>
|
|
||||||
</Grid.Row>
|
|
||||||
</Grid>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
import isEmail from 'validator/lib/isEmail';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import isEmail from 'validator/lib/isEmail';
|
|
||||||
import { Form, Grid, Header, Message } from 'semantic-ui-react';
|
import { Form, Grid, Header, Message } from 'semantic-ui-react';
|
||||||
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
|
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
|
||||||
import { Input } from '../../lib/custom-ui';
|
import { Input } from '../../lib/custom-ui';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import useField from './use-field';
|
import useField from './use-field';
|
||||||
import useForm from './use-form';
|
import useForm from './use-form';
|
||||||
import useSteps from './use-steps';
|
import useSteps from './use-steps';
|
||||||
|
import useModal from './use-modal';
|
||||||
import useClosableForm from './use-closable-form';
|
import useClosableForm from './use-closable-form';
|
||||||
|
|
||||||
export { useField, useForm, useSteps, useClosableForm };
|
export { useField, useForm, useSteps, useModal, useClosableForm };
|
||||||
|
|
15
client/src/hooks/use-modal.js
Normal file
15
client/src/hooks/use-modal.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
export default (initialParams) => {
|
||||||
|
const [modal, setModal] = useState(() => initialParams);
|
||||||
|
|
||||||
|
const open = useCallback((params) => {
|
||||||
|
setModal(params);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setModal(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [modal, open, handleClose];
|
||||||
|
};
|
|
@ -14,7 +14,7 @@ const createStep = (type, params = {}) => {
|
||||||
export default (initialType, initialParams) => {
|
export default (initialType, initialParams) => {
|
||||||
const [step, setStep] = useState(() => createStep(initialType, initialParams));
|
const [step, setStep] = useState(() => createStep(initialType, initialParams));
|
||||||
|
|
||||||
const openStep = useCallback((type, params) => {
|
const open = useCallback((type, params) => {
|
||||||
setStep(createStep(type, params));
|
setStep(createStep(type, params));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -22,5 +22,5 @@ export default (initialType, initialParams) => {
|
||||||
setStep(null);
|
setStep(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return [step, openStep, handleBack];
|
return [step, open, handleBack];
|
||||||
};
|
};
|
||||||
|
|
4
client/src/lib/custom-ui/index.css
vendored
4
client/src/lib/custom-ui/index.css
vendored
|
@ -34694,11 +34694,11 @@ select.ui.dropdown {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrolling.dimmable > .dimmer {
|
/* .scrolling.dimmable > .dimmer {
|
||||||
-webkit-box-pack: start;
|
-webkit-box-pack: start;
|
||||||
-ms-flex-pack: start;
|
-ms-flex-pack: start;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.scrolling.dimmable.dimmed > .dimmer {
|
.scrolling.dimmable.dimmed > .dimmer {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
|
@ -42,6 +42,7 @@ export default {
|
||||||
createLabel_title: 'Create Label',
|
createLabel_title: 'Create Label',
|
||||||
createNewOneOrSelectExistingOne: 'Create a new one or select<br />an existing one',
|
createNewOneOrSelectExistingOne: 'Create a new one or select<br />an existing one',
|
||||||
createProject_title: 'Create Project',
|
createProject_title: 'Create Project',
|
||||||
|
createTextFile_title: 'Create Text File',
|
||||||
currentPassword: 'Current password',
|
currentPassword: 'Current password',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
dueDate: 'Due date',
|
dueDate: 'Due date',
|
||||||
|
@ -55,6 +56,7 @@ export default {
|
||||||
deleteTask_title: 'Delete Task',
|
deleteTask_title: 'Delete Task',
|
||||||
deleteUser_title: 'Delete User',
|
deleteUser_title: 'Delete User',
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
|
dropFileToUpload: 'Drop file to upload',
|
||||||
editAttachment_title: 'Edit Attachment',
|
editAttachment_title: 'Edit Attachment',
|
||||||
editAvatar_title: 'Edit Avatar',
|
editAvatar_title: 'Edit Avatar',
|
||||||
editBoard_title: 'Edit Board',
|
editBoard_title: 'Edit Board',
|
||||||
|
@ -69,6 +71,7 @@ export default {
|
||||||
emailAlreadyInUse: 'E-mail already in use',
|
emailAlreadyInUse: 'E-mail already in use',
|
||||||
enterCardTitle: 'Enter card title...',
|
enterCardTitle: 'Enter card title...',
|
||||||
enterDescription: 'Enter description...',
|
enterDescription: 'Enter description...',
|
||||||
|
enterFilename: 'Enter filename',
|
||||||
enterListTitle: 'Enter list title...',
|
enterListTitle: 'Enter list title...',
|
||||||
enterProjectTitle: 'Enter project title',
|
enterProjectTitle: 'Enter project title',
|
||||||
enterTaskDescription: 'Enter task description...',
|
enterTaskDescription: 'Enter task description...',
|
||||||
|
@ -129,6 +132,7 @@ export default {
|
||||||
addToCard: 'Add to card',
|
addToCard: 'Add to card',
|
||||||
addUser: 'Add user',
|
addUser: 'Add user',
|
||||||
createBoard: 'Create board',
|
createBoard: 'Create board',
|
||||||
|
createFile: 'Create file',
|
||||||
createLabel: 'Create label',
|
createLabel: 'Create label',
|
||||||
createNewLabel: 'Create new label',
|
createNewLabel: 'Create new label',
|
||||||
createProject: 'Create project',
|
createProject: 'Create project',
|
||||||
|
|
|
@ -46,6 +46,7 @@ export default {
|
||||||
createLabel: 'Создание метки',
|
createLabel: 'Создание метки',
|
||||||
createNewOneOrSelectExistingOne: 'Создайте новую или выберите<br />уже существующую',
|
createNewOneOrSelectExistingOne: 'Создайте новую или выберите<br />уже существующую',
|
||||||
createProject: 'Создание проекта',
|
createProject: 'Создание проекта',
|
||||||
|
createTextFile: 'Создание текстового файла',
|
||||||
currentPassword: 'Текущий пароль',
|
currentPassword: 'Текущий пароль',
|
||||||
date: 'Дата',
|
date: 'Дата',
|
||||||
dueDate: 'Срок',
|
dueDate: 'Срок',
|
||||||
|
@ -59,6 +60,7 @@ export default {
|
||||||
deleteTask: 'Удаление задачи',
|
deleteTask: 'Удаление задачи',
|
||||||
deleteUser: 'Удаление пользователя',
|
deleteUser: 'Удаление пользователя',
|
||||||
description: 'Описание',
|
description: 'Описание',
|
||||||
|
dropFileToUpload: 'Перетяните файл, чтобы загрузить',
|
||||||
editAttachment: 'Изменение вложения',
|
editAttachment: 'Изменение вложения',
|
||||||
editAvatar: 'Изменение аватара',
|
editAvatar: 'Изменение аватара',
|
||||||
editBoard: 'Изменение доски',
|
editBoard: 'Изменение доски',
|
||||||
|
@ -73,6 +75,7 @@ export default {
|
||||||
emailAlreadyInUse: 'E-mail уже занят',
|
emailAlreadyInUse: 'E-mail уже занят',
|
||||||
enterCardTitle: 'Введите заголовок для этой карточки...',
|
enterCardTitle: 'Введите заголовок для этой карточки...',
|
||||||
enterDescription: 'Введите описание...',
|
enterDescription: 'Введите описание...',
|
||||||
|
enterFilename: 'Введите название файла',
|
||||||
enterListTitle: 'Введите заголовок списка...',
|
enterListTitle: 'Введите заголовок списка...',
|
||||||
enterProjectTitle: 'Введите название проекта',
|
enterProjectTitle: 'Введите название проекта',
|
||||||
enterTaskDescription: 'Введите описание задачи...',
|
enterTaskDescription: 'Введите описание задачи...',
|
||||||
|
@ -133,6 +136,7 @@ export default {
|
||||||
addToCard: 'Добавить на карточку',
|
addToCard: 'Добавить на карточку',
|
||||||
addUser: 'Добавить пользователя',
|
addUser: 'Добавить пользователя',
|
||||||
createBoard: 'Создать доску',
|
createBoard: 'Создать доску',
|
||||||
|
createFile: 'Создать файл',
|
||||||
createLabel: 'Создать метку',
|
createLabel: 'Создать метку',
|
||||||
createNewLabel: 'Создать новую метку',
|
createNewLabel: 'Создать новую метку',
|
||||||
createProject: 'Создать проект',
|
createProject: 'Создать проект',
|
||||||
|
|
|
@ -60,7 +60,7 @@ module.exports = {
|
||||||
dirname: file.extra.dirname,
|
dirname: file.extra.dirname,
|
||||||
filename: file.filename,
|
filename: file.filename,
|
||||||
isImage: file.extra.isImage,
|
isImage: file.extra.isImage,
|
||||||
name: file.filename,
|
name: file.extra.name,
|
||||||
},
|
},
|
||||||
inputs.requestId,
|
inputs.requestId,
|
||||||
this.req,
|
this.req,
|
||||||
|
|
|
@ -3,6 +3,7 @@ const path = require('path');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const streamToArray = require('stream-to-array');
|
const streamToArray = require('stream-to-array');
|
||||||
|
const filenamify = require('filenamify');
|
||||||
const { v4: uuid } = require('uuid');
|
const { v4: uuid } = require('uuid');
|
||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
|
|
||||||
|
@ -33,10 +34,13 @@ module.exports = {
|
||||||
try {
|
try {
|
||||||
const dirname = uuid();
|
const dirname = uuid();
|
||||||
|
|
||||||
|
// FIXME: https://github.com/sindresorhus/filenamify/issues/13
|
||||||
|
const filename = filenamify(file.filename);
|
||||||
|
|
||||||
const rootPath = path.join(sails.config.custom.attachmentsPath, dirname);
|
const rootPath = path.join(sails.config.custom.attachmentsPath, dirname);
|
||||||
fs.mkdirSync(rootPath);
|
fs.mkdirSync(rootPath);
|
||||||
|
|
||||||
await writeFile(path.join(rootPath, file.filename), buffer);
|
await writeFile(path.join(rootPath, filename), buffer);
|
||||||
|
|
||||||
const image = sharp(buffer);
|
const image = sharp(buffer);
|
||||||
let imageMetadata;
|
let imageMetadata;
|
||||||
|
@ -68,8 +72,12 @@ module.exports = {
|
||||||
file.extra = {
|
file.extra = {
|
||||||
dirname,
|
dirname,
|
||||||
isImage: !!imageMetadata,
|
isImage: !!imageMetadata,
|
||||||
|
name: file.filename,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
file.filename = filename;
|
||||||
|
|
||||||
return done();
|
return done();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return done(error);
|
return done(error);
|
||||||
|
|
31
server/package-lock.json
generated
31
server/package-lock.json
generated
|
@ -2386,6 +2386,21 @@
|
||||||
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
|
||||||
"integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY="
|
"integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY="
|
||||||
},
|
},
|
||||||
|
"filename-reserved-regex": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik="
|
||||||
|
},
|
||||||
|
"filenamify": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-KQV/uJDI9VQgN7sHH1Zbk6+42cD6mnQ2HONzkXUfPJ+K2FC8GZ1dpewbbHw0Sz8Tf5k3EVdHVayM4DoAwWlmtg==",
|
||||||
|
"requires": {
|
||||||
|
"filename-reserved-regex": "^2.0.0",
|
||||||
|
"strip-outer": "^1.0.1",
|
||||||
|
"trim-repeated": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"fill-range": {
|
"fill-range": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
|
||||||
|
@ -6450,6 +6465,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
|
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
|
||||||
},
|
},
|
||||||
|
"strip-outer": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==",
|
||||||
|
"requires": {
|
||||||
|
"escape-string-regexp": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
|
||||||
|
@ -6674,6 +6697,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"trim-repeated": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=",
|
||||||
|
"requires": {
|
||||||
|
"escape-string-regexp": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "1.11.1",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
"bcrypt": "^4.0.1",
|
"bcrypt": "^4.0.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"dotenv-cli": "^3.1.0",
|
"dotenv-cli": "^3.1.0",
|
||||||
|
"filenamify": "^4.1.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"knex": "^0.20.13",
|
"knex": "^0.20.13",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue