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 && (
|
||||
<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>
|
||||
)}
|
||||
{card.stopwatch && (
|
||||
|
||||
@@ -7,7 +7,7 @@ import React, { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
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 selectors from '../../../selectors';
|
||||
@@ -175,6 +175,14 @@ const ProjectContent = React.memo(() => {
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleDueCompletionChange = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentCard({
|
||||
isDueCompleted: !card.isDueCompleted,
|
||||
}),
|
||||
);
|
||||
}, [card.isDueCompleted, dispatch]);
|
||||
|
||||
const handleToggleStopwatchClick = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentCard({
|
||||
@@ -410,19 +418,30 @@ const ProjectContent = React.memo(() => {
|
||||
context: 'title',
|
||||
})}
|
||||
</div>
|
||||
<span className={styles.attachment}>
|
||||
<span className={classNames(styles.attachment, styles.attachmentDueDate)}>
|
||||
{canEditDueDate ? (
|
||||
<>
|
||||
{!card.isClosed && (
|
||||
<Checkbox
|
||||
checked={card.isDueCompleted}
|
||||
disabled={!canEditDueDate}
|
||||
onChange={handleDueCompletionChange}
|
||||
/>
|
||||
)}
|
||||
<EditDueDatePopup cardId={card.id}>
|
||||
<DueDateChip
|
||||
withStatusIcon
|
||||
value={card.dueDate}
|
||||
isCompleted={card.isDueCompleted}
|
||||
withStatus={!card.isClosed}
|
||||
/>
|
||||
</EditDueDatePopup>
|
||||
</>
|
||||
) : (
|
||||
<DueDateChip
|
||||
withStatusIcon
|
||||
value={card.dueDate}
|
||||
isCompleted={card.isDueCompleted}
|
||||
withStatus={!card.isClosed}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -60,6 +60,12 @@
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.attachmentDueDate {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.attachments {
|
||||
display: inline-block;
|
||||
margin: 0 8px 8px 0;
|
||||
|
||||
@@ -24,6 +24,7 @@ const Sizes = {
|
||||
const Statuses = {
|
||||
DUE_SOON: 'dueSoon',
|
||||
OVERDUE: 'overdue',
|
||||
COMPLETED: 'completed',
|
||||
};
|
||||
|
||||
const LONG_DATE_FORMAT_BY_SIZE = {
|
||||
@@ -47,9 +48,17 @@ const STATUS_ICON_PROPS_BY_STATUS = {
|
||||
name: 'hourglass end',
|
||||
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);
|
||||
|
||||
if (secondsLeft <= 0) {
|
||||
@@ -64,12 +73,12 @@ const getStatus = (date) => {
|
||||
};
|
||||
|
||||
const DueDateChip = React.memo(
|
||||
({ value, size, isDisabled, withStatus, withStatusIcon, onClick }) => {
|
||||
({ value, size, isCompleted, isDisabled, withStatus, withStatusIcon, onClick }) => {
|
||||
const [t] = useTranslation();
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const statusRef = useRef(null);
|
||||
statusRef.current = withStatus ? getStatus(value) : null;
|
||||
statusRef.current = withStatus ? getStatus(value, isCompleted) : null;
|
||||
|
||||
const intervalRef = useRef(null);
|
||||
|
||||
@@ -80,9 +89,13 @@ const DueDateChip = React.memo(
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (withStatus && statusRef.current !== Statuses.OVERDUE) {
|
||||
if (
|
||||
withStatus &&
|
||||
statusRef.current !== Statuses.OVERDUE &&
|
||||
statusRef.current !== Statuses.COMPLETED
|
||||
) {
|
||||
intervalRef.current = setInterval(() => {
|
||||
const status = getStatus(value);
|
||||
const status = getStatus(value, isCompleted);
|
||||
|
||||
if (status !== statusRef.current) {
|
||||
forceUpdate();
|
||||
@@ -99,7 +112,7 @@ const DueDateChip = React.memo(
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [value, withStatus, forceUpdate]);
|
||||
}, [value, isCompleted, withStatus, forceUpdate]);
|
||||
|
||||
const contentNode = (
|
||||
<span
|
||||
@@ -134,6 +147,7 @@ const DueDateChip = React.memo(
|
||||
DueDateChip.propTypes = {
|
||||
value: PropTypes.instanceOf(Date).isRequired,
|
||||
size: PropTypes.oneOf(Object.values(Sizes)),
|
||||
isCompleted: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool,
|
||||
withStatus: PropTypes.bool.isRequired,
|
||||
withStatusIcon: PropTypes.bool,
|
||||
|
||||
@@ -62,4 +62,9 @@
|
||||
background: #db2828;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.wrapperCompleted {
|
||||
background: #21ba45;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export default class extends BaseModel {
|
||||
name: attr(),
|
||||
description: attr(),
|
||||
dueDate: attr(),
|
||||
isDueCompleted: attr(),
|
||||
stopwatch: attr(),
|
||||
isClosed: attr(),
|
||||
commentsTotal: attr({
|
||||
@@ -323,6 +324,16 @@ export default class extends BaseModel {
|
||||
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) {
|
||||
cardModel.linkedTasks.update({
|
||||
isCompleted: payload.data.isClosed,
|
||||
@@ -568,6 +579,7 @@ export default class extends BaseModel {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
dueDate: this.dueDate,
|
||||
isDueCompleted: this.isDueCompleted,
|
||||
stopwatch: this.stopwatch,
|
||||
isClosed: this.isClosed,
|
||||
...data,
|
||||
|
||||
@@ -49,6 +49,10 @@ module.exports = {
|
||||
type: 'string',
|
||||
custom: isDueDate,
|
||||
},
|
||||
isDueCompleted: {
|
||||
type: 'boolean',
|
||||
allowNull: true,
|
||||
},
|
||||
stopwatch: {
|
||||
type: 'json',
|
||||
custom: isStopwatch,
|
||||
@@ -93,6 +97,7 @@ module.exports = {
|
||||
'name',
|
||||
'description',
|
||||
'dueDate',
|
||||
'isDueCompleted',
|
||||
'stopwatch',
|
||||
]);
|
||||
|
||||
|
||||
@@ -70,6 +70,10 @@ module.exports = {
|
||||
custom: isDueDate,
|
||||
allowNull: true,
|
||||
},
|
||||
isDueCompleted: {
|
||||
type: 'boolean',
|
||||
allowNull: true,
|
||||
},
|
||||
stopwatch: {
|
||||
type: 'json',
|
||||
custom: isStopwatch,
|
||||
@@ -136,6 +140,7 @@ module.exports = {
|
||||
'name',
|
||||
'description',
|
||||
'dueDate',
|
||||
'isDueCompleted',
|
||||
'stopwatch',
|
||||
);
|
||||
}
|
||||
@@ -195,6 +200,7 @@ module.exports = {
|
||||
'name',
|
||||
'description',
|
||||
'dueDate',
|
||||
'isDueCompleted',
|
||||
'stopwatch',
|
||||
'isSubscribed',
|
||||
]);
|
||||
|
||||
@@ -67,7 +67,7 @@ module.exports = {
|
||||
name: trelloCard.name,
|
||||
description: trelloCard.desc || null,
|
||||
dueDate: trelloCard.due,
|
||||
isClosed: trelloCard.dueComplete,
|
||||
isDueCompleted: trelloCard.due && trelloCard.dueComplete,
|
||||
listChangedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,14 @@ module.exports = {
|
||||
async fn(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 (_.isUndefined(values.position)) {
|
||||
throw 'positionMustBeInValues';
|
||||
|
||||
@@ -90,6 +90,7 @@ module.exports = {
|
||||
'name',
|
||||
'description',
|
||||
'dueDate',
|
||||
'isDueCompleted',
|
||||
'stopwatch',
|
||||
'isClosed',
|
||||
]),
|
||||
|
||||
@@ -101,6 +101,20 @@ module.exports = {
|
||||
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;
|
||||
if (_.isEmpty(values)) {
|
||||
card = inputs.record;
|
||||
|
||||
@@ -45,6 +45,11 @@ module.exports = {
|
||||
type: 'ref',
|
||||
columnName: 'due_date',
|
||||
},
|
||||
isDueCompleted: {
|
||||
type: 'boolean',
|
||||
allowNull: true,
|
||||
columnName: 'is_due_completed',
|
||||
},
|
||||
stopwatch: {
|
||||
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