{
@@ -352,10 +353,7 @@ function FieldGroupTestView ({fields={}}) {
label : 'Suggestion Field:',
labelWidth : 100
}}
- getSuggestions = {(val)=>{
- const suggestions = validSuggestions.filter((el)=>{return el.startsWith(val);});
- return suggestions.length > 0 ? suggestions : validSuggestions;
- }}
+ options={validSuggestions}
/>
{field1}
diff --git a/src/firefly/js/ui/NaifidPanel.jsx b/src/firefly/js/ui/NaifidPanel.jsx
index 7c7360033..e9fd9b66f 100644
--- a/src/firefly/js/ui/NaifidPanel.jsx
+++ b/src/firefly/js/ui/NaifidPanel.jsx
@@ -5,8 +5,9 @@
import React, {memo, useRef} from 'react';
import PropTypes from 'prop-types';
import {sortBy} from 'lodash';
+import {AutocompleteOption} from '@mui/joy';
import {resolveNaifidObj} from './NaifidPanelWorker.js';
-import {SuggestBoxInputFieldView} from './SuggestBoxInputField';
+import {AutoCompleteInputView, useAsyncOptions, useDebounced} from './AutoCompleteInput.jsx';
import {useFieldGroupConnector} from './FieldGroupConnector.jsx';
import {TargetFeedback} from './TargetFeedback';
@@ -15,26 +16,47 @@ const LABEL_DEFAULT = 'Moving Target Name:';
const DEFAULT_FORMAT = 'default';
const searchHistory = {[DEFAULT_FORMAT]: []}; // defining as global to persist it throughout the lifetime of app
+// the naif lookup is done on the server, joy must not filter the result set on top of it
+const noClientFilter= (options) => options;
-function NaifidPanelView({showHelp, valid, message, examples, feedback, value, labelWidth, feedbackStyle, popStyle,
- label= LABEL_DEFAULT, fireValueChange, updateNaifNameValue,
+const SUGGEST_DEBOUNCE_MS= 200;
+
+
+function renderNaifOption(optProps, suggestion) {
+ const {key, ...rest}= optProps ?? {};
+ return (
+
+ Name: {suggestion.name}, NAIF ID: {suggestion.naifid}
+
+ );
+}
+
+
+function NaifidPanelView({showHelp, valid, message, examples, feedback, value, feedbackStyle, popStyle,
+ label= LABEL_DEFAULT, fireValueChange, naifNameRef,
naifIdFormat=DEFAULT_FORMAT}){
+ // the name just picked, so the value change it causes does not trigger an unnecessary lookup
+ const selectedNameRef= useRef(value);
+
const getSuggestions = (val= '') => {
if (!val) return [];
+ if (val===selectedNameRef.current) return []; // just picked, already resolved
if (naifIdFormat && !searchHistory[naifIdFormat]) searchHistory[naifIdFormat] = [];
const getResSuggestionsList = (suggestionsList) => {
const resSuggestionsList = Object.values(suggestionsList).map((v) => ({name: v.naifName, naifid: v.naifId}));
- return sortBy(resSuggestionsList, 'naifid').reverse();
+ // joy uses value/label; naifid rides along for renderOption and onChange
+ return sortBy(resSuggestionsList, 'naifid').reverse()
+ .map((s) => ({...s, value: s.name, label: s.name}));
};
- //if value has been searched previously, no need to request from server
+ // if value has been searched previously, no need to request from server
if (searchHistory[naifIdFormat].length > 0){
const cachedSuggList = Object.values(searchHistory[naifIdFormat]).find((v) => (v.searchVal === val));
if (cachedSuggList?.searchRes) return getResSuggestionsList(cachedSuggList.searchRes);
}
- //else request naif IDs from the server
+ // else request naif IDs from the server
const rval = resolveNaifidObj(val, naifIdFormat);
if (!rval.p) return [];
return rval.p.then((response)=>{
@@ -44,46 +66,46 @@ function NaifidPanelView({showHelp, valid, message, examples, feedback, value, l
return getResSuggestionsList(suggestionsList);
} else {
- //console.error(response);
+ // console.error(response);
fireValueChange({valid: false, message: response.feedback});
}
});
};
+ const {options, loading}= useAsyncOptions(getSuggestions, useDebounced(value, SUGGEST_DEBOUNCE_MS));
- const getValueOnSuggestion = (val, selectedSugg) => {
- if (!selectedSugg) return;
-
- updateNaifNameValue(selectedSugg.name);
+ // only called on an actual pick from the list; free typing goes through fireValueChange below
+ const onOptionSelect = (ev, selectedSugg) => {
+ if (!selectedSugg || typeof selectedSugg === 'string') return; // cleared, or free-solo text
+ naifNameRef.current = selectedSugg.name; // NaifidPanel treats text matching this as valid
+ selectedNameRef.current = selectedSugg.name;
fireValueChange({
feedback: `Object Name: ${selectedSugg.name}, NAIF ID: ${selectedSugg.naifid}`,
valid : true,
+ message: '',
displayValue: selectedSugg.name,
value: selectedSugg.name + ';' + selectedSugg.naifid, //this is the returned value from the component.
});
-
- return selectedSugg.name;
};
-
return (
- ({valid: true, message: ''})}
- getSuggestions={getSuggestions}
- renderSuggestion={(suggestion) =>
- (Name: {suggestion.name}, NAIF ID: {suggestion.naifid})}
- fireValueChange={({message, valid, value}) =>
- fireValueChange({message, valid, displayValue: value, showHelp: value === ''})}/>
+ options={options}
+ loading={loading}
+ filterOptions={noClientFilter}
+ onChange={onOptionSelect}
+ renderOption={renderNaifOption}
+ fireValueChange={({message='', valid=true, value}) => {
+ selectedNameRef.current= undefined; // typed or cleared, so re-typing a picked name looks up again
+ fireValueChange({message, valid, displayValue: value, showHelp: value === ''});
+ }}/>
);
}
@@ -102,7 +124,7 @@ NaifidPanelView.propTypes = {
onUnmountCB : PropTypes.func,
feedbackStyle: PropTypes.object,
fireValueChange: PropTypes.func,
- updateNaifNameValue: PropTypes.func,
+ naifNameRef: PropTypes.object,
naifIdFormat: PropTypes.string
};
@@ -111,17 +133,17 @@ export const NaifidPanel= memo( (props) => {
const {fieldKey='NaifId'} = props;
const {viewProps, fireValueChange} = useFieldGroupConnector({fieldKey, ...props});
- const naifNameValue = useRef(props.value ?? ''); // ref instead of state so that we can mutate it without causing re-renders
- const updateNaifNameValue = (newValue) => {naifNameValue.current = newValue;}; // for mutation by child component
+ // ref instead of state so that we can mutate it without causing re-renders
+ const naifNameRef = useRef(props.value ?? '');
const handleValueChange = (payload, fireValueChange) => {
const newPayload = {...payload};
- newPayload.valid = Boolean(newPayload.value) || (newPayload.displayValue === '' || newPayload.displayValue === naifNameValue.current);
+ newPayload.valid = Boolean(newPayload.value) || (newPayload.displayValue === '' || newPayload.displayValue === naifNameRef.current);
if (!newPayload.valid) {
if (!newPayload.message) {
newPayload.message = 'Please use name from the list';
}
- if (naifNameValue.current) {
+ if (naifNameRef.current) {
newPayload.value = '';
newPayload.feedback = '';
}
@@ -139,7 +161,7 @@ export const NaifidPanel= memo( (props) => {
feedback: viewProps.feedback || '',
showHelp: viewProps?.showHelp ?? true,
fireValueChange: (payload) => handleValueChange(payload, fireValueChange),
- updateNaifNameValue
+ naifNameRef
};
return ;
});
diff --git a/src/firefly/js/ui/NaifidPanelWorker.js b/src/firefly/js/ui/NaifidPanelWorker.js
index 4b801fa73..57801e798 100644
--- a/src/firefly/js/ui/NaifidPanelWorker.js
+++ b/src/firefly/js/ui/NaifidPanelWorker.js
@@ -8,40 +8,10 @@ import {getCmdSrvSyncURL, toBoolean} from '../util/WebUtil';
import {fetchUrl} from '../util/fetch';
-function makeResolverPromise(objName, naifIdFormat) {
- let ignoreSearchResults= null;
- let aborted= false;
-
- const aborter= function() {
- aborted= true;
- if (ignoreSearchResults!==null) ignoreSearchResults();
- };
-
- const workerPromise= new Promise(
- function(resolve, reject) {
- setTimeout( ()=> {
- if (aborted) {
- reject();
- }
- else {
- const {p, rejectFunc}= makeSearchPromise(objName, naifIdFormat);
- ignoreSearchResults= rejectFunc;
- resolve(p);
- }
- }, 200);
- }
- );
-
- return {p:workerPromise, aborter};
-}
-
-
-
function makeSearchPromise(objName, naifIdFormat) {
- let rejectFunc= null;
let url= `${getCmdSrvSyncURL()}?objName=${objName}&cmd=${ServerParams.RESOLVE_NAIFID}`;
if (naifIdFormat) url += `&naifIdFormat=${naifIdFormat}`;
- const searchPromise= new Promise(
+ return new Promise(
function(resolve, reject) {
let fetchOptions = {};
// AbortController might not be available in older browsers
@@ -57,18 +27,13 @@ function makeSearchPromise(objName, naifIdFormat) {
}
fetchUrl(url, fetchOptions).then( (response) => {
- response.json().then((value) => {
+ return response.json().then((value) => {
resolve(value);
});
}).catch( (error) => {
return reject(error);
});
});
-
- const abortPromise= new Promise(function(resolve,reject) {
- rejectFunc= reject;
- });
- return {p:Promise.race([searchPromise,abortPromise]), rejectFunc};
}
@@ -89,7 +54,7 @@ function resolveObject(objName, naifIdFormat) {
};
}
- let {p}= makeResolverPromise(objName, naifIdFormat);
+ let p= makeSearchPromise(objName, naifIdFormat);
p= p.then( (results) =>
{
if (results) {
diff --git a/src/firefly/js/ui/SuggestBoxInputField.css b/src/firefly/js/ui/SuggestBoxInputField.css
deleted file mode 100644
index 7fd8fd343..000000000
--- a/src/firefly/js/ui/SuggestBoxInputField.css
+++ /dev/null
@@ -1,31 +0,0 @@
-.SuggestBoxInputField {
- position: relative;
-}
-
-.SuggestBoxPopup {
- position: absolute;
- overflow: auto;
- top: 100%;
- background: #fff;
- z-index: 1;
- max-height: 200px;
- border: 2px solid #7eadd9;
-}
-
-.SuggestBox {
- list-style-type: none;
- padding: 0;
- margin: 0;
-}
-
-.SuggestBox__Suggestion {
- display: block;
- padding: 2px 20px 2px 10px;
- cursor: default;
-}
-
-.SuggestBox__Suggestion--highlighted {
- outline: 0;
- color: white;
- background: #7eadd9;
-}
\ No newline at end of file
diff --git a/src/firefly/js/ui/SuggestBoxInputField.jsx b/src/firefly/js/ui/SuggestBoxInputField.jsx
deleted file mode 100644
index 3afeca757..000000000
--- a/src/firefly/js/ui/SuggestBoxInputField.jsx
+++ /dev/null
@@ -1,372 +0,0 @@
-import React, {memo, PureComponent} from 'react';
-import PropTypes, {object, shape} from 'prop-types';
-import ReactDOM from 'react-dom';
-import {get, isArray, isUndefined, debounce} from 'lodash';
-import {dispatchHideDialog, dispatchShowDialog, isDialogVisible} from '../core/ComponentCntlr.js';
-import DialogRootContainer from './DialogRootContainer.jsx';
-import {DropDownMenuWrapper} from './DropDownMenu.jsx';
-import {useFieldGroupConnector} from './FieldGroupConnector.jsx';
-import {logger} from '../util/Logger.js';
-
-
-import {InputFieldView} from './InputFieldView.jsx';
-import './SuggestBoxInputField.css';
-
-
-/**
- * Make sure a component (like highlighted suggestion) is visible
- * @param el
- * @param {Number} highlightedIdx
- */
-function ensureVisible(el, highlightedIdx) {
- if (el && highlightedIdx) {
- const nSuggestions = el.children.length;
- if (nSuggestions>1) {
- const highlightedTop = el.childNodes[highlightedIdx].offsetTop;
- const scrollTop = el.parentNode.scrollTop;
- if (highlightedTopscrollTop+180) {
- el.parentNode.scrollTop = scrollTop+180;
- //console.log('highlightedTop: '+highlightedTop+' scrollTop: '+scrollTop+' '+el.parentNode.scrollTop );
- }
- }
- }
-}
-
-
-const Suggestion = (props) => {
- const {suggestion, renderSuggestion, ...otherProps} = props;
- return ( {renderSuggestion(suggestion)});
-};
-
-Suggestion.propTypes = {
- suggestion : PropTypes.any,
- renderSuggestion: PropTypes.func
-};
-
-const SuggestBox = (props) => {
- var {suggestions, highlightedIdx, renderSuggestion, onChange, onComplete, mouseTrigger} = props;
- suggestions = suggestions || [];
-
- var mouseEnterIdx; // to keep track where the mouse is
-
- const handleOptionMouseEnter = (idx, event) => {
- if (mouseTrigger) {
- //console.log('handle mouse enter ' + idx + ' ' + event.target);
- mouseEnterIdx = undefined;
- onChange(idx);
- } else {
- mouseEnterIdx = idx;
- event.stopPropagation();
- }
- };
-
- const handleMouseMove = (event) => {
- if (!mouseTrigger) {
- mouseTrigger = true;
- event.stopPropagation();
- } else {
- if (!isUndefined(mouseEnterIdx)) {
- onChange(mouseEnterIdx);
- }
- }
- };
-
- const handleOptionClick = (idx, event) => {
- onComplete(idx);
- };
-
- return (
-
- {ensureVisible(c, highlightedIdx);}}
- onMouseDown={handleOptionClick.bind(this, highlightedIdx)} //onClick competes with onBlur
- onMouseMove={handleMouseMove.bind(this)}>
- {suggestions.map((suggestion, idx) => {
- const highlighted = idx === highlightedIdx;
- return (
-
- );
- })}
-
- );
-};
-
-function computeDropdownXY(element) {
- const bodyRect = document.body.parentElement.getBoundingClientRect();
- const elemRect = element.getBoundingClientRect();
- const x = elemRect.left - bodyRect.left;
- const y = elemRect.bottom -10;
- return {x,y};
-}
-
-const dropKey= 'suggestion-box-drop';
-
-function showDrop(entryElement,dropDown, offComponentCB) {
-
- const beforeVisible= (e) =>{
- if (!e) return;
- const {x,y}= computeDropdownXY(entryElement);
- e.style.left= x+'px';
- e.style.top= y+'px';
- };
- const dd= ;
- DialogRootContainer.defineDialog(dropKey,dd);
- dispatchShowDialog(dropKey);
- document.removeEventListener('mousedown', offComponentCB);
- setTimeout(() => {
- document.addEventListener('mousedown', offComponentCB);
- },10);
-}
-
-export class SuggestBoxInputFieldView extends PureComponent {
- constructor(props) {
- super(props);
- this.state = {
- isOpen: false,
- displayValue: props.value,
- validator: props.validator,
- valid: get(props, 'valid', true),
- message: get(props.message, ''),
- inputWidth: undefined,
- suggestions: [],
- mouseTrigger: false
- };
-
- this.onValueChange = this.onValueChange.bind(this);
- this.changeValue = this.changeValue.bind(this);
- this.changeHighlighted = this.changeHighlighted.bind(this);
- this.handleKeyPress = this.handleKeyPress.bind(this);
- this.updateSuggestions = debounce(this.updateSuggestions.bind(this), 200);
- this.offComponentCallback= this.offComponentCallback.bind(this);
- }
-
-
-
- static getDerivedStateFromProps(props,state) {
- const {valid, message, value, validator} = props;
- if (valid !== state.valid || message !== state.message || value !== state.displayValue || validator !== state.validator) {
- return {valid, message, displayValue: value, validator};
- }
- return null;
- }
-
- onValueChange(ev) {
- var displayValue = get(ev, 'target.value');
- var {valid,message} = this.props.validator(displayValue);
- const inputWidth = ev.target.offsetWidth ? ev.target.offsetWidth : this.state.inputWidth;
- this.setState({displayValue, valid, message, inputWidth});
- this.updateSuggestions(displayValue);
- this.props.fireValueChange({ value : displayValue, message, valid});
- }
-
- componentWillUnmount() {
- document.removeEventListener('mousedown', this.offComponentCallback);// just in case
- setTimeout(() => {
- if (isDialogVisible(dropKey)) dispatchHideDialog(dropKey);
- },5);
- }
-
- offComponentCallback(ev) {
- this.setState({isOpen: false, highlightedIdx: undefined});
- document.removeEventListener('mousedown', this.offComponentCallback);// just in case
- }
-
- updateSuggestions(displayValue) {
- const arrayOrPromise = this.props.getSuggestions(displayValue);
- this.suggestionsPromise = arrayOrPromise;
- Promise.resolve(arrayOrPromise).then((suggestions) => {
- // make sure the suggestions are still relevant when promise returns
- if (arrayOrPromise === this.suggestionsPromise && isArray(suggestions) && suggestions.length > 0) {
- this.setState({isOpen: true, suggestions});
- } else {
- if (this.state.isOpen) this.setState({isOpen: false, suggestions: []});
- }
- }).catch((err) => logger.error(err));
- }
-
-
- changeHighlighted(mouseTrigger, newHighlightedIdx) {
-
- if (newHighlightedIdx !== this.state.highlightedIdx || mouseTrigger !== this.state.mouseTrigger) {
- //console.log('setting mouse trigger: '+mouseTrigger );
- this.setState({highlightedIdx: newHighlightedIdx, mouseTrigger});
- }
- }
-
-
- /*
- Change value based on a suggest box action
- @param {Array} current suggestions
- @param {Number} index of the highlighted suggestion
- */
- changeValue(highlightedIdx) {
- const {validator, valueOnSuggestion, fireValueChange} = this.props;
- const {displayValue, suggestions} = this.state;
-
- if (!isUndefined(highlightedIdx)) {
- const currentSuggestion = suggestions[highlightedIdx];
- const value = valueOnSuggestion ? valueOnSuggestion(displayValue, currentSuggestion) : currentSuggestion;
- if (value !== displayValue) {
- var {valid,message} = validator? validator(value) : {valid: true, message:''};
- this.setState({
- isOpen: false,
- highlightedIdx: undefined,
- displayValue: value,
- valid,
- message
- });
- fireValueChange({value, valid, message});
- } else {
- this.setState({isOpen: false, highlightedIdx: undefined});
- }
- } else {
- this.setState({isOpen: false});
- }
- }
-
- handleKeyPress(ev) {
- const {isOpen, highlightedIdx, suggestions} = this.state;
- this.divElement= ev.target;
-
- switch (ev.keyCode) {
- case 13: // enter
- case 9: // tab
- isOpen && this.changeValue(highlightedIdx);
- break;
- case 27: // escape
- this.setState({highlightedIdx : undefined, isOpen: false});
- break;
- case 38: // arrow up
- isOpen && this.changeHighlighted(false, isUndefined(highlightedIdx) ? suggestions.length - 1 : Math.max(0, highlightedIdx - 1));
- break;
- case 40: // arrow down
- isOpen && this.changeHighlighted(false, isUndefined(highlightedIdx) ? 0 : Math.min(highlightedIdx + 1, suggestions.length - 1));
- break;
- default:
- break;
- }
- };
-
-
- render() {
-
- const {displayValue, valid, message, highlightedIdx, isOpen, inputWidth, suggestions, mouseTrigger } = this.state;
- const {label, tooltip, renderSuggestion, wrapperStyle, placeholder, slotProps={}, sx,
- popStyle, popupIndex, readonly=false, required=false} = this.props;
-
- const leftOffset = 0;
- const minWidth = (inputWidth?inputWidth-4:50);
- const style = Object.assign({display: 'flex'}, wrapperStyle);
- const pStyle = Object.assign({left: leftOffset, minWidth, zIndex: popupIndex}, popStyle);
-
- if (isOpen) {
- if ((!isDialogVisible(dropKey) || this.lastHightlighedIdx!==highlightedIdx || this.lastSuggestions!==suggestions)) {
- this.lastHightlighedIdx= highlightedIdx;
- this.lastSuggestions= suggestions;
- const box= (
- this.setState({highlightedIdx : undefined})}>
- {suggestion})}
- onChange={this.changeHighlighted.bind(this, true)}
- onComplete={this.changeValue}
- mouseTrigger={mouseTrigger}
- />
-
);
- setTimeout(() => {
- showDrop(this.divElement,box,this.offComponentCallback);
- },5);
- }
- }
- else {
- setTimeout(() => dispatchHideDialog(dropKey),5);
- }
-
- return (
-
- isOpen && this.changeValue(undefined),
- endDecorator: this.props.endDecorator,
- slotProps: { tooltip: {placement: 'right'}, ...slotProps },
- sx
- }} />
-
- );
- }
-}
-
-
-SuggestBoxInputFieldView.propTypes = {
- value: PropTypes.string,
- fieldKey : PropTypes.string,
- inline : PropTypes.bool,
- label: PropTypes.string,
- sx: PropTypes.object,
- placeholder: PropTypes.string,
- tooltip: PropTypes.string,
- popStyle : PropTypes.object, //style for the popup list
- wrapperStyle: PropTypes.object, //style to merge into the container div
- getSuggestions : PropTypes.func, //suggestionsArr = getSuggestions(displayValue)
- valueOnSuggestion : PropTypes.func, //newDisplayValue = valueOnSuggestion(prevValue, suggestion),
- renderSuggestion : PropTypes.func, // ReactElem = renderSuggestion(suggestion)
- popupIndex: PropTypes.number,
- endDecorator : PropTypes.object,
- valid: PropTypes.bool,
- message: PropTypes.string,
- readonly: PropTypes.bool,
- validator: PropTypes.func,
- required: PropTypes.bool,
- slotProps: shape({
- input: object,
- control: object,
- label: object,
- tooltip: object
- }),
-};
-
-
-export const SuggestBoxInputField= memo( (props) => {
- const {viewProps, fireValueChange}= useFieldGroupConnector(props);
- return ;
-
-});
-
-
-SuggestBoxInputField.propTypes = {
- fieldKey : PropTypes.string.isRequired,
- groupKey : PropTypes.string,
- inline : PropTypes.bool,
- label: PropTypes.string,
- placeholder: PropTypes.string,
- sx: PropTypes.object,
- tooltip: PropTypes.string,
- popStyle : PropTypes.object, //style for the popup list
- wrapperStyle: PropTypes.object, //style to merge into the container div
- getSuggestions : PropTypes.func, //suggestionsArr = getSuggestions(displayValue)
- valueOnSuggestion : PropTypes.func, //newDisplayValue = valueOnSuggestion(prevValue, suggestion),
- renderSuggestion : PropTypes.func, // ReactElem = renderSuggestion(suggestion)
- endDecorator : PropTypes.object,
- popupIndex: PropTypes.number,
- required: PropTypes.bool,
- initialState: PropTypes.shape({
- value: PropTypes.string,
- tooltip: PropTypes.string,
- label: PropTypes.string,
- validator: PropTypes.func
- }),
-};
-