mirror of
https://github.com/plankanban/planka.git
synced 2025-12-24 01:11:41 +03:00
feat: Restore toggleable due dates (#1332)
This commit is contained in:
@@ -187,7 +187,12 @@ const ProjectContent = React.memo(({ cardId }) => {
|
|||||||
)}
|
)}
|
||||||
{card.dueDate && (
|
{card.dueDate && (
|
||||||
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
||||||
<DueDateChip value={card.dueDate} size="tiny" withStatus={!card.isClosed} />
|
<DueDateChip
|
||||||
|
value={card.dueDate}
|
||||||
|
size="tiny"
|
||||||
|
isCompleted={card.isDueCompleted}
|
||||||
|
withStatus={!card.isClosed}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{card.stopwatch && (
|
{card.stopwatch && (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import React, { useCallback, useContext, useMemo, useState } from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Grid, Icon } from 'semantic-ui-react';
|
import { Button, Checkbox, Grid, Icon } from 'semantic-ui-react';
|
||||||
import { useDidUpdate } from '../../../lib/hooks';
|
import { useDidUpdate } from '../../../lib/hooks';
|
||||||
|
|
||||||
import selectors from '../../../selectors';
|
import selectors from '../../../selectors';
|
||||||
@@ -175,6 +175,14 @@ const ProjectContent = React.memo(() => {
|
|||||||
[dispatch],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDueCompletionChange = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
entryActions.updateCurrentCard({
|
||||||
|
isDueCompleted: !card.isDueCompleted,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, [card.isDueCompleted, dispatch]);
|
||||||
|
|
||||||
const handleToggleStopwatchClick = useCallback(() => {
|
const handleToggleStopwatchClick = useCallback(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
entryActions.updateCurrentCard({
|
entryActions.updateCurrentCard({
|
||||||
@@ -410,19 +418,30 @@ const ProjectContent = React.memo(() => {
|
|||||||
context: 'title',
|
context: 'title',
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.attachment}>
|
<span className={classNames(styles.attachment, styles.attachmentDueDate)}>
|
||||||
{canEditDueDate ? (
|
{canEditDueDate ? (
|
||||||
<EditDueDatePopup cardId={card.id}>
|
<>
|
||||||
<DueDateChip
|
{!card.isClosed && (
|
||||||
withStatusIcon
|
<Checkbox
|
||||||
value={card.dueDate}
|
checked={card.isDueCompleted}
|
||||||
withStatus={!card.isClosed}
|
disabled={!canEditDueDate}
|
||||||
/>
|
onChange={handleDueCompletionChange}
|
||||||
</EditDueDatePopup>
|
/>
|
||||||
|
)}
|
||||||
|
<EditDueDatePopup cardId={card.id}>
|
||||||
|
<DueDateChip
|
||||||
|
withStatusIcon
|
||||||
|
value={card.dueDate}
|
||||||
|
isCompleted={card.isDueCompleted}
|
||||||
|
withStatus={!card.isClosed}
|
||||||
|
/>
|
||||||
|
</EditDueDatePopup>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<DueDateChip
|
<DueDateChip
|
||||||
withStatusIcon
|
withStatusIcon
|
||||||
value={card.dueDate}
|
value={card.dueDate}
|
||||||
|
isCompleted={card.isDueCompleted}
|
||||||
withStatus={!card.isClosed}
|
withStatus={!card.isClosed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -60,6 +60,12 @@
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attachmentDueDate {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.attachments {
|
.attachments {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 8px 8px 0;
|
margin: 0 8px 8px 0;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const Sizes = {
|
|||||||
const Statuses = {
|
const Statuses = {
|
||||||
DUE_SOON: 'dueSoon',
|
DUE_SOON: 'dueSoon',
|
||||||
OVERDUE: 'overdue',
|
OVERDUE: 'overdue',
|
||||||
|
COMPLETED: 'completed',
|
||||||
};
|
};
|
||||||
|
|
||||||
const LONG_DATE_FORMAT_BY_SIZE = {
|
const LONG_DATE_FORMAT_BY_SIZE = {
|
||||||
@@ -47,9 +48,17 @@ const STATUS_ICON_PROPS_BY_STATUS = {
|
|||||||
name: 'hourglass end',
|
name: 'hourglass end',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
},
|
},
|
||||||
|
[Statuses.COMPLETED]: {
|
||||||
|
name: 'checkmark',
|
||||||
|
color: 'green',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatus = (date) => {
|
const getStatus = (date, isCompleted) => {
|
||||||
|
if (isCompleted) {
|
||||||
|
return Statuses.COMPLETED;
|
||||||
|
}
|
||||||
|
|
||||||
const secondsLeft = Math.floor((date.getTime() - new Date().getTime()) / 1000);
|
const secondsLeft = Math.floor((date.getTime() - new Date().getTime()) / 1000);
|
||||||
|
|
||||||
if (secondsLeft <= 0) {
|
if (secondsLeft <= 0) {
|
||||||
@@ -64,12 +73,12 @@ const getStatus = (date) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DueDateChip = React.memo(
|
const DueDateChip = React.memo(
|
||||||
({ value, size, isDisabled, withStatus, withStatusIcon, onClick }) => {
|
({ value, size, isCompleted, isDisabled, withStatus, withStatusIcon, onClick }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
const statusRef = useRef(null);
|
const statusRef = useRef(null);
|
||||||
statusRef.current = withStatus ? getStatus(value) : null;
|
statusRef.current = withStatus ? getStatus(value, isCompleted) : null;
|
||||||
|
|
||||||
const intervalRef = useRef(null);
|
const intervalRef = useRef(null);
|
||||||
|
|
||||||
@@ -80,9 +89,13 @@ const DueDateChip = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (withStatus && statusRef.current !== Statuses.OVERDUE) {
|
if (
|
||||||
|
withStatus &&
|
||||||
|
statusRef.current !== Statuses.OVERDUE &&
|
||||||
|
statusRef.current !== Statuses.COMPLETED
|
||||||
|
) {
|
||||||
intervalRef.current = setInterval(() => {
|
intervalRef.current = setInterval(() => {
|
||||||
const status = getStatus(value);
|
const status = getStatus(value, isCompleted);
|
||||||
|
|
||||||
if (status !== statusRef.current) {
|
if (status !== statusRef.current) {
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
@@ -99,7 +112,7 @@ const DueDateChip = React.memo(
|
|||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [value, withStatus, forceUpdate]);
|
}, [value, isCompleted, withStatus, forceUpdate]);
|
||||||
|
|
||||||
const contentNode = (
|
const contentNode = (
|
||||||
<span
|
<span
|
||||||
@@ -134,6 +147,7 @@ const DueDateChip = React.memo(
|
|||||||
DueDateChip.propTypes = {
|
DueDateChip.propTypes = {
|
||||||
value: PropTypes.instanceOf(Date).isRequired,
|
value: PropTypes.instanceOf(Date).isRequired,
|
||||||
size: PropTypes.oneOf(Object.values(Sizes)),
|
size: PropTypes.oneOf(Object.values(Sizes)),
|
||||||
|
isCompleted: PropTypes.bool.isRequired,
|
||||||
isDisabled: PropTypes.bool,
|
isDisabled: PropTypes.bool,
|
||||||
withStatus: PropTypes.bool.isRequired,
|
withStatus: PropTypes.bool.isRequired,
|
||||||
withStatusIcon: PropTypes.bool,
|
withStatusIcon: PropTypes.bool,
|
||||||
|
|||||||
@@ -62,4 +62,9 @@
|
|||||||
background: #db2828;
|
background: #db2828;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapperCompleted {
|
||||||
|
background: #21ba45;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default class extends BaseModel {
|
|||||||
name: attr(),
|
name: attr(),
|
||||||
description: attr(),
|
description: attr(),
|
||||||
dueDate: attr(),
|
dueDate: attr(),
|
||||||
|
isDueCompleted: attr(),
|
||||||
stopwatch: attr(),
|
stopwatch: attr(),
|
||||||
isClosed: attr(),
|
isClosed: attr(),
|
||||||
commentsTotal: attr({
|
commentsTotal: attr({
|
||||||
@@ -323,6 +324,16 @@ export default class extends BaseModel {
|
|||||||
payload.data.listChangedAt = new Date(); // eslint-disable-line no-param-reassign
|
payload.data.listChangedAt = new Date(); // eslint-disable-line no-param-reassign
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (payload.data.dueDate !== undefined) {
|
||||||
|
if (payload.data.dueDate) {
|
||||||
|
if (!cardModel.dueDate) {
|
||||||
|
payload.data.isDueCompleted = false; // eslint-disable-line no-param-reassign
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
payload.data.isDueCompleted = null; // eslint-disable-line no-param-reassign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (payload.data.isClosed !== undefined && payload.data.isClosed !== cardModel.isClosed) {
|
if (payload.data.isClosed !== undefined && payload.data.isClosed !== cardModel.isClosed) {
|
||||||
cardModel.linkedTasks.update({
|
cardModel.linkedTasks.update({
|
||||||
isCompleted: payload.data.isClosed,
|
isCompleted: payload.data.isClosed,
|
||||||
@@ -568,6 +579,7 @@ export default class extends BaseModel {
|
|||||||
name: this.name,
|
name: this.name,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
dueDate: this.dueDate,
|
dueDate: this.dueDate,
|
||||||
|
isDueCompleted: this.isDueCompleted,
|
||||||
stopwatch: this.stopwatch,
|
stopwatch: this.stopwatch,
|
||||||
isClosed: this.isClosed,
|
isClosed: this.isClosed,
|
||||||
...data,
|
...data,
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ module.exports = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
custom: isDueDate,
|
custom: isDueDate,
|
||||||
},
|
},
|
||||||
|
isDueCompleted: {
|
||||||
|
type: 'boolean',
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
stopwatch: {
|
stopwatch: {
|
||||||
type: 'json',
|
type: 'json',
|
||||||
custom: isStopwatch,
|
custom: isStopwatch,
|
||||||
@@ -93,6 +97,7 @@ module.exports = {
|
|||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
'dueDate',
|
'dueDate',
|
||||||
|
'isDueCompleted',
|
||||||
'stopwatch',
|
'stopwatch',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ module.exports = {
|
|||||||
custom: isDueDate,
|
custom: isDueDate,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
isDueCompleted: {
|
||||||
|
type: 'boolean',
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
stopwatch: {
|
stopwatch: {
|
||||||
type: 'json',
|
type: 'json',
|
||||||
custom: isStopwatch,
|
custom: isStopwatch,
|
||||||
@@ -136,6 +140,7 @@ module.exports = {
|
|||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
'dueDate',
|
'dueDate',
|
||||||
|
'isDueCompleted',
|
||||||
'stopwatch',
|
'stopwatch',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -195,6 +200,7 @@ module.exports = {
|
|||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
'dueDate',
|
'dueDate',
|
||||||
|
'isDueCompleted',
|
||||||
'stopwatch',
|
'stopwatch',
|
||||||
'isSubscribed',
|
'isSubscribed',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ module.exports = {
|
|||||||
name: trelloCard.name,
|
name: trelloCard.name,
|
||||||
description: trelloCard.desc || null,
|
description: trelloCard.desc || null,
|
||||||
dueDate: trelloCard.due,
|
dueDate: trelloCard.due,
|
||||||
isClosed: trelloCard.dueComplete,
|
isDueCompleted: trelloCard.due && trelloCard.dueComplete,
|
||||||
listChangedAt: new Date().toISOString(),
|
listChangedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ module.exports = {
|
|||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
const { values } = inputs;
|
const { values } = inputs;
|
||||||
|
|
||||||
|
if (values.dueDate) {
|
||||||
|
if (_.isNil(values.isDueCompleted)) {
|
||||||
|
values.isDueCompleted = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete values.isDueCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
if (sails.helpers.lists.isFinite(values.list)) {
|
if (sails.helpers.lists.isFinite(values.list)) {
|
||||||
if (_.isUndefined(values.position)) {
|
if (_.isUndefined(values.position)) {
|
||||||
throw 'positionMustBeInValues';
|
throw 'positionMustBeInValues';
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ module.exports = {
|
|||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
'dueDate',
|
'dueDate',
|
||||||
|
'isDueCompleted',
|
||||||
'stopwatch',
|
'stopwatch',
|
||||||
'isClosed',
|
'isClosed',
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -101,6 +101,20 @@ module.exports = {
|
|||||||
values.coverAttachmentId = values.coverAttachment.id;
|
values.coverAttachmentId = values.coverAttachment.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dueDate = _.isUndefined(values.dueDate) ? inputs.record.dueDate : values.dueDate;
|
||||||
|
|
||||||
|
if (dueDate) {
|
||||||
|
const isDueCompleted = _.isUndefined(values.isDueCompleted)
|
||||||
|
? inputs.record.isDueCompleted
|
||||||
|
: values.isDueCompleted;
|
||||||
|
|
||||||
|
if (_.isNull(isDueCompleted)) {
|
||||||
|
values.isDueCompleted = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
values.isDueCompleted = null;
|
||||||
|
}
|
||||||
|
|
||||||
let card;
|
let card;
|
||||||
if (_.isEmpty(values)) {
|
if (_.isEmpty(values)) {
|
||||||
card = inputs.record;
|
card = inputs.record;
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ module.exports = {
|
|||||||
type: 'ref',
|
type: 'ref',
|
||||||
columnName: 'due_date',
|
columnName: 'due_date',
|
||||||
},
|
},
|
||||||
|
isDueCompleted: {
|
||||||
|
type: 'boolean',
|
||||||
|
allowNull: true,
|
||||||
|
columnName: 'is_due_completed',
|
||||||
|
},
|
||||||
stopwatch: {
|
stopwatch: {
|
||||||
type: 'json',
|
type: 'json',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/*!
|
||||||
|
* 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_due_completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
return knex('card')
|
||||||
|
.update({
|
||||||
|
isDueCompleted: false,
|
||||||
|
})
|
||||||
|
.whereNotNull('due_date');
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = (knex) =>
|
||||||
|
knex.schema.table('card', (table) => {
|
||||||
|
table.dropColumn('is_due_completed');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user