1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-08-10 16:05:35 +02:00

Added sorting of lists. Added sorting for name, id, position, createdAt,

updatedAt, dueDate, creatorUserId
This commit is contained in:
Jens Frost 2023-04-28 14:53:47 +02:00
parent 0d481703da
commit 44a3c2a2ec
18 changed files with 424 additions and 65 deletions

View file

@ -89,6 +89,35 @@ const handleListDelete = (list) => ({
},
});
const sortList = (id) => ({
type: ActionTypes.LIST_SORT,
payload: {
id,
},
});
sortList.success = (list) => ({
type: ActionTypes.LIST_SORT__SUCCESS,
payload: {
list,
},
});
sortList.failure = (id, error) => ({
type: ActionTypes.LIST_SORT__FAILURE,
payload: {
id,
error,
},
});
const handleListSort = (list) => ({
type: ActionTypes.LIST_SORT_HANDLE,
payload: {
list,
},
});
export default {
createList,
handleListCreate,
@ -96,4 +125,6 @@ export default {
handleListUpdate,
deleteList,
handleListDelete,
sortList,
handleListSort,
};

View file

@ -248,6 +248,8 @@ Card.propTypes = {
onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
// onSortTitleAsc: PropTypes.func.isRequired,
// onSortTitleDesc: PropTypes.func.isRequired,
};
Card.defaultProps = {

View file

@ -6,78 +6,108 @@ import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks';
import DeleteStep from '../DeleteStep';
import SortStep from '../SortStep';
import styles from './ActionsStep.module.scss';
const StepTypes = {
DELETE: 'DELETE',
SORT: 'SORT',
};
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const ActionsStep = React.memo(
({ onNameEdit, onCardAdd, onDelete, onClose, onSort, selectedOption, setSelectedOption }) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleEditNameClick = useCallback(() => {
onNameEdit();
onClose();
}, [onNameEdit, onClose]);
const handleEditNameClick = useCallback(() => {
onNameEdit();
onClose();
}, [onNameEdit, onClose]);
const handleAddCardClick = useCallback(() => {
onCardAdd();
onClose();
}, [onCardAdd, onClose]);
const handleAddCardClick = useCallback(() => {
onCardAdd();
onClose();
}, [onCardAdd, onClose]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
const handleSortClick = useCallback(() => {
openStep(StepTypes.SORT);
}, [openStep]);
if (step && step.type === StepTypes.DELETE) {
return (
<DeleteStep
title="common.deleteList"
content="common.areYouSureYouWantToDeleteThisList"
buttonContent="action.deleteList"
onConfirm={onDelete}
onBack={handleBack}
/>
);
}
if (step && step.type === StepTypes.SORT) {
return (
<SortStep
title="common.sortList"
content="common.areYouSureYouWantToSortThisList"
buttonContent="action.sortList"
onConfirm={onSort}
onBack={handleBack}
selectedOption={selectedOption}
setSelectedOption={setSelectedOption}
/>
);
}
if (step && step.type === StepTypes.DELETE) {
return (
<DeleteStep
title="common.deleteList"
content="common.areYouSureYouWantToDeleteThisList"
buttonContent="action.deleteList"
onConfirm={onDelete}
onBack={handleBack}
/>
<>
<Popup.Header>
{t('common.listActions', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
{t('action.editTitle', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleAddCardClick}>
{t('action.addCard', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteList', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleSortClick}>
{t('action.sortList', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
}
return (
<>
<Popup.Header>
{t('common.listActions', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
{t('action.editTitle', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleAddCardClick}>
{t('action.addCard', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteList', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
});
},
);
ActionsStep.propTypes = {
onNameEdit: PropTypes.func.isRequired,
onCardAdd: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
selectedOption: PropTypes.string.isRequired,
setSelectedOption: PropTypes.func.isRequired,
};
export default ActionsStep;

View file

@ -4,7 +4,7 @@ import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Draggable, Droppable } from 'react-beautiful-dnd';
import { Button, Icon } from 'semantic-ui-react';
import { usePopup } from '../../lib/popup';
import { usePopup, closePopup } from '../../lib/popup';
import DroppableTypes from '../../constants/DroppableTypes';
import CardContainer from '../../containers/CardContainer';
@ -19,6 +19,7 @@ const List = React.memo(
({ id, index, name, isPersisted, cardIds, canEdit, onUpdate, onDelete, onCardCreate }) => {
const [t] = useTranslation();
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
const [selectedOption, setSelectedOption] = useState('name');
const nameEdit = useRef(null);
const listWrapper = useRef(null);
@ -40,11 +41,11 @@ const List = React.memo(
const handleAddCardClick = useCallback(() => {
setIsAddCardOpened(true);
}, []);
}, [setIsAddCardOpened]);
const handleAddCardClose = useCallback(() => {
setIsAddCardOpened(false);
}, []);
}, [setIsAddCardOpened]);
const handleNameEdit = useCallback(() => {
nameEdit.current.open();
@ -52,7 +53,12 @@ const List = React.memo(
const handleCardAdd = useCallback(() => {
setIsAddCardOpened(true);
}, []);
}, [setIsAddCardOpened]);
const onSort = useCallback(() => {
onUpdate({ selectedOption: document.querySelector('input[name="sort"]:checked').value });
closePopup();
}, [onUpdate]);
useEffect(() => {
if (isAddCardOpened) {
@ -114,6 +120,9 @@ const List = React.memo(
onNameEdit={handleNameEdit}
onCardAdd={handleCardAdd}
onDelete={onDelete}
onSort={onSort}
selectedOption={selectedOption}
setSelectedOption={setSelectedOption}
>
<Button className={classNames(styles.headerButton, styles.target)}>
<Icon fitted name="pencil" size="small" />

View file

@ -0,0 +1,164 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import styles from './SortStep.module.scss';
const SortStep = React.memo(
({ title, content, buttonContent, onBack, onConfirm, selectedOption, setSelectedOption }) => {
const [t] = useTranslation();
const [checked, setChecked] = useState(false);
function handleChange(event) {
setSelectedOption(event.target.id);
}
const updateState = () => {
if (checked) {
setChecked(false);
} else {
setChecked(true);
}
};
function changeEvent(event) {
handleChange(event);
updateState();
}
return (
<>
<Popup.Header onBack={onBack}>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<div className={styles.content}>{t(content)}</div>
<div>
<label htmlFor="name">
<input
type="radio"
id="name"
name="sort"
value="name"
onChange={changeEvent}
checked={selectedOption === 'name'}
/>
{t('action.sortByTitleList', {
context: 'title',
})}
</label>
<br />
<label htmlFor="id">
<input
type="radio"
id="id"
name="sort"
value="id"
onChange={changeEvent}
checked={selectedOption === 'id'}
/>
{t('action.sortByIdList', {
context: 'title',
})}
</label>
<br />
<label htmlFor="position">
<input
type="radio"
id="position"
name="sort"
value="position"
onChange={changeEvent}
checked={selectedOption === 'position'}
/>
{t('action.sortByPositionList', {
context: 'title',
})}
</label>
<br />
<label htmlFor="createdAt">
<input
type="radio"
id="createdAt"
name="sort"
value="createdAt"
onChange={changeEvent}
checked={selectedOption === 'createdAt'}
/>
{t('action.sortByCreatedAt', {
context: 'title',
})}
</label>
<br />
<label htmlFor="updatedAt">
<input
type="radio"
id="updatedAt"
name="sort"
value="updatedAt"
onChange={changeEvent}
checked={selectedOption === 'updatedAt'}
/>
{t('action.sortByUpdatedAtList', {
context: 'title',
})}
</label>
<br />
<label htmlFor="dueDate">
<input
type="radio"
id="dueDate"
name="sort"
value="dueDate"
onChange={changeEvent}
checked={selectedOption === 'dueDate'}
/>
{t('action.sortByDueDateList', {
context: 'title',
})}
</label>
<br />
<label htmlFor="creatorUserId">
<input
type="radio"
id="creatorUserId"
name="sort"
value="creatorUserId"
onChange={changeEvent}
checked={selectedOption === 'creatorUserId'}
/>
{t('action.sortByCreatorUserIdList', {
context: 'title',
})}
</label>
<br />
</div>
<Button fluid negative content={t(buttonContent)} onClick={onConfirm} />
</Popup.Content>
</>
);
},
);
SortStep.propTypes = {
title: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
buttonContent: PropTypes.string.isRequired,
onConfirm: PropTypes.func.isRequired,
onBack: PropTypes.func,
selectedOption: PropTypes.string.isRequired,
setSelectedOption: PropTypes.func.isRequired,
};
SortStep.defaultProps = {
onBack: undefined,
};
export default SortStep;

View file

@ -0,0 +1,7 @@
:global(#app) {
.content {
color: #212121;
padding-bottom: 6px;
padding-left: 2px;
}
}

View file

@ -0,0 +1,3 @@
import SortStep from './SortStep';
export default SortStep;

View file

@ -172,6 +172,10 @@ export default {
LIST_DELETE__SUCCESS: 'LIST_DELETE__SUCCESS',
LIST_DELETE__FAILURE: 'LIST_DELETE__FAILURE',
LIST_DELETE_HANDLE: 'LIST_DELETE_HANDLE',
LIST_SORT: 'LIST_SORT',
LIST_SORT__SUCCESS: 'LIST_SORT__SUCCESS',
LIST_SORT__FAILURE: 'LIST_SORT__FAILURE',
LIST_SORT_HANDLE: 'LIST_SORT_HANDLE',
/* Cards */

View file

@ -120,6 +120,7 @@ export default {
LIST_MOVE: `${PREFIX}/LIST_MOVE`,
LIST_DELETE: `${PREFIX}/LIST_DELETE`,
LIST_DELETE_HANDLE: `${PREFIX}/LIST_DELETE_HANDLE`,
LIST_SORT_HANDLE: `${PREFIX}/LIST_SORT_HANDLE`,
/* Cards */

View file

@ -24,6 +24,7 @@ const mapDispatchToProps = (dispatch) =>
onUpdate: entryActions.updateBoard,
onMove: entryActions.moveBoard,
onDelete: entryActions.deleteBoard,
onSort: entryActions.sortBoard,
},
dispatch,
);

View file

@ -35,6 +35,7 @@ const mapDispatchToProps = (dispatch, { id }) =>
onUpdate: (data) => entryActions.updateList(id, data),
onDelete: () => entryActions.deleteList(id),
onCardCreate: (data, autoOpen) => entryActions.createCard(id, data, autoOpen),
onSort: (data) => entryActions.sortList(id, data),
},
dispatch,
);

View file

@ -51,6 +51,13 @@ const handleListDelete = (list) => ({
},
});
const handleListSort = (list) => ({
type: EntryActionTypes.LIST_SORT_HANDLE,
payload: {
list,
},
});
export default {
createListInCurrentBoard,
handleListCreate,
@ -59,4 +66,5 @@ export default {
moveList,
deleteList,
handleListDelete,
handleListSort,
};

View file

@ -32,6 +32,7 @@ export default {
'Är du säker på att du vill ta bort den här kommentaren?',
areYouSureYouWantToDeleteThisLabel: 'Är du säker på att du vill ta bort den här etiketten?',
areYouSureYouWantToDeleteThisList: 'Är du säker på att du vill ta bort den här listan?',
areYouSureYouWantToSortThisList: 'Är du säker på att du vill sortera den här listan?',
areYouSureYouWantToDeleteThisProject: 'Är du säker på att du vill ta bort det här projektet?',
areYouSureYouWantToDeleteThisTask: 'Är du säker på att du vill ta bort den här uppgiften?',
areYouSureYouWantToDeleteThisUser: 'Är du säker på att du vill ta bort den här användaren?',
@ -129,6 +130,7 @@ export default {
selectList: 'Välj lista',
selectProject: 'Välj projekt',
settings: 'Inställningar',
sortList: 'Sortera listan',
stopwatch: 'Timer',
subscribeToMyOwnCardsByDefault: 'Prenumerera på mina egna kort som standard',
taskActions_title: 'Uppgiftsåtgärder',
@ -205,6 +207,14 @@ export default {
save: 'Spara',
showAllAttachments: 'Visa alla bilagor ({{hidden}} dolda)',
showFewerAttachments: 'Visa färre bilagor',
sortByCreatedAt: ' Sortera på skapad',
sortByCreatorUserIdList: ' Sortera på skapad av',
sortByDueDateList: ' Sortera på förfallodatum',
sortByIdList: ' Sortera på id',
sortByPositionList: ' Sortera på position',
sortByTitleList: ' Sortera på titel',
sortByUpdatedAtList: ' Sortera på uppdaterad',
sortList: 'Sortera listan',
start: 'Starta',
stop: 'Stoppa',
subscribe: 'Prenumerera',

View file

@ -76,16 +76,77 @@ export default class extends BaseModel {
break;
}
case ActionTypes.LIST_SORT:
List.withId(payload.id).sortList();
break;
case ActionTypes.LIST_SORT__SUCCESS:
case ActionTypes.LIST_SORT_HANDLE: {
const listModel = List.withId(payload.list.id);
if (listModel) {
listModel.sortList();
}
break;
}
default:
}
}
getOrderedCardsQuerySet() {
getOrderedByIdCardsQuerySet() {
return this.cards.orderBy('id');
}
getOrderedByPositionCardsQuerySet() {
return this.cards.orderBy('position');
}
getOrderedByTitelCardsQuerySet() {
return this.cards.orderBy('name');
}
getOrderedByCreatedAtCardsQuerySet() {
return this.cards.orderBy('createdAt');
}
getOrderedByUpdatedAtCardsQuerySet() {
return this.cards.orderBy('updatedAt');
}
getOrderedByDueDateCardsQuerySet() {
return this.cards.orderBy('dueDate');
}
getFilteredOrderedCardsModelArray() {
let cardModels = this.getOrderedCardsQuerySet().toModelArray();
const sortby = this.selectedOption;
let cardModels = null;
switch (sortby) {
case 'name':
cardModels = this.getOrderedByTitelCardsQuerySet().toModelArray();
break;
case 'id':
cardModels = this.getOrderedByIdCardsQuerySet().toModelArray();
break;
case 'position':
cardModels = this.getOrderedByPositionCardsQuerySet().toModelArray();
break;
case 'createdAt':
cardModels = this.getOrderedByCreatedAtCardsQuerySet().toModelArray();
break;
case 'updatedAt':
cardModels = this.getOrderedByUpdatedAtCardsQuerySet().toModelArray();
break;
case 'dueDate':
cardModels = this.getOrderedByDueDateCardsQuerySet().toModelArray();
break;
case 'creatorUserId':
cardModels = this.getOrderedByDueDateCardsQuerySet().toModelArray();
break;
default:
cardModels = this.getOrderedByPositionCardsQuerySet().toModelArray();
}
const filterUserIds = this.board.filterUsers.toRefArray().map((user) => user.id);
const filterLabelIds = this.board.filterLabels.toRefArray().map((label) => label.id);

View file

@ -61,6 +61,24 @@ export function* handleListUpdate(list) {
yield put(actions.handleListUpdate(list));
}
export function* sortList(id, data) {
yield put(actions.sortList(id, data));
let list;
try {
({ item: list } = yield call(request, api.sortList, id, data));
} catch (error) {
yield put(actions.sortList.failure(id, error));
return;
}
yield put(actions.sortList.success(list));
}
export function* handleListSort(list) {
yield put(actions.handleListSort(list));
}
export function* moveList(id, index) {
const { boardId } = yield select(selectors.selectListById, id);
const position = yield select(selectors.selectNextListPosition, boardId, index, id);

View file

@ -17,6 +17,9 @@ export default function* listsWatchers() {
takeEvery(EntryActionTypes.LIST_UPDATE_HANDLE, ({ payload: { list } }) =>
services.handleListUpdate(list),
),
takeEvery(EntryActionTypes.LIST_SORT_HANDLE, ({ payload: { list } }) =>
services.handleListSort(list),
),
takeEvery(EntryActionTypes.LIST_MOVE, ({ payload: { id, index } }) =>
services.moveList(id, index),
),

View file

@ -84,6 +84,10 @@ const createSocketEventsChannel = () =>
emit(entryActions.handleListDelete(item));
};
const handleListSort = ({ item }) => {
emit(entryActions.handleListSort(item));
};
const handleLabelCreate = ({ item }) => {
emit(entryActions.handleLabelCreate(item));
};
@ -193,6 +197,7 @@ const createSocketEventsChannel = () =>
socket.on('listCreate', handleListCreate);
socket.on('listUpdate', handleListUpdate);
socket.on('listDelete', handleListDelete);
socket.on('listSort', handleListSort);
socket.on('labelCreate', handleLabelCreate);
socket.on('labelUpdate', handleLabelUpdate);
@ -249,6 +254,7 @@ const createSocketEventsChannel = () =>
socket.off('listCreate', handleListCreate);
socket.off('listUpdate', handleListUpdate);
socket.off('listDelete', handleListDelete);
socket.off('listSort', handleListSort);
socket.off('labelCreate', handleLabelCreate);
socket.off('labelUpdate', handleLabelUpdate);

12
package-lock.json generated
View file

@ -2039,9 +2039,9 @@
}
},
"node_modules/yaml": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
"integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz",
"integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==",
"engines": {
"node": ">= 14"
}
@ -3510,9 +3510,9 @@
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
},
"yaml": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
"integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg=="
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz",
"integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA=="
},
"yargs": {
"version": "17.6.2",