mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 12:49:43 +02:00
parent
e70b92b52b
commit
3ce2e8ef91
22 changed files with 419 additions and 18 deletions
|
@ -60,6 +60,38 @@ const handleListUpdate = (list) => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sortList = (id, data) => ({
|
||||||
|
type: ActionTypes.LIST_SORT,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
sortList.success = (list, cards) => ({
|
||||||
|
type: ActionTypes.LIST_SORT__SUCCESS,
|
||||||
|
payload: {
|
||||||
|
list,
|
||||||
|
cards,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
sortList.failure = (id, error) => ({
|
||||||
|
type: ActionTypes.LIST_SORT__FAILURE,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleListSort = (list, cards) => ({
|
||||||
|
type: ActionTypes.LIST_SORT_HANDLE,
|
||||||
|
payload: {
|
||||||
|
list,
|
||||||
|
cards,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const deleteList = (id) => ({
|
const deleteList = (id) => ({
|
||||||
type: ActionTypes.LIST_DELETE,
|
type: ActionTypes.LIST_DELETE,
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -94,6 +126,8 @@ export default {
|
||||||
handleListCreate,
|
handleListCreate,
|
||||||
updateList,
|
updateList,
|
||||||
handleListUpdate,
|
handleListUpdate,
|
||||||
|
sortList,
|
||||||
|
handleListSort,
|
||||||
deleteList,
|
deleteList,
|
||||||
handleListDelete,
|
handleListDelete,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import socket from './socket';
|
import socket from './socket';
|
||||||
|
import { transformCard } from './cards';
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
|
|
||||||
|
@ -7,10 +8,33 @@ const createList = (boardId, data, headers) =>
|
||||||
|
|
||||||
const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers);
|
const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers);
|
||||||
|
|
||||||
|
const sortList = (id, data, headers) =>
|
||||||
|
socket.post(`/lists/${id}/sort`, data, headers).then((body) => ({
|
||||||
|
...body,
|
||||||
|
included: {
|
||||||
|
...body.included,
|
||||||
|
cards: body.included.cards.map(transformCard),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers);
|
const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers);
|
||||||
|
|
||||||
|
/* Event handlers */
|
||||||
|
|
||||||
|
const makeHandleListSort = (next) => (body) => {
|
||||||
|
next({
|
||||||
|
...body,
|
||||||
|
included: {
|
||||||
|
...body.included,
|
||||||
|
cards: body.included.cards.map(transformCard),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createList,
|
createList,
|
||||||
updateList,
|
updateList,
|
||||||
|
sortList,
|
||||||
deleteList,
|
deleteList,
|
||||||
|
makeHandleListSort,
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,15 +5,17 @@ import { Menu } from 'semantic-ui-react';
|
||||||
import { Popup } from '../../lib/custom-ui';
|
import { Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useSteps } from '../../hooks';
|
import { useSteps } from '../../hooks';
|
||||||
|
import ListSortStep from '../ListSortStep';
|
||||||
import DeleteStep from '../DeleteStep';
|
import DeleteStep from '../DeleteStep';
|
||||||
|
|
||||||
import styles from './ActionsStep.module.scss';
|
import styles from './ActionsStep.module.scss';
|
||||||
|
|
||||||
const StepTypes = {
|
const StepTypes = {
|
||||||
DELETE: 'DELETE',
|
DELETE: 'DELETE',
|
||||||
|
SORT: 'SORT',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) => {
|
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClose }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [step, openStep, handleBack] = useSteps();
|
const [step, openStep, handleBack] = useSteps();
|
||||||
|
|
||||||
|
@ -27,20 +29,41 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
|
||||||
onClose();
|
onClose();
|
||||||
}, [onCardAdd, onClose]);
|
}, [onCardAdd, onClose]);
|
||||||
|
|
||||||
|
const handleSortClick = useCallback(() => {
|
||||||
|
openStep(StepTypes.SORT);
|
||||||
|
}, [openStep]);
|
||||||
|
|
||||||
const handleDeleteClick = useCallback(() => {
|
const handleDeleteClick = useCallback(() => {
|
||||||
openStep(StepTypes.DELETE);
|
openStep(StepTypes.DELETE);
|
||||||
}, [openStep]);
|
}, [openStep]);
|
||||||
|
|
||||||
if (step && step.type === StepTypes.DELETE) {
|
const handleSortTypeSelect = useCallback(
|
||||||
return (
|
(type) => {
|
||||||
<DeleteStep
|
onSort({
|
||||||
title="common.deleteList"
|
type,
|
||||||
content="common.areYouSureYouWantToDeleteThisList"
|
});
|
||||||
buttonContent="action.deleteList"
|
|
||||||
onConfirm={onDelete}
|
onClose();
|
||||||
onBack={handleBack}
|
},
|
||||||
/>
|
[onSort, onClose],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (step && step.type) {
|
||||||
|
switch (step.type) {
|
||||||
|
case StepTypes.SORT:
|
||||||
|
return <ListSortStep onTypeSelect={handleSortTypeSelect} onBack={handleBack} />;
|
||||||
|
case StepTypes.DELETE:
|
||||||
|
return (
|
||||||
|
<DeleteStep
|
||||||
|
title="common.deleteList"
|
||||||
|
content="common.areYouSureYouWantToDeleteThisList"
|
||||||
|
buttonContent="action.deleteList"
|
||||||
|
onConfirm={onDelete}
|
||||||
|
onBack={handleBack}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -62,6 +85,11 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
|
||||||
context: 'title',
|
context: 'title',
|
||||||
})}
|
})}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item className={styles.menuItem} onClick={handleSortClick}>
|
||||||
|
{t('action.sortList', {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
|
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
|
||||||
{t('action.deleteList', {
|
{t('action.deleteList', {
|
||||||
context: 'title',
|
context: 'title',
|
||||||
|
@ -77,6 +105,7 @@ ActionsStep.propTypes = {
|
||||||
onNameEdit: PropTypes.func.isRequired,
|
onNameEdit: PropTypes.func.isRequired,
|
||||||
onCardAdd: PropTypes.func.isRequired,
|
onCardAdd: PropTypes.func.isRequired,
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
|
onSort: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,18 @@ import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-ic
|
||||||
import styles from './List.module.scss';
|
import styles from './List.module.scss';
|
||||||
|
|
||||||
const List = React.memo(
|
const List = React.memo(
|
||||||
({ id, index, name, isPersisted, cardIds, canEdit, onUpdate, onDelete, onCardCreate }) => {
|
({
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
name,
|
||||||
|
isPersisted,
|
||||||
|
cardIds,
|
||||||
|
canEdit,
|
||||||
|
onUpdate,
|
||||||
|
onDelete,
|
||||||
|
onSort,
|
||||||
|
onCardCreate,
|
||||||
|
}) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
|
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
|
||||||
|
|
||||||
|
@ -114,6 +125,7 @@ const List = React.memo(
|
||||||
onNameEdit={handleNameEdit}
|
onNameEdit={handleNameEdit}
|
||||||
onCardAdd={handleCardAdd}
|
onCardAdd={handleCardAdd}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
|
onSort={onSort}
|
||||||
>
|
>
|
||||||
<Button className={classNames(styles.headerButton, styles.target)}>
|
<Button className={classNames(styles.headerButton, styles.target)}>
|
||||||
<Icon fitted name="pencil" size="small" />
|
<Icon fitted name="pencil" size="small" />
|
||||||
|
@ -159,6 +171,7 @@ List.propTypes = {
|
||||||
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
canEdit: PropTypes.bool.isRequired,
|
canEdit: PropTypes.bool.isRequired,
|
||||||
onUpdate: PropTypes.func.isRequired,
|
onUpdate: PropTypes.func.isRequired,
|
||||||
|
onSort: PropTypes.func.isRequired,
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
onCardCreate: PropTypes.func.isRequired,
|
onCardCreate: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
61
client/src/components/ListSortStep/ListSortStep.jsx
Normal file
61
client/src/components/ListSortStep/ListSortStep.jsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Menu } from 'semantic-ui-react';
|
||||||
|
import { Popup } from '../../lib/custom-ui';
|
||||||
|
import { ListSortTypes } from '../../constants/Enums';
|
||||||
|
|
||||||
|
import styles from './ListSortStep.module.scss';
|
||||||
|
|
||||||
|
const ListSortStep = React.memo(({ onTypeSelect, onBack }) => {
|
||||||
|
const [t] = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Popup.Header onBack={onBack}>
|
||||||
|
{t('common.sortList', {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</Popup.Header>
|
||||||
|
<Popup.Content>
|
||||||
|
<Menu secondary vertical className={styles.menu}>
|
||||||
|
<Menu.Item
|
||||||
|
className={styles.menuItem}
|
||||||
|
onClick={() => onTypeSelect(ListSortTypes.NAME_ASC)}
|
||||||
|
>
|
||||||
|
{t('common.name')}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
className={styles.menuItem}
|
||||||
|
onClick={() => onTypeSelect(ListSortTypes.DUE_DATE_ASC)}
|
||||||
|
>
|
||||||
|
{t('common.dueDate')}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
className={styles.menuItem}
|
||||||
|
onClick={() => onTypeSelect(ListSortTypes.CREATED_AT_ASC)}
|
||||||
|
>
|
||||||
|
{t('common.oldestFirst')}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
className={styles.menuItem}
|
||||||
|
onClick={() => onTypeSelect(ListSortTypes.CREATED_AT_DESC)}
|
||||||
|
>
|
||||||
|
{t('common.newestFirst')}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
</Popup.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ListSortStep.propTypes = {
|
||||||
|
onTypeSelect: PropTypes.func.isRequired,
|
||||||
|
onBack: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
ListSortStep.defaultProps = {
|
||||||
|
onBack: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListSortStep;
|
11
client/src/components/ListSortStep/ListSortStep.module.scss
Normal file
11
client/src/components/ListSortStep/ListSortStep.module.scss
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
:global(#app) {
|
||||||
|
.menu {
|
||||||
|
margin: -7px -12px -5px;
|
||||||
|
width: calc(100% + 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuItem {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 14px;
|
||||||
|
}
|
||||||
|
}
|
3
client/src/components/ListSortStep/index.js
Normal file
3
client/src/components/ListSortStep/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import ListSortStep from './ListSortStep';
|
||||||
|
|
||||||
|
export default ListSortStep;
|
|
@ -173,6 +173,10 @@ export default {
|
||||||
LIST_UPDATE__SUCCESS: 'LIST_UPDATE__SUCCESS',
|
LIST_UPDATE__SUCCESS: 'LIST_UPDATE__SUCCESS',
|
||||||
LIST_UPDATE__FAILURE: 'LIST_UPDATE__FAILURE',
|
LIST_UPDATE__FAILURE: 'LIST_UPDATE__FAILURE',
|
||||||
LIST_UPDATE_HANDLE: 'LIST_UPDATE_HANDLE',
|
LIST_UPDATE_HANDLE: 'LIST_UPDATE_HANDLE',
|
||||||
|
LIST_SORT: 'LIST_SORT',
|
||||||
|
LIST_SORT__SUCCESS: 'LIST_SORT__SUCCESS',
|
||||||
|
LIST_SORT__FAILURE: 'LIST_SORT__FAILURE',
|
||||||
|
LIST_SORT_HANDLE: 'LIST_SORT_HANDLE',
|
||||||
LIST_DELETE: 'LIST_DELETE',
|
LIST_DELETE: 'LIST_DELETE',
|
||||||
LIST_DELETE__SUCCESS: 'LIST_DELETE__SUCCESS',
|
LIST_DELETE__SUCCESS: 'LIST_DELETE__SUCCESS',
|
||||||
LIST_DELETE__FAILURE: 'LIST_DELETE__FAILURE',
|
LIST_DELETE__FAILURE: 'LIST_DELETE__FAILURE',
|
||||||
|
|
|
@ -120,6 +120,8 @@ export default {
|
||||||
LIST_MOVE: `${PREFIX}/LIST_MOVE`,
|
LIST_MOVE: `${PREFIX}/LIST_MOVE`,
|
||||||
LIST_DELETE: `${PREFIX}/LIST_DELETE`,
|
LIST_DELETE: `${PREFIX}/LIST_DELETE`,
|
||||||
LIST_DELETE_HANDLE: `${PREFIX}/LIST_DELETE_HANDLE`,
|
LIST_DELETE_HANDLE: `${PREFIX}/LIST_DELETE_HANDLE`,
|
||||||
|
LIST_SORT: `${PREFIX}/LIST_SORT`,
|
||||||
|
LIST_SORT_HANDLE: `${PREFIX}/LIST_SORT_HANDLE`,
|
||||||
|
|
||||||
/* Cards */
|
/* Cards */
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,13 @@ export const BoardMembershipRoles = {
|
||||||
VIEWER: 'viewer',
|
VIEWER: 'viewer',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ListSortTypes = {
|
||||||
|
NAME_ASC: 'name_asc',
|
||||||
|
DUE_DATE_ASC: 'dueDate_asc',
|
||||||
|
CREATED_AT_ASC: 'createdAt_asc',
|
||||||
|
CREATED_AT_DESC: 'createdAt_desc',
|
||||||
|
};
|
||||||
|
|
||||||
export const ActivityTypes = {
|
export const ActivityTypes = {
|
||||||
CREATE_CARD: 'createCard',
|
CREATE_CARD: 'createCard',
|
||||||
MOVE_CARD: 'moveCard',
|
MOVE_CARD: 'moveCard',
|
||||||
|
|
|
@ -33,6 +33,7 @@ const mapDispatchToProps = (dispatch, { id }) =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
{
|
{
|
||||||
onUpdate: (data) => entryActions.updateList(id, data),
|
onUpdate: (data) => entryActions.updateList(id, data),
|
||||||
|
onSort: (data) => entryActions.sortList(id, data),
|
||||||
onDelete: () => entryActions.deleteList(id),
|
onDelete: () => entryActions.deleteList(id),
|
||||||
onCardCreate: (data, autoOpen) => entryActions.createCard(id, data, autoOpen),
|
onCardCreate: (data, autoOpen) => entryActions.createCard(id, data, autoOpen),
|
||||||
},
|
},
|
||||||
|
|
|
@ -37,6 +37,24 @@ const moveList = (id, index) => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sortList = (id, data) => {
|
||||||
|
return {
|
||||||
|
type: EntryActionTypes.LIST_SORT,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleListSort = (list, cards) => ({
|
||||||
|
type: EntryActionTypes.LIST_SORT_HANDLE,
|
||||||
|
payload: {
|
||||||
|
list,
|
||||||
|
cards,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const deleteList = (id) => ({
|
const deleteList = (id) => ({
|
||||||
type: EntryActionTypes.LIST_DELETE,
|
type: EntryActionTypes.LIST_DELETE,
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -57,6 +75,8 @@ export default {
|
||||||
updateList,
|
updateList,
|
||||||
handleListUpdate,
|
handleListUpdate,
|
||||||
moveList,
|
moveList,
|
||||||
|
sortList,
|
||||||
|
handleListSort,
|
||||||
deleteList,
|
deleteList,
|
||||||
handleListDelete,
|
handleListDelete,
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,6 +60,7 @@ export default {
|
||||||
currentPassword: 'Current password',
|
currentPassword: 'Current password',
|
||||||
dangerZone_title: 'Danger Zone',
|
dangerZone_title: 'Danger Zone',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
|
dueDate: 'Due date',
|
||||||
dueDate_title: 'Due Date',
|
dueDate_title: 'Due Date',
|
||||||
deleteAttachment_title: 'Delete Attachment',
|
deleteAttachment_title: 'Delete Attachment',
|
||||||
deleteBoard_title: 'Delete Board',
|
deleteBoard_title: 'Delete Board',
|
||||||
|
@ -113,6 +114,7 @@ export default {
|
||||||
minutes: 'Minutes',
|
minutes: 'Minutes',
|
||||||
moveCard_title: 'Move Card',
|
moveCard_title: 'Move Card',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
|
newestFirst: 'Newest first',
|
||||||
newEmail: 'New e-mail',
|
newEmail: 'New e-mail',
|
||||||
newPassword: 'New password',
|
newPassword: 'New password',
|
||||||
newUsername: 'New username',
|
newUsername: 'New username',
|
||||||
|
@ -122,6 +124,7 @@ export default {
|
||||||
noProjects: 'No projects',
|
noProjects: 'No projects',
|
||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
noUnreadNotifications: 'No unread notifications.',
|
noUnreadNotifications: 'No unread notifications.',
|
||||||
|
oldestFirst: 'Oldest first',
|
||||||
openBoard_title: 'Open Board',
|
openBoard_title: 'Open Board',
|
||||||
optional_inline: 'optional',
|
optional_inline: 'optional',
|
||||||
organization: 'Organization',
|
organization: 'Organization',
|
||||||
|
@ -142,6 +145,7 @@ export default {
|
||||||
selectPermissions_title: 'Select Permissions',
|
selectPermissions_title: 'Select Permissions',
|
||||||
selectProject: 'Select project',
|
selectProject: 'Select project',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
|
sortList_title: 'Sort List',
|
||||||
stopwatch: 'Stopwatch',
|
stopwatch: 'Stopwatch',
|
||||||
subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default',
|
subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default',
|
||||||
taskActions_title: 'Task Actions',
|
taskActions_title: 'Task Actions',
|
||||||
|
@ -229,6 +233,7 @@ export default {
|
||||||
showAllAttachments: 'Show all attachments ({{hidden}} hidden)',
|
showAllAttachments: 'Show all attachments ({{hidden}} hidden)',
|
||||||
showDetails: 'Show details',
|
showDetails: 'Show details',
|
||||||
showFewerAttachments: 'Show fewer attachments',
|
showFewerAttachments: 'Show fewer attachments',
|
||||||
|
sortList_title: 'Sort List',
|
||||||
start: 'Start',
|
start: 'Start',
|
||||||
stop: 'Stop',
|
stop: 'Stop',
|
||||||
subscribe: 'Subscribe',
|
subscribe: 'Subscribe',
|
||||||
|
|
|
@ -164,6 +164,14 @@ export default class extends BaseModel {
|
||||||
Card.withId(payload.cardLabel.cardId).labels.remove(payload.cardLabel.labelId);
|
Card.withId(payload.cardLabel.cardId).labels.remove(payload.cardLabel.labelId);
|
||||||
} catch {} // eslint-disable-line no-empty
|
} catch {} // eslint-disable-line no-empty
|
||||||
|
|
||||||
|
break;
|
||||||
|
case ActionTypes.LIST_SORT__SUCCESS:
|
||||||
|
case ActionTypes.LIST_SORT_HANDLE:
|
||||||
|
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
|
||||||
|
payload.cards.forEach((card) => {
|
||||||
|
Card.upsert(card);
|
||||||
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.CARD_CREATE:
|
case ActionTypes.CARD_CREATE:
|
||||||
case ActionTypes.CARD_UPDATE__SUCCESS:
|
case ActionTypes.CARD_UPDATE__SUCCESS:
|
||||||
|
@ -282,12 +290,6 @@ export default class extends BaseModel {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
|
|
||||||
payload.cards.forEach((card) => {
|
|
||||||
Card.upsert(card);
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,8 @@ export default class extends BaseModel {
|
||||||
case ActionTypes.LIST_CREATE_HANDLE:
|
case ActionTypes.LIST_CREATE_HANDLE:
|
||||||
case ActionTypes.LIST_UPDATE__SUCCESS:
|
case ActionTypes.LIST_UPDATE__SUCCESS:
|
||||||
case ActionTypes.LIST_UPDATE_HANDLE:
|
case ActionTypes.LIST_UPDATE_HANDLE:
|
||||||
|
case ActionTypes.LIST_SORT__SUCCESS:
|
||||||
|
case ActionTypes.LIST_SORT_HANDLE:
|
||||||
List.upsert(payload.list);
|
List.upsert(payload.list);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -70,6 +70,30 @@ export function* moveList(id, index) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: sort locally
|
||||||
|
export function* sortList(id, data) {
|
||||||
|
yield put(actions.sortList(id, data));
|
||||||
|
|
||||||
|
let list;
|
||||||
|
let cards;
|
||||||
|
|
||||||
|
try {
|
||||||
|
({
|
||||||
|
item: list,
|
||||||
|
included: { cards },
|
||||||
|
} = yield call(request, api.sortList, id, data));
|
||||||
|
} catch (error) {
|
||||||
|
yield put(actions.sortList.failure(id, error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield put(actions.sortList.success(list, cards));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* handleListSort(list, cards) {
|
||||||
|
yield put(actions.handleListSort(list, cards));
|
||||||
|
}
|
||||||
|
|
||||||
export function* deleteList(id) {
|
export function* deleteList(id) {
|
||||||
yield put(actions.deleteList(id));
|
yield put(actions.deleteList(id));
|
||||||
|
|
||||||
|
@ -95,6 +119,8 @@ export default {
|
||||||
updateList,
|
updateList,
|
||||||
handleListUpdate,
|
handleListUpdate,
|
||||||
moveList,
|
moveList,
|
||||||
|
sortList,
|
||||||
|
handleListSort,
|
||||||
deleteList,
|
deleteList,
|
||||||
handleListDelete,
|
handleListDelete,
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,12 @@ export default function* listsWatchers() {
|
||||||
takeEvery(EntryActionTypes.LIST_MOVE, ({ payload: { id, index } }) =>
|
takeEvery(EntryActionTypes.LIST_MOVE, ({ payload: { id, index } }) =>
|
||||||
services.moveList(id, index),
|
services.moveList(id, index),
|
||||||
),
|
),
|
||||||
|
takeEvery(EntryActionTypes.LIST_SORT, ({ payload: { id, data } }) =>
|
||||||
|
services.sortList(id, data),
|
||||||
|
),
|
||||||
|
takeEvery(EntryActionTypes.LIST_SORT_HANDLE, ({ payload: { list, cards } }) =>
|
||||||
|
services.handleListSort(list, cards),
|
||||||
|
),
|
||||||
takeEvery(EntryActionTypes.LIST_DELETE, ({ payload: { id } }) => services.deleteList(id)),
|
takeEvery(EntryActionTypes.LIST_DELETE, ({ payload: { id } }) => services.deleteList(id)),
|
||||||
takeEvery(EntryActionTypes.LIST_DELETE_HANDLE, ({ payload: { list } }) =>
|
takeEvery(EntryActionTypes.LIST_DELETE_HANDLE, ({ payload: { list } }) =>
|
||||||
services.handleListDelete(list),
|
services.handleListDelete(list),
|
||||||
|
|
|
@ -84,6 +84,10 @@ const createSocketEventsChannel = () =>
|
||||||
emit(entryActions.handleListUpdate(item));
|
emit(entryActions.handleListUpdate(item));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleListSort = api.makeHandleListSort(({ item, included: { cards } }) => {
|
||||||
|
emit(entryActions.handleListSort(item, cards));
|
||||||
|
});
|
||||||
|
|
||||||
const handleListDelete = ({ item }) => {
|
const handleListDelete = ({ item }) => {
|
||||||
emit(entryActions.handleListDelete(item));
|
emit(entryActions.handleListDelete(item));
|
||||||
};
|
};
|
||||||
|
@ -198,6 +202,7 @@ const createSocketEventsChannel = () =>
|
||||||
|
|
||||||
socket.on('listCreate', handleListCreate);
|
socket.on('listCreate', handleListCreate);
|
||||||
socket.on('listUpdate', handleListUpdate);
|
socket.on('listUpdate', handleListUpdate);
|
||||||
|
socket.on('listSort', handleListSort);
|
||||||
socket.on('listDelete', handleListDelete);
|
socket.on('listDelete', handleListDelete);
|
||||||
|
|
||||||
socket.on('labelCreate', handleLabelCreate);
|
socket.on('labelCreate', handleLabelCreate);
|
||||||
|
@ -256,6 +261,7 @@ const createSocketEventsChannel = () =>
|
||||||
|
|
||||||
socket.off('listCreate', handleListCreate);
|
socket.off('listCreate', handleListCreate);
|
||||||
socket.off('listUpdate', handleListUpdate);
|
socket.off('listUpdate', handleListUpdate);
|
||||||
|
socket.off('listSort', handleListSort);
|
||||||
socket.off('listDelete', handleListDelete);
|
socket.off('listDelete', handleListDelete);
|
||||||
|
|
||||||
socket.off('labelCreate', handleLabelCreate);
|
socket.off('labelCreate', handleLabelCreate);
|
||||||
|
|
65
server/api/controllers/lists/sort.js
Normal file
65
server/api/controllers/lists/sort.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
const Errors = {
|
||||||
|
NOT_ENOUGH_RIGHTS: {
|
||||||
|
notEnoughRights: 'Not enough rights',
|
||||||
|
},
|
||||||
|
LIST_NOT_FOUND: {
|
||||||
|
listNotFound: 'List not found',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
regex: /^[0-9]+$/,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
isIn: Object.values(List.SortTypes),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
notEnoughRights: {
|
||||||
|
responseType: 'forbidden',
|
||||||
|
},
|
||||||
|
listNotFound: {
|
||||||
|
responseType: 'notFound',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs) {
|
||||||
|
const { currentUser } = this.req;
|
||||||
|
|
||||||
|
const { list } = await sails.helpers.lists
|
||||||
|
.getProjectPath(inputs.id)
|
||||||
|
.intercept('pathNotFound', () => Errors.LIST_NOT_FOUND);
|
||||||
|
|
||||||
|
const boardMembership = await BoardMembership.findOne({
|
||||||
|
boardId: list.boardId,
|
||||||
|
userId: currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!boardMembership) {
|
||||||
|
throw Errors.LIST_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||||
|
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = await sails.helpers.lists.sortOne.with({
|
||||||
|
record: list,
|
||||||
|
type: inputs.type,
|
||||||
|
request: this.req,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
item: list,
|
||||||
|
included: {
|
||||||
|
cards,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
70
server/api/helpers/lists/sort-one.js
Normal file
70
server/api/helpers/lists/sort-one.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
const List = require('../../models/List');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
record: {
|
||||||
|
type: 'ref',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
isIn: Object.values(List.SortTypes),
|
||||||
|
defaultsTo: List.SortTypes.NAME_ASC,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
type: 'ref',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs) {
|
||||||
|
let cards = await sails.helpers.lists.getCards(inputs.record.id);
|
||||||
|
|
||||||
|
switch (inputs.type) {
|
||||||
|
case List.SortTypes.NAME_ASC:
|
||||||
|
cards.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
break;
|
||||||
|
case List.SortTypes.DUE_DATE_ASC:
|
||||||
|
cards.sort((a, b) => {
|
||||||
|
if (a.dueDate === null) return 1;
|
||||||
|
if (b.dueDate === null) return -1;
|
||||||
|
return new Date(a.dueDate) - new Date(b.dueDate);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case List.SortTypes.CREATED_AT_ASC:
|
||||||
|
cards.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
|
||||||
|
break;
|
||||||
|
case List.SortTypes.CREATED_AT_DESC:
|
||||||
|
cards.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid sort type specified');
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = cards.map((c) => c.position).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
cards = await Promise.all(
|
||||||
|
cards.map(({ id }, index) =>
|
||||||
|
Card.updateOne({
|
||||||
|
id,
|
||||||
|
listId: inputs.record.id,
|
||||||
|
}).set({
|
||||||
|
position: positions[index],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
sails.sockets.broadcast(
|
||||||
|
`board:${inputs.record.boardId}`,
|
||||||
|
'listSort',
|
||||||
|
{
|
||||||
|
item: inputs.record,
|
||||||
|
included: {
|
||||||
|
cards,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputs.request,
|
||||||
|
);
|
||||||
|
|
||||||
|
return cards;
|
||||||
|
},
|
||||||
|
};
|
|
@ -5,7 +5,16 @@
|
||||||
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const SortTypes = {
|
||||||
|
NAME_ASC: 'name_asc',
|
||||||
|
DUE_DATE_ASC: 'dueDate_asc',
|
||||||
|
CREATED_AT_ASC: 'createdAt_asc',
|
||||||
|
CREATED_AT_DESC: 'createdAt_desc',
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
SortTypes,
|
||||||
|
|
||||||
attributes: {
|
attributes: {
|
||||||
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
||||||
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||||
|
|
|
@ -50,6 +50,7 @@ module.exports.routes = {
|
||||||
|
|
||||||
'POST /api/boards/:boardId/lists': 'lists/create',
|
'POST /api/boards/:boardId/lists': 'lists/create',
|
||||||
'PATCH /api/lists/:id': 'lists/update',
|
'PATCH /api/lists/:id': 'lists/update',
|
||||||
|
'POST /api/lists/:id/sort': 'lists/sort',
|
||||||
'DELETE /api/lists/:id': 'lists/delete',
|
'DELETE /api/lists/:id': 'lists/delete',
|
||||||
|
|
||||||
'POST /api/lists/:listId/cards': 'cards/create',
|
'POST /api/lists/:listId/cards': 'cards/create',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue