mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 12:49:43 +02:00
feat: Persist closed state per card
This commit is contained in:
parent
69c75a03b1
commit
709a0d1758
19 changed files with 163 additions and 71 deletions
|
@ -15,7 +15,7 @@ import { usePopup } from '../../../lib/popup';
|
|||
|
||||
import selectors from '../../../selectors';
|
||||
import Paths from '../../../constants/Paths';
|
||||
import { BoardMembershipRoles, CardTypes, ListTypes } from '../../../constants/Enums';
|
||||
import { BoardMembershipRoles, CardTypes } from '../../../constants/Enums';
|
||||
import ProjectContent from './ProjectContent';
|
||||
import StoryContent from './StoryContent';
|
||||
import InlineContent from './InlineContent';
|
||||
|
@ -108,10 +108,7 @@ const Card = React.memo(({ id, isInline }) => {
|
|||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
|
||||
jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={classNames(
|
||||
styles.content,
|
||||
list.type === ListTypes.CLOSED && styles.contentDisabled,
|
||||
)}
|
||||
className={classNames(styles.content, card.isClosed && styles.contentDisabled)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Content cardId={id} />
|
||||
|
@ -126,12 +123,7 @@ const Card = React.memo(({ id, isInline }) => {
|
|||
)}
|
||||
</>
|
||||
) : (
|
||||
<span
|
||||
className={classNames(
|
||||
styles.content,
|
||||
list.type === ListTypes.CLOSED && styles.contentDisabled,
|
||||
)}
|
||||
>
|
||||
<span className={classNames(styles.content, card.isClosed && styles.contentDisabled)}>
|
||||
<Content cardId={id} />
|
||||
{colorLineNode}
|
||||
</span>
|
||||
|
|
|
@ -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 UserAvatar from '../../users/UserAvatar';
|
||||
import LabelChip from '../../labels/LabelChip';
|
||||
|
||||
|
@ -54,8 +54,6 @@ const InlineContent = React.memo(({ cardId }) => {
|
|||
[card.description],
|
||||
);
|
||||
|
||||
const isInClosedList = list.type === ListTypes.CLOSED;
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<span className={styles.attachments}>
|
||||
|
@ -90,7 +88,7 @@ const InlineContent = React.memo(({ cardId }) => {
|
|||
</span>
|
||||
)}
|
||||
<span
|
||||
className={classNames(styles.attachments, styles.name, isInClosedList && styles.nameClosed)}
|
||||
className={classNames(styles.attachments, styles.name, card.isClosed && styles.nameClosed)}
|
||||
>
|
||||
<div className={styles.hidable}>{card.name}</div>
|
||||
</span>
|
||||
|
|
|
@ -13,7 +13,7 @@ import selectors from '../../../selectors';
|
|||
import entryActions from '../../../entry-actions';
|
||||
import { startStopwatch, stopStopwatch } from '../../../utils/stopwatch';
|
||||
import { isListArchiveOrTrash } from '../../../utils/record-helpers';
|
||||
import { BoardMembershipRoles, BoardViews, ListTypes } from '../../../constants/Enums';
|
||||
import { BoardMembershipRoles, BoardViews } from '../../../constants/Enums';
|
||||
import TaskList from './TaskList';
|
||||
import DueDateChip from '../DueDateChip';
|
||||
import StopwatchChip from '../StopwatchChip';
|
||||
|
@ -110,8 +110,6 @@ const ProjectContent = React.memo(({ cardId }) => {
|
|||
[cardId, card.stopwatch, dispatch],
|
||||
);
|
||||
|
||||
const isInClosedList = list.type === ListTypes.CLOSED;
|
||||
|
||||
const hasInformation =
|
||||
card.description ||
|
||||
card.dueDate ||
|
||||
|
@ -147,9 +145,7 @@ const ProjectContent = React.memo(({ cardId }) => {
|
|||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={classNames(styles.name, isInClosedList && styles.nameClosed)}>
|
||||
{card.name}
|
||||
</div>
|
||||
<div className={classNames(styles.name, card.isClosed && styles.nameClosed)}>{card.name}</div>
|
||||
{coverUrl && (
|
||||
<div className={styles.coverWrapper}>
|
||||
<img src={coverUrl} alt="" className={styles.cover} />
|
||||
|
@ -191,11 +187,7 @@ const ProjectContent = React.memo(({ cardId }) => {
|
|||
)}
|
||||
{card.dueDate && (
|
||||
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
||||
<DueDateChip
|
||||
value={card.dueDate}
|
||||
size="tiny"
|
||||
withStatus={!isInClosedList && !isListArchiveOrTrash(list)}
|
||||
/>
|
||||
<DueDateChip value={card.dueDate} size="tiny" withStatus={!card.isClosed} />
|
||||
</span>
|
||||
)}
|
||||
{card.stopwatch && (
|
||||
|
|
|
@ -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 }) => {
|
|||
))}
|
||||
</span>
|
||||
)}
|
||||
<div className={classNames(styles.name, isInClosedList && styles.nameClosed)}>
|
||||
<div className={classNames(styles.name, card.isClosed && styles.nameClosed)}>
|
||||
{card.name}
|
||||
</div>
|
||||
{card.description && <div className={styles.descriptionText}>{descriptionText}</div>}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -442,18 +442,14 @@ const ProjectContent = React.memo(({ onClose }) => {
|
|||
<DueDateChip
|
||||
withStatusIcon
|
||||
value={card.dueDate}
|
||||
withStatus={
|
||||
list.type !== ListTypes.CLOSED && !isInArchiveList && !isInTrashList
|
||||
}
|
||||
withStatus={!card.isClosed}
|
||||
/>
|
||||
</EditDueDatePopup>
|
||||
) : (
|
||||
<DueDateChip
|
||||
withStatusIcon
|
||||
value={card.dueDate}
|
||||
withStatus={
|
||||
list.type !== ListTypes.CLOSED && !isInArchiveList && !isInTrashList
|
||||
}
|
||||
withStatus={!card.isClosed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ module.exports = {
|
|||
name: trelloCard.name,
|
||||
description: trelloCard.desc || null,
|
||||
dueDate: trelloCard.due,
|
||||
isClosed: trelloCard.dueComplete,
|
||||
listChangedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -91,6 +91,7 @@ module.exports = {
|
|||
'description',
|
||||
'dueDate',
|
||||
'stopwatch',
|
||||
'isClosed',
|
||||
]),
|
||||
...values,
|
||||
creatorUserId: values.creatorUser.id,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -48,6 +48,11 @@ module.exports = {
|
|||
stopwatch: {
|
||||
type: 'json',
|
||||
},
|
||||
isClosed: {
|
||||
type: 'boolean',
|
||||
defaultsTo: false,
|
||||
columnName: 'is_closed',
|
||||
},
|
||||
commentsTotal: {
|
||||
type: 'number',
|
||||
defaultsTo: 0,
|
||||
|
|
|
@ -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');
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue