Skip to content

Commit

Permalink
feat: Sort cards within list (#717)
Browse files Browse the repository at this point in the history
Closes #390
  • Loading branch information
JustSamuel committed Apr 22, 2024
1 parent 2c84316 commit 934dcdf
Show file tree
Hide file tree
Showing 22 changed files with 419 additions and 18 deletions.
34 changes: 34 additions & 0 deletions client/src/actions/lists.js
Expand Up @@ -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) => ({
type: ActionTypes.LIST_DELETE,
payload: {
Expand Down Expand Up @@ -94,6 +126,8 @@ export default {
handleListCreate,
updateList,
handleListUpdate,
sortList,
handleListSort,
deleteList,
handleListDelete,
};
24 changes: 24 additions & 0 deletions client/src/api/lists.js
@@ -1,4 +1,5 @@
import socket from './socket';
import { transformCard } from './cards';

/* Actions */

Expand All @@ -7,10 +8,33 @@ const createList = (boardId, 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);

/* Event handlers */

const makeHandleListSort = (next) => (body) => {
next({
...body,
included: {
...body.included,
cards: body.included.cards.map(transformCard),
},
});
};

export default {
createList,
updateList,
sortList,
deleteList,
makeHandleListSort,
};
51 changes: 40 additions & 11 deletions client/src/components/List/ActionsStep.jsx
Expand Up @@ -5,15 +5,17 @@ import { Menu } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';

import { useSteps } from '../../hooks';
import ListSortStep from '../ListSortStep';
import DeleteStep from '../DeleteStep';

import styles from './ActionsStep.module.scss';

const StepTypes = {
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 [step, openStep, handleBack] = useSteps();

Expand All @@ -27,20 +29,41 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
onClose();
}, [onCardAdd, onClose]);

const handleSortClick = useCallback(() => {
openStep(StepTypes.SORT);
}, [openStep]);

const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);

if (step && step.type === StepTypes.DELETE) {
return (
<DeleteStep
title="common.deleteList"
content="common.areYouSureYouWantToDeleteThisList"
buttonContent="action.deleteList"
onConfirm={onDelete}
onBack={handleBack}
/>
);
const handleSortTypeSelect = useCallback(
(type) => {
onSort({
type,
});

onClose();
},
[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 (
Expand All @@ -62,6 +85,11 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleSortClick}>
{t('action.sortList', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteList', {
context: 'title',
Expand All @@ -77,6 +105,7 @@ ActionsStep.propTypes = {
onNameEdit: PropTypes.func.isRequired,
onCardAdd: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};

Expand Down
15 changes: 14 additions & 1 deletion client/src/components/List/List.jsx
Expand Up @@ -16,7 +16,18 @@ import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-ic
import styles from './List.module.scss';

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 [isAddCardOpened, setIsAddCardOpened] = useState(false);

Expand Down Expand Up @@ -114,6 +125,7 @@ const List = React.memo(
onNameEdit={handleNameEdit}
onCardAdd={handleCardAdd}
onDelete={onDelete}
onSort={onSort}
>
<Button className={classNames(styles.headerButton, styles.target)}>
<Icon fitted name="pencil" size="small" />
Expand Down Expand Up @@ -159,6 +171,7 @@ List.propTypes = {
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onCardCreate: PropTypes.func.isRequired,
};
Expand Down
61 changes: 61 additions & 0 deletions client/src/components/ListSortStep/ListSortStep.jsx
@@ -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 changes: 11 additions & 0 deletions client/src/components/ListSortStep/ListSortStep.module.scss
@@ -0,0 +1,11 @@
:global(#app) {
.menu {
margin: -7px -12px -5px;
width: calc(100% + 24px);
}

.menuItem {
margin: 0;
padding-left: 14px;
}
}
3 changes: 3 additions & 0 deletions client/src/components/ListSortStep/index.js
@@ -0,0 +1,3 @@
import ListSortStep from './ListSortStep';

export default ListSortStep;
4 changes: 4 additions & 0 deletions client/src/constants/ActionTypes.js
Expand Up @@ -173,6 +173,10 @@ export default {
LIST_UPDATE__SUCCESS: 'LIST_UPDATE__SUCCESS',
LIST_UPDATE__FAILURE: 'LIST_UPDATE__FAILURE',
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__SUCCESS: 'LIST_DELETE__SUCCESS',
LIST_DELETE__FAILURE: 'LIST_DELETE__FAILURE',
Expand Down
2 changes: 2 additions & 0 deletions client/src/constants/EntryActionTypes.js
Expand Up @@ -120,6 +120,8 @@ export default {
LIST_MOVE: `${PREFIX}/LIST_MOVE`,
LIST_DELETE: `${PREFIX}/LIST_DELETE`,
LIST_DELETE_HANDLE: `${PREFIX}/LIST_DELETE_HANDLE`,
LIST_SORT: `${PREFIX}/LIST_SORT`,
LIST_SORT_HANDLE: `${PREFIX}/LIST_SORT_HANDLE`,

/* Cards */

Expand Down
7 changes: 7 additions & 0 deletions client/src/constants/Enums.js
Expand Up @@ -8,6 +8,13 @@ export const BoardMembershipRoles = {
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 = {
CREATE_CARD: 'createCard',
MOVE_CARD: 'moveCard',
Expand Down
1 change: 1 addition & 0 deletions client/src/containers/ListContainer.js
Expand Up @@ -33,6 +33,7 @@ const mapDispatchToProps = (dispatch, { id }) =>
bindActionCreators(
{
onUpdate: (data) => entryActions.updateList(id, data),
onSort: (data) => entryActions.sortList(id, data),
onDelete: () => entryActions.deleteList(id),
onCardCreate: (data, autoOpen) => entryActions.createCard(id, data, autoOpen),
},
Expand Down
20 changes: 20 additions & 0 deletions client/src/entry-actions/lists.js
Expand Up @@ -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) => ({
type: EntryActionTypes.LIST_DELETE,
payload: {
Expand All @@ -57,6 +75,8 @@ export default {
updateList,
handleListUpdate,
moveList,
sortList,
handleListSort,
deleteList,
handleListDelete,
};

0 comments on commit 934dcdf

Please sign in to comment.