mirror of
https://github.com/plankanban/planka.git
synced 2025-07-19 05:09:43 +02:00
feat: Toggle actions details, little redesign
This commit is contained in:
parent
e1ac5959ba
commit
45f35e8042
25 changed files with 301 additions and 81 deletions
|
@ -1,6 +1,5 @@
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
|
||||||
export const fetchActions = (cardId) => ({
|
export const fetchActions = (cardId) => ({
|
||||||
type: ActionTypes.ACTIONS_FETCH,
|
type: ActionTypes.ACTIONS_FETCH,
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -24,3 +23,28 @@ fetchActions.failure = (cardId, error) => ({
|
||||||
error,
|
error,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const toggleActionsDetails = (cardId, isVisible) => ({
|
||||||
|
type: ActionTypes.ACTIONS_DETAILS_TOGGLE,
|
||||||
|
payload: {
|
||||||
|
cardId,
|
||||||
|
isVisible,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleActionsDetails.success = (cardId, actions, users) => ({
|
||||||
|
type: ActionTypes.ACTIONS_DETAILS_TOGGLE__SUCCESS,
|
||||||
|
payload: {
|
||||||
|
cardId,
|
||||||
|
actions,
|
||||||
|
users,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleActionsDetails.failure = (cardId, error) => ({
|
||||||
|
type: ActionTypes.ACTIONS_DETAILS_TOGGLE__FAILURE,
|
||||||
|
payload: {
|
||||||
|
cardId,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import EntryActionTypes from '../../constants/EntryActionTypes';
|
import EntryActionTypes from '../../constants/EntryActionTypes';
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
|
||||||
export const fetchActionsInCurrentCard = () => ({
|
export const fetchActionsInCurrentCard = () => ({
|
||||||
type: EntryActionTypes.ACTIONS_IN_CURRENT_CARD_FETCH,
|
type: EntryActionTypes.ACTIONS_IN_CURRENT_CARD_FETCH,
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const toggleActionsDetailsInCurrentCard = (isVisible) => ({
|
||||||
|
type: EntryActionTypes.ACTIONS_DETAILS_IN_CURRENT_CARD_TOGGLE,
|
||||||
|
payload: {
|
||||||
|
isVisible,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Comment, Icon, Loader, Visibility } from 'semantic-ui-react';
|
import { Button, Comment, Icon, Loader, Visibility } from 'semantic-ui-react';
|
||||||
|
|
||||||
import { ActionTypes } from '../../../constants/Enums';
|
import { ActionTypes } from '../../../constants/Enums';
|
||||||
import CommentAdd from './CommentAdd';
|
import CommentAdd from './CommentAdd';
|
||||||
|
@ -14,15 +14,22 @@ const Actions = React.memo(
|
||||||
items,
|
items,
|
||||||
isFetching,
|
isFetching,
|
||||||
isAllFetched,
|
isAllFetched,
|
||||||
|
isDetailsVisible,
|
||||||
|
isDetailsFetching,
|
||||||
canEdit,
|
canEdit,
|
||||||
canEditAllComments,
|
canEditAllComments,
|
||||||
onFetch,
|
onFetch,
|
||||||
|
onDetailsToggle,
|
||||||
onCommentCreate,
|
onCommentCreate,
|
||||||
onCommentUpdate,
|
onCommentUpdate,
|
||||||
onCommentDelete,
|
onCommentDelete,
|
||||||
}) => {
|
}) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
|
|
||||||
|
const handleToggleDetailsClick = useCallback(() => {
|
||||||
|
onDetailsToggle(!isDetailsVisible);
|
||||||
|
}, [isDetailsVisible, onDetailsToggle]);
|
||||||
|
|
||||||
const handleCommentUpdate = useCallback(
|
const handleCommentUpdate = useCallback(
|
||||||
(id, data) => {
|
(id, data) => {
|
||||||
onCommentUpdate(id, data);
|
onCommentUpdate(id, data);
|
||||||
|
@ -38,20 +45,18 @@ const Actions = React.memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
{canEdit && (
|
|
||||||
<div className={styles.contentModule}>
|
|
||||||
<div className={styles.moduleWrapper}>
|
|
||||||
<Icon name="comment outline" className={styles.moduleIcon} />
|
|
||||||
<div className={styles.moduleHeader}>{t('common.addComment')}</div>
|
|
||||||
<CommentAdd onCreate={onCommentCreate} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.contentModule}>
|
<div className={styles.contentModule}>
|
||||||
<div className={styles.moduleWrapper}>
|
<div className={styles.moduleWrapper}>
|
||||||
<Icon name="list ul" className={styles.moduleIcon} />
|
<Icon name="list ul" className={styles.moduleIcon} />
|
||||||
<div className={styles.moduleHeader}>{t('common.actions')}</div>
|
<div className={styles.moduleHeader}>
|
||||||
|
{t('common.actions')}
|
||||||
|
<Button
|
||||||
|
content={isDetailsVisible ? t('action.hideDetails') : t('action.showDetails')}
|
||||||
|
className={styles.toggleButton}
|
||||||
|
onClick={handleToggleDetailsClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{canEdit && <CommentAdd onCreate={onCommentCreate} />}
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<Comment.Group>
|
<Comment.Group>
|
||||||
{items.map((item) =>
|
{items.map((item) =>
|
||||||
|
@ -78,14 +83,13 @@ const Actions = React.memo(
|
||||||
)}
|
)}
|
||||||
</Comment.Group>
|
</Comment.Group>
|
||||||
</div>
|
</div>
|
||||||
{isFetching ? (
|
{isFetching || isDetailsFetching ? (
|
||||||
<Loader active inverted inline="centered" size="small" className={styles.loader} />
|
<Loader active inverted inline="centered" size="small" className={styles.loader} />
|
||||||
) : (
|
) : (
|
||||||
!isAllFetched && <Visibility fireOnMount onOnScreen={onFetch} />
|
!isAllFetched && <Visibility fireOnMount onOnScreen={onFetch} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -94,9 +98,12 @@ Actions.propTypes = {
|
||||||
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isAllFetched: PropTypes.bool.isRequired,
|
isAllFetched: PropTypes.bool.isRequired,
|
||||||
|
isDetailsVisible: PropTypes.bool.isRequired,
|
||||||
|
isDetailsFetching: PropTypes.bool.isRequired,
|
||||||
canEdit: PropTypes.bool.isRequired,
|
canEdit: PropTypes.bool.isRequired,
|
||||||
canEditAllComments: PropTypes.bool.isRequired,
|
canEditAllComments: PropTypes.bool.isRequired,
|
||||||
onFetch: PropTypes.func.isRequired,
|
onFetch: PropTypes.func.isRequired,
|
||||||
|
onDetailsToggle: PropTypes.func.isRequired,
|
||||||
onCommentCreate: PropTypes.func.isRequired,
|
onCommentCreate: PropTypes.func.isRequired,
|
||||||
onCommentUpdate: PropTypes.func.isRequired,
|
onCommentUpdate: PropTypes.func.isRequired,
|
||||||
onCommentDelete: PropTypes.func.isRequired,
|
onCommentDelete: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -8,12 +8,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.moduleHeader {
|
.moduleHeader {
|
||||||
|
align-items: center;
|
||||||
color: #17394d;
|
color: #17394d;
|
||||||
|
display: flex;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 20px;
|
height: 36px;
|
||||||
|
justify-content: space-between;
|
||||||
margin: 0 0 4px;
|
margin: 0 0 4px;
|
||||||
padding: 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.moduleIcon {
|
.moduleIcon {
|
||||||
|
@ -33,6 +35,24 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggleButton {
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
color: #6b808c;
|
||||||
|
float: right;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-right: 0;
|
||||||
|
padding: 6px 11px;
|
||||||
|
text-align: left;
|
||||||
|
text-decoration: underline;
|
||||||
|
transition: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(9, 30, 66, 0.08);
|
||||||
|
color: #092d42;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
margin-left: -40px;
|
margin-left: -40px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import React, { useCallback, useRef } from 'react';
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import { Button, Form, TextArea } from 'semantic-ui-react';
|
import { Button, Form, TextArea } from 'semantic-ui-react';
|
||||||
|
import { useDidUpdate, useToggle } from '../../../lib/hooks';
|
||||||
|
|
||||||
import { useForm } from '../../../hooks';
|
import { useClosableForm, useForm } from '../../../hooks';
|
||||||
|
|
||||||
import styles from './CommentAdd.module.scss';
|
import styles from './CommentAdd.module.scss';
|
||||||
|
|
||||||
|
@ -14,10 +15,16 @@ const DEFAULT_DATA = {
|
||||||
|
|
||||||
const CommentAdd = React.memo(({ onCreate }) => {
|
const CommentAdd = React.memo(({ onCreate }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
|
const [isOpened, setIsOpened] = useState(false);
|
||||||
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
|
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
|
||||||
|
const [selectTextFieldState, selectTextField] = useToggle();
|
||||||
|
|
||||||
const textField = useRef(null);
|
const textField = useRef(null);
|
||||||
|
|
||||||
|
const close = useCallback(() => {
|
||||||
|
setIsOpened(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const submit = useCallback(() => {
|
const submit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
|
@ -32,7 +39,12 @@ const CommentAdd = React.memo(({ onCreate }) => {
|
||||||
onCreate(cleanData);
|
onCreate(cleanData);
|
||||||
|
|
||||||
setData(DEFAULT_DATA);
|
setData(DEFAULT_DATA);
|
||||||
}, [onCreate, data, setData]);
|
selectTextField();
|
||||||
|
}, [onCreate, data, setData, selectTextField]);
|
||||||
|
|
||||||
|
const handleFieldFocus = useCallback(() => {
|
||||||
|
setIsOpened(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleFieldKeyDown = useCallback(
|
const handleFieldKeyDown = useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
|
@ -43,10 +55,16 @@ const CommentAdd = React.memo(({ onCreate }) => {
|
||||||
[submit],
|
[submit],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(close);
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
submit();
|
submit();
|
||||||
}, [submit]);
|
}, [submit]);
|
||||||
|
|
||||||
|
useDidUpdate(() => {
|
||||||
|
textField.current.ref.current.focus();
|
||||||
|
}, [selectTextFieldState]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<TextArea
|
<TextArea
|
||||||
|
@ -55,15 +73,24 @@ const CommentAdd = React.memo(({ onCreate }) => {
|
||||||
name="text"
|
name="text"
|
||||||
value={data.text}
|
value={data.text}
|
||||||
placeholder={t('common.writeComment')}
|
placeholder={t('common.writeComment')}
|
||||||
minRows={3}
|
minRows={isOpened ? 3 : 1}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className={styles.field}
|
className={styles.field}
|
||||||
|
onFocus={handleFieldFocus}
|
||||||
onKeyDown={handleFieldKeyDown}
|
onKeyDown={handleFieldKeyDown}
|
||||||
onChange={handleFieldChange}
|
onChange={handleFieldChange}
|
||||||
|
onBlur={handleFieldBlur}
|
||||||
/>
|
/>
|
||||||
|
{isOpened && (
|
||||||
<div className={styles.controls}>
|
<div className={styles.controls}>
|
||||||
<Button positive content={t('action.addComment')} disabled={!data.text} />
|
<Button
|
||||||
|
positive
|
||||||
|
content={t('action.addComment')}
|
||||||
|
onMouseOver={handleControlMouseOver}
|
||||||
|
onMouseOut={handleControlMouseOut}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,6 +34,8 @@ const CardModal = React.memo(
|
||||||
isSubscribed,
|
isSubscribed,
|
||||||
isActionsFetching,
|
isActionsFetching,
|
||||||
isAllActionsFetched,
|
isAllActionsFetched,
|
||||||
|
isActionsDetailsVisible,
|
||||||
|
isActionsDetailsFetching,
|
||||||
listId,
|
listId,
|
||||||
boardId,
|
boardId,
|
||||||
projectId,
|
projectId,
|
||||||
|
@ -67,6 +69,7 @@ const CardModal = React.memo(
|
||||||
onAttachmentUpdate,
|
onAttachmentUpdate,
|
||||||
onAttachmentDelete,
|
onAttachmentDelete,
|
||||||
onActionsFetch,
|
onActionsFetch,
|
||||||
|
onActionsDetailsToggle,
|
||||||
onCommentActionCreate,
|
onCommentActionCreate,
|
||||||
onCommentActionUpdate,
|
onCommentActionUpdate,
|
||||||
onCommentActionDelete,
|
onCommentActionDelete,
|
||||||
|
@ -358,9 +361,12 @@ const CardModal = React.memo(
|
||||||
items={actions}
|
items={actions}
|
||||||
isFetching={isActionsFetching}
|
isFetching={isActionsFetching}
|
||||||
isAllFetched={isAllActionsFetched}
|
isAllFetched={isAllActionsFetched}
|
||||||
|
isDetailsVisible={isActionsDetailsVisible}
|
||||||
|
isDetailsFetching={isActionsDetailsFetching}
|
||||||
canEdit={canEdit}
|
canEdit={canEdit}
|
||||||
canEditAllComments={canEditAllCommentActions}
|
canEditAllComments={canEditAllCommentActions}
|
||||||
onFetch={onActionsFetch}
|
onFetch={onActionsFetch}
|
||||||
|
onDetailsToggle={onActionsDetailsToggle}
|
||||||
onCommentCreate={onCommentActionCreate}
|
onCommentCreate={onCommentActionCreate}
|
||||||
onCommentUpdate={onCommentActionUpdate}
|
onCommentUpdate={onCommentActionUpdate}
|
||||||
onCommentDelete={onCommentActionDelete}
|
onCommentDelete={onCommentActionDelete}
|
||||||
|
@ -486,6 +492,8 @@ CardModal.propTypes = {
|
||||||
isSubscribed: PropTypes.bool.isRequired,
|
isSubscribed: PropTypes.bool.isRequired,
|
||||||
isActionsFetching: PropTypes.bool.isRequired,
|
isActionsFetching: PropTypes.bool.isRequired,
|
||||||
isAllActionsFetched: PropTypes.bool.isRequired,
|
isAllActionsFetched: PropTypes.bool.isRequired,
|
||||||
|
isActionsDetailsVisible: PropTypes.bool.isRequired,
|
||||||
|
isActionsDetailsFetching: PropTypes.bool.isRequired,
|
||||||
listId: PropTypes.string.isRequired,
|
listId: PropTypes.string.isRequired,
|
||||||
boardId: PropTypes.string.isRequired,
|
boardId: PropTypes.string.isRequired,
|
||||||
projectId: PropTypes.string.isRequired,
|
projectId: PropTypes.string.isRequired,
|
||||||
|
@ -521,6 +529,7 @@ CardModal.propTypes = {
|
||||||
onAttachmentUpdate: PropTypes.func.isRequired,
|
onAttachmentUpdate: PropTypes.func.isRequired,
|
||||||
onAttachmentDelete: PropTypes.func.isRequired,
|
onAttachmentDelete: PropTypes.func.isRequired,
|
||||||
onActionsFetch: PropTypes.func.isRequired,
|
onActionsFetch: PropTypes.func.isRequired,
|
||||||
|
onActionsDetailsToggle: PropTypes.func.isRequired,
|
||||||
onCommentActionCreate: PropTypes.func.isRequired,
|
onCommentActionCreate: PropTypes.func.isRequired,
|
||||||
onCommentActionUpdate: PropTypes.func.isRequired,
|
onCommentActionUpdate: PropTypes.func.isRequired,
|
||||||
onCommentActionDelete: PropTypes.func.isRequired,
|
onCommentActionDelete: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -225,6 +225,9 @@ export default {
|
||||||
ACTIONS_FETCH: 'ACTIONS_FETCH',
|
ACTIONS_FETCH: 'ACTIONS_FETCH',
|
||||||
ACTIONS_FETCH__SUCCESS: 'ACTIONS_FETCH__SUCCESS',
|
ACTIONS_FETCH__SUCCESS: 'ACTIONS_FETCH__SUCCESS',
|
||||||
ACTIONS_FETCH__FAILURE: 'ACTIONS_FETCH__FAILURE',
|
ACTIONS_FETCH__FAILURE: 'ACTIONS_FETCH__FAILURE',
|
||||||
|
ACTIONS_DETAILS_TOGGLE: 'ACTIONS_DETAILS_TOGGLE',
|
||||||
|
ACTIONS_DETAILS_TOGGLE__SUCCESS: 'ACTIONS_DETAILS_TOGGLE__SUCCESS',
|
||||||
|
ACTIONS_DETAILS_TOGGLE__FAILURE: 'ACTIONS_DETAILS_TOGGLE__FAILURE',
|
||||||
|
|
||||||
/* Action */
|
/* Action */
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ const ACCESS_TOKEN_KEY = 'accessToken';
|
||||||
const ACCESS_TOKEN_EXPIRES = 365;
|
const ACCESS_TOKEN_EXPIRES = 365;
|
||||||
|
|
||||||
const POSITION_GAP = 65535;
|
const POSITION_GAP = 65535;
|
||||||
const ACTIONS_LIMIT = 10;
|
const ACTIONS_LIMIT = 50;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
SERVER_BASE_URL,
|
SERVER_BASE_URL,
|
||||||
|
|
|
@ -155,6 +155,7 @@ export default {
|
||||||
/* Actions */
|
/* Actions */
|
||||||
|
|
||||||
ACTIONS_IN_CURRENT_CARD_FETCH: `${PREFIX}/ACTIONS_IN_CURRENT_CARD_FETCH`,
|
ACTIONS_IN_CURRENT_CARD_FETCH: `${PREFIX}/ACTIONS_IN_CURRENT_CARD_FETCH`,
|
||||||
|
ACTIONS_DETAILS_IN_CURRENT_CARD_TOGGLE: `${PREFIX}/ACTIONS_DETAILS_IN_CURRENT_CARD_TOGGLE`,
|
||||||
|
|
||||||
/* Action */
|
/* Action */
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ import {
|
||||||
moveTask,
|
moveTask,
|
||||||
removeLabelFromCurrentCard,
|
removeLabelFromCurrentCard,
|
||||||
removeUserFromCurrentCard,
|
removeUserFromCurrentCard,
|
||||||
|
toggleActionsDetailsInCurrentCard,
|
||||||
transferCurrentCard,
|
transferCurrentCard,
|
||||||
updateAttachment,
|
updateAttachment,
|
||||||
updateCommentAction,
|
updateCommentAction,
|
||||||
|
@ -60,6 +61,8 @@ const mapStateToProps = (state) => {
|
||||||
timer,
|
timer,
|
||||||
isSubscribed,
|
isSubscribed,
|
||||||
isActionsFetching,
|
isActionsFetching,
|
||||||
|
isActionsDetailsVisible,
|
||||||
|
isActionsDetailsFetching,
|
||||||
isAllActionsFetched,
|
isAllActionsFetched,
|
||||||
boardId,
|
boardId,
|
||||||
listId,
|
listId,
|
||||||
|
@ -79,6 +82,8 @@ const mapStateToProps = (state) => {
|
||||||
isSubscribed,
|
isSubscribed,
|
||||||
isActionsFetching,
|
isActionsFetching,
|
||||||
isAllActionsFetched,
|
isAllActionsFetched,
|
||||||
|
isActionsDetailsVisible,
|
||||||
|
isActionsDetailsFetching,
|
||||||
listId,
|
listId,
|
||||||
boardId,
|
boardId,
|
||||||
projectId,
|
projectId,
|
||||||
|
@ -118,6 +123,7 @@ const mapDispatchToProps = (dispatch) =>
|
||||||
onAttachmentUpdate: updateAttachment,
|
onAttachmentUpdate: updateAttachment,
|
||||||
onAttachmentDelete: deleteAttachment,
|
onAttachmentDelete: deleteAttachment,
|
||||||
onActionsFetch: fetchActionsInCurrentCard,
|
onActionsFetch: fetchActionsInCurrentCard,
|
||||||
|
onActionsDetailsToggle: toggleActionsDetailsInCurrentCard,
|
||||||
onCommentActionCreate: createCommentActionInCurrentCard,
|
onCommentActionCreate: createCommentActionInCurrentCard,
|
||||||
onCommentActionUpdate: updateCommentAction,
|
onCommentActionUpdate: updateCommentAction,
|
||||||
onCommentActionDelete: deleteCommentAction,
|
onCommentActionDelete: deleteCommentAction,
|
||||||
|
|
|
@ -195,6 +195,7 @@ export default {
|
||||||
editTimer_title: 'Timer bearbeiten',
|
editTimer_title: 'Timer bearbeiten',
|
||||||
editTitle_title: 'Titel bearbeiten',
|
editTitle_title: 'Titel bearbeiten',
|
||||||
editUsername_title: 'Benutzername ändern',
|
editUsername_title: 'Benutzername ändern',
|
||||||
|
hideDetails: 'Details ausblenden',
|
||||||
leaveBoard: 'Board verlassen',
|
leaveBoard: 'Board verlassen',
|
||||||
leaveProject: 'Projekt verlassen',
|
leaveProject: 'Projekt verlassen',
|
||||||
logOut_title: 'Ausloggen',
|
logOut_title: 'Ausloggen',
|
||||||
|
@ -210,6 +211,7 @@ export default {
|
||||||
removeMember: 'Mitglied entfernen',
|
removeMember: 'Mitglied entfernen',
|
||||||
save: 'Speichern',
|
save: 'Speichern',
|
||||||
showAllAttachments: 'Alle Anhänge anzeigen ({{hidden}} versteckt)',
|
showAllAttachments: 'Alle Anhänge anzeigen ({{hidden}} versteckt)',
|
||||||
|
showDetails: 'Details anzeigen',
|
||||||
showFewerAttachments: 'Weniger Anhänge anzeigen',
|
showFewerAttachments: 'Weniger Anhänge anzeigen',
|
||||||
start: 'Start',
|
start: 'Start',
|
||||||
stop: 'Stopp',
|
stop: 'Stopp',
|
||||||
|
|
|
@ -188,6 +188,7 @@ export default {
|
||||||
editTimer_title: 'Edit Timer',
|
editTimer_title: 'Edit Timer',
|
||||||
editTitle_title: 'Edit Title',
|
editTitle_title: 'Edit Title',
|
||||||
editUsername_title: 'Edit Username',
|
editUsername_title: 'Edit Username',
|
||||||
|
hideDetails: 'Hide details',
|
||||||
leaveBoard: 'Leave board',
|
leaveBoard: 'Leave board',
|
||||||
leaveProject: 'Leave project',
|
leaveProject: 'Leave project',
|
||||||
logOut_title: 'Log Out',
|
logOut_title: 'Log Out',
|
||||||
|
@ -203,6 +204,7 @@ export default {
|
||||||
removeMember: 'Remove member',
|
removeMember: 'Remove member',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
showAllAttachments: 'Show all attachments ({{hidden}} hidden)',
|
showAllAttachments: 'Show all attachments ({{hidden}} hidden)',
|
||||||
|
showDetails: 'Show details',
|
||||||
showFewerAttachments: 'Show fewer attachments',
|
showFewerAttachments: 'Show fewer attachments',
|
||||||
start: 'Start',
|
start: 'Start',
|
||||||
stop: 'Stop',
|
stop: 'Stop',
|
||||||
|
|
|
@ -178,7 +178,8 @@ export default {
|
||||||
editTask: 'Изменить задачу',
|
editTask: 'Изменить задачу',
|
||||||
editTimer: 'Изменить таймер',
|
editTimer: 'Изменить таймер',
|
||||||
editTitle: 'Изменить название',
|
editTitle: 'Изменить название',
|
||||||
editUsername_title: 'Изменить имя пользователя',
|
editUsername: 'Изменить имя пользователя',
|
||||||
|
hideDetails: 'Скрыть подробности',
|
||||||
logOut: 'Выйти',
|
logOut: 'Выйти',
|
||||||
makeCover: 'Сделать обложкой',
|
makeCover: 'Сделать обложкой',
|
||||||
move: 'Переместить',
|
move: 'Переместить',
|
||||||
|
@ -190,6 +191,7 @@ export default {
|
||||||
removeMember: 'Удалить участника',
|
removeMember: 'Удалить участника',
|
||||||
save: 'Сохранить',
|
save: 'Сохранить',
|
||||||
showAllAttachments: 'Показать все вложения ({{hidden}} скрыто)',
|
showAllAttachments: 'Показать все вложения ({{hidden}} скрыто)',
|
||||||
|
showDetails: 'Показать подробности',
|
||||||
showFewerAttachments: 'Показать меньше вложений',
|
showFewerAttachments: 'Показать меньше вложений',
|
||||||
start: 'Начать',
|
start: 'Начать',
|
||||||
stop: 'Остановить',
|
stop: 'Остановить',
|
||||||
|
|
|
@ -50,6 +50,7 @@ export default class extends Model {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.ACTIONS_FETCH__SUCCESS:
|
case ActionTypes.ACTIONS_FETCH__SUCCESS:
|
||||||
|
case ActionTypes.ACTIONS_DETAILS_TOGGLE__SUCCESS:
|
||||||
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
|
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
|
||||||
payload.actions.forEach((action) => {
|
payload.actions.forEach((action) => {
|
||||||
Action.upsert(action);
|
Action.upsert(action);
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Model, attr, fk, many, oneToOne } from 'redux-orm';
|
||||||
|
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
import Config from '../constants/Config';
|
import Config from '../constants/Config';
|
||||||
|
import { ActionTypes as ActionTypesEnum } from '../constants/Enums';
|
||||||
|
|
||||||
export default class extends Model {
|
export default class extends Model {
|
||||||
static modelName = 'Card';
|
static modelName = 'Card';
|
||||||
|
@ -22,6 +23,12 @@ export default class extends Model {
|
||||||
isAllActionsFetched: attr({
|
isAllActionsFetched: attr({
|
||||||
getDefault: () => false,
|
getDefault: () => false,
|
||||||
}),
|
}),
|
||||||
|
isActionsDetailsVisible: attr({
|
||||||
|
getDefault: () => false,
|
||||||
|
}),
|
||||||
|
isActionsDetailsFetching: attr({
|
||||||
|
getDefault: () => false,
|
||||||
|
}),
|
||||||
boardId: fk({
|
boardId: fk({
|
||||||
to: 'Board',
|
to: 'Board',
|
||||||
as: 'board',
|
as: 'board',
|
||||||
|
@ -195,6 +202,36 @@ export default class extends Model {
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
case ActionTypes.ACTIONS_DETAILS_TOGGLE: {
|
||||||
|
const cardModel = Card.withId(payload.cardId);
|
||||||
|
cardModel.isActionsDetailsVisible = payload.isVisible;
|
||||||
|
|
||||||
|
if (payload.isVisible) {
|
||||||
|
cardModel.isActionsDetailsFetching = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ActionTypes.ACTIONS_DETAILS_TOGGLE__SUCCESS: {
|
||||||
|
const cardModel = Card.withId(payload.cardId);
|
||||||
|
|
||||||
|
cardModel.update({
|
||||||
|
isAllActionsFetched: payload.actions.length < Config.ACTIONS_LIMIT,
|
||||||
|
isActionsDetailsFetching: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
cardModel.actions.toModelArray().forEach((actionModel) => {
|
||||||
|
if (actionModel.notification) {
|
||||||
|
actionModel.update({
|
||||||
|
isInCard: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
actionModel.delete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
|
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
|
||||||
payload.cards.forEach((card) => {
|
payload.cards.forEach((card) => {
|
||||||
Card.upsert(card);
|
Card.upsert(card);
|
||||||
|
@ -213,8 +250,16 @@ export default class extends Model {
|
||||||
return this.attachments.orderBy('id', false);
|
return this.attachments.orderBy('id', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
getOrderedInCardActionsQuerySet() {
|
getFilteredOrderedInCardActionsQuerySet() {
|
||||||
return this.actions.orderBy('id', false);
|
const filter = {
|
||||||
|
isInCard: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.isActionsDetailsVisible) {
|
||||||
|
filter.type = ActionTypesEnum.COMMENT_CARD;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.actions.filter(filter).orderBy('id', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUnreadNotificationsQuerySet() {
|
getUnreadNotificationsQuerySet() {
|
||||||
|
|
|
@ -83,7 +83,7 @@ export default class extends Model {
|
||||||
return this.cards.orderBy('position');
|
return this.cards.orderBy('position');
|
||||||
}
|
}
|
||||||
|
|
||||||
getOrderedFilteredCardsModelArray() {
|
getFilteredOrderedCardsModelArray() {
|
||||||
let cardModels = this.getOrderedCardsQuerySet().toModelArray();
|
let cardModels = this.getOrderedCardsQuerySet().toModelArray();
|
||||||
|
|
||||||
const filterUserIds = this.board.filterUsers.toRefArray().map((user) => user.id);
|
const filterUserIds = this.board.filterUsers.toRefArray().map((user) => user.id);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Model, attr, fk } from 'redux-orm';
|
import { Model, attr, fk, oneToOne } from 'redux-orm';
|
||||||
|
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
@ -15,16 +15,15 @@ export default class extends Model {
|
||||||
as: 'user',
|
as: 'user',
|
||||||
relatedName: 'notifications',
|
relatedName: 'notifications',
|
||||||
}),
|
}),
|
||||||
actionId: fk({
|
|
||||||
to: 'Action',
|
|
||||||
as: 'action',
|
|
||||||
relatedName: 'notifications',
|
|
||||||
}),
|
|
||||||
cardId: fk({
|
cardId: fk({
|
||||||
to: 'Card',
|
to: 'Card',
|
||||||
as: 'card',
|
as: 'card',
|
||||||
relatedName: 'notifications',
|
relatedName: 'notifications',
|
||||||
}),
|
}),
|
||||||
|
actionId: oneToOne({
|
||||||
|
to: 'Action',
|
||||||
|
as: 'action',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
static reducer({ type, payload }, Notification) {
|
static reducer({ type, payload }, Notification) {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { call, put, select } from 'redux-saga/effects';
|
import { call, put, select } from 'redux-saga/effects';
|
||||||
|
|
||||||
import request from '../request';
|
import request from '../request';
|
||||||
import { lastActionIdByCardIdSelector, pathSelector } from '../../../selectors';
|
import { cardByIdSelector, lastActionIdByCardIdSelector, pathSelector } from '../../../selectors';
|
||||||
import { fetchActions } from '../../../actions';
|
import { fetchActions, toggleActionsDetails } from '../../../actions';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
|
|
||||||
export function* fetchActionsService(cardId) {
|
export function* fetchActionsService(cardId) {
|
||||||
|
const { isActionsDetailsVisible } = yield select(cardByIdSelector, cardId);
|
||||||
const lastId = yield select(lastActionIdByCardIdSelector, cardId);
|
const lastId = yield select(lastActionIdByCardIdSelector, cardId);
|
||||||
|
|
||||||
yield put(fetchActions(cardId));
|
yield put(fetchActions(cardId));
|
||||||
|
@ -19,6 +20,7 @@ export function* fetchActionsService(cardId) {
|
||||||
included: { users },
|
included: { users },
|
||||||
} = yield call(request, api.getActions, cardId, {
|
} = yield call(request, api.getActions, cardId, {
|
||||||
beforeId: lastId,
|
beforeId: lastId,
|
||||||
|
withDetails: isActionsDetailsVisible,
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
yield put(fetchActions.failure(cardId, error));
|
yield put(fetchActions.failure(cardId, error));
|
||||||
|
@ -33,3 +35,32 @@ export function* fetchActionsInCurrentCardService() {
|
||||||
|
|
||||||
yield call(fetchActionsService, cardId);
|
yield call(fetchActionsService, cardId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* toggleActionsDetailsService(cardId, isVisible) {
|
||||||
|
yield put(toggleActionsDetails(cardId, isVisible));
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
let actions;
|
||||||
|
let users;
|
||||||
|
|
||||||
|
try {
|
||||||
|
({
|
||||||
|
items: actions,
|
||||||
|
included: { users },
|
||||||
|
} = yield call(request, api.getActions, cardId, {
|
||||||
|
withDetails: isVisible,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
yield put(toggleActionsDetails.failure(cardId, error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield put(toggleActionsDetails.success(cardId, actions, users));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* toggleActionsDetailsInCurrentCardService(isVisible) {
|
||||||
|
const { cardId } = yield select(pathSelector);
|
||||||
|
|
||||||
|
yield call(toggleActionsDetailsService, cardId, isVisible);
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,19 @@
|
||||||
import { takeEvery } from 'redux-saga/effects';
|
import { all, takeEvery } from 'redux-saga/effects';
|
||||||
|
|
||||||
import { fetchActionsInCurrentCardService } from '../services';
|
import {
|
||||||
|
fetchActionsInCurrentCardService,
|
||||||
|
toggleActionsDetailsInCurrentCardService,
|
||||||
|
} from '../services';
|
||||||
import EntryActionTypes from '../../../constants/EntryActionTypes';
|
import EntryActionTypes from '../../../constants/EntryActionTypes';
|
||||||
|
|
||||||
export default function* actionsWatchers() {
|
export default function* actionsWatchers() {
|
||||||
yield takeEvery(EntryActionTypes.ACTIONS_IN_CURRENT_CARD_FETCH, () =>
|
yield all([
|
||||||
|
takeEvery(EntryActionTypes.ACTIONS_IN_CURRENT_CARD_FETCH, () =>
|
||||||
fetchActionsInCurrentCardService(),
|
fetchActionsInCurrentCardService(),
|
||||||
);
|
),
|
||||||
|
takeEvery(
|
||||||
|
EntryActionTypes.ACTIONS_DETAILS_IN_CURRENT_CARD_TOGGLE,
|
||||||
|
({ payload: { isVisible } }) => toggleActionsDetailsInCurrentCardService(isVisible),
|
||||||
|
),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@ export const makeLastActionIdByCardIdSelector = () =>
|
||||||
return cardModel;
|
return cardModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastActionModel = cardModel.getOrderedInCardActionsQuerySet().last();
|
const lastActionModel = cardModel.getFilteredOrderedInCardActionsQuerySet().last();
|
||||||
|
|
||||||
return lastActionModel && lastActionModel.id;
|
return lastActionModel && lastActionModel.id;
|
||||||
},
|
},
|
||||||
|
@ -249,7 +249,7 @@ export const actionsForCurrentCardSelector = createSelector(
|
||||||
}
|
}
|
||||||
|
|
||||||
return cardModel
|
return cardModel
|
||||||
.getOrderedInCardActionsQuerySet()
|
.getFilteredOrderedInCardActionsQuerySet()
|
||||||
.toModelArray()
|
.toModelArray()
|
||||||
.map((actionModel) => ({
|
.map((actionModel) => ({
|
||||||
...actionModel.ref,
|
...actionModel.ref,
|
||||||
|
|
|
@ -73,7 +73,7 @@ export const nextCardPositionSelector = createSelector(
|
||||||
return listModel;
|
return listModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
return nextPosition(listModel.getOrderedFilteredCardsModelArray(), index, excludedId);
|
return nextPosition(listModel.getFilteredOrderedCardsModelArray(), index, excludedId);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ export const makeCardIdsByListIdSelector = () =>
|
||||||
return listModel;
|
return listModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
return listModel.getOrderedFilteredCardsModelArray().map((cardModel) => cardModel.id);
|
return listModel.getFilteredOrderedCardsModelArray().map((cardModel) => cardModel.id);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,9 @@ module.exports = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
regex: /^[0-9]+$/,
|
regex: /^[0-9]+$/,
|
||||||
},
|
},
|
||||||
|
withDetails: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
exits: {
|
exits: {
|
||||||
|
@ -43,7 +46,11 @@ module.exports = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = await sails.helpers.cards.getActions(card.id, inputs.beforeId);
|
const actions = await sails.helpers.cards.getActions(
|
||||||
|
card.id,
|
||||||
|
inputs.beforeId,
|
||||||
|
inputs.withDetails,
|
||||||
|
);
|
||||||
|
|
||||||
const userIds = sails.helpers.utils.mapRecords(actions, 'userId', true);
|
const userIds = sails.helpers.utils.mapRecords(actions, 'userId', true);
|
||||||
const users = await sails.helpers.users.getMany(userIds, true);
|
const users = await sails.helpers.users.getMany(userIds, true);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const LIMIT = 10;
|
const LIMIT = 50;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
inputs: {
|
inputs: {
|
||||||
|
@ -10,6 +10,10 @@ module.exports = {
|
||||||
beforeId: {
|
beforeId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
withDetails: {
|
||||||
|
type: 'boolean',
|
||||||
|
defaultsTo: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
|
@ -23,6 +27,10 @@ module.exports = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!inputs.withDetails) {
|
||||||
|
criteria.type = Action.Types.COMMENT_CARD;
|
||||||
|
}
|
||||||
|
|
||||||
return sails.helpers.actions.getMany(criteria, LIMIT);
|
return sails.helpers.actions.getMany(criteria, LIMIT);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
module.exports.up = (knex) =>
|
||||||
|
knex.schema.table('action', (table) => {
|
||||||
|
/* Indexes */
|
||||||
|
|
||||||
|
table.index('type');
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.down = (knex) =>
|
||||||
|
knex.schema.table('action', (table) => {
|
||||||
|
table.dropIndex('type');
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue