Skip to content

Commit

Permalink
Autocomplete widget added for selecting a Group member (CFM-150) (#164)
Browse files Browse the repository at this point in the history
  • Loading branch information
arlen authored and Ioannis committed Mar 12, 2024
1 parent a541a50 commit bd877bf
Show file tree
Hide file tree
Showing 9 changed files with 697 additions and 6 deletions.
9 changes: 9 additions & 0 deletions app/resources/locales/en_US/operation.po
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ msgstr "Add"
msgid "add.a"
msgstr "Add a New {0}"

msgid "autocomplete.people.desc"
msgstr "Begin typing to find a person (use at least {0} characters from a name, email address, or identifier)"

msgid "autocomplete.people.label"
msgstr "Search for a person"

msgid "autocomplete.people.placeholder"
msgstr "enter name, email address, or identifier"

msgid "api.key.generate"
msgstr "Generate API Key"

Expand Down
71 changes: 71 additions & 0 deletions app/src/View/Helper/FieldHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,77 @@ protected function formNameDiv(string $fieldName, string $labelText=null, string
' . ($desc ? '<div class="field-desc">' . $desc . '</div>' : "") .'
</div>';
}

/**
* Emit a People Autocomplete (PrimeVue) control for selecting a person
*
* @since COmanage Registry v5.0.0
* @param string $fieldName Field name of input field
* @param string $type Type of person autocomplete to use (XXX should be an enum, and a default should be set)
* @return string Source HTML
*/

public function peopleAutocompleteControl(string $fieldName, string $type = 'coperson'): string {
if($this->action == 'view') {
// return the member name value as plaintext
$coptions = ['type' => 'text'];
$entity = $this->getView()->get('vv_obj');
$controlCode = $entity->$fieldName;

// Return this to the generic control() function
return $this->control($fieldName, $coptions, ctrlCode: $controlCode, labelIsTextOnly: true);

} else {
// Create the options array for the (text input) form control
$coptions = [];
$coptions['class'] = 'form-control people-autocomplete';
$coptions['placeholder'] = __d('operation','autocomplete.people.placeholder');
$coptions['id'] = $fieldName;
$coptions['value'] = '';

$entity = $this->getView()->get('vv_obj');

// Get the existing values, if present
if(!empty($entity->$fieldName)) {
$coptions['value'] = ''; // XXX put the ID here.
}

// Create a field name for the autocomplete input
$autoCompleteFieldName = 'cm_autocomplete_' . $fieldName;

// Because we use JavaScript to set the value of the hidden field,
// disable form-tamper checking for the autocomplete fields.
// XXX We ought not have to do this for the hidden field ($fieldName) at least
$this->Form->unlockField($fieldName);
$this->Form->unlockField($autoCompleteFieldName);

$autocompleteArgs = [
'fieldName' => $fieldName,
'type' => $type,
'htmlId' => $autoCompleteFieldName
];

// Create a hidden field to hold our value and emit the autocomplete widget
$controlCode = $this->Form->hidden($fieldName, $coptions)
. $this->getView()->element('autocompletePeople', $autocompleteArgs);

// XXX the numeric value passed to 'autocomplete.people.desc' should be derived from config; it is the minLength value for starting autocomplete.
$autoCompleteDesc = '<div class="field-desc"><span class="material-icons">info</span> ' . __d('operation','autocomplete.people.desc',['2']) . '</div>';

// Specify a class on the <li> form control wrapper
$liClass = "fields-people-autocomplete";

// Pass everything to the generic control() function
return $this->control(
$fieldName,
$coptions,
ctrlCode: $controlCode,
afterField: $autoCompleteDesc,
cssClass: $liClass,
labelIsTextOnly: true
);
}
}

/**
* Emit a source control for an MVEA that has a source_foo_id field pointing
Expand Down
2 changes: 2 additions & 0 deletions app/src/View/Helper/VueHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class VueHelper extends Helper {
'datepicker.hour'
],
'operation' => [
'autocomplete.people.label',
'autocomplete.people.placeholder',
'close'
]
];
Expand Down
8 changes: 6 additions & 2 deletions app/templates/GroupMembers/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@
*/

if($vv_action == 'add') {
// As a temporary hack for development, we accept a manually entered Person ID
print $this->Field->control('person_id', ['type' => 'text']);
// Produce the autocomplete people selector on 'add'
print $this->Field->peopleAutocompleteControl('person_id');

// XXX As a temporary hack for development, we accept a manually entered Person ID
// XXX leave here temporarily.
//print $this->Field->control('person_id', ['type' => 'text']);
} else {
print $this->Form->hidden('person_id');
}
Expand Down
97 changes: 97 additions & 0 deletions app/templates/element/autocompletePeople.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php
/**
* COmanage Registry People Autocomplete Vue.js Component
*
* 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)
*/

// Get parameters
$fieldName = $fieldName ?? "";
$type = $type ?? "";
$htmlId = $htmlId ?? "";

// Get the CSRF Token in JavaScript
$token = $this->request->getAttribute('csrfToken');
// Load my helper functions
$vueHelper = $this->loadHelper('Vue');

// Create a people autocomplete text input.
?>

<script type="module">
<?php if(Cake\Core\Configure::read('debug')): ?>
import AutocompletePeople from "<?= $this->Url->script('comanage/components/autocomplete/cm-autocomplete-people.js') ?>?time=<?= time() ?>";
<?php else: ?>
import AutocompletePeople from "<?= $this->Url->script('comanage/components/autocomplete/cm-autocomplete-people.js') ?>";
<?php endif; ?>

const app = Vue.createApp({
data() {
return {
autocompleteOptions: {
fieldName: '<?= $fieldName ?>',
type: '<?= $type ?>',
minLength: 2, // XXX probably should be set by config and default to 3
htmlId: '<?= $htmlId ?>'
},
error: '',
core: {
webroot: '<?= $this->request->getAttribute('webroot') ?>'
},
txt: JSON.parse('<?= json_encode($vueHelper->locales()) ?>')
}
},
components: {
AutocompletePeople
},
methods: {
setError(txt) {
this.error = txt;
},
generalXhrFailCallback(xhr) {
stopSpinner();
this.successTxt = '';
if(xhr.statusText != undefined && xhr.statusText != '') {
this.setError(xhr.statusText)
console.log('Status Code: ', xhr.status)
} else {
console.error(xhr);
this.setError(this.txt.error500);
}
}
}
});

app.use(primevue.config.default, {unstyled: true});

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

<div id="<?= $htmlId ?>-container" class="cm-autocomplete-container">
<autocomplete-people
:options="this.autocompleteOptions"
:core="this.core"
:txt="this.txt">
</autocomplete-people>
</div>
10 changes: 6 additions & 4 deletions app/templates/layout/default.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,13 @@
'co-responsive'
]) . PHP_EOL ?>

<!-- Load Bootstrap and jQuery (other scripts at bottom) -->
<!-- Load Bootstrap, jQuery, and Vue (other scripts at bottom) -->
<?= $this->Html->script([
'bootstrap/bootstrap.bundle.min.js',
'jquery/jquery.min.js'
'jquery/jquery.min.js',
'vue/vue-3.2.31.global.prod.js',
'vue/primevue-3.48.1.core.min.js',
'vue/primevue-3.48.1.autocomplete.min.js'
]) . PHP_EOL ?>

<!-- Include external files and scripts -->
Expand Down Expand Up @@ -225,8 +228,7 @@

<!-- Load Javascript -->
<!-- XXX js-cookie should be deprecated -->
<?= $this->Html->script([
'vue/vue-3.2.31.global.prod.js',
<?= $this->Html->script([
'js-cookie/js.cookie-2.1.3.min.js',
'comanage/comanage.js'
]) . PHP_EOL ?>
Expand Down
27 changes: 27 additions & 0 deletions app/webroot/css/co-base.css
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,7 @@ ul.form-list .field-desc {
font-size: 0.9em;
font-style: italic;
padding-bottom: 1em;
color: var(--cmg-color-txt-soft);
}
ul.form-list .fields-header {
background-color: var(--cmg-color-body-bg);
Expand Down Expand Up @@ -1523,6 +1524,32 @@ ul.form-list .cm-time-picker-vals li {
}
.cm-time-picker-colon {
padding: 0 1em;
}
/* Autocomplete */
.cm-autocomplete-panel {
}
.cm-autocomplete-panel ul {
padding: 0;
margin: 0;
background-color: var(--cmg-color-body-bg);
border: 1px solid var(--cmg-color-bg-008);
}
.cm-autocomplete-panel li {
padding: 1em;
list-style: none;
border-collapse: collapse;
border-bottom: 1px solid var(--cmg-color-bg-006);
}
.cm-autocomplete-panel li[data-p-focus=true] {
background-color: var(--cmg-color-bg-001);
box-shadow: inset 0 0 0.5em var(--cmg-color-highlight-017);
}
.cm-autocomplete-panel .cm-ac-subitems {
font-size: 0.9em;
margin-left: 1em;
}
.cm-autocomplete-panel .cm-ac-item-primary {

}
/* Vue transitions */
.v-enter-active,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* COmanage Registry PeoplePicker Component JavaScript
*
* 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)
*/

import { camelize } from '../utils/helpers.js';

export default {
props: {
options: Object,
core: Object,
txt: Object
},
components: {
AutoComplete : primevue.autocomplete
},
data() {
return {
people: [],
person: ''
}
},
methods: {
search() {
this.people = [
{
"value": 16,
"label": "Test J Testington IV",
"email": "test.testington@myvo.org 1",
"emailLabel": "Email (official): ",
"identifier": "50010",
"identifierLabel": "Identifier (employeenumber): "
},
{
"value": 17,
"label": "Test JR Testington V",
"email": "test.testington@myvo.org 1",
"emailLabel": "Email (official): ",
"identifier": "50002",
"identifierLabel": "Identifier (employeenumber): "
},
{
"value": 280,
"label": "Test C Testington",
"email": "test.testington@myvo.org 1",
"emailLabel": "Email (official): ",
"identifier": "50052",
"identifierLabel": "Identifier (employeenumber): "
}
];
},
setPerson() {
const field = document.getElementById(this.options.fieldName);
field.value = this.person.value;
}
},
template: `
<label :for="this.inputId">{{ this.txt['autocomplete.people.label'] }}</label>
<AutoComplete
v-model="person"
inputClass="cm-autocomplete"
panelClass="cm-autocomplete-panel"
:inputId="this.options.htmlId"
:suggestions="this.people"
optionLabel="label"
@complete="search"
:minLength="this.options.minLength"
:placeholder="this.txt['autocomplete.people.placeholder']"
forceSelection
@item-select="setPerson">
<template #option="slotProps">
<div class="cm-ac-item">
<div class="cm-ac-item-primary cm-ac-name">{{ slotProps.option.label }}</div>
<div class="cm-ac-subitems">
<div class="cm-ac-subitem cm-ac-email" v-if="slotProps.option.email">
<span class="cm-ac-label" v-if="slotProps.option.emailLabel">{{ slotProps.option.emailLabel }}</span>
<span class="cm-ac-value">{{ slotProps.option.email }}</span>
</div>
<div class="cm-ac-subitem cm-ac-id" v-if="slotProps.option.identifier">
<span class="cm-ac-label" v-if="slotProps.option.identifierLabel">{{ slotProps.option.identifierLabel }}</span>
<span class="cm-ac-value">{{ slotProps.option.identifier }}</span>
</div>
</div>
</div>
</template>
</AutoComplete>
`
}
370 changes: 370 additions & 0 deletions app/webroot/js/vue/primevue-3.48.1.core.min.js

Large diffs are not rendered by default.

0 comments on commit bd877bf

Please sign in to comment.