From 7b59fd50d315677d1abdb6357b9beae205910bf3 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Mon, 18 Nov 2024 22:36:54 +0200 Subject: [PATCH] cfm-31_people-picker (#250) * Sponsors+Managers to people picker * autocomplete default label enable hiding * Internationalize the fallback element text * Fix unable to update picker field * input property value to have the id on update and not only the full name * Fix method signature --- .../Model/Table/EnrollmentAttributesTable.php | 4 ++ .../CoreEnroller/templates/element/field.php | 11 +++- .../templates/element/spa-field.php | 61 +++++++++++++++++++ app/resources/locales/en_US/field.po | 3 + app/src/View/Helper/FieldHelper.php | 39 ++++++++++++ .../element/form/elementFallback.php | 32 ++++++++++ app/templates/element/peopleAutocomplete.php | 5 +- app/webroot/css/co-base.css | 7 +++ .../autocomplete/cm-autocomplete-people.js | 36 +++++++---- 9 files changed, 180 insertions(+), 18 deletions(-) create mode 100644 app/plugins/CoreEnroller/templates/element/spa-field.php create mode 100644 app/templates/element/form/elementFallback.php diff --git a/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php b/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php index 3ca48ba25..ab46c269c 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php @@ -165,6 +165,10 @@ public function initialize(array $config): void { 'type' => 'auxiliary', 'model' => 'CoSettings' ], + 'types' => [ + 'type' => 'auxiliary', + 'model' => 'Types' + ], ]); $this->setLayout([ 'index' => 'iframe', diff --git a/app/plugins/CoreEnroller/templates/element/field.php b/app/plugins/CoreEnroller/templates/element/field.php index 68e4be12b..b5cd48d5a 100644 --- a/app/plugins/CoreEnroller/templates/element/field.php +++ b/app/plugins/CoreEnroller/templates/element/field.php @@ -112,7 +112,12 @@ // HIDDEN Field // We print directly, we do not delegate to the element for further processing // In case this is a hidden field, we need to get only the value - $attr->hidden && $hidden =>$this->Form->hidden($formArguments['fieldName'], ['value' => $options['default']]), - // Default use case - default => $this->element('form/listItem', ['arguments' => $formArguments]) + $attr->hidden && $hidden => $this->Form->hidden($formArguments['fieldName'], ['value' => $options['default']]), + // For the case of xxx_person_id fields, we will render the People a Picker element. + str_ends_with($attr->attribute, 'person_id') => $this->element('CoreEnroller.spa-field', [ + 'vueElementName' => 'peopleAutocomplete', + 'formArguments' => $formArguments + ]), +// Default use case + default => $this->element('form/listItem', ['arguments' => $formArguments]) }; diff --git a/app/plugins/CoreEnroller/templates/element/spa-field.php b/app/plugins/CoreEnroller/templates/element/spa-field.php new file mode 100644 index 000000000..d70069f92 --- /dev/null +++ b/app/plugins/CoreEnroller/templates/element/spa-field.php @@ -0,0 +1,61 @@ + + +
  • +
    +
    +
    + + + element('form/requiredSpan') ?> + +
    + +
    + +
    + +
    + Field->constructSPAField( + // The Default field will be used to harvest the attributes + element: $this->Field->formField(...$formArguments), + // Vue/JS element + vueElementName: $vueElementName + ) ?> +
    +
    +
  • diff --git a/app/resources/locales/en_US/field.po b/app/resources/locales/en_US/field.po index 4c52116c4..6946136c6 100644 --- a/app/resources/locales/en_US/field.po +++ b/app/resources/locales/en_US/field.po @@ -234,6 +234,9 @@ msgstr "IP Address" msgid "required" msgstr "Required" +msgid "element_fallback" +msgstr "Element ID not provided" + msgid "role_key" msgstr "Role Key" diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index 8fcf3ad67..90d6c0de8 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -73,6 +73,7 @@ class FieldHelper extends Helper { * @param array $config The configuration settings provided to this helper. * * @return void + * @since COmanage Registry v5.0.0 */ public function initialize(array $config): void { @@ -94,6 +95,7 @@ public function initialize(array $config): void * @param string $fieldName * * @return array + * @since COmanage Registry v5.0.0 */ public function calculateLabelAndDescription(string $fieldName): array { @@ -180,6 +182,7 @@ public function calculateLabelAndDescription(string $fieldName): array * Calculate the list of classes for the li element * * @return string + * @since COmanage Registry v5.0.0 */ public function calculateLiClasses(): string { @@ -219,6 +222,42 @@ public function calculateLiClasses(): string return $classes; } + /** + * Construct the SPA field element + * + * @param string $element HTML element created with the CAKEPHP HTML Helper + * @param string $vueElementName The name of the JavaScript module + * + * @return string + * @since COmanage Registry v5.0.0 + */ + public function constructSPAField(string $element, string $vueElementName): string { + // Parse the ID attribute + $regexId = '/id="(.*?)"/m'; + preg_match_all($regexId, $element, $matchesId, PREG_SET_ORDER, 0); + + // Parse the Name attribute + $regexName = '/name="(.*?)"/m'; + preg_match_all($regexName, $element, $matchesName, PREG_SET_ORDER, 0); + + // Parse the Class attribute + $regexClass = '/class="(.*?)"/m'; + preg_match_all($regexClass, $element, $matchesClass, PREG_SET_ORDER, 0); + if(!empty($matchesId[0][1]) && !empty($matchesName[0][1])) { + return $this->getView()->element($vueElementName, [ + 'htmlId' => $matchesId[0][1], + 'fieldName' => $matchesName[0][1], + 'containerClasses' => $matchesClass[0][1], + 'type' => 'field', + // we want the label to be an empty string to hide the default label introduced by the module. + 'label' => '' + ]); + } + + // Fallback to an error element + return $this->getView()->element('elementFallback'); + } + /** * Emit a date/time form control. * This is a wrapper function for $this->control() diff --git a/app/templates/element/form/elementFallback.php b/app/templates/element/form/elementFallback.php new file mode 100644 index 000000000..feb181049 --- /dev/null +++ b/app/templates/element/form/elementFallback.php @@ -0,0 +1,32 @@ + + + diff --git a/app/templates/element/peopleAutocomplete.php b/app/templates/element/peopleAutocomplete.php index 7787bbf36..93666844c 100644 --- a/app/templates/element/peopleAutocomplete.php +++ b/app/templates/element/peopleAutocomplete.php @@ -33,6 +33,7 @@ $htmlId = $htmlId ?? 'cmPersonPickerId'; $actionUrl = $actionUrl ?? []; // the url of the page to launch on select for a stand-alone picker $viewConfigParameters = $viewConfigParameters ?? []; + $containerClasses = $containerClasses ?? 'cm-autocomplete-container'; // Get the CSRF Token in JavaScript $token = $this->request->getAttribute('csrfToken'); @@ -132,7 +133,7 @@ } // Mount the component and provide a global reference for this app instance. - window. = app.mount("#-container"); + window. = app.mount("#-container"); -
    +
    diff --git a/app/webroot/css/co-base.css b/app/webroot/css/co-base.css index b172fc3fe..ba62fcd4f 100644 --- a/app/webroot/css/co-base.css +++ b/app/webroot/css/co-base.css @@ -2266,6 +2266,13 @@ td .alert { bottom: 2px; margin-right: -26px; } +.co-loading-mini-container.over-input { + display: none; + position: absolute; + right: 0.5em; + bottom: 0.7em; + z-index: 100; +} #co-loading span, #co-loading-redirect span, .co-loading-mini span { diff --git a/app/webroot/js/comanage/components/autocomplete/cm-autocomplete-people.js b/app/webroot/js/comanage/components/autocomplete/cm-autocomplete-people.js index 0bc1305a5..e1a2272a0 100644 --- a/app/webroot/js/comanage/components/autocomplete/cm-autocomplete-people.js +++ b/app/webroot/js/comanage/components/autocomplete/cm-autocomplete-people.js @@ -177,7 +177,9 @@ export default { return data?.People?.map((item) => { return { "value": item.id, - "label": `${item?.primary_name?.given} ${item?.primary_name?.family}`, + // XXX The label is the value that autocomplete will use to update the input field value property. + "label": `${item?.primary_name?.given} ${item?.primary_name?.family} (ID: ${item?.id})`, + "fullName": `${item?.primary_name?.given} ${item?.primary_name?.family}`, "itemId": `${item?.id}`, "email": this.filterByEmailAddressType(item?.email_addresses), "emailPretty": this.shortenString(this.constructEmailCsv(this.filterByEmailAddressType(item?.email_addresses))), @@ -190,12 +192,8 @@ export default { }) }, setPerson() { - if(this.options.type == 'default') { + if(['default', 'field'].includes(this.options.type)) { this.options.inputProps.dataPersonid = this.person.value - } else if(this.options.type == 'field') { - // The picker is part of a standard form field - const field = document.getElementById(this.options.fieldName); - field.value = this.person.value; } else { // The picker is stand-alone, and should render the configured page in a modal on @item-select const urlForModal = this.options.actionUrl + '&person_id=' + this.person.value; @@ -249,7 +247,7 @@ export default { mounted() { if(this.options.inputValue != undefined && this.options.inputValue != '' - && this.options.htmlId == 'person_id') { + && this.options.htmlId.endsWith('person_id')) { this.options.inputProps.value = `${this.options.formParams?.fullName} (ID: ${this.options.inputValue})` } }, @@ -271,12 +269,23 @@ export default { return this.options.label; } // Otherwise return the default - return this.txt['operation.autocomplete.people.label']; + return this.txt['autocomplete.people.label']; + }, + hasAutoCompleteLabel: function() { + // Check to see if a label has been passed in + return this.options.label !== undefined && this.options.label !== '' + }, + getMiniLoaderClasses: function() { + if(this.options.label !== undefined && this.options.label !== '') { + return "co-loading-mini-container d-inline ms-1" + } else { + return "co-loading-mini-container d-inline ms-1 over-input" + } } }, template: ` - - + +
    - - - {{ this.txt['controller.GroupMembers'] }} + + + + {{ this.txt['GroupMembers'] }}
    ID: {{ slotProps.option.itemId }}