@@ -191,11 +187,7 @@ const ProjectContent = React.memo(({ cardId }) => {
)}
{card.dueDate && (
)}
{card.stopwatch && (
diff --git a/client/src/components/cards/Card/StoryContent.jsx b/client/src/components/cards/Card/StoryContent.jsx
index 2911b2b2..17eaa3f1 100644
--- a/client/src/components/cards/Card/StoryContent.jsx
+++ b/client/src/components/cards/Card/StoryContent.jsx
@@ -11,7 +11,7 @@ import { Icon } from 'semantic-ui-react';
import selectors from '../../../selectors';
import markdownToText from '../../../utils/markdown-to-text';
-import { BoardViews, ListTypes } from '../../../constants/Enums';
+import { BoardViews } from '../../../constants/Enums';
import LabelChip from '../../labels/LabelChip';
import CustomFieldValueChip from '../../custom-field-values/CustomFieldValueChip';
@@ -76,8 +76,6 @@ const StoryContent = React.memo(({ cardId }) => {
[card.description],
);
- const isInClosedList = list.type === ListTypes.CLOSED;
-
return (
<>
{coverUrl && (
@@ -107,7 +105,7 @@ const StoryContent = React.memo(({ cardId }) => {
))}
)}
-
+
{card.name}
{card.description &&
{descriptionText}
}
diff --git a/client/src/components/cards/Card/TaskList/Task.jsx b/client/src/components/cards/Card/TaskList/Task.jsx
index 731aa7ea..f2cb52bd 100644
--- a/client/src/components/cards/Card/TaskList/Task.jsx
+++ b/client/src/components/cards/Card/TaskList/Task.jsx
@@ -9,7 +9,6 @@ import classNames from 'classnames';
import { useSelector } from 'react-redux';
import selectors from '../../../../selectors';
-import { ListTypes } from '../../../../constants/Enums';
import Linkify from '../../../common/Linkify';
import styles from './Task.module.scss';
@@ -17,7 +16,6 @@ import styles from './Task.module.scss';
const Task = React.memo(({ id }) => {
const selectTaskById = useMemo(() => selectors.makeSelectTaskById(), []);
const selectCardById = useMemo(() => selectors.makeSelectCardById(), []);
- const selectListById = useMemo(() => selectors.makeSelectListById(), []);
const task = useSelector((state) => selectTaskById(state, id));
@@ -33,14 +31,11 @@ const Task = React.memo(({ id }) => {
for (const [, cardId] of matches) {
const card = selectCardById(state, cardId);
- if (card) {
- const list = selectListById(state, card.listId);
-
- if (list && list.type === ListTypes.CLOSED) {
- return true;
- }
+ if (card && card.isClosed) {
+ return true;
}
}
+
return false;
});
diff --git a/client/src/components/cards/Card/TaskList/TaskList.jsx b/client/src/components/cards/Card/TaskList/TaskList.jsx
index 368b73c8..078aca60 100644
--- a/client/src/components/cards/Card/TaskList/TaskList.jsx
+++ b/client/src/components/cards/Card/TaskList/TaskList.jsx
@@ -9,7 +9,6 @@ import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { Progress } from 'semantic-ui-react';
import { useToggle } from '../../../../lib/hooks';
-import { ListTypes } from '../../../../constants/Enums';
import selectors from '../../../../selectors';
import Task from './Task';
@@ -18,7 +17,6 @@ import styles from './TaskList.module.scss';
const TaskList = React.memo(({ id }) => {
const selectCardById = useMemo(() => selectors.makeSelectCardById(), []);
- const selectListById = useMemo(() => selectors.makeSelectListById(), []);
const selectTasksByTaskListId = useMemo(() => selectors.makeSelectTasksByTaskListId(), []);
const tasks = useSelector((state) => selectTasksByTaskListId(state, id));
@@ -37,12 +35,8 @@ const TaskList = React.memo(({ id }) => {
for (const [, cardId] of matches) {
const card = selectCardById(state, cardId);
- if (card) {
- const list = selectListById(state, card.listId);
-
- if (list && list.type === ListTypes.CLOSED) {
- return result + 1;
- }
+ if (card && card.isClosed) {
+ return result + 1;
}
}
diff --git a/client/src/components/cards/CardModal/ProjectContent.jsx b/client/src/components/cards/CardModal/ProjectContent.jsx
index 1a434cb7..158e5671 100644
--- a/client/src/components/cards/CardModal/ProjectContent.jsx
+++ b/client/src/components/cards/CardModal/ProjectContent.jsx
@@ -442,18 +442,14 @@ const ProjectContent = React.memo(({ onClose }) => {
) : (
)}
diff --git a/client/src/components/task-lists/TaskList/Task/Task.jsx b/client/src/components/task-lists/TaskList/Task/Task.jsx
index 9458afea..b1d5d6d1 100755
--- a/client/src/components/task-lists/TaskList/Task/Task.jsx
+++ b/client/src/components/task-lists/TaskList/Task/Task.jsx
@@ -16,7 +16,7 @@ import selectors from '../../../../selectors';
import entryActions from '../../../../entry-actions';
import { usePopupInClosableContext } from '../../../../hooks';
import { isListArchiveOrTrash } from '../../../../utils/record-helpers';
-import { BoardMembershipRoles, ListTypes } from '../../../../constants/Enums';
+import { BoardMembershipRoles } from '../../../../constants/Enums';
import { ClosableContext } from '../../../../contexts';
import EditName from './EditName';
import SelectAssigneeStep from './SelectAssigneeStep';
@@ -41,14 +41,11 @@ const Task = React.memo(({ id, index }) => {
for (const [, cardId] of matches) {
const card = selectCardById(state, cardId);
- if (card) {
- const list = selectListById(state, card.listId);
-
- if (list && list.type === ListTypes.CLOSED) {
- return true;
- }
+ if (card && card.isClosed) {
+ return true;
}
}
+
return false;
});
diff --git a/client/src/components/task-lists/TaskList/TaskList.jsx b/client/src/components/task-lists/TaskList/TaskList.jsx
index 8ff3d8c4..84522227 100755
--- a/client/src/components/task-lists/TaskList/TaskList.jsx
+++ b/client/src/components/task-lists/TaskList/TaskList.jsx
@@ -14,7 +14,7 @@ import { useDidUpdate } from '../../../lib/hooks';
import selectors from '../../../selectors';
import { isListArchiveOrTrash } from '../../../utils/record-helpers';
import DroppableTypes from '../../../constants/DroppableTypes';
-import { BoardMembershipRoles, ListTypes } from '../../../constants/Enums';
+import { BoardMembershipRoles } from '../../../constants/Enums';
import { ClosableContext } from '../../../contexts';
import Task from './Task';
import AddTask from './AddTask';
@@ -44,12 +44,8 @@ const TaskList = React.memo(({ id }) => {
for (const [, cardId] of matches) {
const card = selectCardById(state, cardId);
- if (card) {
- const list = selectListById(state, card.listId);
-
- if (list && list.type === ListTypes.CLOSED) {
- return result + 1;
- }
+ if (card && card.isClosed) {
+ return result + 1;
}
}
diff --git a/client/src/models/Card.js b/client/src/models/Card.js
index 669d6255..fb021152 100755
--- a/client/src/models/Card.js
+++ b/client/src/models/Card.js
@@ -20,6 +20,7 @@ export default class extends BaseModel {
description: attr(),
dueDate: attr(),
stopwatch: attr(),
+ isClosed: attr(),
commentsTotal: attr({
getDefault: () => 0,
}),
@@ -554,6 +555,7 @@ export default class extends BaseModel {
description: this.description,
dueDate: this.dueDate,
stopwatch: this.stopwatch,
+ isClosed: this.isClosed,
...data,
});
diff --git a/client/src/models/List.js b/client/src/models/List.js
index 79255cd3..407646e6 100755
--- a/client/src/models/List.js
+++ b/client/src/models/List.js
@@ -101,7 +101,6 @@ export default class extends BaseModel {
case ActionTypes.LIST_CREATE:
case ActionTypes.LIST_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE__SUCCESS:
- case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.LIST_SORT__SUCCESS:
case ActionTypes.LIST_CARDS_MOVE__SUCCESS:
case ActionTypes.LIST_CLEAR__SUCCESS:
@@ -117,10 +116,60 @@ export default class extends BaseModel {
List.withId(payload.localId).delete();
break;
- case ActionTypes.LIST_UPDATE:
- List.withId(payload.id).update(payload.data);
+ case ActionTypes.LIST_UPDATE: {
+ const listModel = List.withId(payload.id);
+
+ let isClosed;
+ if (payload.data.type) {
+ if (payload.data.type === ListTypes.CLOSED) {
+ if (listModel.type === ListTypes.ACTIVE) {
+ isClosed = true;
+ }
+ } else if (listModel.type === ListTypes.CLOSED) {
+ isClosed = false;
+ }
+ }
+
+ listModel.update(payload.data);
+
+ if (isClosed !== undefined) {
+ listModel.cards.toModelArray().forEach((cardModel) => {
+ cardModel.update({
+ isClosed,
+ });
+ });
+ }
break;
+ }
+ case ActionTypes.LIST_UPDATE_HANDLE: {
+ const listModel = List.withId(payload.list.id);
+
+ if (listModel) {
+ let isClosed;
+ if (payload.list.type === ListTypes.CLOSED) {
+ if (listModel.type === ListTypes.ACTIVE) {
+ isClosed = true;
+ }
+ } else if (listModel.type === ListTypes.CLOSED) {
+ isClosed = false;
+ }
+
+ listModel.update(prepareList(payload.list));
+
+ if (isClosed !== undefined) {
+ listModel.cards.toModelArray().forEach((cardModel) => {
+ cardModel.update({
+ isClosed,
+ });
+ });
+ }
+ } else {
+ List.upsert(prepareList(payload.list));
+ }
+
+ break;
+ }
case ActionTypes.LIST_SORT:
List.withId(payload.id).sortCards(payload.data);
diff --git a/client/src/sagas/core/services/cards.js b/client/src/sagas/core/services/cards.js
index b54666b7..788295e4 100644
--- a/client/src/sagas/core/services/cards.js
+++ b/client/src/sagas/core/services/cards.js
@@ -137,6 +137,7 @@ export function* createCard(listId, data, autoOpen) {
id: localId,
boardId: list.boardId,
creatorUserId: currentUserMembership.userId,
+ isClosed: list.type === ListTypes.CLOSED,
},
autoOpen,
),
@@ -225,6 +226,8 @@ export function* handleCardCreate(card) {
export function* updateCard(id, data) {
let prevListId;
+ let isClosed;
+
if (data.listId) {
const list = yield select(selectors.selectListById, data.listId);
@@ -238,6 +241,14 @@ export function* updateCard(id, data) {
} else if (prevList.type === ListTypes.ARCHIVE) {
prevListId = null;
}
+
+ if (card.isClosed) {
+ if (list.type === ListTypes.ACTIVE) {
+ isClosed = false;
+ }
+ } else if (list.type === ListTypes.CLOSED) {
+ isClosed = true;
+ }
}
yield put(
@@ -246,6 +257,9 @@ export function* updateCard(id, data) {
...(prevListId !== undefined && {
prevListId,
}),
+ ...(isClosed !== undefined && {
+ isClosed,
+ }),
}),
);
diff --git a/server/api/helpers/boards/import-from-trello.js b/server/api/helpers/boards/import-from-trello.js
index 6f426ad3..155c1457 100644
--- a/server/api/helpers/boards/import-from-trello.js
+++ b/server/api/helpers/boards/import-from-trello.js
@@ -67,6 +67,7 @@ module.exports = {
name: trelloCard.name,
description: trelloCard.desc || null,
dueDate: trelloCard.due,
+ isClosed: trelloCard.dueComplete,
listChangedAt: new Date().toISOString(),
};
diff --git a/server/api/helpers/cards/create-one.js b/server/api/helpers/cards/create-one.js
index 9040774a..b997231f 100644
--- a/server/api/helpers/cards/create-one.js
+++ b/server/api/helpers/cards/create-one.js
@@ -67,6 +67,10 @@ module.exports = {
delete values.position;
}
+ if (values.list.type === List.Types.CLOSED) {
+ values.isClosed = true;
+ }
+
const card = await Card.qm.createOne({
...values,
boardId: values.board.id,
diff --git a/server/api/helpers/cards/duplicate-one.js b/server/api/helpers/cards/duplicate-one.js
index 3c81b0dd..5414d17c 100644
--- a/server/api/helpers/cards/duplicate-one.js
+++ b/server/api/helpers/cards/duplicate-one.js
@@ -91,6 +91,7 @@ module.exports = {
'description',
'dueDate',
'stopwatch',
+ 'isClosed',
]),
...values,
creatorUserId: values.creatorUser.id,
diff --git a/server/api/helpers/cards/update-one.js b/server/api/helpers/cards/update-one.js
index 049e4512..f9f85123 100644
--- a/server/api/helpers/cards/update-one.js
+++ b/server/api/helpers/cards/update-one.js
@@ -378,8 +378,6 @@ module.exports = {
}
if (values.list) {
- values.listChangedAt = new Date().toISOString();
-
if (values.board || inputs.list.type === List.Types.TRASH) {
values.prevListId = null;
} else if (sails.helpers.lists.isArchiveOrTrash(values.list)) {
@@ -387,6 +385,16 @@ module.exports = {
} else if (inputs.list.type === List.Types.ARCHIVE) {
values.prevListId = null;
}
+
+ if (inputs.record.isClosed) {
+ if (values.list.type === List.Types.ACTIVE) {
+ values.isClosed = false;
+ }
+ } else if (values.list.type === List.Types.CLOSED) {
+ values.isClosed = true;
+ }
+
+ values.listChangedAt = new Date().toISOString();
}
card = await Card.qm.updateOne(inputs.record.id, values);
diff --git a/server/api/helpers/lists/update-one.js b/server/api/helpers/lists/update-one.js
index 72a67eb3..47c2daa2 100644
--- a/server/api/helpers/lists/update-one.js
+++ b/server/api/helpers/lists/update-one.js
@@ -33,6 +33,28 @@ module.exports = {
async fn(inputs) {
const { values } = inputs;
+ if (values.type) {
+ let isClosed;
+ if (values.type === List.Types.CLOSED) {
+ if (inputs.record.type === List.Types.ACTIVE) {
+ isClosed = true;
+ }
+ } else if (inputs.record.type === List.Types.CLOSED) {
+ isClosed = false;
+ }
+
+ if (!_.isUndefined(isClosed)) {
+ await Card.qm.update(
+ {
+ listId: inputs.record.id,
+ },
+ {
+ isClosed,
+ },
+ );
+ }
+ }
+
if (!_.isUndefined(values.position)) {
const lists = await sails.helpers.boards.getFiniteListsById(
inputs.board.id,
diff --git a/server/api/models/Card.js b/server/api/models/Card.js
index dc3073c2..3ac61c65 100755
--- a/server/api/models/Card.js
+++ b/server/api/models/Card.js
@@ -48,6 +48,11 @@ module.exports = {
stopwatch: {
type: 'json',
},
+ isClosed: {
+ type: 'boolean',
+ defaultsTo: false,
+ columnName: 'is_closed',
+ },
commentsTotal: {
type: 'number',
defaultsTo: 0,
diff --git a/server/db/migrations/20250708200908_persist_closed_state_per_card.js b/server/db/migrations/20250708200908_persist_closed_state_per_card.js
new file mode 100644
index 00000000..f03a7959
--- /dev/null
+++ b/server/db/migrations/20250708200908_persist_closed_state_per_card.js
@@ -0,0 +1,28 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+exports.up = async (knex) => {
+ await knex.schema.alterTable('card', (table) => {
+ /* Columns */
+
+ table.boolean('is_closed').notNullable().default(false);
+ });
+
+ await knex.raw(`
+ UPDATE card
+ SET is_closed = TRUE
+ FROM list
+ WHERE card.list_id = list.id AND list.type = 'closed';
+ `);
+
+ return knex.schema.alterTable('card', (table) => {
+ table.boolean('is_closed').notNullable().alter();
+ });
+};
+
+exports.down = (knex) =>
+ knex.schema.table('card', (table) => {
+ table.dropColumn('is_closed');
+ });