Skip to content

Commit

Permalink
cfm-31_people-picker (#250)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Ioannis authored Nov 18, 2024
1 parent 7474127 commit 89bb59d
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ public function initialize(array $config): void {
'type' => 'auxiliary',
'model' => 'CoSettings'
],
'types' => [
'type' => 'auxiliary',
'model' => 'Types'
],
]);

$this->setLayout([ 'index' => 'iframe',
Expand Down
11 changes: 8 additions & 3 deletions app/plugins/CoreEnroller/templates/element/field.php
Original file line number Diff line number Diff line change
Expand Up @@ -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])
};
61 changes: 61 additions & 0 deletions app/plugins/CoreEnroller/templates/element/spa-field.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
/**
* COmanage Registry Single Page Application Field
*
* 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);
// $vueElementName
// $formArguments
$label = $formArguments['fieldLabel'];
$description = $formArguments['fieldDescription'] ?? null;
unset($formArguments['fieldDescription']);
$isRequired = $formArguments['fieldOptions']['required'] ?? false;

?>

<li class="fields-<?= $formArguments['fieldNameAlias'] ?>">
<div class="field">
<div class="field-name">
<div class="field-title">
<?= $label ?>
<?php if($isRequired):?>
<?= $this->element('form/requiredSpan') ?>
<?php endif;?>
</div>
<?php if(isset($description)): ?>
<div class="field-desc">
<?= $description ?>
</div>
<?php endif ?>
</div><div class="field-info">
<?= $this->Field->constructSPAField(
// The Default field will be used to harvest the attributes
element: $this->Field->formField(...$formArguments),
// Vue/JS element
vueElementName: $vueElementName
) ?>
</div>
</div>
</li>
3 changes: 3 additions & 0 deletions app/resources/locales/en_US/field.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
39 changes: 39 additions & 0 deletions app/src/View/Helper/FieldHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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()
Expand Down
32 changes: 32 additions & 0 deletions app/templates/element/form/elementFallback.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
/**
* COmanage Registry Element Fallback Span Element
*
* 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);
?>

<span><?= __d('field', 'element_fallback') ?></span>
5 changes: 3 additions & 2 deletions app/templates/element/peopleAutocomplete.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -132,7 +133,7 @@
}

// Mount the component and provide a global reference for this app instance.
window.<?= $htmlId ?> = app.mount("#<?= $htmlId ?>-container");
window.<?= str_replace('-', '', $htmlId) ?> = app.mount("#<?= $htmlId ?>-container");
</script>

<div id="<?= $htmlId ?>-container" class="cm-autocomplete-container"></div>
<div id="<?= $htmlId ?>-container" class="<?= $containerClasses ?>"></div>
7 changes: 7 additions & 0 deletions app/webroot/css/co-base.css
Original file line number Diff line number Diff line change
Expand Up @@ -2185,6 +2185,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))),
Expand All @@ -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;
Expand Down Expand Up @@ -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})`
}
},
Expand All @@ -272,11 +270,22 @@ export default {
}
// Otherwise return the default
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: `
<label class="mr-2" :for="this.options.htmlId">{{ this.autoCompleteLabel }}</label>
<MiniLoader :isLoading="loading" classes="co-loading-mini-container d-inline ms-1"/>
<label v-if="hasAutoCompleteLabel" class="mr-2" :for="this.options.htmlId">{{ this.autoCompleteLabel }}</label>
<MiniLoader :isLoading="loading" :classes="getMiniLoaderClasses"/>
<AutoComplete
v-model="person"
inputClass="cm-autocomplete"
Expand All @@ -300,8 +309,9 @@ export default {
<div class="cm-ac-item">
<div class="cm-ac-item-primary">
<div class="cm-ac-name">
<span class="cm-ac-name-value" v-if="slotProps.option.isMember" v-html="slotProps.option.label"></span>
<span class="cm-ac-name-value" v-else v-html="this.highlightedquery(slotProps.option.label, query)"></span>
<!-- XXX The input field will be updated with the option.label value. Here we only need the full name -->
<span class="cm-ac-name-value" v-if="slotProps.option.isMember" v-html="slotProps.option.fullName"></span>
<span class="cm-ac-name-value" v-else v-html="this.highlightedquery(slotProps.option.fullName, query)"></span>
<span class="mr-1 badge bg-success" v-if="slotProps.option.isMember">{{ this.txt['GroupMembers'] }}</span>
</div>
<div class="cm-ac-item-id">
Expand Down

0 comments on commit 89bb59d

Please sign in to comment.