feat: Version 2

Closes #627, closes #1047
This commit is contained in:
Maksim Eltyshev
2025-05-10 02:09:06 +02:00
parent ad7fb51cfa
commit 2ee1166747
1557 changed files with 76832 additions and 47042 deletions
+422 -73
View File
@@ -1,32 +1,155 @@
import { call, put, select } from 'redux-saga/effects';
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { call, fork, join, put, race, select, take } from 'redux-saga/effects';
import { LOCATION_CHANGE_HANDLE } from '../../../lib/redux-router';
import { goToBoard, goToCard } from './router';
import request from '../request';
import selectors from '../../../selectors';
import actions from '../../../actions';
import api from '../../../api';
import i18n from '../../../i18n';
import { createLocalId } from '../../../utils/local-id';
import { isListArchiveOrTrash, isListFinite } from '../../../utils/record-helpers';
import ActionTypes from '../../../constants/ActionTypes';
import { BoardViews, ListTypes } from '../../../constants/Enums';
// eslint-disable-next-line no-underscore-dangle
const _preloadImage = (url) =>
new Promise((resolve) => {
const image = new Image();
image.onload = resolve;
image.onerror = resolve;
image.src = url;
});
export function* fetchCards(listId) {
const { boardId, lastCard } = yield select(selectors.selectListById, listId);
const { search } = yield select(selectors.selectBoardById, boardId);
const filterUserIds = yield select(selectors.selectFilterUserIdsForCurrentBoard);
const filterLabelIds = yield select(selectors.selectFilterLabelIdsForCurrentBoard);
function* getCardsRequest() {
const response = {};
try {
response.body = yield call(request, api.getCards, listId, {
search: (search && search.trim()) || undefined,
filterUserIds: filterUserIds.length > 0 ? filterUserIds.join(',') : undefined,
filterLabelIds: filterLabelIds.length > 0 ? filterLabelIds.join(',') : undefined,
before: lastCard || undefined,
});
} catch (error) {
response.error = error;
}
return response;
}
yield put(actions.fetchCards(listId));
const getCardsRequestTask = yield fork(getCardsRequest);
const [response] = yield race([
join(getCardsRequestTask),
take((action) => action.type === ActionTypes.CARDS_FETCH && action.payload.listId === listId),
]);
if (!response) {
return;
}
if (response.error) {
yield put(actions.fetchCards.failure(listId, response.error));
return;
}
const {
body: {
items: cards,
included: {
users,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
},
},
} = response;
yield put(
actions.fetchCards.success(
listId,
cards,
users,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
),
);
}
export function* fetchCardsInCurrentList() {
const currentListId = yield select(selectors.selectCurrentListId);
yield call(fetchCards, currentListId);
}
export function* handleCardsUpdate(cards, activities) {
yield put(actions.handleCardsUpdate(cards, activities));
}
export function* createCard(listId, data, autoOpen) {
const { boardId } = yield select(selectors.selectListById, listId);
const localId = yield call(createLocalId);
const list = yield select(selectors.selectListById, listId);
const currentUserMembership = yield select(
selectors.selectCurrentUserMembershipByBoardId,
list.boardId,
);
const nextData = {
...data,
position: yield select(selectors.selectNextCardPosition, listId),
};
const localId = yield call(createLocalId);
if (isListFinite(list)) {
nextData.position = yield select(selectors.selectNextCardPosition, listId);
}
yield put(
actions.createCard({
...nextData,
boardId,
listId,
id: localId,
}),
actions.createCard(
{
...nextData,
listId,
id: localId,
boardId: list.boardId,
creatorUserId: currentUserMembership.userId,
},
autoOpen,
),
);
// TODO: use race instead
let watchForCreateCardActionTask;
if (autoOpen) {
watchForCreateCardActionTask = yield fork(function* watchForCreateCardAction() {
yield take((action) => action.type === ActionTypes.CARD_CREATE && action.payload.autoOpen);
});
}
let card;
try {
({ item: card } = yield call(request, api.createCard, listId, nextData));
@@ -37,32 +160,94 @@ export function* createCard(listId, data, autoOpen) {
yield put(actions.createCard.success(localId, card));
if (autoOpen) {
if (watchForCreateCardActionTask && watchForCreateCardActionTask.isRunning()) {
yield call(goToCard, card.id);
}
}
export function* handleCardCreate({ id }) {
let card;
export function* createCardInCurrentList(data, autoOpen) {
const currentListId = yield select(selectors.selectCurrentListId);
yield call(createCard, currentListId, data, autoOpen);
}
export function* createCardInFirstFiniteList(data, autoOpen) {
const firstFiniteListId = yield select(selectors.selectFirstFiniteListId);
yield call(createCard, firstFiniteListId, data, autoOpen);
}
export function* handleCardCreate(card) {
let users;
let cardMemberships;
let cardLabels;
let taskLists;
let tasks;
let attachments;
let customFieldGroups;
let customFields;
let customFieldValues;
try {
({
item: card,
included: { cardMemberships, cardLabels, tasks, attachments },
} = yield call(request, api.getCard, id));
} catch (error) {
item: card, // eslint-disable-line no-param-reassign
included: {
users,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
},
} = yield call(request, api.getCard, card.id));
} catch {
return;
}
yield put(actions.handleCardCreate(card, cardMemberships, cardLabels, tasks, attachments));
yield put(
actions.handleCardCreate(
card,
users,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
),
);
}
export function* updateCard(id, data) {
yield put(actions.updateCard(id, data));
let prevListId;
if (data.listId) {
const list = yield select(selectors.selectListById, data.listId);
const card = yield select(selectors.selectCardById, id);
const prevList = yield select(selectors.selectListById, card.listId);
if (prevList.type === ListTypes.TRASH) {
prevListId = null;
} else if (isListArchiveOrTrash(list)) {
prevListId = prevList.id;
} else if (prevList.type === ListTypes.ARCHIVE) {
prevListId = null;
}
}
yield put(
actions.updateCard(id, {
...data,
...(prevListId !== undefined && {
prevListId,
}),
}),
);
let card;
try {
@@ -82,59 +267,142 @@ export function* updateCurrentCard(data) {
}
export function* handleCardUpdate(card) {
const { cardId, boardId } = yield select(selectors.selectPath);
let fetch = false;
if (card.boardId) {
const prevCard = yield select(selectors.selectCardById, card.id);
fetch = !prevCard || prevCard.boardId !== card.boardId;
const isAvailableForCurrentUser = yield select(
selectors.selectIsCardWithIdAvailableForCurrentUser,
card.id,
);
fetch = !isAvailableForCurrentUser;
}
let users;
let cardMemberships;
let cardLabels;
let taskLists;
let tasks;
let attachments;
let customFieldGroups;
let customFields;
let customFieldValues;
if (fetch) {
try {
({
item: card, // eslint-disable-line no-param-reassign
included: { cardMemberships, cardLabels, tasks, attachments },
included: {
users,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
},
} = yield call(request, api.getCard, card.id));
} catch (error) {
} catch {
fetch = false;
}
}
yield put(actions.handleCardUpdate(card, fetch, cardMemberships, cardLabels, tasks, attachments));
yield put(
actions.handleCardUpdate(
card,
fetch,
users,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
),
);
if (card.boardId === null && card.id === cardId) {
yield call(goToBoard, boardId);
}
}
export function* moveCard(id, listId, index = 0) {
const position = yield select(selectors.selectNextCardPosition, listId, index, id);
export function* moveCard(id, listId, index) {
const data = {};
if (listId) {
data.listId = listId;
} else {
// eslint-disable-next-line no-param-reassign
({ listId } = yield select(selectors.selectCardById, id));
}
yield call(updateCard, id, {
listId,
position,
});
const list = yield select(selectors.selectListById, listId);
if (isListFinite(list)) {
data.position = yield select(selectors.selectNextCardPosition, listId, index, id);
}
yield call(updateCard, id, data);
}
export function* moveCurrentCard(listId, index) {
const { cardId } = yield select(selectors.selectPath);
export function* moveCurrentCard(listId, index, autoClose) {
const { cardId, boardId } = yield select(selectors.selectPath);
if (autoClose) {
yield call(goToBoard, boardId);
}
yield call(moveCard, cardId, listId, index);
}
export function* transferCard(id, boardId, listId, index = 0) {
const { cardId: currentCardId, boardId: currentBoardId } = yield select(selectors.selectPath);
const position = yield select(selectors.selectNextCardPosition, listId, index, id);
export function* moveCardToArchive(id) {
const archiveListId = yield select(selectors.selectArchiveListIdForCurrentBoard);
yield call(moveCard, id, archiveListId);
}
export function* moveCurrentCardToArchive() {
const archiveListId = yield select(selectors.selectArchiveListIdForCurrentBoard);
yield call(moveCurrentCard, archiveListId, undefined, true);
}
export function* moveCardToTrash(id) {
const trashListId = yield select(selectors.selectTrashListIdForCurrentBoard);
yield call(moveCard, id, trashListId);
}
export function* moveCurrentCardToTrash() {
const trashListId = yield select(selectors.selectTrashListIdForCurrentBoard);
yield call(moveCurrentCard, trashListId, undefined, true);
}
export function* transferCard(id, boardId, listId, index) {
const { cardId: currentCardId, boardId: currentBoardId } = yield select(selectors.selectPath);
// TODO: hack?
if (id === currentCardId) {
yield call(goToBoard, currentBoardId);
}
yield call(updateCard, id, {
boardId,
const list = yield select(selectors.selectListById, listId);
const data = {
listId,
position,
});
boardId,
};
if (isListFinite(list)) {
data.position = yield select(selectors.selectNextCardPosition, listId, index, id);
}
yield call(updateCard, id, data);
}
export function* transferCurrentCard(boardId, listId, index) {
@@ -143,64 +411,142 @@ export function* transferCurrentCard(boardId, listId, index) {
yield call(transferCard, cardId, boardId, listId, index);
}
export function* duplicateCard(id) {
const { listId, name } = yield select(selectors.selectCardById, id);
export function* duplicateCard(id, data) {
const localId = yield call(createLocalId);
const { boardId, listId } = yield select(selectors.selectCardById, id);
const index = yield select(selectors.selectCardIndexById, id);
const currentUserMembership = yield select(
selectors.selectCurrentUserMembershipByBoardId,
boardId,
);
const nextData = {
...data,
position: yield select(selectors.selectNextCardPosition, listId, index + 1),
name: `${name} (${i18n.t('common.copy', {
context: 'inline',
})})`,
};
const localId = yield call(createLocalId);
const taskIds = yield select(selectors.selectTaskIdsByCardId, id);
yield put(
actions.duplicateCard(
id,
{
...nextData,
id: localId,
},
taskIds,
),
actions.duplicateCard(id, localId, {
...nextData,
creatorUserId: currentUserMembership.userId,
}),
);
let card;
let cardMemberships;
let cardLabels;
let taskLists;
let tasks;
let attachments;
let customFieldGroups;
let customFields;
let customFieldValues;
try {
({
item: card,
included: { cardMemberships, cardLabels, tasks },
included: {
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
},
} = yield call(request, api.duplicateCard, id, nextData));
} catch (error) {
yield put(actions.duplicateCard.failure(localId, error));
return;
}
yield put(actions.duplicateCard.success(localId, card, cardMemberships, cardLabels, tasks));
if (card.coverAttachmentId) {
const coverAttachment = attachments.find(
(attachment) => attachment.id === card.coverAttachmentId,
);
if (coverAttachment) {
yield call(_preloadImage, coverAttachment.data.thumbnailUrls.outside360);
}
}
yield put(
actions.duplicateCard.success(
localId,
card,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
),
);
}
export function* duplicateCurrentCard() {
export function* duplicateCurrentCard(data) {
const { cardId } = yield select(selectors.selectPath);
yield call(duplicateCard, cardId);
yield call(duplicateCard, cardId, data);
}
export function* goToAdjacentCard(direction) {
const card = yield select(selectors.selectCurrentCard);
const list = yield select(selectors.selectListById, card.listId);
let cardIds;
if (isListFinite(list)) {
const { view } = yield select(selectors.selectCurrentBoard);
if (view === BoardViews.KANBAN) {
cardIds = yield select(selectors.selectFilteredCardIdsByListId, card.listId);
} else {
cardIds = yield select(selectors.selectFilteredCardIdsForCurrentBoard);
}
} else {
cardIds = yield select(selectors.selectFilteredCardIdsByListId, card.listId);
if (direction === 1 && card.id === cardIds[cardIds.length - 1]) {
if (list.isCardsFetching || list.isAllCardsFetched) {
return;
}
const [, cancelled] = yield race([call(fetchCards, list.id), take(LOCATION_CHANGE_HANDLE)]);
if (cancelled) {
return;
}
cardIds = yield select(selectors.selectFilteredCardIdsByListId, card.listId);
}
}
const index = cardIds.indexOf(card.id);
if (index === -1) {
return;
}
const adjacentCardId = cardIds[index + direction];
if (adjacentCardId) {
yield call(goToCard, adjacentCardId);
}
}
export function* deleteCard(id) {
const { cardId, boardId } = yield select(selectors.selectPath);
yield put(actions.deleteCard(id));
if (id === cardId) {
yield call(goToBoard, boardId);
}
yield put(actions.deleteCard(id));
let card;
try {
({ item: card } = yield call(request, api.deleteCard, id));
@@ -219,35 +565,38 @@ export function* deleteCurrentCard() {
}
export function* handleCardDelete(card) {
const { cardId, boardId } = yield select(selectors.selectPath);
if (card.id === cardId) {
yield call(goToBoard, boardId);
}
const { cardId } = yield select(selectors.selectPath);
yield put(actions.handleCardDelete(card));
}
export function* handleTextFilter(text) {
const { boardId } = yield select(selectors.selectPath);
yield put(actions.filterText(boardId, text));
if (card.id === cardId) {
yield call(goToBoard, card.boardId);
}
}
export default {
fetchCards,
fetchCardsInCurrentList,
handleCardsUpdate,
createCard,
createCardInCurrentList,
handleCardCreate,
createCardInFirstFiniteList,
updateCard,
updateCurrentCard,
handleCardUpdate,
moveCard,
moveCurrentCard,
moveCardToArchive,
moveCurrentCardToArchive,
moveCardToTrash,
moveCurrentCardToTrash,
transferCard,
transferCurrentCard,
duplicateCard,
duplicateCurrentCard,
goToAdjacentCard,
deleteCard,
deleteCurrentCard,
handleCardDelete,
handleTextFilter,
};