Skip to content

Commit

Permalink
Honor date-only dates using the date picker (CFM-107) (#49)
Browse files Browse the repository at this point in the history
* Align datepicker colors with Registry theme (CFM-107)

* Honor date-only dates using the date picker (CFM-107)

* Allow date fields to be specified as type 'date', 'dateTime', and 'endDateTime'. Ensure that the default times for endDateTime fields are set to '23:59:59'. (CFM-107)

* Add the 'format' attribute to date/time fields to allow browsers to do simple client-side validation. Titles also included for accessibility. (CFM-107)

* Extend the min and max dates to provide a wide range of years in the datepicker widget. (CFM-107)

* Add aria attributes and screenreader features to time picker. (CFM-107)

* Refactor FieldHelper to include a dateControl() wrapper function. Set the default year ranges for the datepicker. (CFM-107)

* Minor update to comments. (CFM-107)

* Clean up code conventions. (CFM-107)
  • Loading branch information
arlen authored Sep 13, 2022
1 parent 3e1b649 commit 9d7d5df
Show file tree
Hide file tree
Showing 13 changed files with 248 additions and 113 deletions.
9 changes: 9 additions & 0 deletions app/resources/locales/en_US/field.po
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ msgstr "Created"
msgid "datepicker.am"
msgstr "AM"

msgid "datepicker.chooseTime"
msgstr "Choose time"

msgid "datepicker.enterDate"
msgstr "Enter a date as YYYY-MM-DD"

msgid "datepicker.enterDateTime"
msgstr "Enter a date as YYYY-MM-DD HH:MM:SS"

msgid "datepicker.hour"
msgstr "Hour"

Expand Down
37 changes: 37 additions & 0 deletions app/src/Lib/Enum/DateTypeEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
/**
* COmanage Registry Date Type Enum, Standard Variant
*
* Portions licensed to the University Corporation for Advanced Internet
* Development, Inc. ("UCAID") under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* UCAID licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @link https://www.internet2.edu/comanage COmanage Project
* @package registry
* @since COmanage Registry v5.0.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/

declare(strict_types = 1);

namespace App\Lib\Enum;

class DateTypeEnum extends StandardEnum {
const Standard = 'standard';
const DateOnly = 'dateonly';
const FromTime = 'fromtime';
const ThroughTime = 'throughtime';
}
187 changes: 127 additions & 60 deletions app/src/View/Helper/FieldHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@

namespace App\View\Helper;

use \Cake\Utility\Inflector;
use Cake\I18n\FrozenTime;
use Cake\Utility\Inflector;
use Cake\View\Helper;
use App\Lib\Enum\DateTypeEnum;

class FieldHelper extends Helper {
public $helpers = ['Form', 'Html', 'Url', 'Alert'];
Expand All @@ -56,7 +58,7 @@ class FieldHelper extends Helper {
* @return string HTML for banner
*/

public function banner(string $info) {
public function banner(string $info): string {
return '<li class="alert-banner">' .
$this->Alert->alert($info, 'warning')
. '</li>';
Expand All @@ -66,25 +68,29 @@ public function banner(string $info) {
* Emit a form control.
*
* @since COmanage Registry v5.0.0
* @param string $fieldName Form field
* @param array $options FormHelper control options
* @param string $labelText Label text (fieldName language key used by default)
* @param array $config Custom FormHelper configuration options
* @param string $fieldName Form field
* @param array $options FormHelper control options
* @param string $labelText Label text (fieldName language key used by default)
* @param array $config Custom FormHelper configuration options
* @param string $ctrlCode Control code passed in from wrapper functions
* @param string $cssClass Start li css class passed in from wrapper functions
* @return string HTML for control
*/

public function control(string $fieldName,
array $options=[],
string $labelText=null,
array $config=[]){
array $config=[],
string $ctrlCode=null,
string $cssClass=''): string {
$coptions = $options;
$coptions['label'] = false;
$coptions['readonly'] = !$this->editable || (isset($options['readonly']) && $options['readonly']);
// Selects, Checkboxes, and Radio Buttons use "disabled"
$coptions['disabled'] = $coptions['readonly'];

// Generate HTML for the control itself
$liClass = "";
// Specify a class on the <li> form control wrapper
$liClass = $cssClass;

// Remove prefix from field value
if(isset($config['prefix'], $this->getView()->get('vv_obj')->$fieldName)) {
Expand All @@ -94,50 +100,20 @@ public function control(string $fieldName,
$vv_obj->$fieldName = $fieldValueTemp;
$this->getView()->set('vv_obj', $vv_obj);
}

// Handle datetime controls specially
if($fieldName == 'valid_from' || $fieldName == 'valid_through') {
// Append the timezone to the label
$label = __d('field', $fieldName.".tz", [$this->_View->get('vv_tz')]);

// A datetime field will be rendered as plain text input with adjacent date and time pickers
// that will interact with the field value. Allowing direct access to the input field is for
// accessibility purposes.
$coptions['class'] = 'form-control datepicker';
$coptions['placeholder'] = 'YYYY-MM-DD HH:MM:SS'; // TODO: test for date-only inputs and send only the date
$coptions['id'] = str_replace("_", "-", $fieldName);

$entity = $this->getView()->get('vv_obj');

$pickerDate = '';
if(!empty($entity->$fieldName)) {
// Adjust the time back to the user's timezone
$coptions['value'] = $entity->$fieldName->i18nFormat("yyyy-MM-dd HH:mm:ss", $this->getView()->get('vv_tz'));
$pickerDate = $entity->$fieldName->i18nFormat("yyyy-MM-dd", $this->getView()->get('vv_tz'));
}

$date_args = [
'fieldName' => $fieldName,
'pickerDate' => $pickerDate
];
// Create a text field to hold our value.
$controlCode = $this->Form->text($fieldName, $coptions)
. $this->getView()->element('datePicker', $date_args);

$liClass = "fields-datepicker";
} else {
if($fieldName != 'status'
&& !isset($options['empty'])
&& (!isset($options['suppressBlank']) || !$options['suppressBlank'])) {
// Cause any select (except status) to render with a blank option, even
// if the field is required. This makes it clear when a value need to be set.
// Note this will be ignored for non-select controls.
$coptions['empty'] = true;
}

$controlCode = $this->Form->control($fieldName, $coptions);

if($fieldName != 'status'
&& !isset($options['empty'])
&& (!isset($options['suppressBlank']) || !$options['suppressBlank'])) {
// Cause any select (except status) to render with a blank option, even
// if the field is required. This makes it clear when a value need to be set.
// Note this will be ignored for non-select controls.
$coptions['empty'] = true;
}

// Generate the form control or pass along the markup generated in a wrapper function
$controlCode = empty($ctrlCode) ? $this->Form->control($fieldName, $coptions) : $ctrlCode;


// Required fields are usually determined by the model validator, but for
// related models the view (currently) has to pass the field as required in
// $options. For fields of the form model.0.field, if $options['required']
Expand All @@ -159,14 +135,98 @@ public function control(string $fieldName,
. $this->endLine();
}

/**
* Emit a date/time form control.
* This is a wrapper function for $this->control()
*
* @since COmanage Registry v5.0.0
* @param string $fieldName Form field
* @param string $dateType Standard, DateOnly, FromTime, ThroughTime
*
* @return string HTML for control
*/

public function dateControl(string $fieldName, string $dateType=DateTypeEnum::Standard): string {
// A datetime field will be rendered as a plain text input with adjacent date and time pickers
// that will interact with the field value. Allowing direct access to the input field is for
// accessibility purposes.

$pickerType = $dateType;
// Special-case the very common "valid_from" and "valid_through" fields so we won't need
// to specify their types in fields.inc.
if($fieldName == 'valid_from') {
$pickerType = DateTypeEnum::FromTime;
}
if($fieldName == 'valid_through') {
$pickerType = DateTypeEnum::ThroughTime;
}

// Append the timezone to the label -- TODO: see that the timezone gets output to the display
$label = __d('field', $fieldName.".tz", [$this->_View->get('vv_tz')]);

// Create the options array for the (text input) form control
$coptions = [];
$coptions['class'] = 'form-control datepicker';

if($pickerType == DateTypeEnum::DateOnly) {
$coptions['placeholder'] = 'YYYY-MM-DD';
$coptions['pattern'] = '\d{4}-\d{2}-\d{2}';
$coptions['title'] = __d('field', 'datepicker.enterDate');
} else {
$coptions['placeholder'] = 'YYYY-MM-DD HH:MM:SS';
$coptions['pattern'] = '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}';
$coptions['title'] = __d('field', 'datepicker.enterDateTime');
}
$coptions['id'] = str_replace("_", "-", $fieldName);

$entity = $this->getView()->get('vv_obj');

// Default the picker date to today
$now = FrozenTime::now();
$pickerDate = $now->i18nFormat('yyyy-MM-dd');

// Get the existing values, if present
if(!empty($entity->$fieldName)) {
// Adjust the time back to the user's timezone
if($pickerType == DateTypeEnum::DateOnly) {
$coptions['value'] = $entity->$fieldName->i18nFormat("yyyy-MM-dd", $this->getView()->get('vv_tz'));
} else {
$coptions['value'] = $entity->$fieldName->i18nFormat("yyyy-MM-dd HH:mm:ss", $this->getView()->get('vv_tz'));
}
$pickerDate = $entity->$fieldName->i18nFormat("yyyy-MM-dd", $this->getView()->get('vv_tz'));
}

// Set the date picker floor year value (-100 years)
$pickerDateFT = new FrozenTime($pickerDate);
$pickerDateFT = $pickerDateFT->subYears(100);
$pickerFloor = $pickerDateFT->i18nFormat("yyyy-MM-dd");

$date_picker_args = [
'fieldName' => $fieldName,
'pickerDate' => $pickerDate,
'pickerType' => $pickerType,
'pickerFloor' => $pickerFloor
];

// Create a text field to hold our value and call the datePicker
$controlCode = $this->Form->text($fieldName, $coptions)
. $this->getView()->element('datePicker', $date_picker_args);

// Specify a class on the <li> form control wrapper
$liClass = "fields-datepicker";

// Pass everything to the generic control() function
return $this->control($fieldName, $coptions, '', [], $controlCode, $liClass);
}

/**
* End a set of form controls.
*
* @since COmanage Registry v5.0.0
* @return string Control Set end HTML
*/

public function endControlSet() {
public function endControlSet(): string {
$this->modelName = null;

return "</ul>\n";
Expand All @@ -179,7 +239,7 @@ public function endControlSet() {
* @return string Line end HTML
*/

protected function endLine() {
protected function endLine(): string {
return "</li>\n";
}

Expand All @@ -191,7 +251,7 @@ protected function endLine() {
* @return string Form Info HTML
*/

protected function formInfoDiv(string $content) {
protected function formInfoDiv(string $content): string {
return '<div class="field-info">
' . $content . '
</div>';
Expand All @@ -206,7 +266,7 @@ protected function formInfoDiv(string $content) {
* @return string Form Info HTML
*/

protected function formInfoWithPrefixDiv(string $context, string $prefix) {
protected function formInfoWithPrefixDiv(string $context, string $prefix): string {
$div = '<div class="field-info">' . PHP_EOL
. '<div class="input-group mb-3">' . PHP_EOL
. '<div class="input-group-prepend">' . PHP_EOL
Expand All @@ -227,7 +287,7 @@ protected function formInfoWithPrefixDiv(string $context, string $prefix) {
* @return string Form Name HTML
*/

protected function formNameDiv(string $fieldName, string $labelText=null) {
protected function formNameDiv(string $fieldName, string $labelText=null): string {
$label = $labelText;
$desc = null;

Expand Down Expand Up @@ -310,7 +370,10 @@ protected function formNameDiv(string $fieldName, string $labelText=null) {
* @return string
*/

public function statusControl(string $fieldName, string $status, array $link=[], string $labelText=null): string {
public function statusControl(string $fieldName,
string $status,
array $link=[],
string $labelText=null): string {
$linkHtml = $status;

if($link) {
Expand Down Expand Up @@ -354,7 +417,11 @@ public function statusControl(string $fieldName, string $status, array $link=[],
* @return string
*/

public function startControlSet(string $modelName, string $action, bool $editable, array $reqFields, $entity=null) {
public function startControlSet(string $modelName,
string $action,
bool $editable,
array $reqFields,
$entity=null): string {
$this->editable = $editable;
$this->modelName = $modelName;
$this->reqFields = $reqFields;
Expand All @@ -371,7 +438,7 @@ public function startControlSet(string $modelName, string $action, bool $editabl
* @return string
*/

protected function startLine(string $class=null) {
protected function startLine(string $class=null): string {
$ret = '<li';

if($class) {
Expand All @@ -391,7 +458,7 @@ protected function startLine(string $class=null) {
* @return string
*/

public function submit(string $label) {
public function submit(string $label): string {
return '<li class="fields-submit">
<div class="field-name">
<span class="required">* ' . __d('field', 'required') . '</span>
Expand Down
4 changes: 2 additions & 2 deletions app/templates/ApiUsers/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ if($vv_action == 'add' || $vv_action == 'edit') {

print $this->Field->control('status', ['empty' => false]);

print $this->Field->control('valid_from');
print $this->Field->dateControl('valid_from');

print $this->Field->control('valid_through');
print $this->Field->dateControl('valid_through');

print $this->Field->control('remote_ip');

Expand Down
2 changes: 1 addition & 1 deletion app/templates/ExternalIdentities/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ if($vv_action == 'add' || $vv_action == 'edit') {
// XXX sync status?
print $this->Field->control('status', ['empty' => false]);

print $this->Field->control('date_of_birth');
print $this->Field->dateControl('date_of_birth', \App\Lib\Enum\DateTypeEnum::DateOnly);
}

// XXX This is a placeholder for canvas... maybe it should become a separate page
Expand Down
8 changes: 2 additions & 6 deletions app/templates/ExternalIdentityRoles/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,9 @@ if($vv_action == 'add' || $vv_action == 'edit') {

print $this->Field->control('manager_identifier', [], __d('field', 'manager'));

// XXX these need to render date pickers
// - we specifically have code in FieldHelper that checks for these two, but not date_of_birth
// - can FieldHelper introspect the date type rather than require a hard coded list of fields?
// though note valid_from/through uses special logic for 00:00:00 and 23:59:59
print $this->Field->control('valid_from');
print $this->Field->dateControl('valid_from');

print $this->Field->control('valid_through');
print $this->Field->dateControl('valid_through');
}

// XXX This is a placeholder for canvas... maybe it should become a separate page
Expand Down
Loading

0 comments on commit 9d7d5df

Please sign in to comment.