diff --git a/client/src/components/cards/Card/TaskList/TaskList.jsx b/client/src/components/cards/Card/TaskList/TaskList.jsx
index e9497848..f39a94fb 100644
--- a/client/src/components/cards/Card/TaskList/TaskList.jsx
+++ b/client/src/components/cards/Card/TaskList/TaskList.jsx
@@ -16,12 +16,19 @@ import Task from './Task';
import styles from './TaskList.module.scss';
const TaskList = React.memo(({ id }) => {
+ const selectTaskListById = useMemo(() => selectors.makeSelectTaskListById(), []);
const selectTasksByTaskListId = useMemo(() => selectors.makeSelectTasksByTaskListId(), []);
+ const taskLists = useSelector((state) => selectTaskListById(state, id));
const tasks = useSelector((state) => selectTasksByTaskListId(state, id));
const [isOpened, toggleOpened] = useToggle();
+ const filteredTasks = useMemo(
+ () => (taskLists.hideCompletedTasks ? tasks.filter((task) => !task.isCompleted) : tasks),
+ [taskLists.hideCompletedTasks, tasks],
+ );
+
// TODO: move to selector?
const completedTasksTotal = useMemo(
() => tasks.reduce((result, task) => (task.isCompleted ? result + 1 : result), 0),
@@ -30,10 +37,14 @@ const TaskList = React.memo(({ id }) => {
const handleToggleClick = useCallback(
(event) => {
+ if (filteredTasks.length === 0) {
+ return;
+ }
+
event.stopPropagation();
toggleOpened();
},
- [toggleOpened],
+ [toggleOpened, filteredTasks.length],
);
if (tasks.length === 0) {
@@ -44,7 +55,7 @@ const TaskList = React.memo(({ id }) => {
<>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
-
+
0 && styles.countOpenable,
+ filteredTasks.length > 0 && (isOpened ? styles.countOpened : styles.countClosed),
+ )}
>
{completedTasksTotal}/{tasks.length}
- {isOpened && (
+ {isOpened && filteredTasks.length > 0 && (
- {tasks.map((task) => (
+ {filteredTasks.map((task) => (
))}
diff --git a/client/src/components/cards/Card/TaskList/TaskList.module.scss b/client/src/components/cards/Card/TaskList/TaskList.module.scss
index 903ea65e..306fb10e 100644
--- a/client/src/components/cards/Card/TaskList/TaskList.module.scss
+++ b/client/src/components/cards/Card/TaskList/TaskList.module.scss
@@ -4,29 +4,21 @@
*/
:global(#app) {
- .button {
- background: transparent;
- border: none;
- line-height: 0;
- margin: 0 -8px;
- outline: none;
- padding: 0px 8px 8px;
- width: calc(100% + 16px);
- }
-
.count {
color: #888;
- display: inline-block;
font-size: 12px;
line-height: 12px;
- text-align: right;
- vertical-align: top;
- width: 50px;
&:after {
content: "";
opacity: 0.4;
}
+ }
+
+ .countOpenable {
+ &:after {
+ margin-left: 2px;
+ }
&:hover {
opacity: 0.75;
@@ -35,13 +27,11 @@
.countOpened:after {
background: url("data:image/gif;base64,R0lGODlhCwALAJEAAAAAAP///xUVFf///yH5BAEAAAMALAAAAAALAAsAAAIPnI+py+0/hJzz0IruwjsVADs=") no-repeat center right;
- margin-left: 2px;
padding: 6px 6px 0px;
}
.countClosed:after {
background: url("data:image/gif;base64,R0lGODlhCwALAJEAAAAAAP///xUVFf///yH5BAEAAAMALAAAAAALAAsAAAIRnC2nKLnT4or00Puy3rx7VQAAOw==") no-repeat center right;
- margin-left: 2px;
padding: 0 6px 6px;
}
@@ -49,11 +39,17 @@
margin: 0;
}
+ .progressRow {
+ display: flex;
+ gap: 8px;
+ justify-content: space-between;
+ margin: 0 -8px;
+ padding: 0px 8px 8px;
+ }
+
.progressWrapper {
- display: inline-block;
padding: 3px 0;
- vertical-align: top;
- width: calc(100% - 50px);
+ width: 100%;
}
.tasks {
diff --git a/client/src/components/cards/CardModal/TaskLists/EditStep.jsx b/client/src/components/cards/CardModal/TaskLists/EditStep.jsx
index a88aa924..46ef072e 100644
--- a/client/src/components/cards/CardModal/TaskLists/EditStep.jsx
+++ b/client/src/components/cards/CardModal/TaskLists/EditStep.jsx
@@ -35,8 +35,9 @@ const EditStep = React.memo(({ taskListId, onClose }) => {
() => ({
name: taskList.name,
showOnFrontOfCard: taskList.showOnFrontOfCard,
+ hideCompletedTasks: taskList.hideCompletedTasks,
}),
- [taskList.name, taskList.showOnFrontOfCard],
+ [taskList.name, taskList.showOnFrontOfCard, taskList.hideCompletedTasks],
);
const [data, handleFieldChange] = useForm(() => ({
@@ -44,6 +45,7 @@ const EditStep = React.memo(({ taskListId, onClose }) => {
context: 'title',
}),
showOnFrontOfCard: true,
+ hideCompletedTasks: false,
...defaultData,
}));
diff --git a/client/src/components/cards/CardModal/TaskLists/Item.jsx b/client/src/components/cards/CardModal/TaskLists/Item.jsx
index 5e85f953..a93be777 100644
--- a/client/src/components/cards/CardModal/TaskLists/Item.jsx
+++ b/client/src/components/cards/CardModal/TaskLists/Item.jsx
@@ -3,13 +3,14 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
-import React, { useMemo } from 'react';
+import React, { useCallback, useMemo } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { Draggable } from 'react-beautiful-dnd';
import { Button, Icon } from 'semantic-ui-react';
+import { useToggle } from '../../../../lib/hooks';
import selectors from '../../../../selectors';
import { usePopupInClosableContext } from '../../../../hooks';
@@ -29,8 +30,16 @@ const Item = React.memo(({ id, index }) => {
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
});
+ const [isCompletedVisible, toggleCompletedVisible] = useToggle();
+
+ const handleToggleCompletedVisibleClick = useCallback(() => {
+ toggleCompletedVisible();
+ }, [toggleCompletedVisible]);
+
const EditPopup = usePopupInClosableContext(EditStep);
+ const withActions = taskList.hideCompletedTasks || canEdit;
+
return (
{
- {taskList.isPersisted && canEdit && (
-
-
-
+ {taskList.isPersisted && withActions && (
+
+ {taskList.hideCompletedTasks && (
+
+ )}
+ {canEdit && (
+
+
+
+ )}
+
)}
{taskList.name}
-
+
);
diff --git a/client/src/components/cards/CardModal/TaskLists/Item.module.scss b/client/src/components/cards/CardModal/TaskLists/Item.module.scss
index f2098135..74038f8e 100644
--- a/client/src/components/cards/CardModal/TaskLists/Item.module.scss
+++ b/client/src/components/cards/CardModal/TaskLists/Item.module.scss
@@ -4,7 +4,15 @@
*/
:global(#app) {
- .editButton {
+ .actions {
+ display: flex;
+ gap: 2px;
+ position: absolute;
+ right: 0;
+ top: 4px;
+ }
+
+ .button {
background: transparent;
box-shadow: none;
line-height: 28px;
@@ -12,9 +20,6 @@
min-height: auto;
opacity: 0;
padding: 0;
- position: absolute;
- right: 0;
- top: 4px;
width: 28px;
&:hover {
@@ -32,14 +37,18 @@
padding: 6px 0;
}
- .moduleHeaderEditable {
+ .moduleHeaderWithActions {
padding-right: 32px;
&:hover {
- .editButton {
+ .button {
opacity: 1;
}
}
+
+ &.both {
+ padding-right: 62px;
+ }
}
.moduleHeaderTitle {
diff --git a/client/src/components/task-lists/AddTaskListStep.jsx b/client/src/components/task-lists/AddTaskListStep.jsx
index c358f1b9..731b8054 100644
--- a/client/src/components/task-lists/AddTaskListStep.jsx
+++ b/client/src/components/task-lists/AddTaskListStep.jsx
@@ -23,6 +23,7 @@ const AddTaskListStep = React.memo(({ onClose }) => {
context: 'title',
}),
showOnFrontOfCard: true,
+ hideCompletedTasks: false,
});
const taskListEditorRef = useRef(null);
diff --git a/client/src/components/task-lists/TaskList/TaskList.jsx b/client/src/components/task-lists/TaskList/TaskList.jsx
index 25e7be7d..29713b4c 100755
--- a/client/src/components/task-lists/TaskList/TaskList.jsx
+++ b/client/src/components/task-lists/TaskList/TaskList.jsx
@@ -21,7 +21,7 @@ import AddTask from './AddTask';
import styles from './TaskList.module.scss';
-const TaskList = React.memo(({ id }) => {
+const TaskList = React.memo(({ id, isCompletedVisible }) => {
const selectTaskListById = useMemo(() => selectors.makeSelectTaskListById(), []);
const selectListById = useMemo(() => selectors.makeSelectListById(), []);
const selectTasksByTaskListId = useMemo(() => selectors.makeSelectTasksByTaskListId(), []);
@@ -45,6 +45,14 @@ const TaskList = React.memo(({ id }) => {
const [isAddOpened, setIsAddOpened] = useState(false);
const [, , setIsClosableActive] = useContext(ClosableContext);
+ const filteredTasks = useMemo(
+ () =>
+ !isCompletedVisible && taskList.hideCompletedTasks
+ ? tasks.filter((task) => !task.isCompleted)
+ : tasks,
+ [isCompletedVisible, taskList.hideCompletedTasks, tasks],
+ );
+
// TODO: move to selector?
const completedTasksTotal = useMemo(
() => tasks.reduce((result, task) => (task.isCompleted ? result + 1 : result), 0),
@@ -66,7 +74,7 @@ const TaskList = React.memo(({ id }) => {
return (
<>
{tasks.length > 0 && (
- <>
+
)}
{
{({ innerRef, droppableProps, placeholder }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
- {tasks.map((task, index) => (
+ {filteredTasks.map((task, index) => (
))}
{placeholder}
@@ -117,6 +125,7 @@ const TaskList = React.memo(({ id }) => {
TaskList.propTypes = {
id: PropTypes.string.isRequired,
+ isCompletedVisible: PropTypes.bool.isRequired,
};
export default TaskList;
diff --git a/client/src/components/task-lists/TaskList/TaskList.module.scss b/client/src/components/task-lists/TaskList/TaskList.module.scss
index 2a8db18c..b229e241 100644
--- a/client/src/components/task-lists/TaskList/TaskList.module.scss
+++ b/client/src/components/task-lists/TaskList/TaskList.module.scss
@@ -6,23 +6,24 @@
:global(#app) {
.count {
color: #8c8c8c;
- display: inline-block;
font-size: 14px;
line-height: 14px;
- text-align: right;
- vertical-align: top;
- width: 50px;
}
.progress {
- margin: 0 0 16px;
+ margin: 0;
+ }
+
+ .progressRow {
+ display: flex;
+ gap: 12px;
+ justify-content: space-between;
+ margin-bottom: 16px;
}
.progressWrapper {
- display: inline-block;
padding: 3px 0;
- vertical-align: top;
- width: calc(100% - 50px);
+ width: 100%;
}
.tasks {
diff --git a/client/src/components/task-lists/TaskListEditor/TaskListEditor.jsx b/client/src/components/task-lists/TaskListEditor/TaskListEditor.jsx
index 80346330..df9f4030 100644
--- a/client/src/components/task-lists/TaskListEditor/TaskListEditor.jsx
+++ b/client/src/components/task-lists/TaskListEditor/TaskListEditor.jsx
@@ -56,6 +56,14 @@ const TaskListEditor = React.forwardRef(({ data, onFieldChange }, ref) => {
className={styles.fieldRadio}
onChange={onFieldChange}
/>
+
>
);
});
diff --git a/client/src/locales/en-GB/core.js b/client/src/locales/en-GB/core.js
index 92f7f85b..7564578b 100644
--- a/client/src/locales/en-GB/core.js
+++ b/client/src/locales/en-GB/core.js
@@ -196,6 +196,7 @@ export default {
general: 'General',
gradients: 'Gradients',
grid: 'Grid',
+ hideCompletedTasks: 'Hide completed tasks',
hideFromProjectListAndFavorites: 'Hide from project list and favorites',
hours: 'Hours',
importBoard_title: 'Import Board',
diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js
index f7db079d..fb804d73 100644
--- a/client/src/locales/en-US/core.js
+++ b/client/src/locales/en-US/core.js
@@ -191,6 +191,7 @@ export default {
general: 'General',
gradients: 'Gradients',
grid: 'Grid',
+ hideCompletedTasks: 'Hide completed tasks',
hideFromProjectListAndFavorites: 'Hide from project list and favorites',
hours: 'Hours',
importBoard_title: 'Import Board',
diff --git a/client/src/models/TaskList.js b/client/src/models/TaskList.js
index 56e02a8c..97ce9fe4 100755
--- a/client/src/models/TaskList.js
+++ b/client/src/models/TaskList.js
@@ -16,6 +16,7 @@ export default class extends BaseModel {
position: attr(),
name: attr(),
showOnFrontOfCard: attr(),
+ hideCompletedTasks: attr(),
cardId: fk({
to: 'Card',
as: 'card',
@@ -111,6 +112,7 @@ export default class extends BaseModel {
position: this.position,
name: this.name,
showOnFrontOfCard: this.showOnFrontOfCard,
+ hideCompletedTasks: this.hideCompletedTasks,
...data,
});
diff --git a/server/api/controllers/task-lists/create.js b/server/api/controllers/task-lists/create.js
index 9f8e1611..288f8481 100755
--- a/server/api/controllers/task-lists/create.js
+++ b/server/api/controllers/task-lists/create.js
@@ -33,6 +33,9 @@ module.exports = {
showOnFrontOfCard: {
type: 'boolean',
},
+ hideCompletedTasks: {
+ type: 'boolean',
+ },
},
exits: {
@@ -64,7 +67,7 @@ module.exports = {
throw Errors.NOT_ENOUGH_RIGHTS;
}
- const values = _.pick(inputs, ['position', 'name', 'showOnFrontOfCard']);
+ const values = _.pick(inputs, ['position', 'name', 'showOnFrontOfCard', 'hideCompletedTasks']);
const taskList = await sails.helpers.taskLists.createOne.with({
project,
diff --git a/server/api/controllers/task-lists/update.js b/server/api/controllers/task-lists/update.js
index 96b5192d..46ec794e 100755
--- a/server/api/controllers/task-lists/update.js
+++ b/server/api/controllers/task-lists/update.js
@@ -32,6 +32,9 @@ module.exports = {
showOnFrontOfCard: {
type: 'boolean',
},
+ hideCompletedTasks: {
+ type: 'boolean',
+ },
},
exits: {
@@ -66,7 +69,7 @@ module.exports = {
throw Errors.NOT_ENOUGH_RIGHTS;
}
- const values = _.pick(inputs, ['position', 'name', 'showOnFrontOfCard']);
+ const values = _.pick(inputs, ['position', 'name', 'showOnFrontOfCard', 'hideCompletedTasks']);
taskList = await sails.helpers.taskLists.updateOne.with({
values,
diff --git a/server/api/helpers/cards/duplicate-one.js b/server/api/helpers/cards/duplicate-one.js
index ebf1a36d..0777cf22 100644
--- a/server/api/helpers/cards/duplicate-one.js
+++ b/server/api/helpers/cards/duplicate-one.js
@@ -138,7 +138,7 @@ module.exports = {
nextTaskListIdByTaskListId[taskList.id] = id;
return {
- ..._.pick(taskList, ['position', 'name', 'showOnFrontOfCard']),
+ ..._.pick(taskList, ['position', 'name', 'showOnFrontOfCard', 'hideCompletedTasks']),
id,
cardId: card.id,
};
diff --git a/server/api/models/TaskList.js b/server/api/models/TaskList.js
index f713881a..f810d7d5 100644
--- a/server/api/models/TaskList.js
+++ b/server/api/models/TaskList.js
@@ -29,6 +29,11 @@ module.exports = {
defaultsTo: true,
columnName: 'show_on_front_of_card',
},
+ hideCompletedTasks: {
+ type: 'boolean',
+ defaultsTo: false,
+ columnName: 'hide_completed_tasks',
+ },
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
diff --git a/server/db/migrations/20250721132312_add_ability_to_hide_completed_tasks.js b/server/db/migrations/20250721132312_add_ability_to_hide_completed_tasks.js
new file mode 100644
index 00000000..e373a686
--- /dev/null
+++ b/server/db/migrations/20250721132312_add_ability_to_hide_completed_tasks.js
@@ -0,0 +1,21 @@
+/*!
+ * 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('task_list', (table) => {
+ /* Columns */
+
+ table.boolean('hide_completed_tasks').notNullable().defaultTo(false);
+ });
+
+ return knex.schema.alterTable('task_list', (table) => {
+ table.boolean('hide_completed_tasks').notNullable().alter();
+ });
+};
+
+exports.down = (knex) =>
+ knex.schema.table('task_list', (table) => {
+ table.dropColumn('hide_completed_tasks');
+ });