Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Card Due Dates #138

Merged
merged 1 commit into from Jan 5, 2017
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file
Failed to load files.

Always

Just for now

@@ -315,7 +315,7 @@
font-weight: 400
font-size: 14px
> pre
margin: 0
margin: 0 0 4px
font-size: 14px
font-family: inherit
white-space: pre-wrap
@@ -9,6 +9,8 @@ import ConfirmationLink from '../ConfirmationLink'
import EditCardForm from './EditCardForm'
import CardLabel from './Card/CardLabel'
import commands from '../../commands'
import Badge from './Card/Badge'
import moment from 'moment'

export default class Card extends Component {
static contextTypes = {
@@ -81,6 +83,10 @@ export default class Card extends Component {
style
} = this.props

const dueDateBadge = card.due_date
? <Badge card={card} shownOn='front'/>
: null

let cardLabels = !board ? null : card.label_ids
.map( labelId => board.labels.find(label => label.id === labelId))
.map( label =>
@@ -131,6 +137,7 @@ export default class Card extends Component {
{cardLabels}
</div>
<pre>{card.content}</pre>
{dueDateBadge}
{archivedFooter}
</Link>
<div className="BoardShowPage-Card-controls">
@@ -0,0 +1,71 @@
import React, {Component} from 'react'
import Link from '../../Link'
import Icon from '../../Icon'
import moment from 'moment'
import PopoverMenuButton from '../../PopoverMenuButton'
import DueDatePopover from './DueDatePopover'
import './Badge.sass'

export default class Badge extends Component {
constructor(props){
super(props)
this.dueStatus = this.dueStatus.bind(this)
}

dueStatus() {
const dueDate = moment(this.props.card.due_date)
const pastDue = moment().isAfter(dueDate)
const status = {
className: '',
preText: '',
postText: ''
}

if (pastDue) {
if (dueDate.isBefore(moment().subtract(1, 'days'), 'day')) {
status.className = 'Badge-due-past-long'
status.preText = dueDate.format('MMM D [at] h:mm')
status.postText = '(past due)'
} else {
status.className = 'Badge-due-past-recent'
status.preText = dueDate.calendar(moment(), 'D hh:mm A');
status.postText = '(recently past due)'
}
} else {
if (dueDate.isAfter(moment().add(1, 'days'), 'day')) {
status.className = 'Badge-due-future-distant'
status.preText = dueDate.format('MMM D [at] h:mm')
} else {
status.className = 'Badge-due-future-near'
status.preText = dueDate.calendar(moment(), 'D hh:mm A');
status.postText = '(due soon)'
}
}
if (this.props.card.complete){

This comment has been minimized.

@deadlyicon

deadlyicon Dec 20, 2016 Contributor

can e do this check first so we can save all the other calculations?

This comment has been minimized.

@mantinone

mantinone Dec 20, 2016 Collaborator

We need to check if the due date is within one day of the current time so we can display "today", "tomorrow", or "yesterday" instead of the date, so running through the previous checks seems the simplest way to avoid repeating ourselves.

This comment has been minimized.

@deadlyicon

deadlyicon Dec 21, 2016 Contributor

doesn't moment have a .fromNow() method? can we just use that?

This comment has been minimized.

@mantinone

mantinone Dec 22, 2016 Collaborator

fromNow() returns a string that says how many hours/days/years away something is, and doesn't provide any method for telling it what level of resolution you want. As far as I can see, isBefore() and isAfter() are the only ways of checking if a date falls within a certain relative range, and calendar() is the thing that will give us "today", "tomorrow", or "yesterday"

I can't think of any way to handle the today tomorrow yesterday thing without essentially repeating a lot of the if-else logic above that part.

This comment has been minimized.

@deadlyicon

deadlyicon Dec 22, 2016 Contributor

fromNow() returns a string that says how many hours/days/years away something is, and doesn't provide any method for telling it what level of resolution you want. As far as I can see, isBefore() and isAfter() are the only ways of checking if a date falls within a certain relative range, and calendar() is the thing that will give us "today", "tomorrow", or "yesterday"

I can't think of any way to handle the today tomorrow yesterday thing without essentially repeating a lot of the if-else logic above that part.
@mantinone

This comment has been minimized.

@deadlyicon

deadlyicon Dec 22, 2016 Contributor

okay as long as you've investigated using moment to achieve this. :)

status.className = 'Badge-due-complete'
status.postText = ''
}
return status
}

render(){
const { card, shownOn } = this.props
let status = this.dueStatus()
let className = `Badge-due ${status.className || ''}`
let renderBadge

if (shownOn === 'front'){
let shortDate = moment(card.due_date).format('MMM D')
return <div className={className}><Icon type="clock-o" className="Badge-due-dueIcon"/><span className="Badge-due-dueText">{shortDate}</span></div>
}
let longDate = status.preText
const dueDatePopover = <DueDatePopover card={card}/>
className += ' Badge-due-large'
return <div className='Badge'>
<div className='Badge-header'>Due Date</div>
<PopoverMenuButton key={card.id} type='unstyled' popover={dueDatePopover} className='Badges-labels-Label Badge-buttonHack'>
<div className={className}><span className="Badge-due-dueText">{longDate + ' ' + status.postText}</span></div>
</PopoverMenuButton>
</div>
}
}
@@ -0,0 +1,45 @@
@import "../../../style.sass"

This comment has been minimized.

@deadlyicon

deadlyicon Dec 22, 2016 Contributor

perfect


.Badge
margin: 4px 28px 16px 0
&-buttonHack
margin-top: 7px

&-due
border-radius: 3px
display: inline
color: white
padding: 1px 2px
margin: 0 4px 4px 0
&-large
font-size: 80%
padding: 0.4em 0.5em
font-weight: bolder
&-dueText, &-dueIcon
padding: 3px
&-dueText
font-size: 12px
&-complete
background-color: $green
&-past
&-long
background-color: $alert-red
&-recent
background-color: $danger-red
&-future
&-distant
background-color: transparent
color: $medium-grey
&-near
background-color: $gold
&-header
color: #999999
font-weight: normal
font-size: .89em
margin: 10px 0 5px
&-labels
display: flex
flex-direction: row
flex-wrap: wrap
&-Label
margin-right: 0.25em
@@ -2,6 +2,7 @@ import React, { Component } from 'react'
import './CardModal.sass'
import moment from 'moment'
import LabelMenu from './LabelMenu'
import DueDatePopover from './DueDatePopover'
import CardLabel from './CardLabel'
import Card from '../Card'
import Link from '../../Link'
@@ -17,6 +18,7 @@ import PopoverMenuButton from '../../PopoverMenuButton'
import CopyCard from '../CopyCard'
import Activity from '../../Activity'
import commands from '../../../commands'
import Badge from './Badge'

export default class CardModal extends Component {
static propTypes = {
@@ -70,15 +72,20 @@ export default class CardModal extends Component {
board={board}
/>

let dueDateBadge = card.due_date ? <Badge card={card} shownOn='back'/>: null

return <div className="CardModal">
<CardModalShroud onClose={this.props.onClose}>
{archivedBanner}
<div className="CardModal-columns">
<div className="CardModal-content">
<CardHeader card={card} list={list}/>
<div className="CardModal-body">
<CardLabels card={card} board={board} labelPanel={labelPanel}/>
<CardDescription card={card} />
<div className="CardModal-badges">
<CardLabels card={card} board={board} labelPanel={labelPanel}/>
{dueDateBadge}
</div>
<CardDescription card={card}/>
</div>
<CardCommentForm card={card} session={session}/>
<CardActivity board={board} card={card}/>
@@ -132,7 +139,7 @@ const CardLabels =({card, board, labelPanel}) => {
.map(label =>
<PopoverMenuButton
key={label.id}
className="CardModal-CardLabels-labels-Label"
className="CardModal-CardBadges-labels-Label"
type="unstyled"
popover={labelPanel}
>
@@ -144,17 +151,18 @@ const CardLabels =({card, board, labelPanel}) => {
</PopoverMenuButton>
)
const labelHeader = cardLabels.length > 0 ?
<div className="CardModal-CardLabels-header">Labels</div> : null
<div className="CardModal-CardBadges-header">Labels</div> : null

return <div className="CardModal-CardLabels">
return <div className="CardModal-CardBadges">
{labelHeader}
<div className="CardModal-CardLabels-labels">
<div className="CardModal-CardBadges-labels">
{cardLabels}
</div>
</div>
}

const Controls = ({board, list, card, closeModal, labelPanel}) => {
const dueDate = <DueDatePopover card={card}/>
const copyCard = <CopyCard card={card} board={board} list={list}/>
const toggleOnArchived = card.archived ?
<div>
@@ -169,6 +177,9 @@ const Controls = ({board, list, card, closeModal, labelPanel}) => {
<PopoverMenuButton className="CardModal-Controls-label" type="default" popover={labelPanel}>
<Icon type="tag" /> Labels
</PopoverMenuButton>
<PopoverMenuButton className="CardModal-Controls-label" type="default" popover={dueDate}>
<Icon type="clock-o" /> Due Date
</PopoverMenuButton>
<div className="CardModal-Controls-title">Actions</div>
{toggleOnArchived}
<PopoverMenuButton className="CardModal-Controls-copy" type="default" popover={copyCard}>
@@ -22,6 +22,8 @@
margin-left: 35px
display: flex
flex-direction: column
&-badges
display: flex

&-CardModalShroud
&-shroud, &-stage
@@ -83,7 +85,8 @@
input:focus
outline: none

&-CardLabels
&-CardBadges
margin: 4px 28px 16px 0
&-header
color: #999999
font-weight: normal
@@ -0,0 +1,28 @@
@import "../../../style.sass"

.Dialog
&-dueDateForm
font-weight: bold
padding: 10px
align-items: flex-start
&:last-child
position: absolute
bottom: 0
right: 0
padding: 0
margin: 0 10px 20px 0
&-inputRow
display: flex
flex-direction: row
justify-content: space-between
width: 100%
&-inputLabel
flex-grow: 1
&:first-child
margin-right: 12px
&-textInput
font: 14px "Helvetica Neue",Arial,Helvetica,sans-serif
&:focus
background-color: white
border-color: $blue
box-shadow: 0 0 2px $blue
@@ -0,0 +1,106 @@
import React, {Component} from 'react'
import './Dialog.sass'
import Form from '../../Form'
import Button from '../../Button'
import DialogBox from '../../DialogBox'
import commands from '../../../commands'
import moment from 'moment'



export default class DueDatePopover extends Component {
constructor(props){
super(props)
this.dateOnChange = this.dateOnChange.bind(this)

This comment has been minimized.

@deadlyicon

deadlyicon Jan 4, 2017 Contributor

please make sure we are only binding the methods that are being given as callbacks. For example I dont think initialState needs binding

this.dateOnBlur = this.dateOnBlur.bind(this)
this.timeOnChange = this.timeOnChange.bind(this)
this.timeOnBlur = this.timeOnBlur.bind(this)
this.addDueDate = this.addDueDate.bind(this)
this.removeDueDate = this.removeDueDate.bind(this)

this.state = this.initialState()
}

defaultDueDate(){
return moment().endOf('day').add(12, 'hours').add(1, 'minutes')
}

initialDueDate(){
return this.props.card.due_date
? moment(this.props.card.due_date)
: this.defaultDueDate()
}

dueDateToState(due_date){
return {
date: due_date.format('MM/DD/YYYY'),
time: due_date.format('hh:mm a'),
}
}

initialState(){
return this.dueDateToState(this.initialDueDate())
}

validateCurrentDueDate(){
const { date, time } = this.state
let due_date = moment(`${date} ${time}`)
return due_date.isValid() ? due_date : this.initialDueDate()
}

setValidDueDate(){
this.setState(this.dueDateToState(this.validateCurrentDueDate()))
}

addDueDate(event){
event.preventDefault()
this.updateCardDueDate( this.validateCurrentDueDate() )
}

removeDueDate(event){
event.preventDefault()
this.updateCardDueDate( null )
}

updateCardDueDate( due_date ){
commands.updateCard( this.props.card.id, { due_date } )
.then( () => {
boardStore.reload()
this.props.onClose()
})
}

dateOnChange(event){
this.setState({date: event.target.value})
}

dateOnBlur(event){
this.setValidDueDate()
}

timeOnChange(event){
this.setState({time: event.target.value})
}

timeOnBlur(event){
this.setValidDueDate()
}

render (){
return <DialogBox className="Dialog" heading='Change Due Date' onClose={this.props.onClose}>
<Form className='Dialog-dueDateForm' onSubmit={this.addDueDate} name='add'>
<div className="Dialog-inputRow">
<label className="Dialog-inputLabel">Date<input type='text' className="Dialog-inputText" value={this.state.date} onChange={this.dateOnChange} onBlur={this.dateOnBlur}></input></label>
<label className="Dialog-inputLabel">Time<input type='text' className="Dialog-inputText" value={this.state.time} onChange={this.timeOnChange} onBlur={this.timeOnBlur}></input></label>
</div>
<p>There will be a calendar and such here. Perhaps its own component?</p>
<div className="Dialog-inputRow">
<Button type='primary' submit>Save</Button>
</div>
</Form>
<Form className='Dialog-dueDateForm' onSubmit={this.removeDueDate} name='delete'>
<Button submit type='danger'>Remove</Button>
</Form>
</DialogBox>
}
}

This comment has been minimized.

@deadlyicon

deadlyicon Dec 22, 2016 Contributor

ok so having read through all of your logic I was thinking this kind of a structure might read cleaner https://gist.github.com/deadlyicon/723a49e91fab2c6a348844b341a46ac0 I have not tested this but I wrote it so you could hopefully see my thinking. I tried to dry things up as much as I could.

This comment has been minimized.

@deadlyicon

deadlyicon Jan 4, 2017 Contributor

did you check this out?

This comment has been minimized.

@mantinone

mantinone Jan 4, 2017 Collaborator

Yes. We refactored a lot of DueDatePopover so it uses most of your example code here, with a few adjustments.

ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.