Skip to content

Commit

Permalink
#18
Browse files Browse the repository at this point in the history
  • Loading branch information
spelkey-ucd committed Jun 2, 2023
1 parent 64f64e3 commit 0a3ce5a
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 26 deletions.
61 changes: 61 additions & 0 deletions app/client/scss/custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,65 @@ ucdlib-employee-search {
max-width: 500px;
margin: auto;
display: block;

.emp-search-top.field-container {
margin-bottom: 0;
}

.emp-search-bar {
position: relative;

input {
flex-grow: 1;
padding-right: 2rem;
}
.emp-search-icon {
position: absolute;
width: 2.5rem;
min-width: 2.5rem;
height: 2.5rem;
min-height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
right: 0;
top: 0;
color: #022851;
}
}
.emp-search-results-parent {
position: relative;
}
.emp-search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 100;
background-color: #fffbed;
border-right: 1px solid #ffbf00;
border-left: 1px solid #ffbf00;
border-bottom: 1px solid #ffbf00;
color: #13639e;
max-height: 250px;
overflow-y: scroll;

.emp-search-result {
padding: .5rem .75rem;
}
.emp-search-result:hover {
background-color: #fde9ac;
}

.highlight {
font-weight: 700;
}
.muted {
font-weight: 400;
color: #545454;
}
.emp-search-more {
padding: .5rem .75rem;
}
}
}
149 changes: 133 additions & 16 deletions app/client/src/elements/components/ucdlib-employee-search.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LitElement } from 'lit';
import { LitElement, html } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import {render} from "./ucdlib-employee-search.tpl.js";


Expand All @@ -11,33 +12,64 @@ export default class UcdlibEmployeeSearch extends window.Mixin(LitElement)

static get properties() {
return {
name: {type: String},
query: {type: String},
labelText: {type: String, attribute: 'label-text'},
hideLabel: {type: Boolean, attribute: 'hide-label'},
results: {state: true},
totalResults: {state: true},
error: {state: true}
resultCtNotShown: {state: true},
noResults: {state: true},
error: {state: true},
status: {state: true},
isSearching: {state: true},
showDropdown: {state: true},
isFocused: {state: true},
selectedText: {state: true},
selectedObject: {state: true},
};
}

willUpdate(p) {
if ( p.has('name') ){
if ( this.searchTimeout ) clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
this.search();
}, 500);
}
}

constructor() {
super();
this.render = render.bind(this);
this.name = '';
this.query = '';
this.results = [];
this.totalResults = 0;
this.resultCtNotShown = 0;
this.error = false;
this.labelText = 'Existing Employee Search';
this.hideLabel = false;
this.status = 'idle';
this.isSearching = false;
this.showDropdown = false;
this.isFocused = false;
this.noResults = false;
this.selectedText = '';
this.selectedObject = {};

this._injectModel('EmployeeModel');
}

/**
* @description LitElement lifecycle called when element is updated
* @param {*} p - Changed properties
*/
willUpdate(p) {
if ( p.has('query') ){
if ( this.searchTimeout ) clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
this.search();
}, 500);
}

if ( p.has('results') || p.has('totalResults') ) {
this.resultCtNotShown = this.totalResults - this.results.length;
}

this._setStatus(p);
this._setShowDropdown(p);
}

/**
* @description Disables the shadowdom
* @returns
Expand All @@ -46,25 +78,110 @@ export default class UcdlibEmployeeSearch extends window.Mixin(LitElement)
return this;
}

/**
* @description Searches for employees by name. Fires when query property changes.
* @returns
*/
async search(){
if ( !this.name ) {
this.noResults = false;
this.selectedText = '';
this.selectedObject = {};
if ( !this.query ) {
this.results = [];
this.totalResults = 0;
this.error = false;
return;
}
const r = await this.EmployeeModel.searchByName(this.name);
this.isSearching = true;
const r = await this.EmployeeModel.searchByName(this.query);
this.isSearching = false;
if ( r.state === 'loaded' ) {
console.log(r.payload);
this.results = r.payload.results;
this.totalResults = r.payload.total;
this.noResults = !this.results.length;
this.error = false;
}
if ( r.state === 'error' ) {
this.error = true;
}
}

/**
* @description Sets the status of the element based on the updated properties
* @param {*} p - Changed properties
*/
_setStatus(p){
if ( p.has('isSearching') || p.has('query') || p.has('noResults') || p.has('selectedText') ){
let status = 'idle';
if ( this.isSearching ) {
status = 'searching';
} else if ( this.noResults ){
status = 'no-results';
} else if ( this.selectedText ){
status = 'selected';
}
this.status = status;
}

}

/**
* @description Shows/hides the results dropdown based on the element's updated properties
* @param {*} p
*/
_setShowDropdown(p){
if ( p.has('isFocused') || p.has('results') || p.has('query') || p.has('selectedText')){
this.showDropdown = this.isFocused && this.results.length && this.query && !this.selectedText;
}
}

/**
* @description Renders a single result item in the results dropdown
* @param {Object} result - an Employee object from the database
* @returns {TemplateResult}
*/
_renderResult(result){
if ( !this.query) return html``;

// highlight search term
let name = `${result.firstName} ${result.lastName}`.replace(/</g, '&lt;').replace(/>/g, '&gt;');
const queries = this.query.replace(/</g, '').replace(/>/g, '').split(' ').filter(q => q);
for (let query of queries) {
const regex = new RegExp(query, 'gi');
name = name.replace(regex, (match) => `<${match}>`);
}
name = name.replace(/</g, '<span class="highlight">').replace(/>/g, '</span>');
name=`<div>${name}</div>`;

return html`
${unsafeHTML(name)}
<div class='muted'>${result.title}</div>
`;

}

/**
* @description Fires when a result is clicked from the dropdown
* @param {Object} result - an Employee object from the database
*/
_onSelect(result){
this.selectedText = `${result.firstName} ${result.lastName}`;
this.selectedObject = result;
this.dispatchEvent(new CustomEvent('select', {
detail: result
}));
}

/**
* @description Hides dropdown box on input blur.
* Must give time for click event to fire on dropdown item.
*/
_onBlur(){
setTimeout(() => {
this.isFocused = false;
}, 100);
}

}

customElements.define('ucdlib-employee-search', UcdlibEmployeeSearch);
47 changes: 41 additions & 6 deletions app/client/src/elements/components/ucdlib-employee-search.tpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,48 @@ import { html } from 'lit';
export function render() {
return html`
<div>
<div class="field-container">
<label>Existing Employee Search</label>
<input
@input=${(e) => this.name = e.target.value}
.value=${this.name}
type="text">
<div>
<div class="field-container emp-search-top">
<label ?hidden=${this.hideLabel}>${this.labelText}</label>
<div class='emp-search-bar'>
<input
@input=${(e) => this.query = e.target.value}
@focus=${() => this.isFocused = true}
@blur=${() => this._onBlur()}
.value=${this.selectedText ? this.selectedText : this.query}
type="text">
<div class='emp-search-icon'>
<div ?hidden=${this.status != 'idle'} >
<i class='fas fa-search'></i>
</div>
<div ?hidden=${this.status != 'searching'}>
<i class="fas fa-circle-notch fa-spin"></i>
</div>
<div ?hidden=${this.status != 'no-results'}>
<i class="fas fa-exclamation double-decker"></i>
</div>
<div ?hidden=${this.status != 'selected'}>
<i class="fas fa-check quad"></i>
</div>
</div>
</div>
</div>
</div>
<div class='emp-search-results-parent'>
<div class='emp-search-results' ?hidden=${!this.showDropdown}>
<div>
${this.results.map(result => html`
<div class='emp-search-result pointer' @click=${() => this._onSelect(result)}>
${this._renderResult(result)}
</div>
`)}
</div>
<div class='muted emp-search-more' ?hidden=${!this.resultCtNotShown}>
And ${this.resultCtNotShown} more employees... <br> Try refining your search.
</div>
</div>
</div>
<div class='brand-textbox u-space-mt--small' ?hidden=${this.status != 'no-results' || !this.isFocused }>No results matched your search!</div>
</div>
`;}
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ export function renderTransferForm(){
return html`
<div id='obn-transfer'>
<ucdlib-employee-search></ucdlib-employee-search>
<p style='text-align:center'>hello world</p>
</div>
`;
}
9 changes: 5 additions & 4 deletions lib/src/utils/employees.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ class UcdlibEmployees {
const names = name.split(' ');
const params = [];
let text = `
SELECT *
FROM employees
SELECT e.*, json_agg(gm.group_id) as group_ids, json_agg(gm.is_head) as is_group_heads
FROM employees as e
LEFT JOIN group_membership as gm on e.id = gm.employee_key
WHERE
`;
let first = true;
Expand All @@ -131,10 +132,10 @@ class UcdlibEmployees {
} else {
first = false;
}
text += `(first_name ILIKE $${params.length + 1} OR last_name ILIKE $${params.length + 1})`;
text += `(e.first_name ILIKE $${params.length + 1} OR e.last_name ILIKE $${params.length + 1})`;
params.push(`%${name}%`);
}
text += ' ORDER BY last_name, first_name';
text += 'GROUP BY e.id ORDER BY e.last_name, e.first_name';
return await pg.query(text, params);
}

Expand Down

0 comments on commit 0a3ce5a

Please sign in to comment.