mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-28 14:48:12 +08:00
* revert all commits to before Typescript migration * update compat workflow to match latest deps (#2335) * update compat workflow to match latest deps * attempt to debug * attempt to debug * remove debugging code * typo * update deps to match desktop (#2340) * fix: don't run lint with `--fix` on push tests (#2273) * fix: don't run lint with `--fix` on push tests * npx Co-authored-by: maniarathi <mani.arathi@gmail.com> Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com> * rename X_approx_distribution to X_approximate_distribution (#2337) * Correctly handle non-finite numbers in heuristic determination of X distribution (#2342) * handle non-finites explicitly * improve and test edge case handling for distribution estimation * revert debugging changes * code readability * clean up type inferencing (#2332) * unit tests for 64 bit conversion * clean up type handling * type inference tests * more type inference fixes * use schema to determine user intent for data typing * stop using deprecated API * fbs type encoding test * add missing test * add more tests * correctly infer X type for CXG adaptor * lint * fix typo * ts migration * cleanup from PR review * lint * PR review changes * remove unused packages from client (#2359) * remove unused packages from client * add missing peer dep * fix: disable FE auth testing on compatibility tests (#2377) * update: release process (#2277) Co-authored-by: maniarathi <mani.arathi@gmail.com> * fix: remove spaces in param setup (#2380) * delete deploy workflow (#2396) * undo reformatting which now does not pass lint * fix snapshots which changed due to npm dep changes * add missing quoting to snapshot * another snapshot typo fix * TS Revert (2) - replay PR #2347 and #2354 (#2403) * replay edits from PR 2347 * TS Revert (3) - replay edits in PR #2327 (#2404) * replay edits in PR 2327 * TS Revert (4) - replay PR #2355 (#2405) * replay edits in PR 2355 * add additional babel config * reformat with new prettier config Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com> Co-authored-by: maniarathi <mani.arathi@gmail.com> Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com>
179 lines
4.9 KiB
JavaScript
179 lines
4.9 KiB
JavaScript
import React from "react";
|
|
import { InputGroup, MenuItem, Keys } from "@blueprintjs/core";
|
|
import { Suggest } from "@blueprintjs/select";
|
|
import fuzzysort from "fuzzysort";
|
|
|
|
export default class LabelInput extends React.PureComponent {
|
|
/*
|
|
Input widget for text labels, which acts like an InputGroup, but will also
|
|
accept a suggestion list (of labels), with sublime-like suggest search.
|
|
|
|
Properties:
|
|
|
|
* labelSuggestions -- array of suggested lables. May be array of string or
|
|
other objects. If array of objects, specify `labelKey`. If null, suggestion
|
|
mode is disabled.
|
|
* onSelect -- optional, callback upon selection of item from labelSuggestions.
|
|
(label) => void
|
|
* onChange -- optional, callback upon change in text input. (label) => void
|
|
* label -- component value, for controlled use
|
|
* newLabelMessage -- text to display when user enters a label not in labelSuggestions
|
|
(not used if suggestion mode disabled)
|
|
* inputProps -- will be passed to InputGroup
|
|
* popoverProps -- will be passed to <Suggest>
|
|
*/
|
|
|
|
/* maxinum number of suggestions */
|
|
static QueryResultLimit = 100;
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
const { label } = props;
|
|
const query = label || "";
|
|
const queryResults = this.filterLabels(query);
|
|
this.state = {
|
|
query,
|
|
queryResults,
|
|
};
|
|
}
|
|
|
|
handleQueryChange = (query, event) => {
|
|
// https://github.com/palantir/blueprint/issues/2983
|
|
if (!event) return;
|
|
|
|
const queryResults = this.filterLabels(query);
|
|
this.setState({
|
|
query,
|
|
queryResults,
|
|
});
|
|
|
|
const { onChange } = this.props;
|
|
if (onChange) onChange(query, event);
|
|
};
|
|
|
|
handleItemSelect = (item, event) => {
|
|
/* only report the select if not already reported via onChange() */
|
|
const { target } = item;
|
|
const { query } = this.state;
|
|
const { onSelect } = this.props;
|
|
if (target !== query && onSelect) onSelect(target, event);
|
|
};
|
|
|
|
handleKeyDown = (e) => {
|
|
/*
|
|
prevent these events from propagating to containing form/dialog
|
|
and causing further side effects (eg, closing dialog, submitting
|
|
form, etc).
|
|
*/
|
|
const { keyCode } = e;
|
|
if (keyCode === Keys.ENTER || keyCode === Keys.ESCAPE) {
|
|
e.preventDefault();
|
|
}
|
|
if (keyCode === Keys.ESCAPE) {
|
|
e.stopPropagation();
|
|
}
|
|
};
|
|
|
|
handleChange = (e) => {
|
|
const { onChange } = this.props;
|
|
if (onChange) onChange(e.target.value);
|
|
};
|
|
|
|
renderLabelSuggestion = (queryResult, { handleClick, modifiers }) => {
|
|
if (queryResult.newLabel) {
|
|
const { newLabelMessage } = this.props;
|
|
return (
|
|
<MenuItem
|
|
icon="flag"
|
|
active={modifiers.active}
|
|
disabled={modifiers.disabled}
|
|
key={queryResult.target}
|
|
onClick={handleClick}
|
|
text={<em>{queryResult.target}</em>}
|
|
label={newLabelMessage || "New label"}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<MenuItem
|
|
active={modifiers.active}
|
|
disabled={modifiers.disabled}
|
|
key={queryResult.target}
|
|
onClick={handleClick}
|
|
text={queryResult.target}
|
|
/>
|
|
);
|
|
};
|
|
|
|
filterLabels(query) {
|
|
const { labelSuggestions } = this.props;
|
|
if (!labelSuggestions) return [];
|
|
|
|
/* empty query is wildcard */
|
|
if (query === "") {
|
|
return labelSuggestions
|
|
.slice(0, LabelInput.QueryResultLimit)
|
|
.map((l) => ({
|
|
target: l,
|
|
score: -10000,
|
|
}));
|
|
}
|
|
|
|
/* else, do a fuzzy query */
|
|
const options = {
|
|
limit: LabelInput.QueryResultLimit,
|
|
threshold: -10000, // don't return bad results
|
|
};
|
|
let queryResults = fuzzysort.go(query, labelSuggestions, options);
|
|
/* exact match will always be first in list */
|
|
if (query !== "" && queryResults[0]?.target !== query)
|
|
queryResults = [{ target: query, newLabel: true }, ...queryResults];
|
|
|
|
return queryResults;
|
|
}
|
|
|
|
render() {
|
|
const { props } = this;
|
|
const { labelSuggestions, label, autoFocus = true } = props;
|
|
const suggestEnabled = !!labelSuggestions && labelSuggestions.length > 0;
|
|
|
|
if (!suggestEnabled) {
|
|
return (
|
|
<InputGroup
|
|
autoFocus={autoFocus}
|
|
{...props.inputProps} // eslint-disable-line react/jsx-props-no-spreading --- Allows for modularity
|
|
value={label}
|
|
onChange={this.handleChange}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const popoverProps = {
|
|
minimal: true,
|
|
...props.popoverProps,
|
|
};
|
|
const inputProps = {
|
|
...props.inputProps,
|
|
autoFocus: false,
|
|
};
|
|
const { queryResults } = this.state;
|
|
return (
|
|
<>
|
|
<Suggest
|
|
fill
|
|
inputValueRenderer={(i) => i.target}
|
|
items={queryResults}
|
|
itemRenderer={this.renderLabelSuggestion}
|
|
onItemSelect={this.handleItemSelect}
|
|
query={label}
|
|
onQueryChange={this.handleQueryChange}
|
|
popoverProps={popoverProps}
|
|
inputProps={inputProps}
|
|
onKeyDown={this.handleKeyDown}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
}
|