ui/lib/shared/addon/components/principal-search/component.js

332 lines
8.2 KiB
JavaScript

import { inject as service } from '@ember/service';
import C from 'ui/utils/constants';
import {
get, set, computed, observer, setProperties
} from '@ember/object';
import { task, timeout } from 'ember-concurrency';
import { isBlank } from '@ember/utils';
import SearchableSelect from '../searchable-select/component';
import { alias } from '@ember/object/computed';
import { later } from '@ember/runloop';
import { on } from '@ember/object/evented';
import { isAlternate, isMore, isRange } from 'ui/utils/platform';
import $ from 'jquery';
const DEBOUNCE_MS = 250;
export default SearchableSelect.extend({
globalStore: service(),
classNames: 'principal-search',
errors: null,
_principals: null,
useLabel: null,
showDropdownArrow: false,
clientSideFiltering: false,
loading: false,
focused: false,
selectExactOnBlur: true,
includeLocal: true,
sendAfterLoad: false,
searchOnlyGroups: false,
content: alias('filteredPrincipals'),
value: alias('filter'),
init() {
this._super(...arguments);
set(this, 'allUsers', get(this, 'globalStore').all('user'));
},
didInsertElement() {
// Explicitly not calling super here to not show until there's content this._super(...arguments);
$(this.element).find('input').on('focus', () => {
if (this.isDestroyed || this.isDestroying) {
return;
}
set(this, 'focused', true);
const term = get(this, 'value');
if ( term ) {
set(this, '_principals', []);
this.search.perform(term);
this.send('show');
}
});
$(this.element).find('input').on('blur', () => {
later(() => {
if (this.isDestroyed || this.isDestroying) {
return;
}
set(this, 'focused', false);
if ( get(this, 'selectExactOnBlur') ) {
this.scheduleSend();
}
this.send('hide');
}, 250);
});
},
actions: {
search(term, e) {
const kc = e.keyCode;
this.send('show');
if ( kc === C.KEY.CR || kc === C.KEY.LF ) {
this.scheduleSend();
return;
}
var isAlpha = (k) => {
return !get(this, 'metas').includes(k)
&& !isAlternate(k)
&& !isRange(k)
&& !isMore(k);
}
if (isAlpha(kc)) {
set(this, 'principal', null);
this.add();
this.search.perform(term);
}
},
show() {
if (get(this, 'showOptions') === true) {
return;
}
const toBottom = $('body').height() - $(this.element).offset().top - 60; // eslint-disable-line
setProperties(this, {
maxHeight: toBottom < get(this, 'maxHeight') ? toBottom : get(this, 'maxHeight'),
showOptions: true,
})
},
hide() {
setProperties(this, {
filter: get(this, 'displayLabel'),
showOptions: false,
'$activeTarget': null,
})
},
},
filteredPrincipals: computed('_principals.@each.{id,state}', function() {
return ( get(this, '_principals') || [] ).map(( principal ) => {
// console.log({label: get(principal, 'displayName') || get(principal, 'loginName') || get(principal, 'name'), value: get(principal, 'id'), provider: get(principal, 'provider'),});
return {
label: get(principal, 'displayName') || get(principal, 'loginName') || get(principal, 'name'),
value: get(principal, 'id'),
provider: get(principal, 'provider'),
type: get(principal, 'principalType'),
principal,
};
});
}),
externalChanged: on('init', observer('external', function(){
let principal = get(this, 'external');
if (principal) {
setProperties(this, {
readOnly: true,
optionValuePath: 'label',
});
this.setSelect({
label: get(principal, 'displayName') || get(principal, 'loginName') || get(principal, 'name'),
value: get(principal, 'id'),
provider: get(principal, 'provider'),
type: get(principal, 'principalType')
});
}
})),
metas: computed(() => {
return Object.keys(C.KEY).map((k) => C.KEY[k]);
}),
displayLabel: computed('interContent.[]', 'intl', 'localizedLabel', 'optionLabelPath', 'optionValuePath', 'prompt', 'value', function() {
const value = get(this, 'value');
if (!value) {
return null;
}
const vp = get(this, 'optionValuePath');
const lp = get(this, 'optionLabelPath');
const selectedItem = get(this, 'interContent').filterBy(vp, value).get('firstObject');
if (selectedItem) {
let label = get(selectedItem, lp);
if (get(this, 'localizedLabel')) {
label = get(this, 'intl').t(label);
}
return label;
}
return value;
}),
showMessage: computed('filtered.[]', 'value', function() {
if ( !get(this, 'value') ) {
return false;
}
return get(this, 'filtered.length') === 0;
}),
scheduleSend() {
if ( get(this, 'loading') ) {
set(this, 'sendExactAfterSearch', true);
} else {
set(this, 'sendExactAfterSearch', false);
this.sendSelectExact();
}
},
sendSelectExact() {
const value = get(this, 'value');
const match = get(this, 'filteredPrincipals').findBy('label', value);
let principal = null;
if ( match ) {
principal = match.principal;
} else {
set(this, 'value', '');
}
this.selectExact(principal);
this.send('hide');
},
setSelect(item) {
const gp = get(this, 'optionGroupPath');
const vp = get(this, 'optionValuePath');
set(this, 'value', get(item, vp));
if (gp && get(item, gp)) {
set(this, 'group', get(item, gp));
}
set(this, 'filter', get(this, 'displayLabel'));
set(this, 'principal', item);
this.add();
this.send('hide');
},
search: task(function * (term) {
if (isBlank(term)) {
setProperties(this, {
'_principals': [],
loading: false,
});
return;
}
// Pause here for DEBOUNCE_MS milliseconds. Because this
// task is `restartable`, if the principal starts typing again,
// the current search will be canceled at this point and
// start over from the beginning. This is the
// ember-concurrency way of debouncing a task.
set(this, 'loading', true);
yield timeout(DEBOUNCE_MS);
let xhr = yield this.goSearch.perform(term);
let neu = [];
if ( xhr.status >= 200 && xhr.status <= 299 && xhr.body && typeof xhr.body === 'object' && xhr.body.data ) {
neu = xhr.body.data;
}
if ( get(this, 'includeLocal') ) {
let normalizedTerm = term.toLowerCase().trim();
let foundIds = {};
neu.forEach((x) => {
foundIds[x.id] = true;
})
let local = get(this, 'allUsers');
local = local.filter((x) => {
if ( (x.name || '').toLowerCase().trim().startsWith(normalizedTerm) ||
(x.username || '').toLowerCase().trim().startsWith(normalizedTerm) ) {
for ( let i = 0 ; i < x.principalIds.length ; i++ ) {
if ( foundIds[ x.principalIds[i] ] ) {
return false;
}
}
return true;
}
return false;
});
const globalStore = get(this, 'globalStore');
local = local.map((x) => {
return globalStore.getById('principal', x.principalIds[0]);
});
local = local.filter((x) => !!x);
neu.addObjects(local);
}
set(this, '_principals', neu);
return xhr;
}).restartable(),
goSearch: task(function * (term) {
const { globalStore } = this;
const data = { name: term };
if (this.searchOnlyGroups) {
set(data, 'principalType', 'group')
}
try {
return yield globalStore.rawRequest({
url: 'principals?action=search',
method: 'POST',
data
});
} catch (xhr) {
set(this, 'errors', [`${ xhr.status }: ${ xhr.statusText }`]);
} finally {
set(this, 'loading', false);
if ( get(this, 'sendExactAfterSearch') ) {
this.scheduleSend();
}
}
}),
add() {
throw new Error('add action is required!');
},
selectExact() {
throw new Error('selectExact action is required!');
}
});