diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index 1e161626e..9be5585e0 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -126,7 +126,15 @@ public function add() { $this->set('vv_subtitle', $subtitle); // Let the view render - $this->render('/Standard/add-edit-view'); + if(in_array($this->name, [ + 'Cous', 'ApiUsers' + ]) + ) { + $this->render('/Standard/add-edit-view-new'); + } else { + // Let the view render + $this->render('/Standard/add-edit-view'); + } } /** @@ -419,9 +427,16 @@ public function edit(string $id) { $this->set('vv_title', $title); $this->set('vv_supertitle', $supertitle); $this->set('vv_subtitle', $subtitle); - - // Let the view render - $this->render('/Standard/add-edit-view'); + + if(in_array($this->name, [ + 'Cous' + ]) + ) { + $this->render('/Standard/add-edit-view-new'); + } else { + // Let the view render + $this->render('/Standard/add-edit-view'); + } } /** @@ -868,6 +883,14 @@ public function view($id = null) { $this->set('vv_subtitle', $subtitle); // Let the view render - $this->render('/Standard/add-edit-view'); + if(in_array($this->name, [ + 'Cous', 'ApiUsers' + ]) + ) { + $this->render('/Standard/add-edit-view-new'); + } else { + // Let the view render + $this->render('/Standard/add-edit-view'); + } } } \ No newline at end of file diff --git a/app/src/View/Helper/FieeldHelper.php b/app/src/View/Helper/FieeldHelper.php new file mode 100644 index 000000000..6168993a9 --- /dev/null +++ b/app/src/View/Helper/FieeldHelper.php @@ -0,0 +1,376 @@ +reqFields = $this->getView()->get('vv_required_fields'); + $this->modelName = $this->getView()->getName(); + $this->action = $this->getView()->get('vv_action'); + $this->editable = \in_array($this->action, ['add', 'edit']); + $this->pluginName = $this->getView()->getPlugin(); + $this->entity = $this->getView()->get('vv_obj'); + } + + + /** + * Emit a date/time form control. + * This is a wrapper function for $this->control() + * + * @param string $fieldName Form field + * @param string $dateType Standard, DateOnly, FromTime, ThroughTime + * @param array|null $queryParams Request Query parameters used by the filtering Blocks to get the date values + * @param string|null $label + * + * @return array HTML for control + * @since COmanage Registry v5.0.0 + */ + + public function dateField(string $fieldName, + string $dateType=DateTypeEnum::Standard, + array $queryParams=null, + string $label=null): array + { + // Initialize + $dateFormat = $dateType === DateTypeEnum::DateOnly ? 'yyyy-MM-dd' : 'yyyy-MM-dd HH:mm:ss'; + $dateTitle = $dateType === DateTypeEnum::DateOnly ? 'datepicker.enterDate' : 'datepicker.enterDateTime'; + $datePattern = $dateType === DateTypeEnum::DateOnly ? '\d{4}-\d{2}-\d{2}' : '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'; + $date_object = null; + + if(isset($queryParams)) { + if(!empty($queryParams[$fieldName])) { + $date_object = FrozenTime::parse($queryParams[$fieldName]); + } + } else { + // This is an entity view. We are getting the data from the object + $entity = $this->getView()->get('vv_obj'); + $date_object = $entity->$fieldName; + } + + // Create the options array for the (text input) form control + $coptions = []; + + // 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. + + // ACTION VIEW + if($this->action == 'view') { + // return the date as plaintext + $element = $this->notSetElement(); + if ($date_object !== null) { + // Adjust the time back to the user's timezone + $element = ''; + } + + // Return this to the generic control() function + return $element; + } + + // Special-case the very common "valid_from" and "valid_through" fields, so we won't need + // to specify their types in fields.inc. + $pickerType = match ($fieldName) { + 'valid_from' => DateTypeEnum::FromTime, + 'valid_through' => DateTypeEnum::ThroughTime, + default => $dateType + }; + + // Append the timezone to the label + $coptions['class'] = 'form-control datepicker'; + $coptions['placeholder'] = $dateFormat; + if(!empty($label)) { + $coptions['label'] = $label; + } + $coptions['pattern'] = $datePattern; + $coptions['title'] = __d('field', $dateTitle); + + $coptions['id'] = str_replace('_', '-', $fieldName); + + + // Default the picker date to today + $now = FrozenTime::now(); + $pickerDate = $now->i18nFormat($dateFormat); + + // Get the existing values, if present + if($date_object !== null) { + // Adjust the time back to the user's timezone + $coptions['value'] = $date_object->i18nFormat($dateFormat); + $pickerDate = $date_object->i18nFormat($dateFormat); + } + + // Set the date picker floor year value (-100 years)() + $pickerDateFT = new FrozenTime($pickerDate); + $pickerDateFT = $pickerDateFT->subYears(100); + $pickerFloor = $pickerDateFT->i18nFormat($dateFormat); + + $date_picker_args = [ + 'fieldName' => $fieldName, + 'pickerDate' => $pickerDate, + 'pickerType' => $pickerType, + 'pickerFloor' => $pickerFloor, + ]; + + // Create a text field to hold our value and call the datePicker + return $this->Form->text($fieldName, $coptions) . $this->getView()->element('datePicker', $date_picker_args); + } + + /** + * Create the actual Form element + * + * @param string $fieldName Form field + * @param array|null $options FormHelper control options + * By setting the options you are requesting a select field + * @param string|null $labelText + * @param string $prefix Field prefix - used for API Usernames + * @param string|null $fieldType + * + * @return string HTML for control + * @since COmanage Registry v5.0.0 + */ + public function formField(string $fieldName, + array $options = null, + string $labelText = null, + string $prefix = '', // api user + string $fieldType = null): string { + $fieldArgs = $options ?? []; + $fieldArgs['label'] = $options['label'] ?? false; + $fieldArgs['readonly'] = !$this->editable + || (isset($options['readonly']) && $options['readonly']) + || ($fieldName == 'plugin' && $this->action == 'edit'); + + // Selects, Checkboxes, and Radio Buttons use "disabled" + $fieldArgs['disabled'] = $fieldArgs['readonly']; + + // Get the field type from the map of fields (e.g. 'boolean', 'string', 'timestamp') + $fieldType = $fieldType ?? $this->getReqField($fieldName); + + // Remove prefix from field value + if(!empty($prefix) && !empty($this->getEntity()->$fieldName)) { + $vv_obj = $this->getView()->get('vv_obj'); + $fieldValue = $vv_obj->$fieldName; + $fieldValueTemp = str_replace($prefix, '', $fieldValue); + $vv_obj->$fieldName = $fieldValueTemp; + $this->getView()->set('vv_obj', $vv_obj); + } + + 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 needs to be set. + // Note this will be ignored for non-select controls. + $fieldArgs['empty'] = true; + } + + // A boolean field is a checkbox. Set the label and class to improve rendering + // and accessibility. + if($fieldType == 'boolean') { + $fieldArgs['label'] = $labelText; + $fieldArgs['class'] = 'form-check-input'; + } + + // Generate the form control or pass along the markup generated in a wrapper function + return $this->Form->control($fieldName, $fieldArgs); + + } + + + /** + * We autogenerate field labels and descriptions from the field name. + * + * @param string|null $pluginDomain + * @param string $modelName + * @param string $fieldName + * + * @return array + */ + public function calculateLabelAndDescription(?string $pluginDomain, string $modelName, string $fieldName): array + { + $desc = null; + $label = null; + + // We try to automagically determine if a description for the field exists by + // looking for the corresponding .desc language translation. + // We autogenerate field labels and descriptions from the field name. + + // We loop over the field generation logic twice, first for a plugin + // context (if set) and then generally (if no plugin localization was found). + + // We use $core as the variable for this loop, so the rest of the code + // is easier to read (!$core = plugin) + for($core = 0;$core < 2;$core++) { + if(!$core && empty($this->pluginName)) { + // No plugin set, go to the core field checks + continue; + } + + // Is there a model specific key? For plugins, this will be in field.Model.Field + + $key = (!$core ? 'field.' : '') . "$modelName.$fieldName"; + $label = __d(($core ? 'field' : $pluginDomain), $key); + + if($label === $key) { + // Model-specific label isn't found, try again for a general label + + $f = null; + + if(preg_match('/^(.*?)_id$/', $fieldName, $f)) { + // Map foreign keys (foo_id) to the controller label + $key = (!$core ? 'controller.' : '') . Inflector::camelize(Inflector::pluralize($f[1])); + $label = __d(($core ? 'controller' : $pluginDomain), $key, [1]); + + if($key !== $label) { + break; + } + } + + // Look up the key + $key = (!$core ? 'field.' : '') . $fieldName; + $label = __d(($core ? 'field' : $pluginDomain), $key); + + if($key !== $label) { + break; + } + } else { + // If we found a key, break the loop + break; + } + } + // We try to automagically determine if a description for the field exists by + // looking for the corresponding .desc language translation. + + for($core = 0;$core < 2;$core++) { + if(!$core && empty($this->pluginName)) { + // No plugin set, just go to the core field checks + continue; + } + + $key = (!$core ? 'field.' : '') . "$modelName.$fieldName.desc"; + $desc = __d(($core ? 'field' : $pluginDomain), $key); + + // If the description is the literal key we just generated, there is no description + if($desc === $key) { + $desc = null; + } else { + break; + } + } + + return [$label, $desc]; + } + + /** + * @return bool + */ + public function isEditable(): bool + { + return $this->editable; + } + + /** + * @return string|null + */ + public function pluginName(): ?string + { + return $this->pluginName; + } + + /** + * @return string|null + */ + public function getModelName(): ?string + { + return $this->modelName; + } + + /** + * @return object|null + */ + public function getEntity(): ?object + { + return $this->entity; + } + + /** + * @return array + */ + public function getReqFields(): array + { + return $this->reqFields; + } + + /** + * @param string $field + * + * @return string|null + */ + public function getReqField(string $field): ?string + { + return $this->reqFields[$field] ?? null; + } +} \ No newline at end of file diff --git a/app/templates/ApiUsers/fields.inc b/app/templates/ApiUsers/fields.inc index 2c3887e6e..007a2681b 100644 --- a/app/templates/ApiUsers/fields.inc +++ b/app/templates/ApiUsers/fields.inc @@ -28,16 +28,21 @@ // This view does not support read-only if($vv_action == 'add' || $vv_action == 'edit') { if($vv_cur_co->id == 1) { - print $this->Field->banner(__d('information', 'api.cmp')); + print $this->element('banner', __d('information', 'api.cmp')); } // AR-ApiUser-3 For namespacing purposes, API Users are named with a prefix consisting of the string "co_#.". - print $this->Field->control('username', prefix: 'co_' . $vv_cur_co->id . '.'); + print $this->element('form/listItem', [ + 'arguments' => [ + 'fieldName' => 'username', // string + 'prefix' => 'co_' . $vv_cur_co->id . '.' + ] + ]); // We link to the "Generate" button on edit only $generateLink = []; $labelIsTextOnly = false; - + if(!empty($vv_obj->id)) { $generateLink = [ 'url' => [ @@ -49,22 +54,48 @@ if($vv_action == 'add' || $vv_action == 'edit') { 'class' => 'provisionbutton nospin btn btn-primary btn-sm', 'confirm' => __d('operation', 'api.key.generate.confirm') ]; - + $labelIsTextOnly = true; } - - print $this->Field->statusControl('api_key', - !empty($vv_obj->api_key) ? __d('enumeration', 'SetBooleanEnum.1') : __d('enumeration', 'SetBooleanEnum.0'), - $generateLink, - labelIsTextOnly: $labelIsTextOnly); - - print $this->Field->control('status', ['empty' => false]); - - print $this->Field->dateControl('valid_from'); - - print $this->Field->dateControl('valid_through'); - - print $this->Field->control('remote_ip'); - - print $this->Field->control('privileged'); + + print $this->element('form/listItem', [ + 'arguments' => [ + 'fieldName' => 'api_key', + 'status' => !empty($vv_obj->api_key) ? __d('enumeration', 'SetBooleanEnum.1') : __d('enumeration', 'SetBooleanEnum.0'), + 'link' => $generateLink, + 'labelIsTextOnly' => $labelIsTextOnly + ] + ]); + + + print $this->element('form/listItem', [ + 'arguments' => [ + 'fieldName' => 'status', // select + ] + ]); + + + print $this->element('form/listItem', [ + 'arguments' => [ + 'fieldName' => 'valid_from', // timestamp + ] + ]); + + print $this->element('form/listItem', [ + 'arguments' => [ + 'fieldName' => 'valid_through', // timestamp + ] + ]); + + print $this->element('form/listItem', [ + 'arguments' => [ + 'fieldName' => 'remote_ip', // string + ] + ]); + + print $this->element('form/listItem', [ + 'arguments' => [ + 'fieldName' => 'privileged', // boolean + ] + ]); } diff --git a/app/templates/Cous/fields.inc b/app/templates/Cous/fields.inc index 89aa3fd11..bdce4df30 100644 --- a/app/templates/Cous/fields.inc +++ b/app/templates/Cous/fields.inc @@ -27,11 +27,20 @@ // This view does not support read-only if($vv_action == 'add' || $vv_action == 'edit') { - print $this->Field->control('name'); - - print $this->Field->control('description'); + print $this->element('form/listItem', [ + 'arguments' => ['fieldName' => 'name'] + ]); + print $this->element('form/listItem', [ + 'arguments' => ['fieldName' => 'description'] + ]); if(!empty($parents)) { - print $this->Field->control('parent_id', ['empty' => true], __d('field', 'parent_id')); + print $this->element('form/listItem', [ + 'arguments' => [ + 'fieldName' => 'parent_id', + 'options' => ['empty' => true], + 'labelText' => __d('field', 'parent_id') + ] + ]); } } diff --git a/app/templates/Standard/add-edit-view-new.php b/app/templates/Standard/add-edit-view-new.php new file mode 100644 index 000000000..d327ce81d --- /dev/null +++ b/app/templates/Standard/add-edit-view-new.php @@ -0,0 +1,227 @@ +template; +// $this->name = Models +$modelsName = $this->name; +// $tablename = models +// XXX backport to match? +$tableName = \Cake\Utility\Inflector::tableize(\Cake\Utility\Inflector::singularize($this->name)); + +// $vv_template_path will be set for plugins +$templatePath = $vv_template_path ?? ROOT . DS . "templates" . DS . $modelsName; + +// If you're looking to set a custom $vv_title, you might be able to use +// generateDisplayField() on the Table instead + +// Include subnavigation structures on add/edit/view pages +// XXX: if CFM-218 (Make fields.inc configuration only) is accepted, move the contents of fields-nav.inc into fields.inc +// When subnav exists, include on all Edit/View views and on Add views for items with a parent. +if($vv_action == 'edit' || $vv_action == 'view' || !empty($vv_bc_parent_obj) || !empty($vv_primary_link_id)) { + if(file_exists($templatePath . DS . "fields-nav.inc")) { + include($templatePath . DS . "fields-nav.inc"); + } +} + +if(file_exists($templatePath . DS . "fields-links.inc")) { + include($templatePath . DS . "fields-links.inc"); +} + +// $linkFilter is used for models that belong to a specific parent model (eg: co_id) +$linkFilter = []; + +if(!empty($vv_primary_link) && !empty($this->request->getQuery($vv_primary_link))) { + $linkFilter = [$vv_primary_link => $this->request->getQuery($vv_primary_link)]; +} + +// $flashArgs pass banner messages to the flash element container +$flashArgs = []; +if(!empty($banners)) { + // XXX this doesn't work yet because we don't include fields.inc until later + // either create a second file to include earlier, or use a function to emit + // the fields (which would be more consistent with how Views render...) + $flashArgs['vv_banners'] = $banners; +} + +// If subnavigation is present a supertitle and the subnavigation will be placed above +// the normal page title. The flash messages will be shown up there as well. +if(!empty($subnav)) { + // Include the $flashArgs for the subnavigation element + $subnav['flashArgs'] = $flashArgs; + if(!empty($topLinks) && ($modelsName == 'People' && $vv_action == 'edit')) { + // We are in Person canvas mode: pass along the top links for building the Actions menu. + $subnav['topLinks'] = $topLinks; + } + // Generate the subnavigation title and tabs + print $this->element('subnavigation', $subnav); +} +?> + +