From 1bf25474d0a7fcb985972d17390561829cdc3023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arkadiusz=20Dzi=C4=99giel?= Date: Mon, 12 Aug 2024 08:12:42 +0200 Subject: [PATCH] feat: Colorize DueDate badge and add toggleable completion flag. Mark overdue dates with red colour, less than 24h with yellow and completed with green. In Card edit modal, DueDate widget now allows toggling completion flag (checkbox). --- client/src/components/Card/Card.jsx | 18 ++- client/src/components/CardModal/CardModal.jsx | 20 +++- client/src/components/DueDate/DueDate.jsx | 107 +++++++++++++----- .../components/DueDate/DueDate.module.scss | 53 +++++++++ client/src/containers/CardContainer.js | 7 +- client/src/containers/CardModalContainer.js | 2 + client/src/models/Card.js | 4 + server/api/controllers/cards/create.js | 5 +- server/api/controllers/cards/update.js | 4 + server/api/helpers/cards/duplicate-one.js | 1 + server/api/models/Card.js | 5 + .../20240812065305_add_due_completion.js.js | 12 ++ 12 files changed, 200 insertions(+), 38 deletions(-) create mode 100644 server/db/migrations/20240812065305_add_due_completion.js.js diff --git a/client/src/components/Card/Card.jsx b/client/src/components/Card/Card.jsx index 4adddbc5..45e91e5d 100755 --- a/client/src/components/Card/Card.jsx +++ b/client/src/components/Card/Card.jsx @@ -24,6 +24,7 @@ const Card = React.memo( index, name, dueDate, + dueCompleted, stopwatch, coverUrl, boardId, @@ -81,6 +82,15 @@ const Card = React.memo( [onUpdate], ); + const handleDueDateCompletionUpdate = useCallback( + (dueDateCompleted) => { + onUpdate({ + dueDateCompleted, + }); + }, + [onUpdate], + ); + const handleNameEdit = useCallback(() => { nameEdit.current.open(); }, []); @@ -120,7 +130,12 @@ const Card = React.memo( )} {dueDate && ( - + )} {stopwatch && ( @@ -221,6 +236,7 @@ Card.propTypes = { index: PropTypes.number.isRequired, name: PropTypes.string.isRequired, dueDate: PropTypes.instanceOf(Date), + dueCompleted: PropTypes.bool.isRequired, stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types coverUrl: PropTypes.string, boardId: PropTypes.string.isRequired, diff --git a/client/src/components/CardModal/CardModal.jsx b/client/src/components/CardModal/CardModal.jsx index 91e89474..889e6d7c 100755 --- a/client/src/components/CardModal/CardModal.jsx +++ b/client/src/components/CardModal/CardModal.jsx @@ -32,6 +32,7 @@ const CardModal = React.memo( name, description, dueDate, + dueCompleted, stopwatch, isSubscribed, isActivitiesFetching, @@ -171,6 +172,15 @@ const CardModal = React.memo( onClose(); }, [onClose]); + const handleDueDateCompletion = useCallback( + (completion) => { + onUpdate({ + dueCompleted: completion, + }); + }, + [onUpdate], + ); + const AttachmentAddPopup = usePopup(AttachmentAddStep); const BoardMembershipsPopup = usePopup(BoardMembershipsStep); const LabelsPopup = usePopup(LabelsStep); @@ -303,10 +313,14 @@ const CardModal = React.memo( {canEdit ? ( - + ) : ( - + )} @@ -562,6 +576,7 @@ CardModal.propTypes = { name: PropTypes.string.isRequired, description: PropTypes.string, dueDate: PropTypes.instanceOf(Date), + dueCompleted: PropTypes.bool, stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types isSubscribed: PropTypes.bool.isRequired, isActivitiesFetching: PropTypes.bool.isRequired, @@ -616,6 +631,7 @@ CardModal.propTypes = { CardModal.defaultProps = { description: undefined, dueDate: undefined, + dueCompleted: false, stopwatch: undefined, }; diff --git a/client/src/components/DueDate/DueDate.jsx b/client/src/components/DueDate/DueDate.jsx index 59f0754c..8c4d78d3 100644 --- a/client/src/components/DueDate/DueDate.jsx +++ b/client/src/components/DueDate/DueDate.jsx @@ -1,8 +1,9 @@ import upperFirst from 'lodash/upperFirst'; -import React from 'react'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; +import { Checkbox } from 'semantic-ui-react'; import getDateFormat from '../../utils/get-date-format'; @@ -26,50 +27,96 @@ const FULL_DATE_FORMAT_BY_SIZE = { medium: 'fullDateTime', }; -const DueDate = React.memo(({ value, size, isDisabled, onClick }) => { - const [t] = useTranslation(); +const getDueClass = (value) => { + const now = new Date(); + const tomorrow = new Date(now).setDate(now.getDate() + 1); - const dateFormat = getDateFormat( - value, - LONG_DATE_FORMAT_BY_SIZE[size], - FULL_DATE_FORMAT_BY_SIZE[size], - ); + if (now > value) return styles.overdue; + if (tomorrow > value) return styles.soon; + return null; +}; - const contentNode = ( - - {t(`format:${dateFormat}`, { - value, - postProcess: 'formatDate', - })} - - ); +const DueDate = React.memo( + ({ value, completed, size, isDisabled, onClick, onUpdateCompletion }) => { + const [t] = useTranslation(); - return onClick ? ( - - ) : ( - contentNode - ); -}); + const dateFormat = getDateFormat( + value, + LONG_DATE_FORMAT_BY_SIZE[size], + FULL_DATE_FORMAT_BY_SIZE[size], + ); + + const classes = [ + styles.wrapper, + styles[`wrapper${upperFirst(size)}`], + onClick && styles.wrapperHoverable, + completed ? styles.completed : getDueClass(value), + ]; + + const handleToggleChange = useCallback( + (event) => { + event.preventDefault(); + event.stopPropagation(); + if (!isDisabled) onUpdateCompletion(!completed); + }, + [onUpdateCompletion, completed, isDisabled], + ); + + return onClick ? ( +
+ + +
+ ) : ( + + {t(`format:${dateFormat}`, { + value, + postProcess: 'formatDate', + })} + + ); + }, +); DueDate.propTypes = { value: PropTypes.instanceOf(Date).isRequired, size: PropTypes.oneOf(Object.values(SIZES)), isDisabled: PropTypes.bool, + completed: PropTypes.bool, onClick: PropTypes.func, + onUpdateCompletion: PropTypes.func, }; DueDate.defaultProps = { size: SIZES.MEDIUM, isDisabled: false, + completed: false, onClick: undefined, + onUpdateCompletion: undefined, }; export default DueDate; diff --git a/client/src/components/DueDate/DueDate.module.scss b/client/src/components/DueDate/DueDate.module.scss index 45ffa50a..42dbae47 100644 --- a/client/src/components/DueDate/DueDate.module.scss +++ b/client/src/components/DueDate/DueDate.module.scss @@ -25,22 +25,75 @@ color: #17394d; } + .wrapperGroup { + display: flex; + align-items: stretch; + justify-content: stretch; + } + + .overdue { + background: #db2828; + color: #ffffff; + &.wrapperHoverable:hover { + background: #d01919; + color: #ffffff; + } + } + + .soon { + background: #fbbd08; + color: #ffffff; + &.wrapperHoverable:hover { + background: #eaae00; + color: #ffffff; + } + } + + .completed { + background: #21ba45; + color: #ffffff; + &.wrapperHoverable:hover { + background: #16ab39; + color: #ffffff; + } + } + /* Sizes */ .wrapperTiny { font-size: 12px; line-height: 20px; padding: 0px 6px; + &.wrapperCheckbox { + padding-right: 6px; + } } .wrapperSmall { font-size: 12px; line-height: 20px; padding: 2px 6px; + &.wrapperCheckbox { + padding-right: 6px; + } } .wrapperMedium { line-height: 20px; padding: 6px 12px; } + + .wrapperCheckbox { + padding-right: 12px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + cursor: pointer; + } + .checkbox { + display: block; + } + .wrapperButton { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } } diff --git a/client/src/containers/CardContainer.js b/client/src/containers/CardContainer.js index e1745644..7c58e549 100755 --- a/client/src/containers/CardContainer.js +++ b/client/src/containers/CardContainer.js @@ -20,10 +20,8 @@ const makeMapStateToProps = () => { const allLabels = selectors.selectLabelsForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); - const { name, dueDate, stopwatch, coverUrl, boardId, listId, isPersisted } = selectCardById( - state, - id, - ); + const { name, dueDate, dueCompleted, stopwatch, coverUrl, boardId, listId, isPersisted } = + selectCardById(state, id); const users = selectUsersByCardId(state, id); const labels = selectLabelsByCardId(state, id); @@ -38,6 +36,7 @@ const makeMapStateToProps = () => { index, name, dueDate, + dueCompleted, stopwatch, coverUrl, boardId, diff --git a/client/src/containers/CardModalContainer.js b/client/src/containers/CardModalContainer.js index a8df6ea0..05ed8a4c 100755 --- a/client/src/containers/CardModalContainer.js +++ b/client/src/containers/CardModalContainer.js @@ -21,6 +21,7 @@ const mapStateToProps = (state) => { name, description, dueDate, + dueCompleted, stopwatch, isSubscribed, isActivitiesFetching, @@ -49,6 +50,7 @@ const mapStateToProps = (state) => { name, description, dueDate, + dueCompleted, stopwatch, isSubscribed, isActivitiesFetching, diff --git a/client/src/models/Card.js b/client/src/models/Card.js index c0066634..f811b3dc 100755 --- a/client/src/models/Card.js +++ b/client/src/models/Card.js @@ -20,6 +20,9 @@ export default class extends BaseModel { relatedName: 'ownCards', }), dueDate: attr(), + dueCompleted: attr({ + getDefault: () => false, + }), stopwatch: attr(), isSubscribed: attr({ getDefault: () => false, @@ -248,6 +251,7 @@ export default class extends BaseModel { 'name', 'description', 'dueDate', + 'dueCompleted', 'stopwatch', ]), ...payload.card, diff --git a/server/api/controllers/cards/create.js b/server/api/controllers/cards/create.js index 33bc6b2d..2d51d0d2 100755 --- a/server/api/controllers/cards/create.js +++ b/server/api/controllers/cards/create.js @@ -57,6 +57,9 @@ module.exports = { type: 'string', custom: dueDateValidator, }, + dueCompleted: { + type: 'boolean', + }, stopwatch: { type: 'json', custom: stopwatchValidator, @@ -95,7 +98,7 @@ module.exports = { throw Errors.NOT_ENOUGH_RIGHTS; } - const values = _.pick(inputs, ['position', 'name', 'description', 'dueDate', 'stopwatch']); + const values = _.pick(inputs, ['position', 'name', 'description', 'dueDate', 'dueCompleted', 'stopwatch']); const card = await sails.helpers.cards.createOne .with({ diff --git a/server/api/controllers/cards/update.js b/server/api/controllers/cards/update.js index d3deaf48..763057a6 100755 --- a/server/api/controllers/cards/update.js +++ b/server/api/controllers/cards/update.js @@ -80,6 +80,9 @@ module.exports = { custom: dueDateValidator, allowNull: true, }, + dueCompleted: { + type: 'boolean', + }, stopwatch: { type: 'json', custom: stopwatchValidator, @@ -173,6 +176,7 @@ module.exports = { 'name', 'description', 'dueDate', + 'dueCompleted', 'stopwatch', 'isSubscribed', ]); diff --git a/server/api/helpers/cards/duplicate-one.js b/server/api/helpers/cards/duplicate-one.js index 94a0d755..3ea30443 100644 --- a/server/api/helpers/cards/duplicate-one.js +++ b/server/api/helpers/cards/duplicate-one.js @@ -77,6 +77,7 @@ module.exports = { 'name', 'description', 'dueDate', + 'dueCompleted', 'stopwatch', ]), ...values, diff --git a/server/api/models/Card.js b/server/api/models/Card.js index e1e7335e..fe6738d6 100755 --- a/server/api/models/Card.js +++ b/server/api/models/Card.js @@ -28,6 +28,11 @@ module.exports = { type: 'ref', columnName: 'due_date', }, + dueCompleted: { + type: 'boolean', + defaultsTo: false, + columnName: 'due_completed', + }, stopwatch: { type: 'json', }, diff --git a/server/db/migrations/20240812065305_add_due_completion.js.js b/server/db/migrations/20240812065305_add_due_completion.js.js new file mode 100644 index 00000000..ebc3d2bc --- /dev/null +++ b/server/db/migrations/20240812065305_add_due_completion.js.js @@ -0,0 +1,12 @@ +module.exports.up = async (knex) => knex.schema.table('card', (table) => { + /* Columns */ + + table.boolean('due_completed').notNullable().defaultTo(false); +}); + + +module.exports.down = async (knex) => { + await knex.schema.table('card', (table) => { + table.dropColumn('due_completed'); + }); +};