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

Adding start/end times to courses #951

Merged
merged 32 commits into from
Sep 30, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
403bfe1
Migrations to change date columns to datetime in courses table
MusikAnimal Sep 12, 2016
170e7a8
Update course model and comments in course_spec
MusikAnimal Sep 13, 2016
4ce4ffd
Fix changed behavior of legacy course end dates
ragesoss Sep 13, 2016
b5cecac
Fix RevisionsController spec for course end as datetime
ragesoss Sep 13, 2016
309c4f9
Update CourseMeetingsManager to handle datetime timeline dates
ragesoss Sep 13, 2016
3bfad7a
Fix last few broken specs for datetime migration
ragesoss Sep 13, 2016
6cbd6f0
Fix date comparisons in course_creation_spec
MusikAnimal Sep 14, 2016
062c107
Update DatePicker to include a hour/min dropdowns, make everything in…
MusikAnimal Sep 14, 2016
d395dde
UI cleanup, define whether to show time control in models
MusikAnimal Sep 14, 2016
19eaf2f
Add before_save in Course to set end times to end_of_day
MusikAnimal Sep 15, 2016
6666beb
Basic frontend for DatePicker w/time picker in place
MusikAnimal Sep 15, 2016
eb11b1a
Address some CR concerns
MusikAnimal Sep 19, 2016
1378232
Attempt to get use_start_and_end_times to show up in CourseCreator
MusikAnimal Sep 19, 2016
c1c4643
use_start_and_end_times now being passed to CourseCreator
MusikAnimal Sep 19, 2016
df9e45b
CourseCreator fixes to work with new date/time picker
MusikAnimal Sep 20, 2016
648ba1f
Move initial start/end times to CourseStore
MusikAnimal Sep 20, 2016
39f8adf
Fix state driven value in DatePicker
MusikAnimal Sep 20, 2016
a8eebbf
Working date/time picker for course creation
MusikAnimal Sep 23, 2016
687d588
Make sure CourseDateUtils.isDateValid() returns a boolean
ragesoss Sep 26, 2016
4cac571
Update unit tests for new end-of-day end dates
ragesoss Sep 26, 2016
d288d93
Remove default dates from CourseStore
MusikAnimal Sep 26, 2016
2982784
Don't validate YYYY-MM-DD format as they type so that DayPicker will …
MusikAnimal Sep 27, 2016
c5b035f
use RegExp.prototype.test() in CourseDateUtils to force a boolean ret…
MusikAnimal Sep 27, 2016
d9ed8b6
attempt to make Travis green
MusikAnimal Sep 27, 2016
2b9ae07
Fix course_cloning_spec
ragesoss Sep 27, 2016
91e978b
Prevent multiple course submissions
ragesoss Sep 28, 2016
5f6178a
More handling of the course save flow
ragesoss Sep 28, 2016
56d18f0
Fix another spec
ragesoss Sep 28, 2016
233f9eb
assert times are saved properly in open_course_creation_spec
MusikAnimal Sep 29, 2016
ae44b38
test BasicCourse revisions are within start/end times
MusikAnimal Sep 30, 2016
3e3f2b9
test default start/end times are set properly when changing course types
MusikAnimal Sep 30, 2016
cece0b9
CR concerns
MusikAnimal Sep 30, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 178 additions & 26 deletions app/assets/javascripts/components/common/date_picker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import DayPicker from 'react-day-picker';
import OnClickOutside from 'react-onclickoutside';
import InputMixin from '../../mixins/input_mixin.js';
import Conditional from '../high_order/conditional.jsx';
import CourseDateUtils from '../../utils/course_date_utils.coffee';

const DatePicker = React.createClass({
displayName: 'DatePicker',
Expand All @@ -13,6 +14,7 @@ const DatePicker = React.createClass({
value_key: React.PropTypes.string,
spacer: React.PropTypes.string,
label: React.PropTypes.string,
timeLabel: React.PropTypes.string,
valueClass: React.PropTypes.string,
editable: React.PropTypes.bool,
enabled: React.PropTypes.bool,
Expand All @@ -23,9 +25,11 @@ const DatePicker = React.createClass({
p_tag_classname: React.PropTypes.string,
onBlur: React.PropTypes.func,
onFocus: React.PropTypes.func,
onChange: React.PropTypes.func,
onClick: React.PropTypes.func,
append: React.PropTypes.string,
date_props: React.PropTypes.object
date_props: React.PropTypes.object,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Along with datepicker unit tests, it would be great to use React.PropTypes.shape to validate the shape of date_props more precisely. Not strictly necessary as part of this PR, but it would be nice.

showTime: React.PropTypes.bool
},

mixins: [InputMixin],
Expand All @@ -37,31 +41,130 @@ const DatePicker = React.createClass({
},

getInitialState() {
if (this.props.value) {
const dateObj = moment(this.props.value).utc();
return {
value: dateObj.format('YYYY-MM-DD'),
hour: dateObj.hour(),
minute: dateObj.minute(),
datePickerVisible: false
};
}
return {
value: this.props.value,
value: null,
hour: 0,
minute: 0,
datePickerVisible: false
};
},

componentWillReceiveProps(nextProps) {
if (this.state.value === null) {
this.setState({ value: nextProps.value });
const dateObj = moment(nextProps.value).utc();
if (dateObj.isValid()) {
this.setState({
value: dateObj.format('YYYY-MM-DD'),
hour: dateObj.hour(),
minute: dateObj.minute()
});
}
},

handleDatePickerChange(e, selectedDate, modifiers) {
if (_.includes(modifiers, 'disabled')) {
/**
* Update parent component with new date value.
* Used instead of onChange() in InputMixin because we need to
* call this.props.onChange with the full date string, not just YYYY-MM-DD
* @return {null}
*/
onChangeHandler() {
this.props.onChange(this.props.value_key, this.getDate().format());
},

/**
* Get moment object of currently select date, hour and minute
* @return {moment}
*/
getDate() {
let dateObj = moment(this.state.value, 'YYYY-MM-DD').utc();
dateObj = dateObj.hour(this.state.hour);
return dateObj.minute(this.state.minute);
},

getFormattedDate() {
return this.getDate().format('YYYY-MM-DD');
},

/**
* Get formatted date to be displayed as text,
* based on whether or not to include the time
* @return {String} formatted date
*/
getFormattedDateTime() {
return CourseDateUtils.formattedDateTime(this.getDate(), this.props.showTime);
},

getTimeDropdownOptions(type) {
return _.range(0, type === 'hour' ? 24 : 60).map(value => {
return (
<option value={value} key={`timedropdown-${type}-${value}`}>
{(`00${value}`).slice(-2)}
</option>
);
});
},

handleDatePickerChange(e, selectedDate) {
const date = moment(selectedDate).utc();
if (this.isDayDisabled(date)) {
return;
}
const date = moment(selectedDate).format('YYYY-MM-DD');
this.onChange({ target: { value: date } });
this.refs.datefield.focus();
this.setState({ datePickerVisible: false });
this.setState({
value: date.format('YYYY-MM-DD'),
datePickerVisible: false
}, this.onChangeHandler);
},

/**
* Update value of date input field.
* Does not issue callbacks to parent component.
* @param {Event} e - input change event
* @return {null}
*/
handleDateFieldChange(e) {
const { value } = e.target;
this.onChange({ target: { value } });
if (value !== this.state.value) {
this.setState({ value });
}
},

/**
* When they blur out of the date input field,
* update the state if valid or revert back to last valid value
* @param {Event} e - blur event
* @return {null}
*/
handleDateFieldBlur(e) {
const { value } = e.target;
if (this.isValidDate(value) && !this.isDayDisabled(value)) {
this.setState({ value }, () => {
this.onChangeHandler();
this.validate(); // make sure validations are set as valid
});
} else {
this.setState({ value: this.getInitialState().value });
}
},

handleHourFieldChange(e) {
this.setState({
hour: e.target.value
}, this.onChangeHandler);
},

handleMinuteFieldChange(e) {
this.setState({
minute: e.target.value
}, this.onChangeHandler);
},

handleClickOutside() {
Expand All @@ -88,40 +191,58 @@ const DatePicker = React.createClass({
},

isDaySelected(date) {
const currentDate = moment(date).format('YYYY-MM-DD');
const currentDate = moment(date).utc().format('YYYY-MM-DD');
return currentDate === this.state.value;
},

isDayDisabled(date) {
const currentDate = moment(date);
const currentDate = moment(date).utc();
if (this.props.date_props) {
const minDate = moment(this.props.date_props.minDate, 'YYYY-MM-DD').startOf('day');
const minDate = moment(this.props.date_props.minDate, 'YYYY-MM-DD').utc().startOf('day');
if (minDate.isValid() && currentDate < minDate) {
return true;
}

const maxDate = moment(this.props.date_props.maxDate, 'YYYY-MM-DD').endOf('day');
const maxDate = moment(this.props.date_props.maxDate, 'YYYY-MM-DD').utc().endOf('day');
if (maxDate.isValid() && currentDate > maxDate) {
return true;
}
}
},

/**
* Validates given date string (should be similar to YYYY-MM-DD).
* This is implemented here to be self-contained within DatePicker.
* @param {String} value - date string
* @return {Boolean} valid or not
*/
isValidDate(value) {
const validationRegex = /^20\d\d\-(0?[1-9]|1[012])\-(0?[1-9]|[12][0-9]|3[01])/;
return validationRegex.test(value) && moment(value, 'YYYY-MM-DD').isValid();
},

showCurrentDate() {
return this.refs.daypicker.showMonth(this.state.month);
},

render() {
const spacer = this.props.spacer || ': ';
let label;
let timeLabel;
let currentMonth;

if (this.props.label) {
label = this.props.label;
label += spacer;
}

const { value } = this.props;
if (this.props.timeLabel) {
timeLabel = this.props.timeLabel;
timeLabel += spacer;
} else {
// use unicode for &nbsp; to account for spacing when there is no label
timeLabel = '\u00A0';
}

let valueClass = 'text-input-component__value ';
if (this.props.valueClass) { valueClass += this.props.valueClass; }
Expand All @@ -135,29 +256,30 @@ const DatePicker = React.createClass({
inputClass += 'invalid';
}

const date = moment(this.state.value, 'YYYY-MM-DD');
let minDate;
if (this.props.date_props && this.props.date_props.minDate) {
const minDateValue = moment(this.props.date_props.minDate, 'YYYY-MM-DD');
const minDateValue = moment(this.props.date_props.minDate, 'YYYY-MM-DD').utc();
if (minDateValue.isValid()) {
minDate = minDateValue;
}
}

// don't validate YYYY-MM-DD format so we can update the daypicker as they type
const date = moment(this.state.value, 'YYYY-MM-DD');
if (date.isValid()) {
currentMonth = date.toDate();
currentMonth = date.utc().toDate();
} else if (minDate) {
currentMonth = minDate.toDate();
currentMonth = minDate.utc().toDate();
} else {
currentMonth = new Date();
currentMonth = moment().utc().toDate();
}

const modifiers = {
selected: this.isDaySelected,
disabled: this.isDayDisabled
};

const input = (
const dateInput = (
<div className="date-input">
<input
id={this.state.id}
Expand Down Expand Up @@ -187,25 +309,55 @@ const DatePicker = React.createClass({
</div>
);

const timeControlNode = (
<span className={`form-group time-picker--form-group ${inputClass}`}>
<label htmlFor={`${this.state.id}-hour`} className={labelClass}>
{timeLabel}
</label>
<div className="time-input">
<select
className="time-input__hour"
onChange={this.handleHourFieldChange}
value={this.state.hour}
>
{this.getTimeDropdownOptions('hour')}
</select>
:
<select
className="time-input__minute"
onChange={this.handleMinuteFieldChange}
value={this.state.minute}
>
{this.getTimeDropdownOptions('minute')}
</select>
</div>
</span>
);

return (
<div className={`form-group ${inputClass}`}>
<label htmlFor={this.state.id}className={labelClass}>{label}</label>
{input}
<div className={`form-group datetime-control ${this.props.id}-datetime-control ${inputClass}`}>
<span className={`form-group date-picker--form-group ${inputClass}`}>
<label htmlFor={this.state.id}className={labelClass}>{label}</label>
{dateInput}
</span>
{this.props.showTime ? timeControlNode : null}
</div>
);
} else if (this.props.label !== null) {
return (
<p className={this.props.p_tag_classname}>
<span className="text-input-component__label"><strong>{label}</strong></span>
<span>{(this.props.value !== null || this.props.editable) && !this.props.label ? spacer : null}</span>
<span onBlur={this.props.onBlur} onClick={this.props.onClick} className={valueClass}>{value}</span>
<span onBlur={this.props.onBlur} onClick={this.props.onClick} className={valueClass}>
{this.getFormattedDateTime()}
</span>
{this.props.append}
</p>
);
}

return (
<span>{value}</span>
<span>{this.getFormattedDateTime()}</span>
);
}
});
Expand Down
Loading