mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 20:59:44 +02:00
Add file attachments
This commit is contained in:
parent
202abacaec
commit
6a68ec9c1e
103 changed files with 1847 additions and 305 deletions
|
@ -36,7 +36,8 @@ COPY --from=client-builder /app/build/index.html views
|
||||||
|
|
||||||
ENV BASE_URL DATABASE_URL
|
ENV BASE_URL DATABASE_URL
|
||||||
|
|
||||||
VOLUME /app/public/uploads
|
VOLUME /app/public/user-avatars
|
||||||
|
VOLUME /app/public/attachments
|
||||||
|
|
||||||
EXPOSE 1337
|
EXPOSE 1337
|
||||||
|
|
||||||
|
|
|
@ -82,11 +82,11 @@ Demo user: demo@demo.demo demo
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
- [x] File attachments
|
||||||
|
- [ ] Member permissions
|
||||||
- [ ] Fetch last data after reconnection
|
- [ ] Fetch last data after reconnection
|
||||||
- [ ] File attachments
|
|
||||||
- [ ] Custom fields
|
- [ ] Custom fields
|
||||||
- [ ] Public boards
|
- [ ] Public boards
|
||||||
- [ ] Member permissions
|
|
||||||
- [ ] Automatic actions
|
- [ ] Automatic actions
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
117
client/src/actions/attachment.js
Normal file
117
client/src/actions/attachment.js
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
|
||||||
|
export const createAttachment = (attachment) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_CREATE,
|
||||||
|
payload: {
|
||||||
|
attachment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateAttachment = (id, data) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_UPDATE,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteAttachment = (id) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_DELETE,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Events */
|
||||||
|
|
||||||
|
export const createAttachmentRequested = (localId, data) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_CREATE_REQUESTED,
|
||||||
|
payload: {
|
||||||
|
localId,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createAttachmentSucceeded = (localId, attachment) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_CREATE_SUCCEEDED,
|
||||||
|
payload: {
|
||||||
|
localId,
|
||||||
|
attachment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createAttachmentFailed = (localId, error) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_CREATE_FAILED,
|
||||||
|
payload: {
|
||||||
|
localId,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createAttachmentReceived = (attachment) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_CREATE_RECEIVED,
|
||||||
|
payload: {
|
||||||
|
attachment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateAttachmentRequested = (id, data) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_UPDATE_REQUESTED,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateAttachmentSucceeded = (attachment) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_UPDATE_SUCCEEDED,
|
||||||
|
payload: {
|
||||||
|
attachment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateAttachmentFailed = (id, error) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_UPDATE_FAILED,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateAttachmentReceived = (attachment) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_UPDATE_RECEIVED,
|
||||||
|
payload: {
|
||||||
|
attachment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteAttachmentRequested = (id) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_DELETE_REQUESTED,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteAttachmentSucceeded = (attachment) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_DELETE_SUCCEEDED,
|
||||||
|
payload: {
|
||||||
|
attachment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteAttachmentFailed = (id, error) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_DELETE_FAILED,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteAttachmentReceived = (attachment) => ({
|
||||||
|
type: ActionTypes.ATTACHMENT_DELETE_RECEIVED,
|
||||||
|
payload: {
|
||||||
|
attachment,
|
||||||
|
},
|
||||||
|
});
|
|
@ -76,6 +76,7 @@ export const fetchBoardSucceeded = (
|
||||||
cardMemberships,
|
cardMemberships,
|
||||||
cardLabels,
|
cardLabels,
|
||||||
tasks,
|
tasks,
|
||||||
|
attachments,
|
||||||
) => ({
|
) => ({
|
||||||
type: ActionTypes.BOARD_FETCH_SUCCEEDED,
|
type: ActionTypes.BOARD_FETCH_SUCCEEDED,
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -86,6 +87,7 @@ export const fetchBoardSucceeded = (
|
||||||
cardMemberships,
|
cardMemberships,
|
||||||
cardLabels,
|
cardLabels,
|
||||||
tasks,
|
tasks,
|
||||||
|
attachments,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
23
client/src/actions/entry/attachment.js
Normal file
23
client/src/actions/entry/attachment.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import EntryActionTypes from '../../constants/EntryActionTypes';
|
||||||
|
|
||||||
|
export const createAttachmentInCurrentCard = (data) => ({
|
||||||
|
type: EntryActionTypes.ATTACHMENT_IN_CURRENT_CARD_CREATE,
|
||||||
|
payload: {
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateAttachment = (id, data) => ({
|
||||||
|
type: EntryActionTypes.ATTACHMENT_UPDATE,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteAttachment = (id) => ({
|
||||||
|
type: EntryActionTypes.ATTACHMENT_DELETE,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
|
@ -8,6 +8,7 @@ export * from './list';
|
||||||
export * from './label';
|
export * from './label';
|
||||||
export * from './card';
|
export * from './card';
|
||||||
export * from './task';
|
export * from './task';
|
||||||
|
export * from './attachment';
|
||||||
export * from './actions';
|
export * from './actions';
|
||||||
export * from './comment-action';
|
export * from './comment-action';
|
||||||
export * from './notification';
|
export * from './notification';
|
||||||
|
|
|
@ -63,10 +63,10 @@ export const clearCurrentUserUsernameUpdateError = () => ({
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const uploadCurrentUserAvatar = (file) => ({
|
export const updateCurrentUserAvatar = (data) => ({
|
||||||
type: EntryActionTypes.CURRENT_USER_AVATAR_UPLOAD,
|
type: EntryActionTypes.CURRENT_USER_AVATAR_UPDATE,
|
||||||
payload: {
|
payload: {
|
||||||
file,
|
data,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ export * from './card';
|
||||||
export * from './card-membership';
|
export * from './card-membership';
|
||||||
export * from './card-label';
|
export * from './card-label';
|
||||||
export * from './task';
|
export * from './task';
|
||||||
|
export * from './attachment';
|
||||||
export * from './actions';
|
export * from './actions';
|
||||||
export * from './action';
|
export * from './action';
|
||||||
export * from './comment-action';
|
export * from './comment-action';
|
||||||
|
|
|
@ -233,23 +233,23 @@ export const updateUserUsernameFailed = (id, error) => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const uploadUserAvatarRequested = (id) => ({
|
export const updateUserAvatarRequested = (id) => ({
|
||||||
type: ActionTypes.USER_AVATAR_UPLOAD_REQUESTED,
|
type: ActionTypes.USER_AVATAR_UPDATE_REQUESTED,
|
||||||
payload: {
|
payload: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const uploadUserAvatarSucceeded = (id, avatar) => ({
|
export const updateUserAvatarSucceeded = (id, avatarUrl) => ({
|
||||||
type: ActionTypes.USER_AVATAR_UPLOAD_SUCCEEDED,
|
type: ActionTypes.USER_AVATAR_UPDATE_SUCCEEDED,
|
||||||
payload: {
|
payload: {
|
||||||
id,
|
id,
|
||||||
avatar,
|
avatarUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const uploadUserAvatarFailed = (id, error) => ({
|
export const updateUserAvatarFailed = (id, error) => ({
|
||||||
type: ActionTypes.USER_AVATAR_UPLOAD_FAILED,
|
type: ActionTypes.USER_AVATAR_UPDATE_FAILED,
|
||||||
payload: {
|
payload: {
|
||||||
id,
|
id,
|
||||||
error,
|
error,
|
||||||
|
|
51
client/src/api/attachments.js
Executable file
51
client/src/api/attachments.js
Executable file
|
@ -0,0 +1,51 @@
|
||||||
|
import http from './http';
|
||||||
|
import socket from './socket';
|
||||||
|
|
||||||
|
/* Transformers */
|
||||||
|
|
||||||
|
export const transformAttachment = (attachment) => ({
|
||||||
|
...attachment,
|
||||||
|
createdAt: new Date(attachment.createdAt),
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
|
||||||
|
const createAttachment = (cardId, data, headers) =>
|
||||||
|
http.post(`/cards/${cardId}/attachments`, data, headers).then((body) => ({
|
||||||
|
...body,
|
||||||
|
item: transformAttachment(body.item),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updateAttachment = (id, data, headers) =>
|
||||||
|
socket.patch(`/attachments/${id}`, data, headers).then((body) => ({
|
||||||
|
...body,
|
||||||
|
item: transformAttachment(body.item),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const deleteAttachment = (id, headers) =>
|
||||||
|
socket.delete(`/attachments/${id}`, undefined, headers).then((body) => ({
|
||||||
|
...body,
|
||||||
|
item: transformAttachment(body.item),
|
||||||
|
}));
|
||||||
|
|
||||||
|
/* Event handlers */
|
||||||
|
|
||||||
|
const makeHandleAttachmentCreate = (next) => (body) => {
|
||||||
|
next({
|
||||||
|
...body,
|
||||||
|
item: transformAttachment(body.item),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeHandleAttachmentUpdate = makeHandleAttachmentCreate;
|
||||||
|
|
||||||
|
const makeHandleAttachmentDelete = makeHandleAttachmentCreate;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createAttachment,
|
||||||
|
updateAttachment,
|
||||||
|
deleteAttachment,
|
||||||
|
makeHandleAttachmentCreate,
|
||||||
|
makeHandleAttachmentUpdate,
|
||||||
|
makeHandleAttachmentDelete,
|
||||||
|
};
|
|
@ -1,5 +1,6 @@
|
||||||
import socket from './socket';
|
import socket from './socket';
|
||||||
import { transformCard } from './cards';
|
import { transformCard } from './cards';
|
||||||
|
import { transformAttachment } from './attachments';
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
|
|
||||||
|
@ -12,6 +13,7 @@ const getBoard = (id, headers) =>
|
||||||
included: {
|
included: {
|
||||||
...body.included,
|
...body.included,
|
||||||
cards: body.included.cards.map(transformCard),
|
cards: body.included.cards.map(transformCard),
|
||||||
|
attachments: body.included.attachments.map(transformAttachment),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import cards from './cards';
|
||||||
import cardMemberships from './card-memberships';
|
import cardMemberships from './card-memberships';
|
||||||
import cardLabels from './card-labels';
|
import cardLabels from './card-labels';
|
||||||
import tasks from './tasks';
|
import tasks from './tasks';
|
||||||
|
import attachments from './attachments';
|
||||||
import actions from './actions';
|
import actions from './actions';
|
||||||
import commentActions from './comment-actions';
|
import commentActions from './comment-actions';
|
||||||
import notifications from './notifications';
|
import notifications from './notifications';
|
||||||
|
@ -29,6 +30,7 @@ export default {
|
||||||
...cardMemberships,
|
...cardMemberships,
|
||||||
...cardLabels,
|
...cardLabels,
|
||||||
...tasks,
|
...tasks,
|
||||||
|
...attachments,
|
||||||
...actions,
|
...actions,
|
||||||
...commentActions,
|
...commentActions,
|
||||||
...notifications,
|
...notifications,
|
||||||
|
|
|
@ -19,14 +19,8 @@ const updateUserPassword = (id, data, headers) =>
|
||||||
const updateUserUsername = (id, data, headers) =>
|
const updateUserUsername = (id, data, headers) =>
|
||||||
socket.patch(`/users/${id}/username`, data, headers);
|
socket.patch(`/users/${id}/username`, data, headers);
|
||||||
|
|
||||||
const uploadUserAvatar = (id, file, headers) =>
|
const updateUserAvatar = (id, data, headers) =>
|
||||||
http.post(
|
http.post(`/users/${id}/update-avatar`, data, headers);
|
||||||
`/users/${id}/upload-avatar`,
|
|
||||||
{
|
|
||||||
file,
|
|
||||||
},
|
|
||||||
headers,
|
|
||||||
);
|
|
||||||
|
|
||||||
const deleteUser = (id, headers) => socket.delete(`/users/${id}`, undefined, headers);
|
const deleteUser = (id, headers) => socket.delete(`/users/${id}`, undefined, headers);
|
||||||
|
|
||||||
|
@ -38,6 +32,6 @@ export default {
|
||||||
updateUserEmail,
|
updateUserEmail,
|
||||||
updateUserPassword,
|
updateUserPassword,
|
||||||
updateUserUsername,
|
updateUserUsername,
|
||||||
uploadUserAvatar,
|
updateUserAvatar,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,7 +60,7 @@ const Filter = React.memo(
|
||||||
<span key={user.id} className={styles.filterItem}>
|
<span key={user.id} className={styles.filterItem}>
|
||||||
<User
|
<User
|
||||||
name={user.name}
|
name={user.name}
|
||||||
avatar={user.avatar}
|
avatarUrl={user.avatarUrl}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleUserRemoveClick(user.id)}
|
onClick={() => handleUserRemoveClick(user.id)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -103,7 +103,7 @@ const Card = React.memo(
|
||||||
<span className={classNames(styles.attachments, styles.attachmentsRight)}>
|
<span className={classNames(styles.attachments, styles.attachmentsRight)}>
|
||||||
{users.map((user) => (
|
{users.map((user) => (
|
||||||
<span key={user.id} className={classNames(styles.attachment, styles.attachmentRight)}>
|
<span key={user.id} className={classNames(styles.attachment, styles.attachmentRight)}>
|
||||||
<User name={user.name} avatar={user.avatar} size="tiny" />
|
<User name={user.name} avatarUrl={user.avatarUrl} size="tiny" />
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
background-color: #f5f6f7;
|
background-color: #f5f6f7;
|
||||||
border-bottom-color: rgba(9, 45, 66, 0.25);
|
border-bottom-color: rgba(9, 30, 66, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover .target {
|
.card:hover .target {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
background: #fff !important;
|
background: #fff !important;
|
||||||
border: 1px solid rgba(9, 45, 66, 0.13) !important;
|
border: 1px solid rgba(9, 30, 66, 0.13) !important;
|
||||||
border-radius: 3px !important;
|
border-radius: 3px !important;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: #333 !important;
|
color: #333 !important;
|
||||||
|
|
|
@ -62,7 +62,7 @@ const Item = React.memo(({ type, data, createdAt, user }) => {
|
||||||
return (
|
return (
|
||||||
<Comment>
|
<Comment>
|
||||||
<span className={styles.user}>
|
<span className={styles.user}>
|
||||||
<User name={user.name} avatar={user.avatar} />
|
<User name={user.name} avatarUrl={user.avatarUrl} />
|
||||||
</span>
|
</span>
|
||||||
<div className={classNames(styles.content)}>
|
<div className={classNames(styles.content)}>
|
||||||
<div>{contentNode}</div>
|
<div>{contentNode}</div>
|
||||||
|
|
|
@ -24,7 +24,7 @@ const ItemComment = React.memo(
|
||||||
return (
|
return (
|
||||||
<Comment>
|
<Comment>
|
||||||
<span className={styles.user}>
|
<span className={styles.user}>
|
||||||
<User name={user.name} avatar={user.avatar} />
|
<User name={user.name} avatarUrl={user.avatarUrl} />
|
||||||
</span>
|
</span>
|
||||||
<div className={classNames(styles.content)}>
|
<div className={classNames(styles.content)}>
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>
|
||||||
|
|
|
@ -24,8 +24,8 @@
|
||||||
.text {
|
.text {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 0px 8px 8px;
|
border-radius: 0px 8px 8px;
|
||||||
box-shadow: 0 1px 2px -1px rgba(9, 45, 66, 0.25),
|
box-shadow: 0 1px 2px -1px rgba(9, 30, 66, 0.25),
|
||||||
0 0 0 1px rgba(9, 45, 66, 0.08);
|
0 0 0 1px rgba(9, 30, 66, 0.08);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: #17394d;
|
color: #17394d;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
45
client/src/components/CardModal/Attachments/Attachments.jsx
Normal file
45
client/src/components/CardModal/Attachments/Attachments.jsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Item from './Item';
|
||||||
|
|
||||||
|
const Attachments = React.memo(({ items, onUpdate, onDelete }) => {
|
||||||
|
const handleUpdate = useCallback(
|
||||||
|
(id, data) => {
|
||||||
|
onUpdate(id, data);
|
||||||
|
},
|
||||||
|
[onUpdate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(
|
||||||
|
(id) => {
|
||||||
|
onDelete(id);
|
||||||
|
},
|
||||||
|
[onDelete],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.map((item) => (
|
||||||
|
<Item
|
||||||
|
key={item.id}
|
||||||
|
name={item.name}
|
||||||
|
url={item.url}
|
||||||
|
thumbnailUrl={item.thumbnailUrl}
|
||||||
|
createdAt={item.createdAt}
|
||||||
|
isPersisted={item.isPersisted}
|
||||||
|
onUpdate={(data) => handleUpdate(item.id, data)}
|
||||||
|
onDelete={() => handleDelete(item.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Attachments.propTypes = {
|
||||||
|
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
|
onUpdate: PropTypes.func.isRequired,
|
||||||
|
onDelete: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Attachments;
|
107
client/src/components/CardModal/Attachments/EditPopup.jsx
Executable file
107
client/src/components/CardModal/Attachments/EditPopup.jsx
Executable file
|
@ -0,0 +1,107 @@
|
||||||
|
import dequal from 'dequal';
|
||||||
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button, Form } from 'semantic-ui-react';
|
||||||
|
import { withPopup } from '../../../lib/popup';
|
||||||
|
import { Input, Popup } from '../../../lib/custom-ui';
|
||||||
|
|
||||||
|
import { useForm, useSteps } from '../../../hooks';
|
||||||
|
import DeleteStep from '../../DeleteStep';
|
||||||
|
|
||||||
|
import styles from './EditPopup.module.css';
|
||||||
|
|
||||||
|
const StepTypes = {
|
||||||
|
DELETE: 'DELETE',
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditStep = React.memo(({ defaultData, onUpdate, onDelete, onClose }) => {
|
||||||
|
const [t] = useTranslation();
|
||||||
|
|
||||||
|
const [data, handleFieldChange] = useForm(() => ({
|
||||||
|
name: '',
|
||||||
|
...defaultData,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [step, openStep, handleBack] = useSteps();
|
||||||
|
|
||||||
|
const nameField = useRef(null);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
const cleanData = {
|
||||||
|
...data,
|
||||||
|
name: data.name.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!cleanData.name) {
|
||||||
|
nameField.current.select();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dequal(cleanData, defaultData)) {
|
||||||
|
onUpdate(cleanData);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
}, [defaultData, onUpdate, onClose, data]);
|
||||||
|
|
||||||
|
const handleDeleteClick = useCallback(() => {
|
||||||
|
openStep(StepTypes.DELETE);
|
||||||
|
}, [openStep]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
nameField.current.select();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (step && step.type === StepTypes.DELETE) {
|
||||||
|
return (
|
||||||
|
<DeleteStep
|
||||||
|
title={t('common.deleteAttachment', {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
content={t('common.areYouSureYouWantToDeleteThisAttachment')}
|
||||||
|
buttonContent={t('action.deleteAttachment')}
|
||||||
|
onConfirm={onDelete}
|
||||||
|
onBack={handleBack}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Popup.Header>
|
||||||
|
{t('common.editAttachment', {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</Popup.Header>
|
||||||
|
<Popup.Content>
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<div className={styles.text}>{t('common.title')}</div>
|
||||||
|
<Input
|
||||||
|
fluid
|
||||||
|
ref={nameField}
|
||||||
|
name="name"
|
||||||
|
value={data.name}
|
||||||
|
className={styles.field}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
/>
|
||||||
|
<Button positive content={t('action.save')} />
|
||||||
|
</Form>
|
||||||
|
<Button
|
||||||
|
content={t('action.delete')}
|
||||||
|
className={styles.deleteButton}
|
||||||
|
onClick={handleDeleteClick}
|
||||||
|
/>
|
||||||
|
</Popup.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
EditStep.propTypes = {
|
||||||
|
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
|
onUpdate: PropTypes.func.isRequired,
|
||||||
|
onDelete: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withPopup(EditStep);
|
|
@ -0,0 +1,17 @@
|
||||||
|
.deleteButton {
|
||||||
|
bottom: 12px;
|
||||||
|
box-shadow: 0 1px 0 #cbcccc;
|
||||||
|
position: absolute;
|
||||||
|
right: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: #444444;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
85
client/src/components/CardModal/Attachments/Item.jsx
Normal file
85
client/src/components/CardModal/Attachments/Item.jsx
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button, Icon, Loader } from 'semantic-ui-react';
|
||||||
|
|
||||||
|
import EditPopup from './EditPopup';
|
||||||
|
|
||||||
|
import styles from './Item.module.css';
|
||||||
|
|
||||||
|
const Item = React.memo(
|
||||||
|
({ name, url, thumbnailUrl, createdAt, isPersisted, onUpdate, onDelete }) => {
|
||||||
|
const [t] = useTranslation();
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
if (!isPersisted) {
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.wrapper, styles.wrapperSubmitting)}>
|
||||||
|
<Loader inverted />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = url.split('/').pop();
|
||||||
|
const extension = filename.slice((Math.max(0, filename.lastIndexOf('.')) || Infinity) + 1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events,
|
||||||
|
jsx-a11y/no-static-element-interactions */
|
||||||
|
<div className={styles.wrapper} onClick={handleClick}>
|
||||||
|
{/* eslint-enable jsx-a11y/click-events-have-key-events,
|
||||||
|
jsx-a11y/no-static-element-interactions */}
|
||||||
|
<div
|
||||||
|
className={styles.thumbnail}
|
||||||
|
style={{
|
||||||
|
backgroundImage: thumbnailUrl && `url(${thumbnailUrl}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!thumbnailUrl && <span className={styles.extension}>{extension || '-'}</span>}
|
||||||
|
</div>
|
||||||
|
<div className={styles.details}>
|
||||||
|
<span className={styles.name}>{name}</span>
|
||||||
|
<span className={styles.options}>
|
||||||
|
{t('format:longDateTime', {
|
||||||
|
postProcess: 'formatDate',
|
||||||
|
value: createdAt,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<EditPopup
|
||||||
|
defaultData={{
|
||||||
|
name,
|
||||||
|
}}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
onDelete={onDelete}
|
||||||
|
>
|
||||||
|
<Button className={classNames(styles.button, styles.target)}>
|
||||||
|
<Icon fitted name="pencil" size="small" />
|
||||||
|
</Button>
|
||||||
|
</EditPopup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Item.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string,
|
||||||
|
thumbnailUrl: PropTypes.string,
|
||||||
|
createdAt: PropTypes.instanceOf(Date),
|
||||||
|
isPersisted: PropTypes.bool.isRequired,
|
||||||
|
onUpdate: PropTypes.func.isRequired,
|
||||||
|
onDelete: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
Item.defaultProps = {
|
||||||
|
url: undefined,
|
||||||
|
thumbnailUrl: undefined,
|
||||||
|
createdAt: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Item;
|
90
client/src/components/CardModal/Attachments/Item.module.css
Normal file
90
client/src/components/CardModal/Attachments/Item.module.css
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
.button {
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
line-height: 28px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
min-height: auto !important;
|
||||||
|
opacity: 0;
|
||||||
|
padding: 0 !important;
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
top: 2px;
|
||||||
|
width: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: rgba(9, 30, 66, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 8px 32px 8px 128px;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 80px;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extension {
|
||||||
|
color: #5e6c84;
|
||||||
|
display: block;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 80px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
color: #17394d;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 20px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
display: block;
|
||||||
|
color: #6b808c;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: rgba(9, 30, 66, 0.04);
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 80px;
|
||||||
|
left: 0;
|
||||||
|
margin-top: -40px;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
top: 50%;
|
||||||
|
width: 112px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
min-height: 80px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper:hover .details {
|
||||||
|
background-color: rgba(9, 30, 66, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper:hover .target {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapperSubmitting {
|
||||||
|
background-color: rgba(9, 30, 66, 0.04);
|
||||||
|
}
|
3
client/src/components/CardModal/Attachments/index.js
Normal file
3
client/src/components/CardModal/Attachments/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import Attachments from './Attachments';
|
||||||
|
|
||||||
|
export default Attachments;
|
|
@ -3,11 +3,12 @@ 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 { Markdown } from '../../lib/custom-ui';
|
import { FilePicker, 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 Actions from './Actions';
|
import Actions from './Actions';
|
||||||
import User from '../User';
|
import User from '../User';
|
||||||
import Label from '../Label';
|
import Label from '../Label';
|
||||||
|
@ -33,6 +34,7 @@ const CardModal = React.memo(
|
||||||
users,
|
users,
|
||||||
labels,
|
labels,
|
||||||
tasks,
|
tasks,
|
||||||
|
attachments,
|
||||||
actions,
|
actions,
|
||||||
allProjectMemberships,
|
allProjectMemberships,
|
||||||
allLabels,
|
allLabels,
|
||||||
|
@ -49,6 +51,9 @@ const CardModal = React.memo(
|
||||||
onTaskCreate,
|
onTaskCreate,
|
||||||
onTaskUpdate,
|
onTaskUpdate,
|
||||||
onTaskDelete,
|
onTaskDelete,
|
||||||
|
onAttachmentCreate,
|
||||||
|
onAttachmentUpdate,
|
||||||
|
onAttachmentDelete,
|
||||||
onActionsFetch,
|
onActionsFetch,
|
||||||
onCommentActionCreate,
|
onCommentActionCreate,
|
||||||
onCommentActionUpdate,
|
onCommentActionUpdate,
|
||||||
|
@ -93,6 +98,15 @@ 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,
|
||||||
|
@ -134,7 +148,7 @@ const CardModal = React.memo(
|
||||||
onUserSelect={onUserAdd}
|
onUserSelect={onUserAdd}
|
||||||
onUserDeselect={onUserRemove}
|
onUserDeselect={onUserRemove}
|
||||||
>
|
>
|
||||||
<User name={user.name} avatar={user.avatar} />
|
<User name={user.name} avatarUrl={user.avatarUrl} />
|
||||||
</ProjectMembershipsPopup>
|
</ProjectMembershipsPopup>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
@ -255,6 +269,19 @@ const CardModal = React.memo(
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{attachments.length > 0 && (
|
||||||
|
<div className={styles.contentModule}>
|
||||||
|
<div className={styles.moduleWrapper}>
|
||||||
|
<Icon name="attach" className={styles.moduleIcon} />
|
||||||
|
<div className={styles.moduleHeader}>{t('common.attachments')}</div>
|
||||||
|
<Attachments
|
||||||
|
items={attachments}
|
||||||
|
onUpdate={onAttachmentUpdate}
|
||||||
|
onDelete={onAttachmentDelete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Actions
|
<Actions
|
||||||
items={actions}
|
items={actions}
|
||||||
isFetching={isActionsFetching}
|
isFetching={isActionsFetching}
|
||||||
|
@ -306,6 +333,12 @@ const CardModal = React.memo(
|
||||||
{t('common.timer')}
|
{t('common.timer')}
|
||||||
</Button>
|
</Button>
|
||||||
</EditTimerPopup>
|
</EditTimerPopup>
|
||||||
|
<FilePicker onSelect={handleAttachmentFileSelect}>
|
||||||
|
<Button fluid className={styles.actionButton}>
|
||||||
|
<Icon name="attach" className={styles.actionIcon} />
|
||||||
|
{t('common.attachment')}
|
||||||
|
</Button>
|
||||||
|
</FilePicker>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<span className={styles.actionsTitle}>{t('common.actions')}</span>
|
<span className={styles.actionsTitle}>{t('common.actions')}</span>
|
||||||
|
@ -347,6 +380,7 @@ CardModal.propTypes = {
|
||||||
users: PropTypes.array.isRequired,
|
users: PropTypes.array.isRequired,
|
||||||
labels: PropTypes.array.isRequired,
|
labels: PropTypes.array.isRequired,
|
||||||
tasks: PropTypes.array.isRequired,
|
tasks: PropTypes.array.isRequired,
|
||||||
|
attachments: PropTypes.array.isRequired,
|
||||||
actions: PropTypes.array.isRequired,
|
actions: PropTypes.array.isRequired,
|
||||||
allProjectMemberships: PropTypes.array.isRequired,
|
allProjectMemberships: PropTypes.array.isRequired,
|
||||||
allLabels: PropTypes.array.isRequired,
|
allLabels: PropTypes.array.isRequired,
|
||||||
|
@ -364,6 +398,9 @@ CardModal.propTypes = {
|
||||||
onTaskCreate: PropTypes.func.isRequired,
|
onTaskCreate: PropTypes.func.isRequired,
|
||||||
onTaskUpdate: PropTypes.func.isRequired,
|
onTaskUpdate: PropTypes.func.isRequired,
|
||||||
onTaskDelete: PropTypes.func.isRequired,
|
onTaskDelete: PropTypes.func.isRequired,
|
||||||
|
onAttachmentCreate: PropTypes.func.isRequired,
|
||||||
|
onAttachmentUpdate: PropTypes.func.isRequired,
|
||||||
|
onAttachmentDelete: PropTypes.func.isRequired,
|
||||||
onActionsFetch: PropTypes.func.isRequired,
|
onActionsFetch: PropTypes.func.isRequired,
|
||||||
onCommentActionCreate: PropTypes.func.isRequired,
|
onCommentActionCreate: PropTypes.func.isRequired,
|
||||||
onCommentActionUpdate: PropTypes.func.isRequired,
|
onCommentActionUpdate: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.actionButton {
|
.actionButton {
|
||||||
background: #ebeef0 !important;
|
background: #ebeef0 !important;
|
||||||
box-shadow: 0 1px 0 0 rgba(9, 45, 66, 0.13) !important;
|
box-shadow: 0 1px 0 0 rgba(9, 30, 66, 0.13) !important;
|
||||||
color: #444 !important;
|
color: #444 !important;
|
||||||
margin-top: 8px !important;
|
margin-top: 8px !important;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
.actionButton:hover {
|
.actionButton:hover {
|
||||||
background: #dfe3e6 !important;
|
background: #dfe3e6 !important;
|
||||||
box-shadow: 0 1px 0 0 rgba(9, 45, 66, 0.25) !important;
|
box-shadow: 0 1px 0 0 rgba(9, 30, 66, 0.25) !important;
|
||||||
color: #4c4c4c !important;
|
color: #4c4c4c !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dueDate {
|
.dueDate {
|
||||||
background: #dce0e4;
|
background: rgba(9, 30, 66, 0.04);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
color: #6b808c;
|
color: #6b808c;
|
||||||
|
@ -79,12 +79,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dueDate:hover {
|
.dueDate:hover {
|
||||||
background: #d2d8dc;
|
background: rgba(9, 30, 66, 0.08);
|
||||||
color: #17394d;
|
color: #17394d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.descriptionButton {
|
.descriptionButton {
|
||||||
background: rgba(9, 45, 66, 0.08);
|
background: rgba(9, 30, 66, 0.04);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -100,7 +100,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.descriptionButton:hover {
|
.descriptionButton:hover {
|
||||||
background-color: rgba(9, 45, 66, 0.13);
|
background-color: rgba(9, 30, 66, 0.08);
|
||||||
color: #092d42;
|
color: #092d42;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
background: #fff !important;
|
background: #fff !important;
|
||||||
border: 1px solid rgba(9, 45, 66, 0.13) !important;
|
border: 1px solid rgba(9, 30, 66, 0.13) !important;
|
||||||
border-radius: 3px !important;
|
border-radius: 3px !important;
|
||||||
color: #17394d !important;
|
color: #17394d !important;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
background: #fff !important;
|
background: #fff !important;
|
||||||
border: 1px solid rgba(9, 45, 66, 0.13) !important;
|
border: 1px solid rgba(9, 30, 66, 0.08) !important;
|
||||||
border-radius: 3px !important;
|
border-radius: 3px !important;
|
||||||
color: #17394d !important;
|
color: #17394d !important;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
background: #fff !important;
|
background: #fff !important;
|
||||||
border: 1px solid rgba(9, 45, 66, 0.13) !important;
|
border: 1px solid rgba(9, 30, 66, 0.13) !important;
|
||||||
border-radius: 3px !important;
|
border-radius: 3px !important;
|
||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
color: #17394d !important;
|
color: #17394d !important;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
background-color: rgba(9, 45, 66, 0.13) !important;
|
background-color: rgba(9, 30, 66, 0.08) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkboxWrapper {
|
.checkboxWrapper {
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.content:hover {
|
.content:hover {
|
||||||
background-color: rgba(9, 45, 66, 0.08);
|
background-color: rgba(9, 30, 66, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content:hover .target {
|
.content:hover .target {
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.taskButton:hover {
|
.taskButton:hover {
|
||||||
background-color: rgba(9, 45, 66, 0.13);
|
background-color: rgba(9, 30, 66, 0.04);
|
||||||
color: #092d42;
|
color: #092d42;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
|
||||||
<>
|
<>
|
||||||
<User
|
<User
|
||||||
name={item.action.user.name}
|
name={item.action.user.name}
|
||||||
avatar={item.action.user.avatar}
|
avatarUrl={item.action.user.avatarUrl}
|
||||||
size="large"
|
size="large"
|
||||||
/>
|
/>
|
||||||
<span className={styles.content}>{renderItemContent(item)}</span>
|
<span className={styles.content}>{renderItemContent(item)}</span>
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerButton:hover {
|
.headerButton:hover {
|
||||||
background-color: rgba(9, 45, 66, 0.13) !important;
|
background-color: rgba(9, 30, 66, 0.13) !important;
|
||||||
color: #516b7a !important;
|
color: #516b7a !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ const AddMembershipStep = React.memo(({ users, currentUserIds, onCreate, onClose
|
||||||
<UserItem
|
<UserItem
|
||||||
key={user.id}
|
key={user.id}
|
||||||
name={user.name}
|
name={user.name}
|
||||||
avatar={user.avatar}
|
avatarUrl={user.avatarUrl}
|
||||||
isActive={currentUserIds.includes(user.id)}
|
isActive={currentUserIds.includes(user.id)}
|
||||||
onSelect={() => handleUserSelect(user.id)}
|
onSelect={() => handleUserSelect(user.id)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import User from '../../User';
|
||||||
|
|
||||||
import styles from './UserItem.module.css';
|
import styles from './UserItem.module.css';
|
||||||
|
|
||||||
const UserItem = React.memo(({ name, avatar, isActive, onSelect }) => (
|
const UserItem = React.memo(({ name, avatarUrl, isActive, onSelect }) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isActive}
|
disabled={isActive}
|
||||||
|
@ -14,7 +14,7 @@ const UserItem = React.memo(({ name, avatar, isActive, onSelect }) => (
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
>
|
>
|
||||||
<span className={styles.user}>
|
<span className={styles.user}>
|
||||||
<User name={name} avatar={avatar} />
|
<User name={name} avatarUrl={avatarUrl} />
|
||||||
</span>
|
</span>
|
||||||
<div className={classNames(styles.menuItemText, isActive && styles.menuItemTextActive)}>
|
<div className={classNames(styles.menuItemText, isActive && styles.menuItemTextActive)}>
|
||||||
{name}
|
{name}
|
||||||
|
@ -24,13 +24,13 @@ const UserItem = React.memo(({ name, avatar, isActive, onSelect }) => (
|
||||||
|
|
||||||
UserItem.propTypes = {
|
UserItem.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
avatar: PropTypes.string,
|
avatarUrl: PropTypes.string,
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
UserItem.defaultProps = {
|
UserItem.defaultProps = {
|
||||||
avatar: undefined,
|
avatarUrl: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserItem;
|
export default UserItem;
|
||||||
|
|
|
@ -39,7 +39,7 @@ const EditMembershipStep = React.memo(({ user, isEditable, onDelete }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span className={styles.user}>
|
<span className={styles.user}>
|
||||||
<User name={user.name} avatar={user.avatar} size="large" />
|
<User name={user.name} avatarUrl={user.avatarUrl} size="large" />
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.content}>
|
<span className={styles.content}>
|
||||||
<div className={styles.name}>{user.name}</div>
|
<div className={styles.name}>{user.name}</div>
|
||||||
|
|
|
@ -56,7 +56,7 @@ const Project = React.memo(
|
||||||
>
|
>
|
||||||
<User
|
<User
|
||||||
name={membership.user.name}
|
name={membership.user.name}
|
||||||
avatar={membership.user.avatar}
|
avatarUrl={membership.user.avatarUrl}
|
||||||
size="large"
|
size="large"
|
||||||
isDisabled={!membership.isPersisted}
|
isDisabled={!membership.isPersisted}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -24,7 +24,7 @@ const Item = React.memo(({ isPersisted, isActive, user, onUserSelect, onUserDese
|
||||||
onClick={handleToggleClick}
|
onClick={handleToggleClick}
|
||||||
>
|
>
|
||||||
<span className={styles.user}>
|
<span className={styles.user}>
|
||||||
<User name={user.name} avatar={user.avatar} />
|
<User name={user.name} avatarUrl={user.avatarUrl} />
|
||||||
</span>
|
</span>
|
||||||
<div className={classNames(styles.menuItemText, isActive && styles.menuItemTextActive)}>
|
<div className={classNames(styles.menuItemText, isActive && styles.menuItemTextActive)}>
|
||||||
{user.name}
|
{user.name}
|
||||||
|
|
|
@ -72,10 +72,10 @@ const getColor = (name) => {
|
||||||
return COLORS[sum % COLORS.length];
|
return COLORS[sum % COLORS.length];
|
||||||
};
|
};
|
||||||
|
|
||||||
const User = React.memo(({ name, avatar, size, isDisabled, onClick }) => {
|
const User = React.memo(({ name, avatarUrl, size, isDisabled, onClick }) => {
|
||||||
const style = {
|
const style = {
|
||||||
...STYLES[size],
|
...STYLES[size],
|
||||||
background: avatar ? `url("${avatar}")` : getColor(name),
|
background: avatarUrl ? `url("${avatarUrl}")` : getColor(name),
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentNode = (
|
const contentNode = (
|
||||||
|
@ -84,7 +84,7 @@ const User = React.memo(({ name, avatar, size, isDisabled, onClick }) => {
|
||||||
className={classNames(styles.wrapper, onClick && styles.hoverable)}
|
className={classNames(styles.wrapper, onClick && styles.hoverable)}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{!avatar && <span className={styles.initials}>{initials(name)}</span>}
|
{!avatarUrl && <span className={styles.initials}>{initials(name)}</span>}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -99,14 +99,14 @@ const User = React.memo(({ name, avatar, size, isDisabled, onClick }) => {
|
||||||
|
|
||||||
User.propTypes = {
|
User.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
avatar: PropTypes.string,
|
avatarUrl: PropTypes.string,
|
||||||
size: PropTypes.oneOf(Object.values(SIZES)),
|
size: PropTypes.oneOf(Object.values(SIZES)),
|
||||||
isDisabled: PropTypes.bool,
|
isDisabled: PropTypes.bool,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
User.defaultProps = {
|
User.defaultProps = {
|
||||||
avatar: undefined,
|
avatarUrl: undefined,
|
||||||
size: SIZES.MEDIUM,
|
size: SIZES.MEDIUM,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
onClick: undefined,
|
onClick: undefined,
|
||||||
|
|
|
@ -17,15 +17,15 @@ const AccountPane = React.memo(
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
username,
|
username,
|
||||||
avatar,
|
avatarUrl,
|
||||||
phone,
|
phone,
|
||||||
organization,
|
organization,
|
||||||
isAvatarUploading,
|
isAvatarUpdating,
|
||||||
usernameUpdateForm,
|
usernameUpdateForm,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onAvatarUpload,
|
onAvatarUpdate,
|
||||||
onUsernameUpdate,
|
onUsernameUpdate,
|
||||||
onUsernameUpdateMessageDismiss,
|
onUsernameUpdateMessageDismiss,
|
||||||
onEmailUpdate,
|
onEmailUpdate,
|
||||||
|
@ -37,18 +37,18 @@ const AccountPane = React.memo(
|
||||||
|
|
||||||
const handleAvatarDelete = useCallback(() => {
|
const handleAvatarDelete = useCallback(() => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
avatar: null,
|
avatarUrl: null,
|
||||||
});
|
});
|
||||||
}, [onUpdate]);
|
}, [onUpdate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||||
<EditAvatarPopup
|
<EditAvatarPopup
|
||||||
defaultValue={avatar}
|
defaultValue={avatarUrl}
|
||||||
onUpload={onAvatarUpload}
|
onUpdate={onAvatarUpdate}
|
||||||
onDelete={handleAvatarDelete}
|
onDelete={handleAvatarDelete}
|
||||||
>
|
>
|
||||||
<User name={name} avatar={avatar} size="massive" isDisabled={isAvatarUploading} />
|
<User name={name} avatarUrl={avatarUrl} size="massive" isDisabled={isAvatarUpdating} />
|
||||||
</EditAvatarPopup>
|
</EditAvatarPopup>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
@ -123,17 +123,17 @@ AccountPane.propTypes = {
|
||||||
email: PropTypes.string.isRequired,
|
email: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
avatar: PropTypes.string,
|
avatarUrl: PropTypes.string,
|
||||||
phone: PropTypes.string,
|
phone: PropTypes.string,
|
||||||
organization: PropTypes.string,
|
organization: PropTypes.string,
|
||||||
isAvatarUploading: PropTypes.bool.isRequired,
|
isAvatarUpdating: PropTypes.bool.isRequired,
|
||||||
/* eslint-disable react/forbid-prop-types */
|
/* eslint-disable react/forbid-prop-types */
|
||||||
usernameUpdateForm: PropTypes.object.isRequired,
|
usernameUpdateForm: PropTypes.object.isRequired,
|
||||||
emailUpdateForm: PropTypes.object.isRequired,
|
emailUpdateForm: PropTypes.object.isRequired,
|
||||||
passwordUpdateForm: PropTypes.object.isRequired,
|
passwordUpdateForm: PropTypes.object.isRequired,
|
||||||
/* eslint-enable react/forbid-prop-types */
|
/* eslint-enable react/forbid-prop-types */
|
||||||
onUpdate: PropTypes.func.isRequired,
|
onUpdate: PropTypes.func.isRequired,
|
||||||
onAvatarUpload: PropTypes.func.isRequired,
|
onAvatarUpdate: PropTypes.func.isRequired,
|
||||||
onUsernameUpdate: PropTypes.func.isRequired,
|
onUsernameUpdate: PropTypes.func.isRequired,
|
||||||
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
|
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
|
||||||
onEmailUpdate: PropTypes.func.isRequired,
|
onEmailUpdate: PropTypes.func.isRequired,
|
||||||
|
@ -144,7 +144,7 @@ AccountPane.propTypes = {
|
||||||
|
|
||||||
AccountPane.defaultProps = {
|
AccountPane.defaultProps = {
|
||||||
username: undefined,
|
username: undefined,
|
||||||
avatar: undefined,
|
avatarUrl: undefined,
|
||||||
phone: undefined,
|
phone: undefined,
|
||||||
organization: undefined,
|
organization: undefined,
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,23 +3,24 @@ import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from 'semantic-ui-react';
|
import { Button } from 'semantic-ui-react';
|
||||||
import { withPopup } from '../../../lib/popup';
|
import { withPopup } from '../../../lib/popup';
|
||||||
import { Popup } from '../../../lib/custom-ui';
|
import { FilePicker, Popup } from '../../../lib/custom-ui';
|
||||||
|
|
||||||
import styles from './EditAvatarPopup.module.css';
|
import styles from './EditAvatarPopup.module.css';
|
||||||
|
|
||||||
const EditAvatarStep = React.memo(({ defaultValue, onUpload, onDelete, onClose }) => {
|
const EditAvatarStep = React.memo(({ defaultValue, onUpdate, onDelete, onClose }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
|
|
||||||
const field = useRef(null);
|
const field = useRef(null);
|
||||||
|
|
||||||
const handleFieldChange = useCallback(
|
const handleFileSelect = useCallback(
|
||||||
({ target }) => {
|
(file) => {
|
||||||
if (target.files[0]) {
|
onUpdate({
|
||||||
onUpload(target.files[0]);
|
file,
|
||||||
|
});
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[onUpload, onClose],
|
[onUpdate, onClose],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteClick = useCallback(() => {
|
const handleDeleteClick = useCallback(() => {
|
||||||
|
@ -39,15 +40,14 @@ const EditAvatarStep = React.memo(({ defaultValue, onUpload, onDelete, onClose }
|
||||||
})}
|
})}
|
||||||
</Popup.Header>
|
</Popup.Header>
|
||||||
<Popup.Content>
|
<Popup.Content>
|
||||||
<div className={styles.input}>
|
<div className={styles.action}>
|
||||||
<Button content={t('action.uploadNewAvatar')} className={styles.customButton} />
|
<FilePicker accept="image/*" onSelect={handleFileSelect}>
|
||||||
<input
|
<Button
|
||||||
ref={field}
|
ref={field}
|
||||||
type="file"
|
content={t('action.uploadNewAvatar')}
|
||||||
accept="image/*"
|
className={styles.actionButton}
|
||||||
className={styles.file}
|
|
||||||
onChange={handleFieldChange}
|
|
||||||
/>
|
/>
|
||||||
|
</FilePicker>
|
||||||
</div>
|
</div>
|
||||||
{defaultValue && (
|
{defaultValue && (
|
||||||
<Button negative content={t('action.deleteAvatar')} onClick={handleDeleteClick} />
|
<Button negative content={t('action.deleteAvatar')} onClick={handleDeleteClick} />
|
||||||
|
@ -59,7 +59,7 @@ const EditAvatarStep = React.memo(({ defaultValue, onUpload, onDelete, onClose }
|
||||||
|
|
||||||
EditAvatarStep.propTypes = {
|
EditAvatarStep.propTypes = {
|
||||||
defaultValue: PropTypes.string,
|
defaultValue: PropTypes.string,
|
||||||
onUpload: PropTypes.func.isRequired,
|
onUpdate: PropTypes.func.isRequired,
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,25 +1,4 @@
|
||||||
.customButton {
|
.action {
|
||||||
background: transparent !important;
|
|
||||||
color: #6b808c !important;
|
|
||||||
font-weight: normal !important;
|
|
||||||
height: 36px;
|
|
||||||
line-height: 24px !important;
|
|
||||||
padding: 6px 12px !important;
|
|
||||||
text-align: left !important;
|
|
||||||
text-decoration: underline !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file {
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
opacity: 0;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
border: none;
|
border: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
@ -29,6 +8,18 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input:hover {
|
.action:hover {
|
||||||
background: #e9e9e9 !important;
|
background: #e9e9e9 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actionButton {
|
||||||
|
background: transparent !important;
|
||||||
|
color: #6b808c !important;
|
||||||
|
font-weight: normal !important;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 24px !important;
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
text-align: left !important;
|
||||||
|
text-decoration: underline !important;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
|
@ -11,16 +11,16 @@ const UserSettingsModal = React.memo(
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
username,
|
username,
|
||||||
avatar,
|
avatarUrl,
|
||||||
phone,
|
phone,
|
||||||
organization,
|
organization,
|
||||||
subscribeToOwnCards,
|
subscribeToOwnCards,
|
||||||
isAvatarUploading,
|
isAvatarUpdating,
|
||||||
usernameUpdateForm,
|
usernameUpdateForm,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onAvatarUpload,
|
onAvatarUpdate,
|
||||||
onUsernameUpdate,
|
onUsernameUpdate,
|
||||||
onUsernameUpdateMessageDismiss,
|
onUsernameUpdateMessageDismiss,
|
||||||
onEmailUpdate,
|
onEmailUpdate,
|
||||||
|
@ -41,15 +41,15 @@ const UserSettingsModal = React.memo(
|
||||||
email={email}
|
email={email}
|
||||||
name={name}
|
name={name}
|
||||||
username={username}
|
username={username}
|
||||||
avatar={avatar}
|
avatarUrl={avatarUrl}
|
||||||
phone={phone}
|
phone={phone}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
isAvatarUploading={isAvatarUploading}
|
isAvatarUpdating={isAvatarUpdating}
|
||||||
usernameUpdateForm={usernameUpdateForm}
|
usernameUpdateForm={usernameUpdateForm}
|
||||||
emailUpdateForm={emailUpdateForm}
|
emailUpdateForm={emailUpdateForm}
|
||||||
passwordUpdateForm={passwordUpdateForm}
|
passwordUpdateForm={passwordUpdateForm}
|
||||||
onUpdate={onUpdate}
|
onUpdate={onUpdate}
|
||||||
onAvatarUpload={onAvatarUpload}
|
onAvatarUpdate={onAvatarUpdate}
|
||||||
onUsernameUpdate={onUsernameUpdate}
|
onUsernameUpdate={onUsernameUpdate}
|
||||||
onUsernameUpdateMessageDismiss={onUsernameUpdateMessageDismiss}
|
onUsernameUpdateMessageDismiss={onUsernameUpdateMessageDismiss}
|
||||||
onEmailUpdate={onEmailUpdate}
|
onEmailUpdate={onEmailUpdate}
|
||||||
|
@ -89,18 +89,18 @@ UserSettingsModal.propTypes = {
|
||||||
email: PropTypes.string.isRequired,
|
email: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
avatar: PropTypes.string,
|
avatarUrl: PropTypes.string,
|
||||||
phone: PropTypes.string,
|
phone: PropTypes.string,
|
||||||
organization: PropTypes.string,
|
organization: PropTypes.string,
|
||||||
subscribeToOwnCards: PropTypes.bool.isRequired,
|
subscribeToOwnCards: PropTypes.bool.isRequired,
|
||||||
isAvatarUploading: PropTypes.bool.isRequired,
|
isAvatarUpdating: PropTypes.bool.isRequired,
|
||||||
/* eslint-disable react/forbid-prop-types */
|
/* eslint-disable react/forbid-prop-types */
|
||||||
usernameUpdateForm: PropTypes.object.isRequired,
|
usernameUpdateForm: PropTypes.object.isRequired,
|
||||||
emailUpdateForm: PropTypes.object.isRequired,
|
emailUpdateForm: PropTypes.object.isRequired,
|
||||||
passwordUpdateForm: PropTypes.object.isRequired,
|
passwordUpdateForm: PropTypes.object.isRequired,
|
||||||
/* eslint-enable react/forbid-prop-types */
|
/* eslint-enable react/forbid-prop-types */
|
||||||
onUpdate: PropTypes.func.isRequired,
|
onUpdate: PropTypes.func.isRequired,
|
||||||
onAvatarUpload: PropTypes.func.isRequired,
|
onAvatarUpdate: PropTypes.func.isRequired,
|
||||||
onUsernameUpdate: PropTypes.func.isRequired,
|
onUsernameUpdate: PropTypes.func.isRequired,
|
||||||
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
|
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
|
||||||
onEmailUpdate: PropTypes.func.isRequired,
|
onEmailUpdate: PropTypes.func.isRequired,
|
||||||
|
@ -112,7 +112,7 @@ UserSettingsModal.propTypes = {
|
||||||
|
|
||||||
UserSettingsModal.defaultProps = {
|
UserSettingsModal.defaultProps = {
|
||||||
username: undefined,
|
username: undefined,
|
||||||
avatar: undefined,
|
avatarUrl: undefined,
|
||||||
phone: undefined,
|
phone: undefined,
|
||||||
organization: undefined,
|
organization: undefined,
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,9 +60,9 @@ export default {
|
||||||
USER_USERNAME_UPDATE_REQUESTED: 'USER_USERNAME_UPDATE_REQUESTED',
|
USER_USERNAME_UPDATE_REQUESTED: 'USER_USERNAME_UPDATE_REQUESTED',
|
||||||
USER_USERNAME_UPDATE_SUCCEEDED: 'USER_USERNAME_UPDATE_SUCCEEDED',
|
USER_USERNAME_UPDATE_SUCCEEDED: 'USER_USERNAME_UPDATE_SUCCEEDED',
|
||||||
USER_USERNAME_UPDATE_FAILED: 'USER_USERNAME_UPDATE_FAILED',
|
USER_USERNAME_UPDATE_FAILED: 'USER_USERNAME_UPDATE_FAILED',
|
||||||
USER_AVATAR_UPLOAD_REQUESTED: 'USER_AVATAR_UPLOAD_REQUESTED',
|
USER_AVATAR_UPDATE_REQUESTED: 'USER_AVATAR_UPDATE_REQUESTED',
|
||||||
USER_AVATAR_UPLOAD_SUCCEEDED: 'USER_AVATAR_UPLOAD_SUCCEEDED',
|
USER_AVATAR_UPDATE_SUCCEEDED: 'USER_AVATAR_UPDATE_SUCCEEDED',
|
||||||
USER_AVATAR_UPLOAD_FAILED: 'USER_AVATAR_UPLOAD_FAILED',
|
USER_AVATAR_UPDATE_FAILED: 'USER_AVATAR_UPDATE_FAILED',
|
||||||
USER_DELETE_REQUESTED: 'USER_DELETE_REQUESTED',
|
USER_DELETE_REQUESTED: 'USER_DELETE_REQUESTED',
|
||||||
USER_DELETE_SUCCEEDED: 'USER_DELETE_SUCCEEDED',
|
USER_DELETE_SUCCEEDED: 'USER_DELETE_SUCCEEDED',
|
||||||
USER_DELETE_FAILED: 'USER_DELETE_FAILED',
|
USER_DELETE_FAILED: 'USER_DELETE_FAILED',
|
||||||
|
@ -227,6 +227,24 @@ export default {
|
||||||
TASK_DELETE_FAILED: 'TASK_DELETE_FAILED',
|
TASK_DELETE_FAILED: 'TASK_DELETE_FAILED',
|
||||||
TASK_DELETE_RECEIVED: 'TASK_DELETE_RECEIVED',
|
TASK_DELETE_RECEIVED: 'TASK_DELETE_RECEIVED',
|
||||||
|
|
||||||
|
/* Task */
|
||||||
|
|
||||||
|
ATTACHMENT_CREATE: 'ATTACHMENT_CREATE',
|
||||||
|
ATTACHMENT_UPDATE: 'ATTACHMENT_UPDATE',
|
||||||
|
ATTACHMENT_DELETE: 'ATTACHMENT_DELETE',
|
||||||
|
ATTACHMENT_CREATE_REQUESTED: 'ATTACHMENT_CREATE_REQUESTED',
|
||||||
|
ATTACHMENT_CREATE_SUCCEEDED: 'ATTACHMENT_CREATE_SUCCEEDED',
|
||||||
|
ATTACHMENT_CREATE_FAILED: 'ATTACHMENT_CREATE_FAILED',
|
||||||
|
ATTACHMENT_CREATE_RECEIVED: 'ATTACHMENT_CREATE_RECEIVED',
|
||||||
|
ATTACHMENT_UPDATE_REQUESTED: 'ATTACHMENT_UPDATE_REQUESTED',
|
||||||
|
ATTACHMENT_UPDATE_SUCCEEDED: 'ATTACHMENT_UPDATE_SUCCEEDED',
|
||||||
|
ATTACHMENT_UPDATE_FAILED: 'ATTACHMENT_UPDATE_FAILED',
|
||||||
|
ATTACHMENT_UPDATE_RECEIVED: 'ATTACHMENT_UPDATE_RECEIVED',
|
||||||
|
ATTACHMENT_DELETE_REQUESTED: 'ATTACHMENT_DELETE_REQUESTED',
|
||||||
|
ATTACHMENT_DELETE_SUCCEEDED: 'ATTACHMENT_DELETE_SUCCEEDED',
|
||||||
|
ATTACHMENT_DELETE_FAILED: 'ATTACHMENT_DELETE_FAILED',
|
||||||
|
ATTACHMENT_DELETE_RECEIVED: 'ATTACHMENT_DELETE_RECEIVED',
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
|
|
||||||
ACTIONS_FETCH_REQUESTED: 'ACTIONS_FETCH_REQUESTED',
|
ACTIONS_FETCH_REQUESTED: 'ACTIONS_FETCH_REQUESTED',
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default {
|
||||||
CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR`,
|
CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR`,
|
||||||
CURRENT_USER_USERNAME_UPDATE: `${PREFIX}/CURRENT_USER_USERNAME_UPDATE`,
|
CURRENT_USER_USERNAME_UPDATE: `${PREFIX}/CURRENT_USER_USERNAME_UPDATE`,
|
||||||
CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR`,
|
CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR`,
|
||||||
CURRENT_USER_AVATAR_UPLOAD: `${PREFIX}/CURRENT_USER_AVATAR_UPLOAD`,
|
CURRENT_USER_AVATAR_UPDATE: `${PREFIX}/CURRENT_USER_AVATAR_UPDATE`,
|
||||||
USER_DELETE: `${PREFIX}/USER_DELETE`,
|
USER_DELETE: `${PREFIX}/USER_DELETE`,
|
||||||
USER_TO_CARD_ADD: `${PREFIX}/USER_TO_CARD_ADD`,
|
USER_TO_CARD_ADD: `${PREFIX}/USER_TO_CARD_ADD`,
|
||||||
USER_TO_CURRENT_CARD_ADD: `${PREFIX}/USER_TO_CURRENT_CARD_ADD`,
|
USER_TO_CURRENT_CARD_ADD: `${PREFIX}/USER_TO_CURRENT_CARD_ADD`,
|
||||||
|
@ -87,6 +87,12 @@ export default {
|
||||||
TASK_UPDATE: `${PREFIX}/TASK_UPDATE`,
|
TASK_UPDATE: `${PREFIX}/TASK_UPDATE`,
|
||||||
TASK_DELETE: `${PREFIX}/TASK_DELETE`,
|
TASK_DELETE: `${PREFIX}/TASK_DELETE`,
|
||||||
|
|
||||||
|
/* Attachment */
|
||||||
|
|
||||||
|
ATTACHMENT_IN_CURRENT_CARD_CREATE: `${PREFIX}/ATTACHMENT_IN_CURRENT_CARD_CREATE`,
|
||||||
|
ATTACHMENT_UPDATE: `${PREFIX}/ATTACHMENT_UPDATE`,
|
||||||
|
ATTACHMENT_DELETE: `${PREFIX}/ATTACHMENT_DELETE`,
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
|
|
||||||
ACTIONS_IN_CURRENT_CARD_FETCH: `${PREFIX}/ACTIONS_IN_CURRENT_CARD_FETCH`,
|
ACTIONS_IN_CURRENT_CARD_FETCH: `${PREFIX}/ACTIONS_IN_CURRENT_CARD_FETCH`,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import omit from 'lodash/omit';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
actionsForCurrentCardSelector,
|
actionsForCurrentCardSelector,
|
||||||
|
attachmentsForCurrentCardSelector,
|
||||||
currentCardSelector,
|
currentCardSelector,
|
||||||
currentUserSelector,
|
currentUserSelector,
|
||||||
labelsForCurrentBoardSelector,
|
labelsForCurrentBoardSelector,
|
||||||
|
@ -16,9 +17,11 @@ import {
|
||||||
import {
|
import {
|
||||||
addLabelToCurrentCard,
|
addLabelToCurrentCard,
|
||||||
addUserToCurrentCard,
|
addUserToCurrentCard,
|
||||||
|
createAttachmentInCurrentCard,
|
||||||
createCommentActionInCurrentCard,
|
createCommentActionInCurrentCard,
|
||||||
createLabelInCurrentBoard,
|
createLabelInCurrentBoard,
|
||||||
createTaskInCurrentCard,
|
createTaskInCurrentCard,
|
||||||
|
deleteAttachment,
|
||||||
deleteCommentAction,
|
deleteCommentAction,
|
||||||
deleteCurrentCard,
|
deleteCurrentCard,
|
||||||
deleteLabel,
|
deleteLabel,
|
||||||
|
@ -26,6 +29,7 @@ import {
|
||||||
fetchActionsInCurrentCard,
|
fetchActionsInCurrentCard,
|
||||||
removeLabelFromCurrentCard,
|
removeLabelFromCurrentCard,
|
||||||
removeUserFromCurrentCard,
|
removeUserFromCurrentCard,
|
||||||
|
updateAttachment,
|
||||||
updateCommentAction,
|
updateCommentAction,
|
||||||
updateCurrentCard,
|
updateCurrentCard,
|
||||||
updateLabel,
|
updateLabel,
|
||||||
|
@ -53,6 +57,7 @@ const mapStateToProps = (state) => {
|
||||||
const users = usersForCurrentCardSelector(state);
|
const users = usersForCurrentCardSelector(state);
|
||||||
const labels = labelsForCurrentCardSelector(state);
|
const labels = labelsForCurrentCardSelector(state);
|
||||||
const tasks = tasksForCurrentCardSelector(state);
|
const tasks = tasksForCurrentCardSelector(state);
|
||||||
|
const attachments = attachmentsForCurrentCardSelector(state);
|
||||||
const actions = actionsForCurrentCardSelector(state);
|
const actions = actionsForCurrentCardSelector(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -66,6 +71,7 @@ const mapStateToProps = (state) => {
|
||||||
users,
|
users,
|
||||||
labels,
|
labels,
|
||||||
tasks,
|
tasks,
|
||||||
|
attachments,
|
||||||
actions,
|
actions,
|
||||||
allProjectMemberships,
|
allProjectMemberships,
|
||||||
allLabels,
|
allLabels,
|
||||||
|
@ -89,6 +95,9 @@ const mapDispatchToProps = (dispatch) =>
|
||||||
onTaskCreate: createTaskInCurrentCard,
|
onTaskCreate: createTaskInCurrentCard,
|
||||||
onTaskUpdate: updateTask,
|
onTaskUpdate: updateTask,
|
||||||
onTaskDelete: deleteTask,
|
onTaskDelete: deleteTask,
|
||||||
|
onAttachmentCreate: createAttachmentInCurrentCard,
|
||||||
|
onAttachmentUpdate: updateAttachment,
|
||||||
|
onAttachmentDelete: deleteAttachment,
|
||||||
onActionsFetch: fetchActionsInCurrentCard,
|
onActionsFetch: fetchActionsInCurrentCard,
|
||||||
onCommentActionCreate: createCommentActionInCurrentCard,
|
onCommentActionCreate: createCommentActionInCurrentCard,
|
||||||
onCommentActionUpdate: updateCommentAction,
|
onCommentActionUpdate: updateCommentAction,
|
||||||
|
|
|
@ -8,10 +8,10 @@ import {
|
||||||
clearCurrentUserUsernameUpdateError,
|
clearCurrentUserUsernameUpdateError,
|
||||||
closeModal,
|
closeModal,
|
||||||
updateCurrentUser,
|
updateCurrentUser,
|
||||||
|
updateCurrentUserAvatar,
|
||||||
updateCurrentUserEmail,
|
updateCurrentUserEmail,
|
||||||
updateCurrentUserPassword,
|
updateCurrentUserPassword,
|
||||||
updateCurrentUserUsername,
|
updateCurrentUserUsername,
|
||||||
uploadCurrentUserAvatar,
|
|
||||||
} from '../actions/entry';
|
} from '../actions/entry';
|
||||||
import UserSettingsModal from '../components/UserSettingsModal';
|
import UserSettingsModal from '../components/UserSettingsModal';
|
||||||
|
|
||||||
|
@ -20,11 +20,11 @@ const mapStateToProps = (state) => {
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
username,
|
username,
|
||||||
avatar,
|
avatarUrl,
|
||||||
phone,
|
phone,
|
||||||
organization,
|
organization,
|
||||||
subscribeToOwnCards,
|
subscribeToOwnCards,
|
||||||
isAvatarUploading,
|
isAvatarUpdating,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
usernameUpdateForm,
|
usernameUpdateForm,
|
||||||
|
@ -34,11 +34,11 @@ const mapStateToProps = (state) => {
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
username,
|
username,
|
||||||
avatar,
|
avatarUrl,
|
||||||
phone,
|
phone,
|
||||||
organization,
|
organization,
|
||||||
subscribeToOwnCards,
|
subscribeToOwnCards,
|
||||||
isAvatarUploading,
|
isAvatarUpdating,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
usernameUpdateForm,
|
usernameUpdateForm,
|
||||||
|
@ -49,7 +49,7 @@ const mapDispatchToProps = (dispatch) =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
{
|
{
|
||||||
onUpdate: updateCurrentUser,
|
onUpdate: updateCurrentUser,
|
||||||
onAvatarUpload: uploadCurrentUserAvatar,
|
onAvatarUpdate: updateCurrentUserAvatar,
|
||||||
onUsernameUpdate: updateCurrentUserUsername,
|
onUsernameUpdate: updateCurrentUserUsername,
|
||||||
onUsernameUpdateMessageDismiss: clearCurrentUserUsernameUpdateError,
|
onUsernameUpdateMessageDismiss: clearCurrentUserUsernameUpdateError,
|
||||||
onEmailUpdate: updateCurrentUserEmail,
|
onEmailUpdate: updateCurrentUserEmail,
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import React, { useCallback, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import styles from './FilePicker.module.css';
|
||||||
|
|
||||||
|
const FilePicker = React.memo(({ children, accept, onSelect }) => {
|
||||||
|
const field = useRef(null);
|
||||||
|
|
||||||
|
const handleTriggerClick = useCallback(() => {
|
||||||
|
field.current.click();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFieldChange = useCallback(
|
||||||
|
({ target }) => {
|
||||||
|
if (target.files[0]) {
|
||||||
|
onSelect(target.files[0]);
|
||||||
|
|
||||||
|
target.value = null; // eslint-disable-line no-param-reassign
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onSelect],
|
||||||
|
);
|
||||||
|
|
||||||
|
const tigger = React.cloneElement(children, {
|
||||||
|
onClick: handleTriggerClick,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{tigger}
|
||||||
|
<input
|
||||||
|
ref={field}
|
||||||
|
type="file"
|
||||||
|
accept={accept}
|
||||||
|
className={styles.field}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
FilePicker.propTypes = {
|
||||||
|
children: PropTypes.element.isRequired,
|
||||||
|
accept: PropTypes.string,
|
||||||
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
FilePicker.defaultProps = {
|
||||||
|
accept: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilePicker;
|
|
@ -0,0 +1,3 @@
|
||||||
|
.field {
|
||||||
|
display: none;
|
||||||
|
}
|
3
client/src/lib/custom-ui/components/FilePicker/index.js
Normal file
3
client/src/lib/custom-ui/components/FilePicker/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import FilePicker from './FilePicker';
|
||||||
|
|
||||||
|
export default FilePicker;
|
|
@ -1,6 +1,7 @@
|
||||||
import Input from './components/Input';
|
import Input from './components/Input';
|
||||||
import Popup from './components/Popup';
|
import Popup from './components/Popup';
|
||||||
import Markdown from './components/Markdown';
|
import Markdown from './components/Markdown';
|
||||||
|
import FilePicker from './components/FilePicker';
|
||||||
import DragScroller from './components/DragScroller';
|
import DragScroller from './components/DragScroller';
|
||||||
|
|
||||||
export { Input, Popup, Markdown, DragScroller };
|
export { Input, Popup, Markdown, FilePicker, DragScroller };
|
||||||
|
|
|
@ -18,6 +18,7 @@ export default {
|
||||||
all: 'All',
|
all: 'All',
|
||||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||||
'All changes will be automatically saved<br />after connection restored',
|
'All changes will be automatically saved<br />after connection restored',
|
||||||
|
areYouSureYouWantToDeleteThisAttachment: 'Are you sure you want to delete this attachment?',
|
||||||
areYouSureYouWantToDeleteThisBoard: 'Are you sure you want to delete this board?',
|
areYouSureYouWantToDeleteThisBoard: 'Are you sure you want to delete this board?',
|
||||||
areYouSureYouWantToDeleteThisCard: 'Are you sure you want to delete this card?',
|
areYouSureYouWantToDeleteThisCard: 'Are you sure you want to delete this card?',
|
||||||
areYouSureYouWantToDeleteThisComment: 'Are you sure you want to delete this comment?',
|
areYouSureYouWantToDeleteThisComment: 'Are you sure you want to delete this comment?',
|
||||||
|
@ -28,6 +29,8 @@ export default {
|
||||||
areYouSureYouWantToDeleteThisUser: 'Are you sure you want to delete this user?',
|
areYouSureYouWantToDeleteThisUser: 'Are you sure you want to delete this user?',
|
||||||
areYouSureYouWantToRemoveThisMemberFromProject:
|
areYouSureYouWantToRemoveThisMemberFromProject:
|
||||||
'Are you sure you want to remove this member from project?',
|
'Are you sure you want to remove this member from project?',
|
||||||
|
attachment: 'Attachment',
|
||||||
|
attachments: 'Attachments',
|
||||||
authentication: 'Authentication',
|
authentication: 'Authentication',
|
||||||
boardNotFound_title: 'Board Not Found',
|
boardNotFound_title: 'Board Not Found',
|
||||||
cardActions_title: 'Card Actions',
|
cardActions_title: 'Card Actions',
|
||||||
|
@ -42,6 +45,7 @@ export default {
|
||||||
currentPassword: 'Current password',
|
currentPassword: 'Current password',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
dueDate: 'Due date',
|
dueDate: 'Due date',
|
||||||
|
deleteAttachment_title: 'Delete Attachment',
|
||||||
deleteBoard_title: 'Delete Board',
|
deleteBoard_title: 'Delete Board',
|
||||||
deleteCard_title: 'Delete Card',
|
deleteCard_title: 'Delete Card',
|
||||||
deleteComment_title: 'Delete Comment',
|
deleteComment_title: 'Delete Comment',
|
||||||
|
@ -51,6 +55,7 @@ export default {
|
||||||
deleteTask_title: 'Delete Task',
|
deleteTask_title: 'Delete Task',
|
||||||
deleteUser_title: 'Delete User',
|
deleteUser_title: 'Delete User',
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
|
editAttachment_title: 'Edit Attachment',
|
||||||
editAvatar_title: 'Edit Avatar',
|
editAvatar_title: 'Edit Avatar',
|
||||||
editBoard_title: 'Edit Board',
|
editBoard_title: 'Edit Board',
|
||||||
editDueDate_title: 'Edit Due Date',
|
editDueDate_title: 'Edit Due Date',
|
||||||
|
@ -128,6 +133,7 @@ export default {
|
||||||
createNewLabel: 'Create new label',
|
createNewLabel: 'Create new label',
|
||||||
createProject: 'Create project',
|
createProject: 'Create project',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
|
deleteAttachment: 'Delete attachment',
|
||||||
deleteAvatar: 'Delete avatar',
|
deleteAvatar: 'Delete avatar',
|
||||||
deleteBoard: 'Delete board',
|
deleteBoard: 'Delete board',
|
||||||
deleteCard: 'Delete card',
|
deleteCard: 'Delete card',
|
||||||
|
|
|
@ -22,6 +22,7 @@ export default {
|
||||||
all: 'Все',
|
all: 'Все',
|
||||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||||
'Все изменения сохранятся автоматически,<br />как только подключение восстановится',
|
'Все изменения сохранятся автоматически,<br />как только подключение восстановится',
|
||||||
|
areYouSureYouWantToDeleteThisAttachment: 'Вы уверены, что хотите удалить это вложение?',
|
||||||
areYouSureYouWantToDeleteThisBoard: 'Вы уверены, что хотите удалить эту доску?',
|
areYouSureYouWantToDeleteThisBoard: 'Вы уверены, что хотите удалить эту доску?',
|
||||||
areYouSureYouWantToDeleteThisCard: 'Вы уверены, что хотите удалить эту карточку?',
|
areYouSureYouWantToDeleteThisCard: 'Вы уверены, что хотите удалить эту карточку?',
|
||||||
areYouSureYouWantToDeleteThisComment: 'Вы уверены, что хотите удалить этот комментарий?',
|
areYouSureYouWantToDeleteThisComment: 'Вы уверены, что хотите удалить этот комментарий?',
|
||||||
|
@ -32,6 +33,8 @@ export default {
|
||||||
areYouSureYouWantToDeleteThisUser: 'Вы уверены, что хотите удалить этого пользователя?',
|
areYouSureYouWantToDeleteThisUser: 'Вы уверены, что хотите удалить этого пользователя?',
|
||||||
areYouSureYouWantToRemoveThisMemberFromProject:
|
areYouSureYouWantToRemoveThisMemberFromProject:
|
||||||
'Вы уверены, что хотите удалить этого участника из проекта?',
|
'Вы уверены, что хотите удалить этого участника из проекта?',
|
||||||
|
attachment: 'Вложение',
|
||||||
|
attachments: 'Вложения',
|
||||||
authentication: 'Аутентификация',
|
authentication: 'Аутентификация',
|
||||||
boardNotFound: 'Доска не найдена',
|
boardNotFound: 'Доска не найдена',
|
||||||
cardActions: 'Действия с карточкой',
|
cardActions: 'Действия с карточкой',
|
||||||
|
@ -46,6 +49,7 @@ export default {
|
||||||
currentPassword: 'Текущий пароль',
|
currentPassword: 'Текущий пароль',
|
||||||
date: 'Дата',
|
date: 'Дата',
|
||||||
dueDate: 'Срок',
|
dueDate: 'Срок',
|
||||||
|
deleteAttachment: 'Удаление вложения',
|
||||||
deleteBoard: 'Удаление доски',
|
deleteBoard: 'Удаление доски',
|
||||||
deleteCard: 'Удаление карточки',
|
deleteCard: 'Удаление карточки',
|
||||||
deleteComment: 'Удаление комментария',
|
deleteComment: 'Удаление комментария',
|
||||||
|
@ -55,6 +59,7 @@ export default {
|
||||||
deleteTask: 'Удаление задачи',
|
deleteTask: 'Удаление задачи',
|
||||||
deleteUser: 'Удаление пользователя',
|
deleteUser: 'Удаление пользователя',
|
||||||
description: 'Описание',
|
description: 'Описание',
|
||||||
|
editAttachment: 'Изменение вложения',
|
||||||
editAvatar: 'Изменение аватара',
|
editAvatar: 'Изменение аватара',
|
||||||
editBoard: 'Изменение доски',
|
editBoard: 'Изменение доски',
|
||||||
editDueDate: 'Изменение срока',
|
editDueDate: 'Изменение срока',
|
||||||
|
@ -132,6 +137,7 @@ export default {
|
||||||
createNewLabel: 'Создать новую метку',
|
createNewLabel: 'Создать новую метку',
|
||||||
createProject: 'Создать проект',
|
createProject: 'Создать проект',
|
||||||
delete: 'Удалить',
|
delete: 'Удалить',
|
||||||
|
deleteAttachment: 'Удалить вложение',
|
||||||
deleteAvatar: 'Удалить аватар',
|
deleteAvatar: 'Удалить аватар',
|
||||||
deleteBoard: 'Удалить доску',
|
deleteBoard: 'Удалить доску',
|
||||||
deleteCard: 'Удалить карточку',
|
deleteCard: 'Удалить карточку',
|
||||||
|
|
57
client/src/models/Attachment.js
Normal file
57
client/src/models/Attachment.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { Model, attr, fk } from 'redux-orm';
|
||||||
|
|
||||||
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
export default class extends Model {
|
||||||
|
static modelName = 'Attachment';
|
||||||
|
|
||||||
|
static fields = {
|
||||||
|
id: attr(),
|
||||||
|
url: attr(),
|
||||||
|
thumbnailUrl: attr(),
|
||||||
|
name: attr(),
|
||||||
|
cardId: fk({
|
||||||
|
to: 'Card',
|
||||||
|
as: 'card',
|
||||||
|
relatedName: 'attachments',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static reducer({ type, payload }, Attachment) {
|
||||||
|
switch (type) {
|
||||||
|
case ActionTypes.BOARD_FETCH_SUCCEEDED:
|
||||||
|
payload.attachments.forEach((attachment) => {
|
||||||
|
Attachment.upsert(attachment);
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case ActionTypes.ATTACHMENT_CREATE:
|
||||||
|
case ActionTypes.ATTACHMENT_CREATE_RECEIVED:
|
||||||
|
Attachment.upsert(payload.attachment);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case ActionTypes.ATTACHMENT_UPDATE:
|
||||||
|
Attachment.withId(payload.id).update(payload.data);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case ActionTypes.ATTACHMENT_DELETE:
|
||||||
|
Attachment.withId(payload.id).delete();
|
||||||
|
|
||||||
|
break;
|
||||||
|
case ActionTypes.ATTACHMENT_CREATE_SUCCEEDED:
|
||||||
|
Attachment.withId(payload.localId).delete();
|
||||||
|
Attachment.upsert(payload.attachment);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case ActionTypes.ATTACHMENT_UPDATE_RECEIVED:
|
||||||
|
Attachment.withId(payload.attachment.id).update(payload.attachment);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case ActionTypes.ATTACHMENT_DELETE_RECEIVED:
|
||||||
|
Attachment.withId(payload.attachment.id).delete();
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -145,6 +145,10 @@ export default class extends Model {
|
||||||
return this.tasks.orderBy('id');
|
return this.tasks.orderBy('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOrderedAttachmentsQuerySet() {
|
||||||
|
return this.attachments.orderBy('id', false);
|
||||||
|
}
|
||||||
|
|
||||||
getOrderedInCardActionsQuerySet() {
|
getOrderedInCardActionsQuerySet() {
|
||||||
return this.actions.orderBy('id', false);
|
return this.actions.orderBy('id', false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default class extends Model {
|
||||||
id: attr(),
|
id: attr(),
|
||||||
email: attr(),
|
email: attr(),
|
||||||
name: attr(),
|
name: attr(),
|
||||||
avatar: attr(),
|
avatarUrl: attr(),
|
||||||
phone: attr(),
|
phone: attr(),
|
||||||
organization: attr(),
|
organization: attr(),
|
||||||
subscribeToOwnCards: attr(),
|
subscribeToOwnCards: attr(),
|
||||||
|
@ -44,7 +44,7 @@ export default class extends Model {
|
||||||
isAdmin: attr({
|
isAdmin: attr({
|
||||||
getDefault: () => false,
|
getDefault: () => false,
|
||||||
}),
|
}),
|
||||||
isAvatarUploading: attr({
|
isAvatarUpdating: attr({
|
||||||
getDefault: () => false,
|
getDefault: () => false,
|
||||||
}),
|
}),
|
||||||
emailUpdateForm: attr({
|
emailUpdateForm: attr({
|
||||||
|
@ -228,22 +228,22 @@ export default class extends Model {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ActionTypes.USER_AVATAR_UPLOAD_REQUESTED:
|
case ActionTypes.USER_AVATAR_UPDATE_REQUESTED:
|
||||||
User.withId(payload.id).update({
|
User.withId(payload.id).update({
|
||||||
isAvatarUploading: true,
|
isAvatarUpdating: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.USER_AVATAR_UPLOAD_SUCCEEDED:
|
case ActionTypes.USER_AVATAR_UPDATE_SUCCEEDED:
|
||||||
User.withId(payload.id).update({
|
User.withId(payload.id).update({
|
||||||
avatar: payload.avatar,
|
avatarUrl: payload.avatarUrl,
|
||||||
isAvatarUploading: false,
|
isAvatarUpdating: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.USER_AVATAR_UPLOAD_FAILED:
|
case ActionTypes.USER_AVATAR_UPDATE_FAILED:
|
||||||
User.withId(payload.id).update({
|
User.withId(payload.id).update({
|
||||||
isAvatarUploading: false,
|
isAvatarUpdating: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -6,7 +6,20 @@ import List from './List';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
import Card from './Card';
|
import Card from './Card';
|
||||||
import Task from './Task';
|
import Task from './Task';
|
||||||
|
import Attachment from './Attachment';
|
||||||
import Action from './Action';
|
import Action from './Action';
|
||||||
import Notification from './Notification';
|
import Notification from './Notification';
|
||||||
|
|
||||||
export { User, Project, ProjectMembership, Board, List, Label, Card, Task, Action, Notification };
|
export {
|
||||||
|
User,
|
||||||
|
Project,
|
||||||
|
ProjectMembership,
|
||||||
|
Board,
|
||||||
|
List,
|
||||||
|
Label,
|
||||||
|
Card,
|
||||||
|
Task,
|
||||||
|
Attachment,
|
||||||
|
Action,
|
||||||
|
Notification,
|
||||||
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { ORM } from 'redux-orm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
|
Attachment,
|
||||||
Board,
|
Board,
|
||||||
Card,
|
Card,
|
||||||
Label,
|
Label,
|
||||||
|
@ -26,6 +27,7 @@ orm.register(
|
||||||
Label,
|
Label,
|
||||||
Card,
|
Card,
|
||||||
Task,
|
Task,
|
||||||
|
Attachment,
|
||||||
Action,
|
Action,
|
||||||
Notification,
|
Notification,
|
||||||
);
|
);
|
||||||
|
|
92
client/src/sagas/app/requests/attachment.js
Normal file
92
client/src/sagas/app/requests/attachment.js
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import { call, put } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import request from './request';
|
||||||
|
import {
|
||||||
|
createAttachmentFailed,
|
||||||
|
createAttachmentRequested,
|
||||||
|
createAttachmentSucceeded,
|
||||||
|
deleteAttachmentFailed,
|
||||||
|
deleteAttachmentRequested,
|
||||||
|
deleteAttachmentSucceeded,
|
||||||
|
updateAttachmentFailed,
|
||||||
|
updateAttachmentRequested,
|
||||||
|
updateAttachmentSucceeded,
|
||||||
|
} from '../../../actions';
|
||||||
|
import api from '../../../api';
|
||||||
|
|
||||||
|
export function* createAttachmentRequest(cardId, localId, data) {
|
||||||
|
yield put(
|
||||||
|
createAttachmentRequested(localId, {
|
||||||
|
...data,
|
||||||
|
cardId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { item } = yield call(request, api.createAttachment, cardId, data);
|
||||||
|
|
||||||
|
const action = createAttachmentSucceeded(localId, item);
|
||||||
|
yield put(action);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
payload: action.payload,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const action = createAttachmentFailed(localId, error);
|
||||||
|
yield put(action);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
payload: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* updateAttachmentRequest(id, data) {
|
||||||
|
yield put(updateAttachmentRequested(id, data));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { item } = yield call(request, api.updateAttachment, id, data);
|
||||||
|
|
||||||
|
const action = updateAttachmentSucceeded(item);
|
||||||
|
yield put(action);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
payload: action.payload,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const action = updateAttachmentFailed(error);
|
||||||
|
yield put(action);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
payload: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* deleteAttachmentRequest(id) {
|
||||||
|
yield put(deleteAttachmentRequested(id));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { item } = yield call(request, api.deleteAttachment, id);
|
||||||
|
|
||||||
|
const action = deleteAttachmentSucceeded(item);
|
||||||
|
yield put(action);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
payload: action.payload,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const action = deleteAttachmentFailed(error);
|
||||||
|
yield put(action);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
payload: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,7 +55,7 @@ export function* fetchBoardRequest(id) {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
item,
|
item,
|
||||||
included: { lists, labels, cards, cardMemberships, cardLabels, tasks },
|
included: { lists, labels, cards, cardMemberships, cardLabels, tasks, attachments },
|
||||||
} = yield call(request, api.getBoard, id);
|
} = yield call(request, api.getBoard, id);
|
||||||
|
|
||||||
const action = fetchBoardSucceeded(
|
const action = fetchBoardSucceeded(
|
||||||
|
@ -66,6 +66,7 @@ export function* fetchBoardRequest(id) {
|
||||||
cardMemberships,
|
cardMemberships,
|
||||||
cardLabels,
|
cardLabels,
|
||||||
tasks,
|
tasks,
|
||||||
|
attachments,
|
||||||
);
|
);
|
||||||
yield put(action);
|
yield put(action);
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ export * from './card';
|
||||||
export * from './card-membership';
|
export * from './card-membership';
|
||||||
export * from './card-label';
|
export * from './card-label';
|
||||||
export * from './task';
|
export * from './task';
|
||||||
|
export * from './attachment';
|
||||||
export * from './actions';
|
export * from './actions';
|
||||||
export * from './comment-action';
|
export * from './comment-action';
|
||||||
export * from './notifications';
|
export * from './notifications';
|
||||||
|
|
|
@ -11,6 +11,9 @@ import {
|
||||||
fetchCurrentUserFailed,
|
fetchCurrentUserFailed,
|
||||||
fetchCurrentUserRequested,
|
fetchCurrentUserRequested,
|
||||||
fetchCurrentUserSucceeded,
|
fetchCurrentUserSucceeded,
|
||||||
|
updateUserAvatarFailed,
|
||||||
|
updateUserAvatarRequested,
|
||||||
|
updateUserAvatarSucceeded,
|
||||||
updateUserEmailFailed,
|
updateUserEmailFailed,
|
||||||
updateUserEmailRequested,
|
updateUserEmailRequested,
|
||||||
updateUserEmailSucceeded,
|
updateUserEmailSucceeded,
|
||||||
|
@ -23,9 +26,6 @@ import {
|
||||||
updateUserUsernameFailed,
|
updateUserUsernameFailed,
|
||||||
updateUserUsernameRequested,
|
updateUserUsernameRequested,
|
||||||
updateUserUsernameSucceeded,
|
updateUserUsernameSucceeded,
|
||||||
uploadUserAvatarFailed,
|
|
||||||
uploadUserAvatarRequested,
|
|
||||||
uploadUserAvatarSucceeded,
|
|
||||||
} from '../../../actions';
|
} from '../../../actions';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
|
|
||||||
|
@ -173,13 +173,13 @@ export function* updateUserUsernameRequest(id, data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* uploadUserAvatarRequest(id, file) {
|
export function* updateUserAvatarRequest(id, data) {
|
||||||
yield put(uploadUserAvatarRequested(id));
|
yield put(updateUserAvatarRequested(id));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { item } = yield call(request, api.uploadUserAvatar, id, file);
|
const { item } = yield call(request, api.updateUserAvatar, id, data);
|
||||||
|
|
||||||
const action = uploadUserAvatarSucceeded(id, item);
|
const action = updateUserAvatarSucceeded(id, item);
|
||||||
yield put(action);
|
yield put(action);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -187,7 +187,7 @@ export function* uploadUserAvatarRequest(id, file) {
|
||||||
payload: action.payload,
|
payload: action.payload,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const action = uploadUserAvatarFailed(id, error);
|
const action = updateUserAvatarFailed(id, error);
|
||||||
yield put(action);
|
yield put(action);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
40
client/src/sagas/app/services/attachment.js
Normal file
40
client/src/sagas/app/services/attachment.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { call, put, select } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createAttachmentRequest,
|
||||||
|
deleteAttachmentRequest,
|
||||||
|
updateAttachmentRequest,
|
||||||
|
} from '../requests';
|
||||||
|
import { pathSelector } from '../../../selectors';
|
||||||
|
import { createAttachment, deleteAttachment, updateAttachment } from '../../../actions';
|
||||||
|
import { createLocalId } from '../../../utils/local-id';
|
||||||
|
|
||||||
|
export function* createAttachmentService(cardId, data) {
|
||||||
|
const localId = yield call(createLocalId);
|
||||||
|
|
||||||
|
yield put(
|
||||||
|
createAttachment({
|
||||||
|
cardId,
|
||||||
|
id: localId,
|
||||||
|
name: data.file.name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
yield call(createAttachmentRequest, cardId, localId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* createAttachmentInCurrentCardService(data) {
|
||||||
|
const { cardId } = yield select(pathSelector);
|
||||||
|
|
||||||
|
yield call(createAttachmentService, cardId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* updateAttachmentService(id, data) {
|
||||||
|
yield put(updateAttachment(id, data));
|
||||||
|
yield call(updateAttachmentRequest, id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* deleteAttachmentService(id) {
|
||||||
|
yield put(deleteAttachment(id));
|
||||||
|
yield call(deleteAttachmentRequest, id);
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ export * from './list';
|
||||||
export * from './label';
|
export * from './label';
|
||||||
export * from './card';
|
export * from './card';
|
||||||
export * from './task';
|
export * from './task';
|
||||||
|
export * from './attachment';
|
||||||
export * from './actions';
|
export * from './actions';
|
||||||
export * from './comment-action';
|
export * from './comment-action';
|
||||||
export * from './notifications';
|
export * from './notifications';
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
} from '../../../selectors';
|
} from '../../../selectors';
|
||||||
import {
|
import {
|
||||||
createActionReceived,
|
createActionReceived,
|
||||||
|
createAttachmentReceived,
|
||||||
createBoardReceived,
|
createBoardReceived,
|
||||||
createCardLabelReceived,
|
createCardLabelReceived,
|
||||||
createCardMembershipReceived,
|
createCardMembershipReceived,
|
||||||
|
@ -24,6 +25,7 @@ import {
|
||||||
createTaskReceived,
|
createTaskReceived,
|
||||||
createUserReceived,
|
createUserReceived,
|
||||||
deleteActionReceived,
|
deleteActionReceived,
|
||||||
|
deleteAttachmentReceived,
|
||||||
deleteCardLabelReceived,
|
deleteCardLabelReceived,
|
||||||
deleteCardMembershipReceived,
|
deleteCardMembershipReceived,
|
||||||
deleteCardReceived,
|
deleteCardReceived,
|
||||||
|
@ -38,6 +40,7 @@ import {
|
||||||
socketDisconnected,
|
socketDisconnected,
|
||||||
socketReconnected,
|
socketReconnected,
|
||||||
updateActionReceived,
|
updateActionReceived,
|
||||||
|
updateAttachmentReceived,
|
||||||
updateBoardReceived,
|
updateBoardReceived,
|
||||||
updateCardReceived,
|
updateCardReceived,
|
||||||
updateLabelReceived,
|
updateLabelReceived,
|
||||||
|
@ -205,6 +208,18 @@ export function* deleteTaskReceivedService(task) {
|
||||||
yield put(deleteTaskReceived(task));
|
yield put(deleteTaskReceived(task));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* createAttachmentReceivedService(attachment) {
|
||||||
|
yield put(createAttachmentReceived(attachment));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* updateAttachmentReceivedService(attachment) {
|
||||||
|
yield put(updateAttachmentReceived(attachment));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* deleteAttachmentReceivedService(attachment) {
|
||||||
|
yield put(deleteAttachmentReceived(attachment));
|
||||||
|
}
|
||||||
|
|
||||||
export function* createActionReceivedService(action) {
|
export function* createActionReceivedService(action) {
|
||||||
yield put(createActionReceived(action));
|
yield put(createActionReceived(action));
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,11 @@ import {
|
||||||
createUserRequest,
|
createUserRequest,
|
||||||
deleteCardMembershipRequest,
|
deleteCardMembershipRequest,
|
||||||
deleteUserRequest,
|
deleteUserRequest,
|
||||||
|
updateUserAvatarRequest,
|
||||||
updateUserEmailRequest,
|
updateUserEmailRequest,
|
||||||
updateUserPasswordRequest,
|
updateUserPasswordRequest,
|
||||||
updateUserRequest,
|
updateUserRequest,
|
||||||
updateUserUsernameRequest,
|
updateUserUsernameRequest,
|
||||||
uploadUserAvatarRequest,
|
|
||||||
} from '../requests';
|
} from '../requests';
|
||||||
import { currentUserIdSelector, pathSelector } from '../../../selectors';
|
import { currentUserIdSelector, pathSelector } from '../../../selectors';
|
||||||
import {
|
import {
|
||||||
|
@ -106,14 +106,14 @@ export function* clearCurrentUserUsernameUpdateErrorService() {
|
||||||
yield call(clearUserUsernameUpdateErrorService, id);
|
yield call(clearUserUsernameUpdateErrorService, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* uploadUserAvatarService(id, file) {
|
export function* updateUserAvatarService(id, data) {
|
||||||
yield call(uploadUserAvatarRequest, id, file);
|
yield call(updateUserAvatarRequest, id, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* uploadCurrentUserAvatarService(file) {
|
export function* updateCurrentUserAvatarService(data) {
|
||||||
const id = yield select(currentUserIdSelector);
|
const id = yield select(currentUserIdSelector);
|
||||||
|
|
||||||
yield call(uploadUserAvatarService, id, file);
|
yield call(updateUserAvatarService, id, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* deleteUserService(id) {
|
export function* deleteUserService(id) {
|
||||||
|
|
22
client/src/sagas/app/watchers/attachment.js
Normal file
22
client/src/sagas/app/watchers/attachment.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { all, takeLatest } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createAttachmentInCurrentCardService,
|
||||||
|
deleteAttachmentService,
|
||||||
|
updateAttachmentService,
|
||||||
|
} from '../services';
|
||||||
|
import EntryActionTypes from '../../../constants/EntryActionTypes';
|
||||||
|
|
||||||
|
export default function* () {
|
||||||
|
yield all([
|
||||||
|
takeLatest(EntryActionTypes.ATTACHMENT_IN_CURRENT_CARD_CREATE, ({ payload: { data } }) =>
|
||||||
|
createAttachmentInCurrentCardService(data),
|
||||||
|
),
|
||||||
|
takeLatest(EntryActionTypes.ATTACHMENT_UPDATE, ({ payload: { id, data } }) =>
|
||||||
|
updateAttachmentService(id, data),
|
||||||
|
),
|
||||||
|
takeLatest(EntryActionTypes.ATTACHMENT_DELETE, ({ payload: { id } }) =>
|
||||||
|
deleteAttachmentService(id),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import list from './list';
|
||||||
import label from './label';
|
import label from './label';
|
||||||
import card from './card';
|
import card from './card';
|
||||||
import task from './task';
|
import task from './task';
|
||||||
|
import attachment from './attachment';
|
||||||
import actions from './actions';
|
import actions from './actions';
|
||||||
import commentAction from './comment-action';
|
import commentAction from './comment-action';
|
||||||
import notification from './notification';
|
import notification from './notification';
|
||||||
|
@ -27,6 +28,7 @@ export default [
|
||||||
label,
|
label,
|
||||||
card,
|
card,
|
||||||
task,
|
task,
|
||||||
|
attachment,
|
||||||
actions,
|
actions,
|
||||||
commentAction,
|
commentAction,
|
||||||
notification,
|
notification,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { call, cancelled, take } from 'redux-saga/effects';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createActionReceivedService,
|
createActionReceivedService,
|
||||||
|
createAttachmentReceivedService,
|
||||||
createBoardReceivedService,
|
createBoardReceivedService,
|
||||||
createCardLabelReceivedService,
|
createCardLabelReceivedService,
|
||||||
createCardMembershipReceivedService,
|
createCardMembershipReceivedService,
|
||||||
|
@ -15,6 +16,7 @@ import {
|
||||||
createTaskReceivedService,
|
createTaskReceivedService,
|
||||||
createUserReceivedService,
|
createUserReceivedService,
|
||||||
deleteActionReceivedService,
|
deleteActionReceivedService,
|
||||||
|
deleteAttachmentReceivedService,
|
||||||
deleteCardLabelReceivedService,
|
deleteCardLabelReceivedService,
|
||||||
deleteCardMembershipReceivedService,
|
deleteCardMembershipReceivedService,
|
||||||
deleteCardReceivedService,
|
deleteCardReceivedService,
|
||||||
|
@ -29,6 +31,7 @@ import {
|
||||||
socketDisconnectedService,
|
socketDisconnectedService,
|
||||||
socketReconnectedService,
|
socketReconnectedService,
|
||||||
updateActionReceivedService,
|
updateActionReceivedService,
|
||||||
|
updateAttachmentReceivedService,
|
||||||
updateBoardReceivedService,
|
updateBoardReceivedService,
|
||||||
updateCardReceivedService,
|
updateCardReceivedService,
|
||||||
updateLabelReceivedService,
|
updateLabelReceivedService,
|
||||||
|
@ -153,6 +156,18 @@ const createSocketEventsChannel = () =>
|
||||||
emit([deleteTaskReceivedService, item]);
|
emit([deleteTaskReceivedService, item]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAttachmentCreate = api.makeHandleAttachmentCreate(({ item }) => {
|
||||||
|
emit([createAttachmentReceivedService, item]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAttachmentUpdate = api.makeHandleAttachmentUpdate(({ item }) => {
|
||||||
|
emit([updateAttachmentReceivedService, item]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAttachmentDelete = api.makeHandleAttachmentDelete(({ item }) => {
|
||||||
|
emit([deleteAttachmentReceivedService, item]);
|
||||||
|
});
|
||||||
|
|
||||||
const handleActionCreate = api.makeHandleActionCreate(({ item }) => {
|
const handleActionCreate = api.makeHandleActionCreate(({ item }) => {
|
||||||
emit([createActionReceivedService, item]);
|
emit([createActionReceivedService, item]);
|
||||||
});
|
});
|
||||||
|
@ -222,6 +237,10 @@ const createSocketEventsChannel = () =>
|
||||||
socket.on('taskUpdate', handleTaskUpdate);
|
socket.on('taskUpdate', handleTaskUpdate);
|
||||||
socket.on('taskDelete', handleTaskDelete);
|
socket.on('taskDelete', handleTaskDelete);
|
||||||
|
|
||||||
|
socket.on('attachmentCreate', handleAttachmentCreate);
|
||||||
|
socket.on('attachmentUpdate', handleAttachmentUpdate);
|
||||||
|
socket.on('attachmentDelete', handleAttachmentDelete);
|
||||||
|
|
||||||
socket.on('actionCreate', handleActionCreate);
|
socket.on('actionCreate', handleActionCreate);
|
||||||
socket.on('actionUpdate', handleActionUpdate);
|
socket.on('actionUpdate', handleActionUpdate);
|
||||||
socket.on('actionDelete', handleActionDelete);
|
socket.on('actionDelete', handleActionDelete);
|
||||||
|
@ -270,6 +289,10 @@ const createSocketEventsChannel = () =>
|
||||||
socket.off('taskUpdate', handleTaskUpdate);
|
socket.off('taskUpdate', handleTaskUpdate);
|
||||||
socket.off('taskDelete', handleTaskDelete);
|
socket.off('taskDelete', handleTaskDelete);
|
||||||
|
|
||||||
|
socket.off('attachmentCreate', handleAttachmentCreate);
|
||||||
|
socket.off('attachmentUpdate', handleAttachmentUpdate);
|
||||||
|
socket.off('attachmentDelete', handleAttachmentDelete);
|
||||||
|
|
||||||
socket.off('actionCreate', handleActionCreate);
|
socket.off('actionCreate', handleActionCreate);
|
||||||
socket.off('actionUpdate', handleActionUpdate);
|
socket.off('actionUpdate', handleActionUpdate);
|
||||||
socket.off('actionDelete', handleActionDelete);
|
socket.off('actionDelete', handleActionDelete);
|
||||||
|
|
|
@ -14,11 +14,11 @@ import {
|
||||||
removeUserFromCurrentCardService,
|
removeUserFromCurrentCardService,
|
||||||
removeUserFromFilterInCurrentBoardService,
|
removeUserFromFilterInCurrentBoardService,
|
||||||
updateUserService,
|
updateUserService,
|
||||||
|
updateCurrentUserAvatarService,
|
||||||
updateCurrentUserEmailService,
|
updateCurrentUserEmailService,
|
||||||
updateCurrentUserPasswordService,
|
updateCurrentUserPasswordService,
|
||||||
updateCurrentUserService,
|
updateCurrentUserService,
|
||||||
updateCurrentUserUsernameService,
|
updateCurrentUserUsernameService,
|
||||||
uploadCurrentUserAvatarService,
|
|
||||||
} from '../services';
|
} from '../services';
|
||||||
import EntryActionTypes from '../../../constants/EntryActionTypes';
|
import EntryActionTypes from '../../../constants/EntryActionTypes';
|
||||||
|
|
||||||
|
@ -50,8 +50,8 @@ export default function* () {
|
||||||
takeLatest(EntryActionTypes.CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR, () =>
|
takeLatest(EntryActionTypes.CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR, () =>
|
||||||
clearCurrentUserUsernameUpdateErrorService(),
|
clearCurrentUserUsernameUpdateErrorService(),
|
||||||
),
|
),
|
||||||
takeLatest(EntryActionTypes.CURRENT_USER_AVATAR_UPLOAD, ({ payload: { file } }) =>
|
takeLatest(EntryActionTypes.CURRENT_USER_AVATAR_UPDATE, ({ payload: { data } }) =>
|
||||||
uploadCurrentUserAvatarService(file),
|
updateCurrentUserAvatarService(data),
|
||||||
),
|
),
|
||||||
takeLatest(EntryActionTypes.USER_DELETE, ({ payload: { id } }) => deleteUserService(id)),
|
takeLatest(EntryActionTypes.USER_DELETE, ({ payload: { id } }) => deleteUserService(id)),
|
||||||
takeLatest(EntryActionTypes.USER_TO_CARD_ADD, ({ payload: { id, cardId } }) =>
|
takeLatest(EntryActionTypes.USER_TO_CARD_ADD, ({ payload: { id, cardId } }) =>
|
||||||
|
|
|
@ -336,6 +336,30 @@ export const tasksForCurrentCardSelector = createSelector(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const attachmentsForCurrentCardSelector = createSelector(
|
||||||
|
orm,
|
||||||
|
(state) => pathSelector(state).cardId,
|
||||||
|
({ Card }, id) => {
|
||||||
|
if (!id) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardModel = Card.withId(id);
|
||||||
|
|
||||||
|
if (!cardModel) {
|
||||||
|
return cardModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cardModel
|
||||||
|
.getOrderedAttachmentsQuerySet()
|
||||||
|
.toRefArray()
|
||||||
|
.map((attachment) => ({
|
||||||
|
...attachment,
|
||||||
|
isPersisted: !isLocalId(attachment.id),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const actionsForCurrentCardSelector = createSelector(
|
export const actionsForCurrentCardSelector = createSelector(
|
||||||
orm,
|
orm,
|
||||||
(state) => pathSelector(state).cardId,
|
(state) => pathSelector(state).cardId,
|
||||||
|
|
|
@ -13,7 +13,8 @@ services:
|
||||||
done; (exit $$s)"
|
done; (exit $$s)"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- uploads:/app/public/uploads
|
- user-avatars:/app/public/user-avatars
|
||||||
|
- attachments:/app/public/attachments
|
||||||
ports:
|
ports:
|
||||||
- 3000:1337
|
- 3000:1337
|
||||||
environment:
|
environment:
|
||||||
|
@ -30,7 +31,9 @@ services:
|
||||||
- db-data:/var/lib/postgresql/data
|
- db-data:/var/lib/postgresql/data
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=planka
|
- POSTGRES_DB=planka
|
||||||
|
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
uploads:
|
user-avatars:
|
||||||
|
attachments:
|
||||||
db-data:
|
db-data:
|
||||||
|
|
9
server/.gitignore
vendored
9
server/.gitignore
vendored
|
@ -128,9 +128,12 @@ lib-cov
|
||||||
*.pid
|
*.pid
|
||||||
|
|
||||||
public/*
|
public/*
|
||||||
!public/uploads
|
!public/user-avatars
|
||||||
public/uploads/*
|
public/user-avatars/*
|
||||||
!public/uploads/.gitkeep
|
!public/user-avatars/.gitkeep
|
||||||
|
!public/attachments
|
||||||
|
public/attachments/*
|
||||||
|
!public/attachments/.gitkeep
|
||||||
|
|
||||||
views/*
|
views/*
|
||||||
!views/.gitkeep
|
!views/.gitkeep
|
||||||
|
|
68
server/api/controllers/attachments/create.js
Normal file
68
server/api/controllers/attachments/create.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
const Errors = {
|
||||||
|
CARD_NOT_FOUND: {
|
||||||
|
cardNotFound: 'Card not found',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
cardId: {
|
||||||
|
type: 'string',
|
||||||
|
regex: /^[0-9]+$/,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
cardNotFound: {
|
||||||
|
responseType: 'notFound',
|
||||||
|
},
|
||||||
|
uploadError: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs, exits) {
|
||||||
|
const { currentUser } = this.req;
|
||||||
|
|
||||||
|
const { card, project } = await sails.helpers
|
||||||
|
.getCardToProjectPath(inputs.cardId)
|
||||||
|
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
|
||||||
|
|
||||||
|
const isUserMemberForProject = await sails.helpers.isUserMemberForProject(
|
||||||
|
project.id,
|
||||||
|
currentUser.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUserMemberForProject) {
|
||||||
|
throw Errors.CARD_NOT_FOUND; // Forbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
this.req.file('file').upload(sails.helpers.createAttachmentReceiver(), async (error, files) => {
|
||||||
|
if (error) {
|
||||||
|
return exits.uploadError(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
return exits.uploadError('No file was uploaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
const attachment = await sails.helpers.createAttachment(
|
||||||
|
card,
|
||||||
|
{
|
||||||
|
dirname: file.extra.dirname,
|
||||||
|
filename: file.filename,
|
||||||
|
isImage: file.extra.isImage,
|
||||||
|
name: file.filename,
|
||||||
|
},
|
||||||
|
this.req,
|
||||||
|
);
|
||||||
|
|
||||||
|
return exits.success({
|
||||||
|
item: attachment.toJSON(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
51
server/api/controllers/attachments/delete.js
Executable file
51
server/api/controllers/attachments/delete.js
Executable file
|
@ -0,0 +1,51 @@
|
||||||
|
const Errors = {
|
||||||
|
ATTACHMENT_NOT_FOUND: {
|
||||||
|
attachmentNotFound: 'Attachment not found',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
regex: /^[0-9]+$/,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
attachmentNotFound: {
|
||||||
|
responseType: 'notFound',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs, exits) {
|
||||||
|
const { currentUser } = this.req;
|
||||||
|
|
||||||
|
const attachmentToProjectPath = await sails.helpers
|
||||||
|
.getAttachmentToProjectPath(inputs.id)
|
||||||
|
.intercept('pathNotFound', () => Errors.ATTACHMENT_NOT_FOUND);
|
||||||
|
|
||||||
|
let { attachment } = attachmentToProjectPath;
|
||||||
|
const { board, project } = attachmentToProjectPath;
|
||||||
|
|
||||||
|
const isUserMemberForProject = await sails.helpers.isUserMemberForProject(
|
||||||
|
project.id,
|
||||||
|
currentUser.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUserMemberForProject) {
|
||||||
|
throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
attachment = await sails.helpers.deleteAttachment(attachment, board, this.req);
|
||||||
|
|
||||||
|
if (!attachment) {
|
||||||
|
throw Errors.ATTACHMENT_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exits.success({
|
||||||
|
item: attachment,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
57
server/api/controllers/attachments/update.js
Executable file
57
server/api/controllers/attachments/update.js
Executable file
|
@ -0,0 +1,57 @@
|
||||||
|
const Errors = {
|
||||||
|
ATTACHMENT_NOT_FOUND: {
|
||||||
|
attachmentNotFound: 'Attachment not found',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
regex: /^[0-9]+$/,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
isNotEmptyString: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
attachmentNotFound: {
|
||||||
|
responseType: 'notFound',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs, exits) {
|
||||||
|
const { currentUser } = this.req;
|
||||||
|
|
||||||
|
const attachmentToProjectPath = await sails.helpers
|
||||||
|
.getAttachmentToProjectPath(inputs.id)
|
||||||
|
.intercept('pathNotFound', () => Errors.ATTACHMENT_NOT_FOUND);
|
||||||
|
|
||||||
|
let { attachment } = attachmentToProjectPath;
|
||||||
|
const { board, project } = attachmentToProjectPath;
|
||||||
|
|
||||||
|
const isUserMemberForProject = await sails.helpers.isUserMemberForProject(
|
||||||
|
project.id,
|
||||||
|
currentUser.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUserMemberForProject) {
|
||||||
|
throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = _.pick(inputs, ['name']);
|
||||||
|
|
||||||
|
attachment = await sails.helpers.updateAttachment(attachment, values, board, this.req);
|
||||||
|
|
||||||
|
if (!attachment) {
|
||||||
|
throw Errors.ATTACHMENT_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exits.success({
|
||||||
|
item: attachment,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -55,6 +55,7 @@ module.exports = {
|
||||||
const cardLabels = await sails.helpers.getCardLabelsForCard(cardIds);
|
const cardLabels = await sails.helpers.getCardLabelsForCard(cardIds);
|
||||||
|
|
||||||
const tasks = await sails.helpers.getTasksForCard(cardIds);
|
const tasks = await sails.helpers.getTasksForCard(cardIds);
|
||||||
|
const attachments = await sails.helpers.getAttachmentsForCard(cardIds);
|
||||||
|
|
||||||
const isSubscribedByCardId = cardSubscriptions.reduce(
|
const isSubscribedByCardId = cardSubscriptions.reduce(
|
||||||
(result, cardSubscription) => ({
|
(result, cardSubscription) => ({
|
||||||
|
@ -80,6 +81,7 @@ module.exports = {
|
||||||
cardMemberships,
|
cardMemberships,
|
||||||
cardLabels,
|
cardLabels,
|
||||||
tasks,
|
tasks,
|
||||||
|
attachments,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
67
server/api/controllers/users/update-avatar.js
Executable file
67
server/api/controllers/users/update-avatar.js
Executable file
|
@ -0,0 +1,67 @@
|
||||||
|
const Errors = {
|
||||||
|
USER_NOT_FOUND: {
|
||||||
|
userNotFound: 'User not found',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
regex: /^[0-9]+$/,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
userNotFound: {
|
||||||
|
responseType: 'notFound',
|
||||||
|
},
|
||||||
|
uploadError: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs, exits) {
|
||||||
|
const { currentUser } = this.req;
|
||||||
|
|
||||||
|
let user;
|
||||||
|
if (currentUser.isAdmin) {
|
||||||
|
user = await sails.helpers.getUser(inputs.id);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw Errors.USER_NOT_FOUND;
|
||||||
|
}
|
||||||
|
} else if (inputs.id !== currentUser.id) {
|
||||||
|
throw Errors.USER_NOT_FOUND; // Forbidden
|
||||||
|
} else {
|
||||||
|
user = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.req.file('file').upload(sails.helpers.createAvatarReceiver(), async (error, files) => {
|
||||||
|
if (error) {
|
||||||
|
return exits.uploadError(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
return exits.uploadError('No file was uploaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
user = await sails.helpers.updateUser(
|
||||||
|
user,
|
||||||
|
{
|
||||||
|
avatarDirname: files[0].extra.dirname,
|
||||||
|
},
|
||||||
|
this.req,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw Errors.USER_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exits.success({
|
||||||
|
item: user.toJSON().avatarUrl,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -18,7 +18,7 @@ module.exports = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
isNotEmptyString: true,
|
isNotEmptyString: true,
|
||||||
},
|
},
|
||||||
avatar: {
|
avatarUrl: {
|
||||||
type: 'json',
|
type: 'json',
|
||||||
custom: (value) => _.isNull(value),
|
custom: (value) => _.isNull(value),
|
||||||
},
|
},
|
||||||
|
@ -63,7 +63,7 @@ module.exports = {
|
||||||
const values = _.pick(inputs, [
|
const values = _.pick(inputs, [
|
||||||
'isAdmin',
|
'isAdmin',
|
||||||
'name',
|
'name',
|
||||||
'avatar',
|
'avatarUrl',
|
||||||
'phone',
|
'phone',
|
||||||
'organization',
|
'organization',
|
||||||
'subscribeToOwnCards',
|
'subscribeToOwnCards',
|
||||||
|
|
|
@ -1,122 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const util = require('util');
|
|
||||||
const stream = require('stream');
|
|
||||||
const { v4: uuid } = require('uuid');
|
|
||||||
const sharp = require('sharp');
|
|
||||||
|
|
||||||
const Errors = {
|
|
||||||
USER_NOT_FOUND: {
|
|
||||||
userNotFound: 'User not found',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
|
||||||
|
|
||||||
const createReceiver = () => {
|
|
||||||
const receiver = stream.Writable({ objectMode: true });
|
|
||||||
|
|
||||||
let firstFileHandled = false;
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
|
||||||
receiver._write = async (file, receiverEncoding, done) => {
|
|
||||||
if (firstFileHandled) {
|
|
||||||
file.pipe(
|
|
||||||
new stream.Writable({
|
|
||||||
write(chunk, streamEncoding, callback) {
|
|
||||||
callback();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return done();
|
|
||||||
}
|
|
||||||
firstFileHandled = true;
|
|
||||||
|
|
||||||
const resize = sharp().resize(100, 100).jpeg();
|
|
||||||
|
|
||||||
const transform = new stream.Transform({
|
|
||||||
transform(chunk, streamEncoding, callback) {
|
|
||||||
callback(null, chunk);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await pipeline(file, resize, transform);
|
|
||||||
|
|
||||||
file.fd = `${uuid()}.jpg`; // eslint-disable-line no-param-reassign
|
|
||||||
|
|
||||||
await pipeline(
|
|
||||||
transform,
|
|
||||||
fs.createWriteStream(path.join(sails.config.custom.uploadsPath, file.fd)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return done();
|
|
||||||
} catch (error) {
|
|
||||||
return done(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return receiver;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
inputs: {
|
|
||||||
id: {
|
|
||||||
type: 'string',
|
|
||||||
regex: /^[0-9]+$/,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
exits: {
|
|
||||||
userNotFound: {
|
|
||||||
responseType: 'notFound',
|
|
||||||
},
|
|
||||||
uploadError: {
|
|
||||||
responseType: 'unprocessableEntity',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fn(inputs, exits) {
|
|
||||||
const { currentUser } = this.req;
|
|
||||||
|
|
||||||
let user;
|
|
||||||
if (currentUser.isAdmin) {
|
|
||||||
user = await sails.helpers.getUser(inputs.id);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw Errors.USER_NOT_FOUND;
|
|
||||||
}
|
|
||||||
} else if (inputs.id !== currentUser.id) {
|
|
||||||
throw Errors.USER_NOT_FOUND; // Forbidden
|
|
||||||
} else {
|
|
||||||
user = currentUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.req.file('file').upload(createReceiver(), async (error, files) => {
|
|
||||||
if (error) {
|
|
||||||
return exits.uploadError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
return exits.uploadError('No file was uploaded');
|
|
||||||
}
|
|
||||||
|
|
||||||
user = await sails.helpers.updateUser(
|
|
||||||
user,
|
|
||||||
{
|
|
||||||
avatar: files[0].fd,
|
|
||||||
},
|
|
||||||
this.req,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw Errors.USER_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
return exits.success({
|
|
||||||
item: user.toJSON().avatar,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
65
server/api/helpers/create-attachment-receiver.js
Normal file
65
server/api/helpers/create-attachment-receiver.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const util = require('util');
|
||||||
|
const stream = require('stream');
|
||||||
|
const streamToArray = require('stream-to-array');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
|
||||||
|
const writeFile = util.promisify(fs.writeFile);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sync: true,
|
||||||
|
|
||||||
|
fn(inputs, exits) {
|
||||||
|
const receiver = stream.Writable({
|
||||||
|
objectMode: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let firstFileHandled = false;
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
receiver._write = async (file, receiverEncoding, done) => {
|
||||||
|
if (firstFileHandled) {
|
||||||
|
file.pipe(new stream.Writable());
|
||||||
|
|
||||||
|
return done();
|
||||||
|
}
|
||||||
|
firstFileHandled = true;
|
||||||
|
|
||||||
|
const buffer = await streamToArray(file).then((parts) =>
|
||||||
|
Buffer.concat(parts.map((part) => (util.isBuffer(part) ? part : Buffer.from(part)))),
|
||||||
|
);
|
||||||
|
|
||||||
|
let thumbnailBuffer;
|
||||||
|
|
||||||
|
try {
|
||||||
|
thumbnailBuffer = await sharp(buffer).resize(240, 240).jpeg().toBuffer();
|
||||||
|
} catch (error) {} // eslint-disable-line no-empty
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dirname = uuid();
|
||||||
|
const dirPath = path.join(sails.config.custom.attachmentsPath, dirname);
|
||||||
|
|
||||||
|
fs.mkdirSync(dirPath);
|
||||||
|
|
||||||
|
if (thumbnailBuffer) {
|
||||||
|
await writeFile(path.join(dirPath, '240.jpg'), thumbnailBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile(path.join(dirPath, file.filename), buffer);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
file.extra = {
|
||||||
|
dirname,
|
||||||
|
isImage: !!thumbnailBuffer,
|
||||||
|
};
|
||||||
|
|
||||||
|
return done();
|
||||||
|
} catch (error) {
|
||||||
|
return done(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return exits.success(receiver);
|
||||||
|
},
|
||||||
|
};
|
33
server/api/helpers/create-attachment.js
Normal file
33
server/api/helpers/create-attachment.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
card: {
|
||||||
|
type: 'ref',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
type: 'json',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
type: 'ref',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs, exits) {
|
||||||
|
const attachment = await Attachment.create({
|
||||||
|
...inputs.values,
|
||||||
|
cardId: inputs.card.id,
|
||||||
|
}).fetch();
|
||||||
|
|
||||||
|
sails.sockets.broadcast(
|
||||||
|
`board:${inputs.card.boardId}`,
|
||||||
|
'attachmentCreate',
|
||||||
|
{
|
||||||
|
item: attachment,
|
||||||
|
},
|
||||||
|
inputs.request,
|
||||||
|
);
|
||||||
|
|
||||||
|
return exits.success(attachment);
|
||||||
|
},
|
||||||
|
};
|
54
server/api/helpers/create-avatar-receiver.js
Normal file
54
server/api/helpers/create-avatar-receiver.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const util = require('util');
|
||||||
|
const stream = require('stream');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
|
||||||
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sync: true,
|
||||||
|
|
||||||
|
fn(inputs, exits) {
|
||||||
|
const receiver = stream.Writable({
|
||||||
|
objectMode: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let firstFileHandled = false;
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
receiver._write = async (file, receiverEncoding, done) => {
|
||||||
|
if (firstFileHandled) {
|
||||||
|
file.pipe(new stream.Writable());
|
||||||
|
|
||||||
|
return done();
|
||||||
|
}
|
||||||
|
firstFileHandled = true;
|
||||||
|
|
||||||
|
const resize = sharp().resize(100, 100).jpeg();
|
||||||
|
const passThrought = new stream.PassThrough();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pipeline(file, resize, passThrought);
|
||||||
|
|
||||||
|
const dirname = uuid();
|
||||||
|
const dirPath = path.join(sails.config.custom.userAvatarsPath, dirname);
|
||||||
|
|
||||||
|
fs.mkdirSync(dirPath);
|
||||||
|
|
||||||
|
await pipeline(passThrought, fs.createWriteStream(path.join(dirPath, '100.jpg')));
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
file.extra = {
|
||||||
|
dirname,
|
||||||
|
};
|
||||||
|
|
||||||
|
return done();
|
||||||
|
} catch (error) {
|
||||||
|
return done(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return exits.success(receiver);
|
||||||
|
},
|
||||||
|
};
|
41
server/api/helpers/delete-attachment.js
Normal file
41
server/api/helpers/delete-attachment.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
const path = require('path');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
record: {
|
||||||
|
type: 'ref',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
board: {
|
||||||
|
type: 'ref',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
type: 'ref',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs, exits) {
|
||||||
|
const attachment = await Attachment.archiveOne(inputs.record.id);
|
||||||
|
|
||||||
|
if (attachment) {
|
||||||
|
try {
|
||||||
|
rimraf.sync(path.join(sails.config.custom.attachmentsPath, attachment.dirname));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
sails.sockets.broadcast(
|
||||||
|
`board:${inputs.board.id}`,
|
||||||
|
'attachmentDelete',
|
||||||
|
{
|
||||||
|
item: attachment,
|
||||||
|
},
|
||||||
|
inputs.request,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return exits.success(attachment);
|
||||||
|
},
|
||||||
|
};
|
34
server/api/helpers/get-attachment-to-project-path.js
Executable file
34
server/api/helpers/get-attachment-to-project-path.js
Executable file
|
@ -0,0 +1,34 @@
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
criteria: {
|
||||||
|
type: 'json',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
pathNotFound: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs, exits) {
|
||||||
|
const attachment = await Attachment.findOne(inputs.criteria);
|
||||||
|
|
||||||
|
if (!attachment) {
|
||||||
|
throw 'pathNotFound';
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = await sails.helpers
|
||||||
|
.getCardToProjectPath(attachment.cardId)
|
||||||
|
.intercept('pathNotFound', (nodes) => ({
|
||||||
|
pathNotFound: {
|
||||||
|
attachment,
|
||||||
|
...nodes,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return exits.success({
|
||||||
|
attachment,
|
||||||
|
...path,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
17
server/api/helpers/get-attachments-for-card.js
Normal file
17
server/api/helpers/get-attachments-for-card.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
id: {
|
||||||
|
type: 'json',
|
||||||
|
custom: (value) => _.isString(value) || _.isArray(value),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs, exits) {
|
||||||
|
const attachments = await sails.helpers.getAttachments({
|
||||||
|
cardId: inputs.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return exits.success(attachments);
|
||||||
|
},
|
||||||
|
};
|
14
server/api/helpers/get-attachments.js
Normal file
14
server/api/helpers/get-attachments.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
criteria: {
|
||||||
|
type: 'json',
|
||||||
|
custom: (value) => _.isArray(value) || _.isPlainObject(value),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs, exits) {
|
||||||
|
const attachments = await Attachment.find(inputs.criteria).sort('id');
|
||||||
|
|
||||||
|
return exits.success(attachments);
|
||||||
|
},
|
||||||
|
};
|
36
server/api/helpers/update-attachment.js
Normal file
36
server/api/helpers/update-attachment.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
record: {
|
||||||
|
type: 'ref',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
type: 'json',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
board: {
|
||||||
|
type: 'ref',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
type: 'ref',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs, exits) {
|
||||||
|
const attachment = await Attachment.updateOne(inputs.record.id).set(inputs.values);
|
||||||
|
|
||||||
|
if (attachment) {
|
||||||
|
sails.sockets.broadcast(
|
||||||
|
`board:${inputs.board.id}`,
|
||||||
|
'attachmentUpdate',
|
||||||
|
{
|
||||||
|
item: attachment,
|
||||||
|
},
|
||||||
|
inputs.request,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return exits.success(attachment);
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
inputs: {
|
inputs: {
|
||||||
|
@ -14,7 +14,8 @@ module.exports = {
|
||||||
_.isPlainObject(value) &&
|
_.isPlainObject(value) &&
|
||||||
(_.isUndefined(value.email) || _.isString(value.email)) &&
|
(_.isUndefined(value.email) || _.isString(value.email)) &&
|
||||||
(_.isUndefined(value.password) || _.isString(value.password)) &&
|
(_.isUndefined(value.password) || _.isString(value.password)) &&
|
||||||
(!value.username || _.isString(value.username)),
|
(!value.username || _.isString(value.username)) &&
|
||||||
|
(_.isUndefined(value.avatarUrl) || _.isNull(value.avatarUrl)),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
request: {
|
request: {
|
||||||
|
@ -49,6 +50,13 @@ module.exports = {
|
||||||
inputs.values.username = inputs.values.username.toLowerCase();
|
inputs.values.username = inputs.values.username.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_.isUndefined(inputs.values.avatarUrl)) {
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
inputs.values.avatarDirname = null;
|
||||||
|
delete inputs.values.avatarUrl;
|
||||||
|
/* eslint-enable no-param-reassign */
|
||||||
|
}
|
||||||
|
|
||||||
const user = await User.updateOne({
|
const user = await User.updateOne({
|
||||||
id: inputs.record.id,
|
id: inputs.record.id,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
|
@ -70,9 +78,9 @@ module.exports = {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
if (inputs.record.avatar && user.avatar !== inputs.record.avatar) {
|
if (inputs.record.avatarDirname && user.avatarDirname !== inputs.record.avatarDirname) {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(path.join(sails.config.custom.uploadsPath, inputs.record.avatar));
|
rimraf.sync(path.join(sails.config.custom.userAvatarsPath, inputs.record.avatarDirname));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error.stack); // eslint-disable-line no-console
|
console.warn(error.stack); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
|
|
56
server/api/models/Attachment.js
Normal file
56
server/api/models/Attachment.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/**
|
||||||
|
* Attachment.js
|
||||||
|
*
|
||||||
|
* @description :: A model definition represents a database table/collection.
|
||||||
|
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
attributes: {
|
||||||
|
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
||||||
|
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||||
|
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
||||||
|
|
||||||
|
dirname: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
filename: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isImage: {
|
||||||
|
type: 'boolean',
|
||||||
|
required: true,
|
||||||
|
columnName: 'is_image',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||||
|
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||||
|
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
|
||||||
|
|
||||||
|
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
|
||||||
|
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
||||||
|
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||||
|
|
||||||
|
cardId: {
|
||||||
|
model: 'Card',
|
||||||
|
required: true,
|
||||||
|
columnName: 'card_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
customToJSON() {
|
||||||
|
return {
|
||||||
|
..._.omit(this, ['dirname', 'filename', 'isImage']),
|
||||||
|
url: `${sails.config.custom.attachmentsUrl}/${this.dirname}/${this.filename}`,
|
||||||
|
thumbnailUrl: this.isImage
|
||||||
|
? `${sails.config.custom.attachmentsUrl}/${this.dirname}/240.jpg`
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
|
@ -69,5 +69,13 @@ module.exports = {
|
||||||
collection: 'Task',
|
collection: 'Task',
|
||||||
via: 'cardId',
|
via: 'cardId',
|
||||||
},
|
},
|
||||||
|
attachments: {
|
||||||
|
collection: 'Attachment',
|
||||||
|
via: 'cardId',
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
collection: 'Action',
|
||||||
|
via: 'cardId',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -37,10 +37,11 @@ module.exports = {
|
||||||
regex: /^[a-zA-Z0-9]+(_?[a-zA-Z0-9])*$/,
|
regex: /^[a-zA-Z0-9]+(_?[a-zA-Z0-9])*$/,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
avatar: {
|
avatarDirname: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
isNotEmptyString: true,
|
isNotEmptyString: true,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
columnName: 'avatar_dirname',
|
||||||
},
|
},
|
||||||
phone: {
|
phone: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -91,8 +92,9 @@ module.exports = {
|
||||||
|
|
||||||
customToJSON() {
|
customToJSON() {
|
||||||
return {
|
return {
|
||||||
..._.omit(this, 'password'),
|
..._.omit(this, ['password', 'avatarDirname']),
|
||||||
avatar: this.avatar && `${sails.config.custom.uploadsUrl}/${this.avatar}`,
|
avatarUrl:
|
||||||
|
this.avatarDirname && `${sails.config.custom.userAvatarsUrl}/${this.avatarDirname}/100.jpg`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,9 @@ module.exports.custom = {
|
||||||
|
|
||||||
baseUrl: process.env.BASE_URL,
|
baseUrl: process.env.BASE_URL,
|
||||||
|
|
||||||
uploadsPath: path.join(sails.config.paths.public, 'uploads'),
|
userAvatarsPath: path.join(sails.config.paths.public, 'user-avatars'),
|
||||||
uploadsUrl: `${process.env.BASE_URL}/uploads`,
|
userAvatarsUrl: `${process.env.BASE_URL}/user-avatars`,
|
||||||
|
|
||||||
|
attachmentsPath: path.join(sails.config.paths.public, 'attachments'),
|
||||||
|
attachmentsUrl: `${process.env.BASE_URL}/attachments`,
|
||||||
};
|
};
|
||||||
|
|
7
server/config/env/production.js
vendored
7
server/config/env/production.js
vendored
|
@ -329,7 +329,10 @@ module.exports = {
|
||||||
custom: {
|
custom: {
|
||||||
baseUrl: process.env.BASE_URL,
|
baseUrl: process.env.BASE_URL,
|
||||||
|
|
||||||
uploadsPath: path.join(sails.config.paths.public, 'uploads'),
|
userAvatarsPath: path.join(sails.config.paths.public, 'user-avatars'),
|
||||||
uploadsUrl: `${process.env.BASE_URL}/uploads`,
|
userAvatarsUrl: `${process.env.BASE_URL}/user-avatars`,
|
||||||
|
|
||||||
|
attachmentsPath: path.join(sails.config.paths.public, 'attachments'),
|
||||||
|
attachmentsUrl: `${process.env.BASE_URL}/attachments`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,7 +18,7 @@ module.exports.routes = {
|
||||||
'PATCH /api/users/:id/email': 'users/update-email',
|
'PATCH /api/users/:id/email': 'users/update-email',
|
||||||
'PATCH /api/users/:id/password': 'users/update-password',
|
'PATCH /api/users/:id/password': 'users/update-password',
|
||||||
'PATCH /api/users/:id/username': 'users/update-username',
|
'PATCH /api/users/:id/username': 'users/update-username',
|
||||||
'POST /api/users/:id/upload-avatar': 'users/upload-avatar',
|
'POST /api/users/:id/update-avatar': 'users/update-avatar',
|
||||||
'DELETE /api/users/:id': 'users/delete',
|
'DELETE /api/users/:id': 'users/delete',
|
||||||
|
|
||||||
'GET /api/projects': 'projects/index',
|
'GET /api/projects': 'projects/index',
|
||||||
|
@ -55,6 +55,10 @@ module.exports.routes = {
|
||||||
'PATCH /api/tasks/:id': 'tasks/update',
|
'PATCH /api/tasks/:id': 'tasks/update',
|
||||||
'DELETE /api/tasks/:id': 'tasks/delete',
|
'DELETE /api/tasks/:id': 'tasks/delete',
|
||||||
|
|
||||||
|
'POST /api/cards/:cardId/attachments': 'attachments/create',
|
||||||
|
'PATCH /api/attachments/:id': 'attachments/update',
|
||||||
|
'DELETE /api/attachments/:id': 'attachments/delete',
|
||||||
|
|
||||||
'GET /api/cards/:cardId/actions': 'actions/index',
|
'GET /api/cards/:cardId/actions': 'actions/index',
|
||||||
|
|
||||||
'POST /api/cards/:cardId/comment-actions': 'comment-actions/create',
|
'POST /api/cards/:cardId/comment-actions': 'comment-actions/create',
|
||||||
|
|
|
@ -10,7 +10,7 @@ module.exports.up = (knex) =>
|
||||||
table.boolean('is_admin').notNullable();
|
table.boolean('is_admin').notNullable();
|
||||||
table.text('name').notNullable();
|
table.text('name').notNullable();
|
||||||
table.text('username');
|
table.text('username');
|
||||||
table.text('avatar');
|
table.text('avatar_dirname');
|
||||||
table.text('phone');
|
table.text('phone');
|
||||||
table.text('organization');
|
table.text('organization');
|
||||||
table.boolean('subscribe_to_own_cards').notNullable();
|
table.boolean('subscribe_to_own_cards').notNullable();
|
||||||
|
|
22
server/db/migrations/20180722006688_create_attachment_table.js
Executable file
22
server/db/migrations/20180722006688_create_attachment_table.js
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
module.exports.up = (knex) =>
|
||||||
|
knex.schema.createTable('attachment', (table) => {
|
||||||
|
/* Columns */
|
||||||
|
|
||||||
|
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
|
||||||
|
|
||||||
|
table.bigInteger('card_id').notNullable();
|
||||||
|
|
||||||
|
table.text('dirname').notNullable();
|
||||||
|
table.text('filename').notNullable();
|
||||||
|
table.boolean('is_image').notNullable();
|
||||||
|
table.text('name').notNullable();
|
||||||
|
|
||||||
|
table.timestamp('created_at', true);
|
||||||
|
table.timestamp('updated_at', true);
|
||||||
|
|
||||||
|
/* Indexes */
|
||||||
|
|
||||||
|
table.index('card_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.down = (knex) => knex.schema.dropTable('attachment');
|
47
server/package-lock.json
generated
47
server/package-lock.json
generated
|
@ -190,6 +190,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
|
||||||
"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
|
"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
|
||||||
},
|
},
|
||||||
|
"any-promise": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||||
|
"integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
|
||||||
|
},
|
||||||
"anymatch": {
|
"anymatch": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
|
||||||
|
@ -2555,6 +2560,16 @@
|
||||||
"klaw": "^1.0.0",
|
"klaw": "^1.0.0",
|
||||||
"path-is-absolute": "^1.0.0",
|
"path-is-absolute": "^1.0.0",
|
||||||
"rimraf": "^2.2.8"
|
"rimraf": "^2.2.8"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"rimraf": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||||
|
"requires": {
|
||||||
|
"glob": "^7.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fs-minipass": {
|
"fs-minipass": {
|
||||||
|
@ -4168,6 +4183,16 @@
|
||||||
"rimraf": "^2.6.1",
|
"rimraf": "^2.6.1",
|
||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
"tar": "^4.4.2"
|
"tar": "^4.4.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"rimraf": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||||
|
"requires": {
|
||||||
|
"glob": "^7.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nodemon": {
|
"nodemon": {
|
||||||
|
@ -5273,9 +5298,9 @@
|
||||||
"integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs="
|
"integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs="
|
||||||
},
|
},
|
||||||
"rimraf": {
|
"rimraf": {
|
||||||
"version": "2.7.1",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"glob": "^7.1.3"
|
"glob": "^7.1.3"
|
||||||
}
|
}
|
||||||
|
@ -6357,6 +6382,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
|
||||||
"integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
|
"integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
|
||||||
},
|
},
|
||||||
|
"stream-to-array": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz",
|
||||||
|
"integrity": "sha1-u/azn19D7DC8cbq8s3VXrOzzQ1M=",
|
||||||
|
"requires": {
|
||||||
|
"any-promise": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"streamifier": {
|
"streamifier": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz",
|
||||||
|
@ -6931,6 +6964,14 @@
|
||||||
"version": "0.2.10",
|
"version": "0.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
|
||||||
"integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E="
|
"integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E="
|
||||||
|
},
|
||||||
|
"rimraf": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||||
|
"requires": {
|
||||||
|
"glob": "^7.1.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue