Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AutoComplete and DropDown operator View improvements #5375

Merged
merged 8 commits into from
Jan 13, 2025
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
1 change: 1 addition & 0 deletions app/packages/core/src/plugins/OperatorIO/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { types } from "@fiftyone/operators";
import { DropdownView } from "../../SchemaIO/components";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✂️


const inputComponentsByType = {
Object: "ObjectView",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { Autocomplete, MenuItem, TextField } from "@mui/material";
import { Autocomplete, MenuItem, Select, TextField } from "@mui/material";
import FieldWrapper from "./FieldWrapper";
import autoFocus from "../utils/auto-focus";
import { getComponentProps } from "../utils";
Expand All @@ -12,20 +12,35 @@ export default function AutocompleteView(props) {
const { choices = [], readOnly } = view;

const multiple = schema.type === "array";
const allowDups = view.allow_duplicates !== false;
const [key, setUserChanged] = useKey(path, schema, data, true);

const valuesOnly = getValuesOnlySettingFromSchema(schema);
const allowUserInput = view.allow_user_input !== false;
const allowClearing = view.allow_clearing !== false;
return (
<FieldWrapper {...props}>
<Autocomplete
disableClearable={!allowClearing}
key={key}
disabled={readOnly}
autoHighlight
clearOnBlur={multiple}
defaultValue={getDefaultValue(data, choices)}
freeSolo
freeSolo={allowUserInput}
size="small"
onChange={(e, choice) => {
onChange(path, choice?.value || choice);
if (choice === null) {
onChange(path, null);
setUserChanged();
return;
}
const changedValue = resolveChangedValues(
schema,
choice,
valuesOnly,
multiple
);
onChange(path, changedValue);
setUserChanged();
}}
options={choices.map((choice) => ({
Expand All @@ -35,7 +50,6 @@ export default function AutocompleteView(props) {
}))}
renderInput={(params) => (
<TextField
autoFocus={autoFocus(props)}
{...params}
placeholder={
multiple
Expand All @@ -45,12 +59,22 @@ export default function AutocompleteView(props) {
/>
)}
onInputChange={(e) => {
if (!multiple && e) {
if (!e) return;
if (!e.target.value && !multiple) {
onChange(path, null);
setUserChanged();
}
if (!multiple && e && allowUserInput) {
onChange(path, e.target.value);
setUserChanged();
}
}}
isOptionEqualToValue={() => false} // allow duplicates
isOptionEqualToValue={(option, value) => {
if (allowDups) return false;
option = resolveChangedValue(schema, option, true);
value = resolveChangedValue(schema, value, true);
return option == value;
}}
multiple={multiple}
renderOption={(props, option) => {
return (
Expand All @@ -68,7 +92,53 @@ export default function AutocompleteView(props) {
);
}

// TODO: move these functions to a utils file

function getDefaultValue(defaultValue, choices = []) {
const choice = choices.find(({ value }) => value === defaultValue);
return choice || defaultValue;
}

function getValuesOnlySettingFromSchema(schema) {
const { view = {} } = schema;
const isObject = schema.type === "object";
const providedSetting = view.value_only;
const isDefined = providedSetting !== undefined && providedSetting !== null;
if (isDefined) return providedSetting;
if (isObject) return false;
return true;
}

function resolveChangedValues(schema, choice, valuesOnly, multiple) {
if (multiple) {
if (Array.isArray(choice))
return choice.map((c) => resolveChangedValue(schema, c, valuesOnly));
return [];
} else {
return resolveChangedValue(schema, choice, valuesOnly);
}
}

function resolveChangedValue(schema, choice, valuesOnly) {
let resolvedValue;
const isObjectSchema = schema.type === "object";
const isStringSchema = schema.type === "string";
const isChoiceObject =
typeof choice === "object" &&
choice.value !== undefined &&
choice.value !== null;
const valuesOnlyIsDefault = valuesOnly == undefined || valuesOnly == null;
if (!isChoiceObject) {
choice = { id: choice, value: choice };
}
if (isStringSchema && valuesOnlyIsDefault) {
valuesOnly = true;
}
if (isObjectSchema) {
resolvedValue = choice;
}
if (valuesOnly) {
resolvedValue = choice.value;
}
return resolvedValue;
}
18 changes: 18 additions & 0 deletions app/packages/core/src/plugins/SchemaIO/components/DropdownView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ViewPropsType } from "../utils/types";
import AlertView from "./AlertView";
import ChoiceMenuItemBody from "./ChoiceMenuItemBody";
import FieldWrapper from "./FieldWrapper";
import { AutocompleteView } from ".";

// if we want to support more icons in the future, add them here
const iconImports: {
Expand Down Expand Up @@ -155,6 +156,23 @@ export default function DropdownView(props: ViewPropsType) {
);
};

if (multiple) {
return (
<AutocompleteView
{...props}
schema={{
...schema,
view: {
value_only: true,
allow_user_input: false,
allow_duplicates: false,
...view,
},
}}
/>
);
}

return (
<FieldWrapper {...props} hideHeader={compact}>
<Tooltip title={tooltipTitle}>
Expand Down
12 changes: 12 additions & 0 deletions app/packages/core/src/plugins/SchemaIO/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function SchemaIOComponent(props) {

const onIOChange = useCallback(
(path, value, schema, ancestors) => {
value = coerceValue(value, schema);
const currentState = stateRef.current;
const updatedState = cloneDeep(currentState);
set(updatedState, path, cloneDeep(value));
Expand Down Expand Up @@ -63,6 +64,17 @@ export function SchemaIOComponent(props) {
);
}

function coerceValue(value, schema) {
// coerce the value to None if it is an empty string or empty array
if (schema.type === "array" && Array.isArray(value) && value.length === 0) {
return null;
}
if (schema.type === "string" && value === "") {
return null;
}
return value;
}

registerComponent({
name: "SchemaIOComponent",
label: "SchemaIOComponent",
Expand Down
4 changes: 3 additions & 1 deletion fiftyone/operators/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,8 @@ class AutocompleteView(Choices):
Args:
choices (None): a list of :class:`Choice` instances
read_only (False): whether the view is read-only
allow_user_input (True): when True the user can input a value that is not in the choices
allow_duplicates (True): when True the user can select the same choice multiple times
"""

def __init__(self, **kwargs):
Expand Down Expand Up @@ -1924,7 +1926,7 @@ def clone(self):
clone.tooltips = [tooltip.clone() for tooltip in self.tooltips]
clone._tooltip_map = {
(tooltip.row, tooltip.column): tooltip
for tooltip in clone.tooltips
for tooltip in clone.tooltips # pylint: disable=no-member
}
return clone

Expand Down
Loading