From 948d69364aae79b57d6b2f3a6de06f741716eb0a Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 4 May 2015 10:08:49 -0700 Subject: [PATCH] Manage date control enabled state as part of DateControlValue Summary: Ref T8024. Allow `DateControlValue` to manage enabled/disabled state, so we can eventually delete the copy of this logic in `DateControl`. Test Plan: - Used Calendar ApplicationSearch queries to observe improved behaviors: - Error for invalid start date, if enabled. - Error for invalid end date, if enabled. - Error for invalid date range, if both enabled. - When submitting an invalid date (for example, with the time "Tea Time"), form retains invalid date verbatim instead of discarding information. - Created an event, using existing date controls to check that I didn't break anything. Reviewers: chad, lpriestley, btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T8024 Differential Revision: https://secure.phabricator.com/D12673 --- resources/celerity/map.php | 24 ++--- .../PhabricatorCalendarEventSearchEngine.php | 73 ++++++++++---- .../PhabricatorApplicationSearchEngine.php | 12 ++- .../form/control/AphrontFormDateControl.php | 19 +++- .../control/AphrontFormDateControlValue.php | 94 ++++++++++++++++++- webroot/rsrc/css/phui/phui-form-view.css | 12 +++ .../rsrc/js/core/behavior-fancy-datepicker.js | 9 +- 7 files changed, 194 insertions(+), 49 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 16029cb0f1..b8d2563f75 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'd445f9b8', + 'core.pkg.css' => 'ca3f6a60', 'core.pkg.js' => '3331b919', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '0a253fbe', @@ -132,7 +132,7 @@ return array( 'rsrc/css/phui/phui-document.css' => '94d5dcd8', 'rsrc/css/phui/phui-feed-story.css' => 'c9f3a0b5', 'rsrc/css/phui/phui-fontkit.css' => 'dd8ddf27', - 'rsrc/css/phui/phui-form-view.css' => 'b147d2ed', + 'rsrc/css/phui/phui-form-view.css' => '17eace76', 'rsrc/css/phui/phui-form.css' => 'f535f938', 'rsrc/css/phui/phui-header-view.css' => 'da4586b1', 'rsrc/css/phui/phui-icon.css' => 'bc766998', @@ -463,7 +463,7 @@ return array( 'rsrc/js/core/behavior-device.js' => 'a205cf28', 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '6d49590e', 'rsrc/js/core/behavior-error-log.js' => '6882e80a', - 'rsrc/js/core/behavior-fancy-datepicker.js' => 'c51ae228', + 'rsrc/js/core/behavior-fancy-datepicker.js' => '5c0f680f', 'rsrc/js/core/behavior-file-tree.js' => '88236f00', 'rsrc/js/core/behavior-form.js' => '5c54cbf3', 'rsrc/js/core/behavior-gesture.js' => '3ab51e2c', @@ -590,7 +590,7 @@ return array( 'javelin-behavior-doorkeeper-tag' => 'e5822781', 'javelin-behavior-durable-column' => '657c2b50', 'javelin-behavior-error-log' => '6882e80a', - 'javelin-behavior-fancy-datepicker' => 'c51ae228', + 'javelin-behavior-fancy-datepicker' => '5c0f680f', 'javelin-behavior-global-drag-and-drop' => 'c203e6ee', 'javelin-behavior-herald-rule-editor' => '7ebaeed3', 'javelin-behavior-high-security-warning' => 'a464fe03', @@ -789,7 +789,7 @@ return array( 'phui-font-icon-base-css' => '3dad2ae3', 'phui-fontkit-css' => 'dd8ddf27', 'phui-form-css' => 'f535f938', - 'phui-form-view-css' => 'b147d2ed', + 'phui-form-view-css' => '17eace76', 'phui-header-view-css' => 'da4586b1', 'phui-icon-view-css' => 'bc766998', 'phui-image-mask-css' => '5a8b09c8', @@ -1215,6 +1215,13 @@ return array( 'javelin-uri', 'javelin-routable', ), + '5c0f680f' => array( + 'javelin-behavior', + 'javelin-util', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-vector', + ), '5c54cbf3' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1768,13 +1775,6 @@ return array( 'javelin-mask', 'phabricator-drag-and-drop-file-upload', ), - 'c51ae228' => array( - 'javelin-behavior', - 'javelin-util', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-vector', - ), 'c90a04fc' => array( 'javelin-dom', 'javelin-dynval', diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index 95f84ee797..0e258eb50c 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -53,16 +53,8 @@ final class PhabricatorCalendarEventSearchEngine $query = id(new PhabricatorCalendarEventQuery()); $viewer = $this->requireViewer(); - $min_range = null; - $max_range = null; - - if ($saved->getParameter('rangeStart')) { - $min_range = $saved->getParameter('rangeStart'); - } - - if ($saved->getParameter('rangeEnd')) { - $max_range = $saved->getParameter('rangeEnd'); - } + $min_range = $this->getDateFrom($saved)->getEpoch(); + $max_range = $this->getDateTo($saved)->getEpoch(); if ($saved->getParameter('display') == 'month') { list($start_month, $start_year) = $this->getDisplayMonthAndYear($saved); @@ -130,8 +122,31 @@ final class PhabricatorCalendarEventSearchEngine AphrontFormView $form, PhabricatorSavedQuery $saved) { - $range_start = $saved->getParameter('rangeStart'); - $range_end = $saved->getParameter('rangeEnd'); + $range_start = $this->getDateFrom($saved); + $e_start = null; + + $range_end = $this->getDateTo($saved); + $e_end = null; + + if (!$range_start->isValid()) { + $this->addError(pht('Start date is not valid.')); + $e_start = pht('Invalid'); + } + + if (!$range_end->isValid()) { + $this->addError(pht('End date is not valid.')); + $e_end = pht('Invalid'); + } + + $start_epoch = $range_start->getEpoch(); + $end_epoch = $range_end->getEpoch(); + + if ($start_epoch && $end_epoch && ($start_epoch > $end_epoch)) { + $this->addError(pht('End date must be after start date.')); + $e_start = pht('Invalid'); + $e_end = pht('Invalid'); + } + $upcoming = $saved->getParameter('upcoming'); $is_cancelled = $saved->getParameter('isCancelled', 'active'); $display = $saved->getParameter('display', 'month'); @@ -167,14 +182,14 @@ final class PhabricatorCalendarEventSearchEngine ->setLabel(pht('Occurs After')) ->setUser($this->requireViewer()) ->setName('rangeStart') - ->setAllowNull(true) + ->setError($e_start) ->setValue($range_start)) ->appendChild( id(new AphrontFormDateControl()) ->setLabel(pht('Occurs Before')) ->setUser($this->requireViewer()) ->setName('rangeEnd') - ->setAllowNull(true) + ->setError($e_end) ->setValue($range_end)) ->appendChild( id(new AphrontFormCheckboxControl()) @@ -391,9 +406,9 @@ final class PhabricatorCalendarEventSearchEngine $start_year = $this->calendarYear; $start_month = $this->calendarMonth; } else { - $epoch = $query->getParameter('rangeStart'); + $epoch = $this->getDateFrom($query)->getEpoch(); if (!$epoch) { - $epoch = $query->getParameter('rangeEnd'); + $epoch = $this->getDateTo($query)->getEpoch(); if (!$epoch) { $epoch = time(); } @@ -431,4 +446,30 @@ final class PhabricatorCalendarEventSearchEngine return $saved->getParameter('limit', 1000); } + private function getDateFrom(PhabricatorSavedQuery $saved) { + return $this->getDate($saved, 'rangeStart'); + } + + private function getDateTo(PhabricatorSavedQuery $saved) { + return $this->getDate($saved, 'rangeEnd'); + } + + private function getDate(PhabricatorSavedQuery $saved, $key) { + $viewer = $this->requireViewer(); + + $wild = $saved->getParameter($key); + if ($wild) { + $value = AphrontFormDateControlValue::newFromWild($viewer, $wild); + } else { + $value = AphrontFormDateControlValue::newFromEpoch( + $viewer, + PhabricatorTime::getNow()); + $value->setEnabled(false); + } + + $value->setOptional(true); + + return $value; + } + } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index d260f03aeb..a0e7245a76 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -566,11 +566,13 @@ abstract class PhabricatorApplicationSearchEngine { AphrontRequest $request, $key) { - return id(new AphrontFormDateControl()) - ->setUser($this->requireViewer()) - ->setName($key) - ->setAllowNull(true) - ->readValueFromRequest($request); + $value = AphrontFormDateControlValue::newFromRequest($request, $key); + + if ($value->isEmpty()) { + return null; + } + + return $value->getDictionary(); } protected function readBoolFromRequest( diff --git a/src/view/form/control/AphrontFormDateControl.php b/src/view/form/control/AphrontFormDateControl.php index 10c53aa679..ed79392fbb 100644 --- a/src/view/form/control/AphrontFormDateControl.php +++ b/src/view/form/control/AphrontFormDateControl.php @@ -11,6 +11,7 @@ final class AphrontFormDateControl extends AphrontFormControl { private $valueTime; private $allowNull; private $continueOnInvalidDate = false; + private $isDisabled; public function setAllowNull($allow_null) { $this->allowNull = $allow_null; @@ -91,6 +92,8 @@ final class AphrontFormDateControl extends AphrontFormControl { $this->valueMonth = $epoch->getValueMonth(); $this->valueDay = $epoch->getValueDay(); $this->valueTime = $epoch->getValueTime(); + $this->allowNull = $epoch->getOptional(); + $this->isDisabled = $epoch->isDisabled(); return parent::setValue($epoch->getEpoch()); } @@ -183,6 +186,10 @@ final class AphrontFormDateControl extends AphrontFormControl { } } + if ($this->isDisabled) { + $disabled = 'disabled'; + } + $min_year = $this->getMinYear(); $max_year = $this->getMaxYear(); @@ -227,7 +234,6 @@ final class AphrontFormDateControl extends AphrontFormControl { array( 'name' => $this->getDayInputName(), 'sigil' => 'day-input', - 'disabled' => $disabled, )); $months_sel = AphrontFormSelectControl::renderSelectTag( @@ -236,7 +242,6 @@ final class AphrontFormDateControl extends AphrontFormControl { array( 'name' => $this->getMonthInputName(), 'sigil' => 'month-input', - 'disabled' => $disabled, )); $years_sel = AphrontFormSelectControl::renderSelectTag( @@ -245,7 +250,6 @@ final class AphrontFormDateControl extends AphrontFormControl { array( 'name' => $this->getYearInputName(), 'sigil' => 'year-input', - 'disabled' => $disabled, )); $cicon = id(new PHUIIconView()) @@ -268,16 +272,21 @@ final class AphrontFormDateControl extends AphrontFormControl { 'value' => $this->getTimeInputValue(), 'type' => 'text', 'class' => 'aphront-form-date-time-input', - 'disabled' => $disabled, ), ''); Javelin::initBehavior('fancy-datepicker', array()); + $classes = array(); + $classes[] = 'aphront-form-date-container'; + if ($disabled) { + $classes[] = 'datepicker-disabled'; + } + return javelin_tag( 'div', array( - 'class' => 'aphront-form-date-container', + 'class' => implode(' ', $classes), 'sigil' => 'phabricator-date-control', 'meta' => array( 'disabled' => (bool)$disabled, diff --git a/src/view/form/control/AphrontFormDateControlValue.php b/src/view/form/control/AphrontFormDateControlValue.php index 0874475884..8a1e274135 100644 --- a/src/view/form/control/AphrontFormDateControlValue.php +++ b/src/view/form/control/AphrontFormDateControlValue.php @@ -6,9 +6,11 @@ final class AphrontFormDateControlValue extends Phobject { private $valueMonth; private $valueYear; private $valueTime; + private $valueEnabled; private $viewer; private $zone; + private $optional; public function getValueDay() { return $this->valueDay; @@ -27,27 +29,70 @@ final class AphrontFormDateControlValue extends Phobject { } public function isValid() { + if ($this->isDisabled()) { + return true; + } return ($this->getEpoch() !== null); } + public function isEmpty() { + if ($this->valueDay) { + return false; + } + + if ($this->valueMonth) { + return false; + } + + if ($this->valueYear) { + return false; + } + + if ($this->valueTime) { + return false; + } + + return true; + } + + public function isDisabled() { + return ($this->optional && !$this->valueEnabled); + } + + public function setEnabled($enabled) { + $this->valueEnabled = $enabled; + return $this; + } + + public function setOptional($optional) { + $this->optional = $optional; + return $this; + } + + public function getOptional() { + return $this->optional; + } + public static function newFromParts( PhabricatorUser $viewer, $year, $month, $day, - $time = '12:00 AM') { + $time = null, + $enabled = true) { $value = new AphrontFormDateControlValue(); $value->viewer = $viewer; $value->valueYear = $year; $value->valueMonth = $month; $value->valueDay = $day; - $value->valueTime = $time; + $value->valueTime = coalesce($time, '12:00 AM'); + $value->valueEnabled = $enabled; return $value; } - public static function newFromRequest($request, $key) { + public static function newFromRequest(AphrontRequest $request, $key) { $value = new AphrontFormDateControlValue(); $value->viewer = $request->getViewer(); @@ -55,6 +100,7 @@ final class AphrontFormDateControlValue extends Phobject { $value->valueMonth = $request->getInt($key.'_m'); $value->valueYear = $request->getInt($key.'_y'); $value->valueTime = $request->getStr($key.'_t'); + $value->valueEnabled = $request->getStr($key.'_e'); return $value; } @@ -73,6 +119,44 @@ final class AphrontFormDateControlValue extends Phobject { return $value; } + public static function newFromDictionary( + PhabricatorUser $viewer, + array $dictionary) { + $value = new AphrontFormDateControlValue(); + $value->viewer = $viewer; + + $value->valueYear = idx($dictionary, 'y'); + $value->valueMonth = idx($dictionary, 'm'); + $value->valueDay = idx($dictionary, 'd'); + $value->valueTime = idx($dictionary, 't'); + $value->valueEnabled = idx($dictionary, 'e'); + + return $value; + } + + public static function newFromWild(PhabricatorUser $viewer, $wild) { + if (is_array($wild)) { + return self::newFromDictionary($viewer, $wild); + } else if (is_numeric($wild)) { + return self::newFromEpoch($viewer, $wild); + } else { + throw new Exception( + pht( + 'Unable to construct a date value from value of type "%s".', + gettype($wild))); + } + } + + public function getDictionary() { + return array( + 'y' => $this->valueYear, + 'm' => $this->valueMonth, + 'd' => $this->valueDay, + 't' => $this->valueTime, + 'e' => $this->valueEnabled, + ); + } + private function formatTime($epoch, $format) { return phabricator_format_local_time( $epoch, @@ -81,6 +165,10 @@ final class AphrontFormDateControlValue extends Phobject { } public function getEpoch() { + if ($this->isDisabled()) { + return null; + } + $year = $this->valueYear; $month = $this->valueMonth; $day = $this->valueDay; diff --git a/webroot/rsrc/css/phui/phui-form-view.css b/webroot/rsrc/css/phui/phui-form-view.css index 09d2762ca0..02c7e55bb8 100644 --- a/webroot/rsrc/css/phui/phui-form-view.css +++ b/webroot/rsrc/css/phui/phui-form-view.css @@ -446,6 +446,18 @@ table.aphront-form-control-checkbox-layout th { border-radius: 3px; } +/* When the activation checkbox for the control is toggled off, visually +disable the individual controls. We don't actually use the "disabled" property +because we still want the values to submit. This is just a visual hint that +the controls won't be used. The controls themselves are still live, work +properly, and submit values. */ +.datepicker-disabled select, +.datepicker-disabled .calendar-button, +.datepicker-disabled input[type="text"] { + opacity: 0.5; +} + + .login-to-comment { margin: 12px; } diff --git a/webroot/rsrc/js/core/behavior-fancy-datepicker.js b/webroot/rsrc/js/core/behavior-fancy-datepicker.js index 3168eb0a62..f2d092faee 100644 --- a/webroot/rsrc/js/core/behavior-fancy-datepicker.js +++ b/webroot/rsrc/js/core/behavior-fancy-datepicker.js @@ -109,15 +109,8 @@ JX.behavior('fancy-datepicker', function() { }; var redraw_inputs = function() { - var inputs = get_inputs(); var disabled = JX.Stratcom.getData(root).disabled; - for (var k in inputs) { - if (disabled) { - inputs[k].setAttribute('disabled', 'disabled'); - } else { - inputs[k].removeAttribute('disabled'); - } - } + JX.DOM.alterClass(root, 'datepicker-disabled', disabled); var box = JX.DOM.scry(root, 'input', 'calendar-enable'); if (box.length) {