Skip to content

cfm-31_people-picker #250

Merged
merged 6 commits into from Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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
@@ -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
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
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
@@ -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
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
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
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