Add in VUE date picker component
This commit is contained in:
parent
5766819213
commit
454d7f10df
@ -1,3 +1,4 @@
|
|||||||
|
import datetime as dt
|
||||||
from flask import Blueprint, render_template, g, request as http_request, redirect
|
from flask import Blueprint, render_template, g, request as http_request, redirect
|
||||||
from atst.forms.edit_user import EditUserForm
|
from atst.forms.edit_user import EditUserForm
|
||||||
from atst.domain.users import Users
|
from atst.domain.users import Users
|
||||||
@ -16,7 +17,14 @@ def user():
|
|||||||
if next_:
|
if next_:
|
||||||
flash("user_must_complete_profile")
|
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"])
|
@bp.route("/user", methods=["POST"])
|
||||||
|
172
js/components/__tests__/date_selector.test.js
Normal file
172
js/components/__tests__/date_selector.test.js
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
101
js/components/date_selector.js
Normal file
101
js/components/date_selector.js
Normal 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')
|
||||||
|
},
|
||||||
|
})
|
@ -30,6 +30,7 @@ import LocalDatetime from './components/local_datetime'
|
|||||||
import RequestsList from './components/requests_list'
|
import RequestsList from './components/requests_list'
|
||||||
import ConfirmationPopover from './components/confirmation_popover'
|
import ConfirmationPopover from './components/confirmation_popover'
|
||||||
import { isNotInVerticalViewport } from './lib/viewport'
|
import { isNotInVerticalViewport } from './lib/viewport'
|
||||||
|
import DateSelector from './components/date_selector'
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
@ -62,6 +63,7 @@ const app = new Vue({
|
|||||||
RequestsList,
|
RequestsList,
|
||||||
ConfirmationPopover,
|
ConfirmationPopover,
|
||||||
funding,
|
funding,
|
||||||
|
DateSelector,
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted: function() {
|
mounted: function() {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
70
templates/components/date_picker.html
Normal file
70
templates/components/date_picker.html
Normal 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 %}
|
@ -2,6 +2,7 @@
|
|||||||
{% from "components/options_input.html" import OptionsInput %}
|
{% from "components/options_input.html" import OptionsInput %}
|
||||||
{% from "components/date_input.html" import DateInput %}
|
{% from "components/date_input.html" import DateInput %}
|
||||||
{% from "components/phone_input.html" import PhoneInput %}
|
{% from "components/phone_input.html" import PhoneInput %}
|
||||||
|
{% from "components/date_picker.html" import DatePicker %}
|
||||||
|
|
||||||
<form method="POST" action='{{ form_action }}'>
|
<form method="POST" action='{{ form_action }}'>
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
@ -23,14 +24,16 @@
|
|||||||
{{ OptionsInput(form.service_branch) }}
|
{{ OptionsInput(form.service_branch) }}
|
||||||
{{ OptionsInput(form.citizenship) }}
|
{{ OptionsInput(form.citizenship) }}
|
||||||
{{ OptionsInput(form.designation) }}
|
{{ OptionsInput(form.designation) }}
|
||||||
{{
|
|
||||||
DateInput(
|
|
||||||
form.date_latest_training,
|
<div class="usa-input">
|
||||||
tooltip=("fragments.edit_user_form.date_last_training_tooltip" | translate),
|
<label>
|
||||||
placeholder="MM / DD / YYYY",
|
<div class="usa-input__title">{{ form.date_latest_training.label }}</div>
|
||||||
validation="date"
|
</label>
|
||||||
)
|
|
||||||
}}
|
<span class="usa-form-hint">{{ "forms.date_hint" | translate }}</span>
|
||||||
|
{{ DatePicker(form.date_latest_training, mindate=mindate, maxdate=maxdate) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ footer:
|
|||||||
browser_support: JEDI Cloud supported on these web browsers
|
browser_support: JEDI Cloud supported on these web browsers
|
||||||
jedi_help_link_text: Questions? Contact your CCPO Representative
|
jedi_help_link_text: Questions? Contact your CCPO Representative
|
||||||
forms:
|
forms:
|
||||||
|
date_hint: "For example: 11 28 1986"
|
||||||
ccpo_review:
|
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_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
|
comment_label: Instructions or comments
|
||||||
|
Loading…
x
Reference in New Issue
Block a user