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

fix: Little improvements and refactoring

This commit is contained in:
Maksim Eltyshev 2024-04-22 21:44:58 +02:00
parent 7b3155efe5
commit 6cec8fea44
22 changed files with 305 additions and 223 deletions

View file

@ -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: {
@ -89,43 +121,13 @@ const handleListDelete = (list) => ({
},
});
const sortList = (id, data) => ({
type: ActionTypes.LIST_SORT,
payload: {
id,
data,
},
});
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,
updateList,
handleListUpdate,
deleteList,
handleListDelete,
sortList,
handleListSort,
deleteList,
handleListDelete,
};

View file

@ -1,4 +1,5 @@
import socket from './socket';
import { transformCard } from './cards';
/* Actions */
@ -7,13 +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);
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,
};

View file

@ -5,17 +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';
import SortStep from '../SortStep';
const StepTypes = {
DELETE: 'DELETE',
SORT: 'SORT',
};
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onSort, onClose }) => {
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClose }) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
@ -29,16 +29,29 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onSort, onClo
onClose();
}, [onCardAdd, onClose]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
const handleSortClick = useCallback(() => {
openStep(StepTypes.SORT);
}, [openStep]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
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
@ -49,8 +62,6 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onSort, onClo
onBack={handleBack}
/>
);
case StepTypes.SORT:
return <SortStep title="common.sortList" onSort={onSort} onBack={handleBack} />;
default:
}
}
@ -64,11 +75,6 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onSort, onClo
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleSortClick}>
{t('action.sort', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
{t('action.editTitle', {
context: 'title',
@ -79,6 +85,11 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onSort, onClo
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',

View file

@ -171,8 +171,8 @@ List.propTypes = {
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onCardCreate: PropTypes.func.isRequired,
};

View 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;

View file

@ -0,0 +1,11 @@
:global(#app) {
.menu {
margin: -7px -12px -5px;
width: calc(100% + 24px);
}
.menuItem {
margin: 0;
padding-left: 14px;
}
}

View file

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

View file

@ -1,59 +0,0 @@
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 styles from './SortStep.module.scss';
const SortStep = React.memo(({ title, onSort, onBack }) => {
const [t] = useTranslation();
const handeClick = (sortType) => onSort({ sortType });
return (
<>
<Popup.Header onBack={onBack}>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={() => handeClick('createdat_asc')}>
{t('action.sort.createdFirst', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={() => handeClick('createdat_desc')}>
{t('action.sort.createdLast', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={() => handeClick('name_asc')}>
{t('action.sort.name', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={() => handeClick('duedate_asc')}>
{t('action.sort.due', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
});
SortStep.propTypes = {
title: PropTypes.string.isRequired,
onSort: PropTypes.func.isRequired,
onBack: PropTypes.func,
};
SortStep.defaultProps = {
onBack: undefined,
};
export default SortStep;

View file

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

View file

@ -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',

View file

@ -33,8 +33,8 @@ const mapDispatchToProps = (dispatch, { id }) =>
bindActionCreators(
{
onUpdate: (data) => entryActions.updateList(id, data),
onDelete: () => entryActions.deleteList(id),
onSort: (data) => entryActions.sortList(id, data),
onDelete: () => entryActions.deleteList(id),
onCardCreate: (data, autoOpen) => entryActions.createCard(id, data, autoOpen),
},
dispatch,

View file

@ -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: {
@ -51,31 +69,14 @@ const handleListDelete = (list) => ({
},
});
const sortList = (id, data) => {
return {
type: EntryActionTypes.LIST_SORT,
payload: {
id,
data,
},
};
};
const handleListSort = (list) => ({
type: EntryActionTypes.LIST_SORT_HANDLE,
payload: {
list,
},
});
export default {
createListInCurrentBoard,
handleListCreate,
updateList,
handleListUpdate,
moveList,
deleteList,
handleListDelete,
sortList,
handleListSort,
deleteList,
handleListDelete,
};

View file

@ -60,6 +60,7 @@ export default {
currentPassword: 'Current password',
dangerZone_title: 'Danger Zone',
date: 'Date',
dueDate: 'Due date',
dueDate_title: 'Due Date',
deleteAttachment_title: 'Delete Attachment',
deleteBoard_title: 'Delete Board',
@ -112,6 +113,7 @@ export default {
minutes: 'Minutes',
moveCard_title: 'Move Card',
name: 'Name',
newestFirst: 'Newest first',
newEmail: 'New e-mail',
newPassword: 'New password',
newUsername: 'New username',
@ -121,6 +123,7 @@ export default {
noProjects: 'No projects',
notifications: 'Notifications',
noUnreadNotifications: 'No unread notifications.',
oldestFirst: 'Oldest first',
openBoard_title: 'Open Board',
optional_inline: 'optional',
organization: 'Organization',
@ -141,6 +144,7 @@ export default {
selectPermissions_title: 'Select Permissions',
selectProject: 'Select project',
settings: 'Settings',
sortList_title: 'Sort List',
stopwatch: 'Stopwatch',
subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default',
taskActions_title: 'Task Actions',
@ -228,6 +232,7 @@ export default {
showAllAttachments: 'Show all attachments ({{hidden}} hidden)',
showDetails: 'Show details',
showFewerAttachments: 'Show fewer attachments',
sortList_title: 'Sort List',
start: 'Start',
stop: 'Stop',
subscribe: 'Subscribe',

View file

@ -164,6 +164,14 @@ export default class extends BaseModel {
Card.withId(payload.cardLabel.cardId).labels.remove(payload.cardLabel.labelId);
} 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;
case ActionTypes.CARD_CREATE:
case ActionTypes.CARD_UPDATE__SUCCESS:
@ -282,12 +290,6 @@ export default class extends BaseModel {
break;
}
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
payload.cards.forEach((card) => {
Card.upsert(card);
});
break;
default:
}
}

View file

@ -50,6 +50,8 @@ export default class extends BaseModel {
case ActionTypes.LIST_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE__SUCCESS:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.LIST_SORT__SUCCESS:
case ActionTypes.LIST_SORT_HANDLE:
List.upsert(payload.list);
break;
@ -66,9 +68,6 @@ export default class extends BaseModel {
List.withId(payload.id).deleteWithRelated();
break;
// Possible improved flow for updating
// case ActionTypes.LIST_SORT__SUCCESS:
// case ActionTypes.LIST_SORT_HANDLE:
case ActionTypes.LIST_DELETE__SUCCESS:
case ActionTypes.LIST_DELETE_HANDLE: {
const listModel = List.withId(payload.list.id);

View file

@ -61,24 +61,6 @@ 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);
@ -88,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) {
yield put(actions.deleteList(id));
@ -112,9 +118,9 @@ export default {
handleListCreate,
updateList,
handleListUpdate,
moveList,
sortList,
handleListSort,
moveList,
deleteList,
handleListDelete,
};

View file

@ -17,14 +17,14 @@ export default function* listsWatchers() {
takeEvery(EntryActionTypes.LIST_UPDATE_HANDLE, ({ payload: { list } }) =>
services.handleListUpdate(list),
),
takeEvery(EntryActionTypes.LIST_MOVE, ({ payload: { id, index } }) =>
services.moveList(id, index),
),
takeEvery(EntryActionTypes.LIST_SORT, ({ payload: { id, data } }) =>
services.sortList(id, data),
),
takeEvery(EntryActionTypes.LIST_SORT_HANDLE, ({ payload: { list } }) =>
services.handleListSort(list),
),
takeEvery(EntryActionTypes.LIST_MOVE, ({ payload: { id, index } }) =>
services.moveList(id, index),
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_HANDLE, ({ payload: { list } }) =>

View file

@ -84,6 +84,10 @@ const createSocketEventsChannel = () =>
emit(entryActions.handleListUpdate(item));
};
const handleListSort = api.makeHandleListSort(({ item, included: { cards } }) => {
emit(entryActions.handleListSort(item, cards));
});
const handleListDelete = ({ item }) => {
emit(entryActions.handleListDelete(item));
};
@ -198,6 +202,7 @@ const createSocketEventsChannel = () =>
socket.on('listCreate', handleListCreate);
socket.on('listUpdate', handleListUpdate);
socket.on('listSort', handleListSort);
socket.on('listDelete', handleListDelete);
socket.on('labelCreate', handleLabelCreate);
@ -256,6 +261,7 @@ const createSocketEventsChannel = () =>
socket.off('listCreate', handleListCreate);
socket.off('listUpdate', handleListUpdate);
socket.off('listSort', handleListSort);
socket.off('listDelete', handleListDelete);
socket.off('labelCreate', handleLabelCreate);

View file

@ -14,10 +14,9 @@ module.exports = {
regex: /^[0-9]+$/,
required: true,
},
sortType: {
type: {
type: 'string',
isIn: ['name_asc', 'duedate_asc', 'createdat_asc', 'createdat_desc'],
defaultsTo: 'name_asc',
isIn: Object.values(List.SortTypes),
},
},
@ -33,7 +32,7 @@ module.exports = {
async fn(inputs) {
const { currentUser } = this.req;
let { list } = await sails.helpers.lists
const { list } = await sails.helpers.lists
.getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.LIST_NOT_FOUND);
@ -43,25 +42,24 @@ module.exports = {
});
if (!boardMembership) {
throw Errors.LIST_NOT_FOUND; // This should rather be NOT_ENOUGH_RIGHTS if it's a permissions issue
throw Errors.LIST_NOT_FOUND;
}
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
list = await sails.helpers.lists.sortOne.with({
const cards = await sails.helpers.lists.sortOne.with({
record: list,
type: inputs.type,
request: this.req,
sortType: inputs.sortType,
});
if (!list) {
throw Errors.LIST_NOT_FOUND;
}
return {
item: list,
included: {
cards,
},
};
},
};

View file

@ -1,68 +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',
},
sortType: {
type: 'string',
isIn: ['name_asc', 'duedate_asc', 'createdat_asc', 'createdat_desc'],
defaultsTo: 'name_asc',
},
},
async fn(inputs) {
const list = await List.findOne({ id: inputs.record.id });
if (list) {
let cards = await Card.find({ listId: inputs.record.id });
let cards = await sails.helpers.lists.getCards(inputs.record.id);
switch (inputs.sortType) {
case 'name_asc':
cards.sort((a, b) => a.name.localeCompare(b.name));
break;
case 'duedate_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 'createdat_asc':
cards.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
break;
case 'createdat_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);
for (let i = 0; i < cards.length; i++) {
let card = cards[i];
let nextPosition = positions[i];
await Card.updateOne({ id: card.id }).set({
position: nextPosition,
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);
});
sails.sockets.broadcast(`board:${card.boardId}`, 'cardUpdate', {
item: {
id: card.id,
position: nextPosition,
},
});
}
sails.sockets.broadcast(`board:${list.boardId}`, 'listSorted', {
item: list,
}, inputs.request);
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');
}
return list;
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;
},
};

View file

@ -5,7 +5,16 @@
* @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 = {
SortTypes,
attributes: {
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗