Skip to content

Autocomplete widget added for selecting a Group member (CFM-150) #164

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