From 12f05adde73d8cd41d8f4cd2e2ee38da13e86502 Mon Sep 17 00:00:00 2001 From: Maksim Eltyshev Date: Mon, 12 Aug 2024 23:17:17 +0200 Subject: [PATCH] fix: Fix styles, refactoring --- client/src/components/Card/Card.jsx | 21 +- client/src/components/CardModal/CardModal.jsx | 46 +++-- .../CardModal/CardModal.module.scss | 6 + client/src/components/DueDate/DueDate.jsx | 184 ++++++++++-------- .../components/DueDate/DueDate.module.scss | 71 ++----- client/src/components/Label/Label.jsx | 4 +- client/src/components/Label/Label.module.scss | 6 +- .../Stopwatch/Stopwatch.module.scss | 3 - client/src/components/User/User.module.scss | 2 - client/src/containers/CardContainer.js | 4 +- client/src/containers/CardModalContainer.js | 4 +- client/src/models/Card.js | 17 +- server/api/controllers/cards/create.js | 6 +- server/api/controllers/cards/update.js | 5 +- server/api/helpers/cards/create-one.js | 8 + server/api/helpers/cards/duplicate-one.js | 2 +- server/api/helpers/cards/update-one.js | 14 ++ server/api/models/Card.js | 6 +- .../20240812065305_add_due_completion.js.js | 12 -- ...20240812065305_make_due_date_toggleable.js | 18 ++ 20 files changed, 233 insertions(+), 206 deletions(-) delete mode 100644 server/db/migrations/20240812065305_add_due_completion.js.js create mode 100644 server/db/migrations/20240812065305_make_due_date_toggleable.js diff --git a/client/src/components/Card/Card.jsx b/client/src/components/Card/Card.jsx index 45e91e5d0..43708f108 100755 --- a/client/src/components/Card/Card.jsx +++ b/client/src/components/Card/Card.jsx @@ -24,7 +24,7 @@ const Card = React.memo( index, name, dueDate, - dueCompleted, + isDueDateCompleted, stopwatch, coverUrl, boardId, @@ -82,15 +82,6 @@ const Card = React.memo( [onUpdate], ); - const handleDueDateCompletionUpdate = useCallback( - (dueDateCompleted) => { - onUpdate({ - dueDateCompleted, - }); - }, - [onUpdate], - ); - const handleNameEdit = useCallback(() => { nameEdit.current.open(); }, []); @@ -130,12 +121,7 @@ const Card = React.memo( )} {dueDate && ( - + )} {stopwatch && ( @@ -236,7 +222,7 @@ Card.propTypes = { index: PropTypes.number.isRequired, name: PropTypes.string.isRequired, dueDate: PropTypes.instanceOf(Date), - dueCompleted: PropTypes.bool.isRequired, + isDueDateCompleted: PropTypes.bool, stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types coverUrl: PropTypes.string, boardId: PropTypes.string.isRequired, @@ -271,6 +257,7 @@ Card.propTypes = { Card.defaultProps = { dueDate: undefined, + isDueDateCompleted: undefined, stopwatch: undefined, coverUrl: undefined, }; diff --git a/client/src/components/CardModal/CardModal.jsx b/client/src/components/CardModal/CardModal.jsx index 889e6d7ca..473664e33 100755 --- a/client/src/components/CardModal/CardModal.jsx +++ b/client/src/components/CardModal/CardModal.jsx @@ -2,7 +2,7 @@ import React, { useCallback, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; -import { Button, Grid, Icon, Modal } from 'semantic-ui-react'; +import { Button, Checkbox, Grid, Icon, Modal } from 'semantic-ui-react'; import { usePopup } from '../../lib/popup'; import { Markdown } from '../../lib/custom-ui'; @@ -32,7 +32,7 @@ const CardModal = React.memo( name, description, dueDate, - dueCompleted, + isDueDateCompleted, stopwatch, isSubscribed, isActivitiesFetching, @@ -119,6 +119,12 @@ const CardModal = React.memo( [onUpdate], ); + const handleDueDateCompletionChange = useCallback(() => { + onUpdate({ + isDueDateCompleted: !isDueDateCompleted, + }); + }, [isDueDateCompleted, onUpdate]); + const handleStopwatchUpdate = useCallback( (newStopwatch) => { onUpdate({ @@ -172,15 +178,6 @@ 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); @@ -310,17 +307,24 @@ const CardModal = React.memo( context: 'title', })} - + {canEdit ? ( - - + - + + + + ) : ( - + )} @@ -576,7 +580,7 @@ CardModal.propTypes = { name: PropTypes.string.isRequired, description: PropTypes.string, dueDate: PropTypes.instanceOf(Date), - dueCompleted: PropTypes.bool, + isDueDateCompleted: PropTypes.bool, stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types isSubscribed: PropTypes.bool.isRequired, isActivitiesFetching: PropTypes.bool.isRequired, @@ -631,7 +635,7 @@ CardModal.propTypes = { CardModal.defaultProps = { description: undefined, dueDate: undefined, - dueCompleted: false, + isDueDateCompleted: false, stopwatch: undefined, }; diff --git a/client/src/components/CardModal/CardModal.module.scss b/client/src/components/CardModal/CardModal.module.scss index 7b20e9023..fc03472f3 100644 --- a/client/src/components/CardModal/CardModal.module.scss +++ b/client/src/components/CardModal/CardModal.module.scss @@ -58,6 +58,12 @@ max-width: 100%; } + .attachmentDueDate { + align-items: center; + display: flex; + gap: 4px; + } + .attachments { display: inline-block; margin: 0 8px 8px 0; diff --git a/client/src/components/DueDate/DueDate.jsx b/client/src/components/DueDate/DueDate.jsx index 8c4d78d33..34f8b3d82 100644 --- a/client/src/components/DueDate/DueDate.jsx +++ b/client/src/components/DueDate/DueDate.jsx @@ -1,9 +1,10 @@ import upperFirst from 'lodash/upperFirst'; -import React, { useCallback } from 'react'; +import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; -import { Checkbox } from 'semantic-ui-react'; +import { Icon } from 'semantic-ui-react'; +import { useForceUpdate } from '../../lib/hooks'; import getDateFormat from '../../utils/get-date-format'; @@ -15,6 +16,12 @@ const SIZES = { MEDIUM: 'medium', }; +const STATUSES = { + DUE_SOON: 'dueSoon', + OVERDUE: 'overdue', + COMPLETED: 'completed', +}; + const LONG_DATE_FORMAT_BY_SIZE = { tiny: 'longDate', small: 'longDate', @@ -27,96 +34,121 @@ const FULL_DATE_FORMAT_BY_SIZE = { medium: 'fullDateTime', }; -const getDueClass = (value) => { - const now = new Date(); - const tomorrow = new Date(now).setDate(now.getDate() + 1); +const STATUS_ICON_PROPS_BY_STATUS = { + [STATUSES.DUE_SOON]: { + name: 'hourglass half', + color: 'orange', + }, + [STATUSES.OVERDUE]: { + name: 'hourglass end', + color: 'red', + }, + [STATUSES.COMPLETED]: { + name: 'checkmark', + color: 'green', + }, +}; + +const getStatus = (dateTime, isCompleted) => { + if (isCompleted) { + return STATUSES.COMPLETED; + } + + const secondsLeft = Math.floor((dateTime.getTime() - new Date().getTime()) / 1000); + + if (secondsLeft <= 0) { + return STATUSES.OVERDUE; + } + + if (secondsLeft <= 24 * 60 * 60) { + return STATUSES.DUE_SOON; + } - if (now > value) return styles.overdue; - if (tomorrow > value) return styles.soon; return null; }; -const DueDate = React.memo( - ({ value, completed, size, isDisabled, onClick, onUpdateCompletion }) => { - const [t] = useTranslation(); - - 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', - })} - - ); - }, -); +const DueDate = React.memo(({ value, size, isCompleted, isDisabled, withStatusIcon, onClick }) => { + const [t] = useTranslation(); + const forceUpdate = useForceUpdate(); + + const statusRef = useRef(null); + statusRef.current = getStatus(value, isCompleted); + + const intervalRef = useRef(null); + + const dateFormat = getDateFormat( + value, + LONG_DATE_FORMAT_BY_SIZE[size], + FULL_DATE_FORMAT_BY_SIZE[size], + ); + + useEffect(() => { + if ([null, STATUSES.DUE_SOON].includes(statusRef.current)) { + intervalRef.current = setInterval(() => { + const status = getStatus(value, isCompleted); + + if (status !== statusRef.current) { + forceUpdate(); + } + + if (status === STATUSES.OVERDUE) { + clearInterval(intervalRef.current); + } + }, 1000); + } + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [value, isCompleted, forceUpdate]); + + const contentNode = ( + + {t(`format:${dateFormat}`, { + value, + postProcess: 'formatDate', + })} + {withStatusIcon && statusRef.current && ( + // eslint-disable-next-line react/jsx-props-no-spreading + + )} + + ); + + return onClick ? ( + + ) : ( + contentNode + ); +}); DueDate.propTypes = { value: PropTypes.instanceOf(Date).isRequired, size: PropTypes.oneOf(Object.values(SIZES)), + isCompleted: PropTypes.bool.isRequired, isDisabled: PropTypes.bool, - completed: PropTypes.bool, + withStatusIcon: PropTypes.bool, onClick: PropTypes.func, - onUpdateCompletion: PropTypes.func, + onCompletionToggle: PropTypes.func, }; DueDate.defaultProps = { size: SIZES.MEDIUM, isDisabled: false, - completed: false, + withStatusIcon: false, onClick: undefined, - onUpdateCompletion: undefined, + onCompletionToggle: undefined, }; export default DueDate; diff --git a/client/src/components/DueDate/DueDate.module.scss b/client/src/components/DueDate/DueDate.module.scss index 42dbae476..62ed7cf9e 100644 --- a/client/src/components/DueDate/DueDate.module.scss +++ b/client/src/components/DueDate/DueDate.module.scss @@ -8,16 +8,17 @@ padding: 0; } + .statusIcon { + line-height: 1; + margin: 0 0 0 8px; + } + .wrapper { background: #dce0e4; - border: none; border-radius: 3px; color: #6a808b; display: inline-block; - outline: none; - text-align: left; transition: background 0.3s ease; - vertical-align: top; } .wrapperHoverable:hover { @@ -25,57 +26,18 @@ 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 { @@ -83,17 +45,20 @@ padding: 6px 12px; } - .wrapperCheckbox { - padding-right: 12px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - cursor: pointer; + /* Statuses */ + + .wrapperDueSoon { + background: #f2711c; + color: #fff; } - .checkbox { - display: block; + + .wrapperOverdue { + background: #db2828; + color: #fff; } - .wrapperButton { - border-top-left-radius: 0; - border-bottom-left-radius: 0; + + .wrapperCompleted { + background: #21ba45; + color: #fff; } } diff --git a/client/src/components/Label/Label.jsx b/client/src/components/Label/Label.jsx index dbd796ef2..28e6db755 100644 --- a/client/src/components/Label/Label.jsx +++ b/client/src/components/Label/Label.jsx @@ -17,7 +17,7 @@ const SIZES = { const Label = React.memo(({ name, color, size, isDisabled, onClick }) => { const contentNode = ( -
{ )} > {name || '\u00A0'} -
+
); return onClick ? ( diff --git a/client/src/components/Label/Label.module.scss b/client/src/components/Label/Label.module.scss index 364c2c7e7..7e0c12194 100644 --- a/client/src/components/Label/Label.module.scss +++ b/client/src/components/Label/Label.module.scss @@ -11,17 +11,17 @@ .wrapper { border-radius: 3px; - box-sizing: border-box; color: #fff; + display: inline-block; font-weight: 400; - outline: none; overflow: hidden; text-overflow: ellipsis; text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2); + vertical-align: top; white-space: nowrap; } - .wrapperNameless{ + .wrapperNameless { width: 40px; } diff --git a/client/src/components/Stopwatch/Stopwatch.module.scss b/client/src/components/Stopwatch/Stopwatch.module.scss index ce7653150..c34ccea6a 100644 --- a/client/src/components/Stopwatch/Stopwatch.module.scss +++ b/client/src/components/Stopwatch/Stopwatch.module.scss @@ -10,13 +10,10 @@ .wrapper { background: #dce0e4; - border: none; border-radius: 3px; color: #6a808b; display: inline-block; font-variant-numeric: tabular-nums; - outline: none; - text-align: left; transition: background 0.3s ease; vertical-align: top; } diff --git a/client/src/components/User/User.module.scss b/client/src/components/User/User.module.scss index 9d8cf9ea1..8c5eddc52 100644 --- a/client/src/components/User/User.module.scss +++ b/client/src/components/User/User.module.scss @@ -14,12 +14,10 @@ } .wrapper { - border: none; border-radius: 50%; color: #fff; display: inline-block; line-height: 1; - outline: none; text-align: center; vertical-align: top; } diff --git a/client/src/containers/CardContainer.js b/client/src/containers/CardContainer.js index 7c58e549d..16e663bcb 100755 --- a/client/src/containers/CardContainer.js +++ b/client/src/containers/CardContainer.js @@ -20,7 +20,7 @@ const makeMapStateToProps = () => { const allLabels = selectors.selectLabelsForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); - const { name, dueDate, dueCompleted, stopwatch, coverUrl, boardId, listId, isPersisted } = + const { name, dueDate, isDueDateCompleted, stopwatch, coverUrl, boardId, listId, isPersisted } = selectCardById(state, id); const users = selectUsersByCardId(state, id); @@ -36,7 +36,7 @@ const makeMapStateToProps = () => { index, name, dueDate, - dueCompleted, + isDueDateCompleted, stopwatch, coverUrl, boardId, diff --git a/client/src/containers/CardModalContainer.js b/client/src/containers/CardModalContainer.js index 05ed8a4cb..6a343699f 100755 --- a/client/src/containers/CardModalContainer.js +++ b/client/src/containers/CardModalContainer.js @@ -21,7 +21,7 @@ const mapStateToProps = (state) => { name, description, dueDate, - dueCompleted, + isDueDateCompleted, stopwatch, isSubscribed, isActivitiesFetching, @@ -50,7 +50,7 @@ const mapStateToProps = (state) => { name, description, dueDate, - dueCompleted, + isDueDateCompleted, stopwatch, isSubscribed, isActivitiesFetching, diff --git a/client/src/models/Card.js b/client/src/models/Card.js index f811b3dc7..ae60c4a43 100755 --- a/client/src/models/Card.js +++ b/client/src/models/Card.js @@ -20,9 +20,7 @@ export default class extends BaseModel { relatedName: 'ownCards', }), dueDate: attr(), - dueCompleted: attr({ - getDefault: () => false, - }), + isDueDateCompleted: attr(), stopwatch: attr(), isSubscribed: attr({ getDefault: () => false, @@ -211,7 +209,16 @@ export default class extends BaseModel { if (payload.data.boardId && payload.data.boardId !== cardModel.boardId) { cardModel.deleteWithRelated(); } else { - cardModel.update(payload.data); + cardModel.update({ + ...payload.data, + ...(payload.data.dueDate === null && { + isDueDateCompleted: null, + }), + ...(payload.data.dueDate && + !cardModel.dueDate && { + isDueDateCompleted: false, + }), + }); } break; @@ -251,7 +258,7 @@ export default class extends BaseModel { 'name', 'description', 'dueDate', - 'dueCompleted', + 'isDueDateCompleted', 'stopwatch', ]), ...payload.card, diff --git a/server/api/controllers/cards/create.js b/server/api/controllers/cards/create.js index 28285cb9e..a89cdbbc6 100755 --- a/server/api/controllers/cards/create.js +++ b/server/api/controllers/cards/create.js @@ -56,9 +56,11 @@ module.exports = { dueDate: { type: 'string', custom: dueDateValidator, + allowNull: true, }, - dueCompleted: { + isDueDateCompleted: { type: 'boolean', + allowNull: true, }, stopwatch: { type: 'json', @@ -103,7 +105,7 @@ module.exports = { 'name', 'description', 'dueDate', - 'dueCompleted', + 'isDueDateCompleted', 'stopwatch', ]); diff --git a/server/api/controllers/cards/update.js b/server/api/controllers/cards/update.js index 763057a6f..df1c8c66b 100755 --- a/server/api/controllers/cards/update.js +++ b/server/api/controllers/cards/update.js @@ -80,8 +80,9 @@ module.exports = { custom: dueDateValidator, allowNull: true, }, - dueCompleted: { + isDueDateCompleted: { type: 'boolean', + allowNull: true, }, stopwatch: { type: 'json', @@ -176,7 +177,7 @@ module.exports = { 'name', 'description', 'dueDate', - 'dueCompleted', + 'isDueDateCompleted', 'stopwatch', 'isSubscribed', ]); diff --git a/server/api/helpers/cards/create-one.js b/server/api/helpers/cards/create-one.js index 36cdb81aa..7f1e2652a 100644 --- a/server/api/helpers/cards/create-one.js +++ b/server/api/helpers/cards/create-one.js @@ -49,6 +49,14 @@ module.exports = { throw 'positionMustBeInValues'; } + if (values.dueDate) { + if (_.isNil(values.isDueDateCompleted)) { + values.isDueDateCompleted = false; + } + } else { + delete values.isDueDateCompleted; + } + const cards = await sails.helpers.lists.getCards(values.list.id); const { position, repositions } = sails.helpers.utils.insertToPositionables( diff --git a/server/api/helpers/cards/duplicate-one.js b/server/api/helpers/cards/duplicate-one.js index 3ea304439..55fd6c984 100644 --- a/server/api/helpers/cards/duplicate-one.js +++ b/server/api/helpers/cards/duplicate-one.js @@ -77,7 +77,7 @@ module.exports = { 'name', 'description', 'dueDate', - 'dueCompleted', + 'isDueDateCompleted', 'stopwatch', ]), ...values, diff --git a/server/api/helpers/cards/update-one.js b/server/api/helpers/cards/update-one.js index cd978b7e4..9be5c5764 100644 --- a/server/api/helpers/cards/update-one.js +++ b/server/api/helpers/cards/update-one.js @@ -135,6 +135,20 @@ module.exports = { }); } + const dueDate = _.isUndefined(values.dueDate) ? inputs.record.dueDate : values.dueDate; + + if (dueDate) { + const isDueDateCompleted = _.isUndefined(values.isDueDateCompleted) + ? inputs.record.isDueDateCompleted + : values.isDueDateCompleted; + + if (_.isNull(isDueDateCompleted)) { + values.isDueDateCompleted = false; + } + } else { + values.isDueDateCompleted = null; + } + let card; if (_.isEmpty(values)) { card = inputs.record; diff --git a/server/api/models/Card.js b/server/api/models/Card.js index fe6738d67..c33763009 100755 --- a/server/api/models/Card.js +++ b/server/api/models/Card.js @@ -28,10 +28,10 @@ module.exports = { type: 'ref', columnName: 'due_date', }, - dueCompleted: { + isDueDateCompleted: { type: 'boolean', - defaultsTo: false, - columnName: 'due_completed', + allowNull: true, + columnName: 'is_due_date_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 deleted file mode 100644 index 331782aec..000000000 --- a/server/db/migrations/20240812065305_add_due_completion.js.js +++ /dev/null @@ -1,12 +0,0 @@ -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'); - }); -}; diff --git a/server/db/migrations/20240812065305_make_due_date_toggleable.js b/server/db/migrations/20240812065305_make_due_date_toggleable.js new file mode 100644 index 000000000..bc55c0c25 --- /dev/null +++ b/server/db/migrations/20240812065305_make_due_date_toggleable.js @@ -0,0 +1,18 @@ +module.exports.up = async (knex) => { + await knex.schema.table('card', (table) => { + /* Columns */ + + table.boolean('is_due_date_completed'); + }); + + return knex('card') + .update({ + isDueDateCompleted: false, + }) + .whereNotNull('due_date'); +}; + +module.exports.down = (knex) => + knex.schema.table('card', (table) => { + table.dropColumn('is_due_date_completed'); + });