Add in VUE date picker component

This commit is contained in:
George Drummond 2019-01-22 14:41:18 -05:00
parent 5766819213
commit 454d7f10df
No known key found for this signature in database
GPG Key ID: 296DD6077123BF17
8 changed files with 388 additions and 9 deletions

View File

@ -1,3 +1,4 @@
import datetime as dt
from flask import Blueprint, render_template, g, request as http_request, redirect
from atst.forms.edit_user import EditUserForm
from atst.domain.users import Users
@ -16,7 +17,14 @@ def user():
if next_:
flash("user_must_complete_profile")
return render_template("user/edit.html", next=next_, form=form, user=user)
return render_template(
"user/edit.html",
next=next_,
form=form,
user=user,
mindate=(dt.datetime.now() - dt.timedelta(days=365)),
maxdate=dt.datetime.now(),
)
@bp.route("/user", methods=["POST"])

View File

@ -0,0 +1,172 @@
import Vue from 'vue'
import DateSelector from '../date_selector'
describe('DateSelector', () => {
const component = new Vue(DateSelector).$mount()
describe('isDateValid', () => {
it('returns true when a valid date', () => {
component.day = 4
component.month = 8
component.year = 1776
expect(component.isDateValid).toEqual(true)
})
it('returns false when an invalid date', () => {
component.day = 32
component.month = 13
component.year = 2019
expect(component.isDateValid).toEqual(false)
})
it('returns false when parts of the date are missing', () => {
component.day = 31
component.year = 2019
expect(component.isDateValid).toEqual(false)
})
})
describe('daysMaxCalculation', () => {
it('calculates correctly for each month', () => {
let months = {
'1': 31,
'2': 29,
'3': 31,
'4': 30,
'5': 31,
'6': 30,
'7': 31,
'8': 31,
'9': 30,
'10': 31,
'11': 30,
'12': 31,
}
for (var month in months) {
component.month = parseInt(month)
expect(component.daysMaxCalculation).toEqual(months[month])
}
})
})
describe('isMonthValid', () => {
it('returns false when over 12', () => {
component.month = 13
expect(component.isMonthValid).toEqual(false)
})
it('returns true when between 1 and 12', () => {
component.month = 3
expect(component.isMonthValid).toEqual(true)
})
it('returns false when null', () => {
component.month = null
expect(component.isMonthValid).toEqual(false)
})
})
describe('isDayValid', () => {
it('returns true when 31 and no month', () => {
component.day = 31
component.month = null
expect(component.isDayValid).toEqual(true)
})
it('returns false when 31 and in February', () => {
component.day = 31
component.month = 2
expect(component.isDayValid).toEqual(false)
})
it('returns false when 32 and no month', () => {
component.day = 32
component.month = null
expect(component.isDayValid).toEqual(false)
})
it('returns false when null', () => {
component.day = null
expect(component.isDayValid).toEqual(false)
})
})
describe('isWithinDateRange', () => {
beforeEach(() => {
component.day = 24
component.month = 1
component.year = 2019
})
it('always returns true when no mindate or maxdate', () => {
expect(component.isWithinDateRange).toEqual(true)
})
it('handles mindate only', () => {
component.mindate = '2019-01-25'
expect(component.isWithinDateRange).toEqual(false)
component.mindate = '2014-01-25'
expect(component.isWithinDateRange).toEqual(true)
})
it('handles maxdate only', () => {
component.maxdate = '2019-01-25'
expect(component.isWithinDateRange).toEqual(true)
component.maxdate = '2014-01-25'
expect(component.isWithinDateRange).toEqual(false)
})
it('handles mindate and maxdate', () => {
component.mindate = '2019-01-25'
component.maxdate = '2019-02-28'
expect(component.isWithinDateRange).toEqual(false)
component.mindate = '2013-01-25'
component.maxdate = '2016-02-28'
expect(component.isWithinDateRange).toEqual(false)
component.mindate = '2014-01-25'
component.maxdate = '2020-02-28'
expect(component.isWithinDateRange).toEqual(true)
})
})
describe('isYearValid', () => {
it('returns false if year is null', () => {
component.year = null
expect(component.isYearValid).toEqual(false)
})
it('returns true if year is present', () => {
component.year = new Date().getFullYear()
expect(component.isYearValid).toEqual(true)
})
})
describe('formattedDate', () => {
it('returns null if not all parts are present', () => {
component.day = null
component.month = 1
component.year = 1988
expect(component.formattedDate).toBeNull()
})
it('joins date components into a JS date', () => {
component.mindate = null
component.maxdate = null
component.day = 22
component.month = 1
component.year = 1988
expect(component.formattedDate).toEqual('01/22/1988')
})
})
})

View File

@ -0,0 +1,101 @@
import Vue from 'vue'
var paddedNumber = function(number) {
if ((number + '').length === 1) {
return `0${number}`
} else {
return number
}
}
export default Vue.component('date-selector', {
props: ['initialday', 'initialmonth', 'initialyear', 'mindate', 'maxdate'],
data: function() {
return {
day: this.initialday,
month: this.initialmonth,
year: this.initialyear,
}
},
computed: {
formattedDate: function() {
if (!this.isDateValid) {
return null
}
let day = paddedNumber(this.day)
let month = paddedNumber(this.month)
return `${month}/${day}/${this.year}`
},
isMonthValid: function() {
var _month = parseInt(this.month)
return _month >= 0 && _month <= 12
},
isDayValid: function() {
var _day = parseInt(this.day)
return _day >= 0 && _day <= this.daysMaxCalculation
},
isYearValid: function() {
return parseInt(this.year) >= 1
},
isWithinDateRange: function() {
let _mindate = this.mindate ? Date.parse(this.mindate) : null
let _maxdate = this.maxdate ? Date.parse(this.maxdate) : null
let _dateTimestamp = Date.UTC(this.year, this.month - 1, this.day)
if (_mindate !== null && _mindate >= _dateTimestamp) {
return false
}
if (_maxdate !== null && _maxdate <= _dateTimestamp) {
return false
}
return true
},
isDateValid: function() {
return (
this.day &&
this.month &&
this.year &&
this.isDayValid &&
this.isMonthValid &&
this.isYearValid &&
this.isWithinDateRange
)
},
daysMaxCalculation: function() {
switch (parseInt(this.month)) {
case 2: // February
return 29
break
case 4: // April
case 6: // June
case 9: // September
case 11: // November
return 30
break
default:
// All other months, or null, go with 31
return 31
}
},
},
render: function(createElement) {
return createElement('p', 'Please implement inline-template')
},
})

View File

@ -30,6 +30,7 @@ import LocalDatetime from './components/local_datetime'
import RequestsList from './components/requests_list'
import ConfirmationPopover from './components/confirmation_popover'
import { isNotInVerticalViewport } from './lib/viewport'
import DateSelector from './components/date_selector'
Vue.config.productionTip = false
@ -62,6 +63,7 @@ const app = new Vue({
RequestsList,
ConfirmationPopover,
funding,
DateSelector,
},
mounted: function() {

View File

@ -116,3 +116,25 @@
}
}
.date-picker {
display: inline-block;
margin-top: 10px;
width: 100%;
label {
font-weight: 400;
}
input[type="number"]::-webkit-inner-spin-button {
appearance: none;
}
input.usa-input-error {
right: 0;
}
.usa-form-group-date-ok {
padding-top: 30px;
}
}

View File

@ -0,0 +1,70 @@
{% from "components/icon.html" import Icon %}
{% macro DatePicker(field, mindate=None, maxdate=None) -%}
<date-selector
{% if maxdate %}maxdate="{{ maxdate.strftime("%Y-%m-%d") }}"{% endif %}
{% if mindate %}mindate="{{ mindate.strftime("%Y-%m-%d") }}"{% endif %}
initialmonth="{{ field.data.month }}"
initialday="{{ field.data.day }}"
initialyear="{{ field.data.year }}"
inline-template>
<div class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid }">
<p v-if="!isWithinDateRange" class="usa-input-error-message">
{% if maxdate and mindate %}Date must be between {{maxdate.strftime("%Y-%m-%d")}} and {{mindate.strftime("%Y-%m-%d")}}{% endif %}
{% if maxdate and not mindate %}Date must be before or on {{maxdate.strftime("%Y-%m-%d")}}{% endif %}
{% if mindate and not maxdate %}Date must be after or on {{mindate.strftime("%Y-%m-%d")}}{% endif %}
</p>
<div>
<input v-bind:value="formattedDate" type="hidden" />
<div class="usa-form-group usa-form-group-month">
<label>Month</label>
<input
name="date-month"
max="12"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (month && !isMonthValid) }"
v-model="month"
/>
</div>
<div class="usa-form-group usa-form-group-day">
<label>Day</label>
<input
name="date-day"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (day && !isDayValid) }"
v-bind:max="daysMaxCalculation"
v-model="day"
/>
</div>
<div class="usa-form-group usa-form-group-year">
<label>Year</label>
<input
id="date-year"
maxlength="2"
type="number"
v-model="year"
{% if maxdate %}max="{{ maxdate.year }}"{% endif %}
{% if mindate %}min="{{ mindate.year }}"{% endif %}
/>
</div>
<div class="usa-form-group-date-ok" v-if="isDateValid">
{{ Icon("ok", classes="icon--green") }}
</div>
<input name="{{ field.name }}" v-model="formattedDate" type="hidden" />
</div>
</div>
</date-selector>
{%- endmacro %}

View File

@ -2,6 +2,7 @@
{% from "components/options_input.html" import OptionsInput %}
{% from "components/date_input.html" import DateInput %}
{% from "components/phone_input.html" import PhoneInput %}
{% from "components/date_picker.html" import DatePicker %}
<form method="POST" action='{{ form_action }}'>
{{ form.csrf_token }}
@ -23,14 +24,16 @@
{{ OptionsInput(form.service_branch) }}
{{ OptionsInput(form.citizenship) }}
{{ OptionsInput(form.designation) }}
{{
DateInput(
form.date_latest_training,
tooltip=("fragments.edit_user_form.date_last_training_tooltip" | translate),
placeholder="MM / DD / YYYY",
validation="date"
)
}}
<div class="usa-input">
<label>
<div class="usa-input__title">{{ form.date_latest_training.label }}</div>
</label>
<span class="usa-form-hint">{{ "forms.date_hint" | translate }}</span>
{{ DatePicker(form.date_latest_training, mindate=mindate, maxdate=maxdate) }}
</div>
</div>
</div>

View File

@ -43,6 +43,7 @@ footer:
browser_support: JEDI Cloud supported on these web browsers
jedi_help_link_text: Questions? Contact your CCPO Representative
forms:
date_hint: "For example: 11 28 1986"
ccpo_review:
comment_description: Provide instructions or notes for additional information that is necessary to approve the request here. The requestor may then re-submit the updated request or initiate contact outside of AT-AT if further discussion is required. <strong>This message will be shared with the person making the JEDI request.</strong>.
comment_label: Instructions or comments