diff --git a/README.md b/README.md index 642cd8f2ec..7929adb4b5 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ models** [![PyPI python](https://img.shields.io/pypi/pyversions/fiftyone)](https://pypi.org/project/fiftyone) [![PyPI version](https://badge.fury.io/py/fiftyone.svg)](https://pypi.org/project/fiftyone) [![Downloads](https://pepy.tech/badge/fiftyone)](https://pepy.tech/project/fiftyone) +[![Docker Pulls](https://badgen.net/docker/pulls/voxel51/fiftyone?icon=docker&label=pulls)](https://hub.docker.com/r/voxel51/fiftyone/) [![Build](https://github.com/voxel51/fiftyone/workflows/Build/badge.svg?branch=develop&event=push)](https://github.com/voxel51/fiftyone/actions?query=workflow%3ABuild) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![Slack](https://img.shields.io/badge/Slack-4A154B?logo=slack&logoColor=white)](https://slack.voxel51.com) diff --git a/app/package.json b/app/package.json index 96018190d4..b87856532f 100644 --- a/app/package.json +++ b/app/package.json @@ -16,7 +16,7 @@ "start": "yarn workspace @fiftyone/app start", "start-desktop": "yarn workspace FiftyOne start-desktop", "test": "yarn vitest run", - "test-ui": "yarn vitest --ui", + "test-ui": "yarn vitest --ui --coverage", "gen:schema": "strawberry export-schema fiftyone.server.app:schema > schema.graphql" }, "devDependencies": { diff --git a/app/packages/core/src/components/Filters/BooleanFieldFilter.tsx b/app/packages/core/src/components/Filters/BooleanFieldFilter.tsx index 3407a01193..12769a7491 100644 --- a/app/packages/core/src/components/Filters/BooleanFieldFilter.tsx +++ b/app/packages/core/src/components/Filters/BooleanFieldFilter.tsx @@ -1,12 +1,10 @@ -import React from "react"; - import { + boolExcludeAtom, + boolIsMatchingAtom, booleanCountResults, booleanSelectedValuesAtom, - boolIsMatchingAtom, - boolOnlyMatchAtom, - boolExcludeAtom, } from "@fiftyone/state"; +import React from "react"; import CategoricalFilter from "./categoricalFilter/CategoricalFilter"; const BooleanFieldFilter = ({ @@ -27,7 +25,6 @@ const BooleanFieldFilter = ({ selectedValuesAtom={booleanSelectedValuesAtom({ path, modal })} isMatchingAtom={boolIsMatchingAtom({ path, modal })} - onlyMatchAtom={boolOnlyMatchAtom({ path, modal })} excludeAtom={boolExcludeAtom({ path, modal })} countsAtom={booleanCountResults({ path, diff --git a/app/packages/core/src/components/Filters/LabelFieldFilter.tsx b/app/packages/core/src/components/Filters/LabelFieldFilter.tsx index ea672b231f..14cfc9b2e9 100644 --- a/app/packages/core/src/components/Filters/LabelFieldFilter.tsx +++ b/app/packages/core/src/components/Filters/LabelFieldFilter.tsx @@ -2,12 +2,11 @@ import React from "react"; import { isMatchingAtom, - onlyMatchAtom, stringExcludeAtom, stringSelectedValuesAtom, } from "@fiftyone/state"; -import CategoricalFilter from "./categoricalFilter/CategoricalFilter"; import { labelTagsCount } from "../Sidebar/Entries/EntryCounts"; +import CategoricalFilter from "./categoricalFilter/CategoricalFilter"; const LabelTagFieldFilter = ({ path, @@ -27,7 +26,6 @@ const LabelTagFieldFilter = ({ selectedValuesAtom={stringSelectedValuesAtom({ modal, path })} excludeAtom={stringExcludeAtom({ modal, path })} - onlyMatchAtom={onlyMatchAtom({ modal, path })} isMatchingAtom={isMatchingAtom({ modal, path })} countsAtom={labelTagsCount({ modal, extended: false })} path={path} diff --git a/app/packages/core/src/components/Filters/NumericFieldFilter.tsx b/app/packages/core/src/components/Filters/NumericFieldFilter.tsx index 890fd81764..e33b386204 100644 --- a/app/packages/core/src/components/Filters/NumericFieldFilter.tsx +++ b/app/packages/core/src/components/Filters/NumericFieldFilter.tsx @@ -10,14 +10,14 @@ import styled from "styled-components"; import * as fos from "@fiftyone/state"; -import RangeSlider from "../Common/RangeSlider"; -import Checkbox from "../Common/Checkbox"; -import { Button } from "../utils"; import { DATE_FIELD, DATE_TIME_FIELD, FLOAT_FIELD } from "@fiftyone/utilities"; import { formatDateTime } from "../../utils/generic"; -import withSuspense from "./withSuspense"; +import Checkbox from "../Common/Checkbox"; +import RangeSlider from "../Common/RangeSlider"; import FieldLabelAndInfo from "../FieldLabelAndInfo"; +import { Button } from "../utils"; import FilterOption from "./categoricalFilter/filterOption/FilterOption"; +import withSuspense from "./withSuspense"; const NamedRangeSliderContainer = styled.div` margin: 3px; @@ -135,11 +135,6 @@ const NumericFieldFilter = ({ modal, defaultRange, }); - const onlyMatchAtom = fos.numericOnlyMatchAtom({ - path, - modal, - defaultRange, - }); const values = useRecoilValue( fos.rangeAtom({ modal, @@ -149,7 +144,6 @@ const NumericFieldFilter = ({ }) ); const setExcluded = excludeAtom ? useSetRecoilState(excludeAtom) : null; - const setOnlyMatch = onlyMatchAtom ? useSetRecoilState(onlyMatchAtom) : null; const setIsMatching = isMatchingAtom ? useSetRecoilState(isMatchingAtom) : null; @@ -207,7 +201,6 @@ const NumericFieldFilter = ({ const initializeSettings = () => { setFilter([null, null]); setExcluded && setExcluded(false); - setOnlyMatch && setOnlyMatch(true); setIsMatching && setIsMatching(!nestedField); }; @@ -304,7 +297,6 @@ const NumericFieldFilter = ({ nestedField={nestedField} shouldNotShowExclude={false} // only boolean fields don't use exclude excludeAtom={excludeAtom} - onlyMatchAtom={onlyMatchAtom} isMatchingAtom={isMatchingAtom} valueName={field?.name ?? ""} path={path} diff --git a/app/packages/core/src/components/Filters/StringFieldFilter.tsx b/app/packages/core/src/components/Filters/StringFieldFilter.tsx index aa8f3df7c1..a2697f5598 100644 --- a/app/packages/core/src/components/Filters/StringFieldFilter.tsx +++ b/app/packages/core/src/components/Filters/StringFieldFilter.tsx @@ -1,12 +1,10 @@ -import React from "react"; import * as fos from "@fiftyone/state"; - import { isMatchingAtom, - onlyMatchAtom, stringExcludeAtom, stringSelectedValuesAtom, } from "@fiftyone/state"; +import React from "react"; import CategoricalFilter from "./categoricalFilter/CategoricalFilter"; const StringFieldFilter = ({ @@ -27,7 +25,6 @@ const StringFieldFilter = ({ selectedValuesAtom={stringSelectedValuesAtom({ modal, path })} excludeAtom={stringExcludeAtom({ modal, path })} - onlyMatchAtom={onlyMatchAtom({ modal, path })} isMatchingAtom={isMatchingAtom({ modal, path })} countsAtom={fos.stringCountResults({ modal, diff --git a/app/packages/core/src/components/Filters/categoricalFilter/CategoricalFilter.tsx b/app/packages/core/src/components/Filters/categoricalFilter/CategoricalFilter.tsx index efe73f4366..bcb5ceac19 100644 --- a/app/packages/core/src/components/Filters/categoricalFilter/CategoricalFilter.tsx +++ b/app/packages/core/src/components/Filters/categoricalFilter/CategoricalFilter.tsx @@ -175,7 +175,6 @@ interface Props { selectedValuesAtom: RecoilState; excludeAtom: RecoilState; // toggles select or exclude isMatchingAtom: RecoilState; // toggles match or filter - onlyMatchAtom: RecoilState; // toggles onlyMatch mode (omit empty samples) countsAtom: RecoilValue<{ count: number; results: [T["value"], number][]; @@ -190,7 +189,6 @@ const CategoricalFilter = ({ countsAtom, selectedValuesAtom, excludeAtom, - onlyMatchAtom, isMatchingAtom, path, modal, @@ -203,6 +201,7 @@ const CategoricalFilter = ({ : path.startsWith("_label_tags") ? "label tag" : name; + const selectedCounts = useRef(new Map()); const onSelect = useOnSelect(selectedValuesAtom, selectedCounts); const useSearch = getUseSearch({ modal, path }); @@ -213,7 +212,7 @@ const CategoricalFilter = ({ // id fields should always use filter mode const neverShowExpansion = field?.ftype?.includes("ObjectIdField"); - + if (countsLoadable.state === "hasError") throw countsLoadable.contents; if (countsLoadable.state !== "hasValue") return null; const { count, results } = countsLoadable.contents; @@ -267,7 +266,6 @@ const CategoricalFilter = ({ selectedValuesAtom={selectedValuesAtom} excludeAtom={excludeAtom} isMatchingAtom={isMatchingAtom} - onlyMatchAtom={onlyMatchAtom} modal={modal} totalCount={count} selectedCounts={selectedCounts} diff --git a/app/packages/core/src/components/Filters/categoricalFilter/Wrapper.tsx b/app/packages/core/src/components/Filters/categoricalFilter/Wrapper.tsx index 2853f8b614..381371ca3a 100644 --- a/app/packages/core/src/components/Filters/categoricalFilter/Wrapper.tsx +++ b/app/packages/core/src/components/Filters/categoricalFilter/Wrapper.tsx @@ -8,18 +8,17 @@ import { import * as fos from "@fiftyone/state"; -import FilterOption from "./filterOption/FilterOption"; import Checkbox from "../../Common/Checkbox"; import { Button } from "../../utils"; import { CHECKBOX_LIMIT, nullSort } from "../utils"; -import { isKeypointLabel, V } from "./CategoricalFilter"; +import { V, isKeypointLabel } from "./CategoricalFilter"; +import FilterOption from "./filterOption/FilterOption"; interface WrapperProps { results: [V["value"], number][]; selectedValuesAtom: RecoilState; excludeAtom: RecoilState; isMatchingAtom: RecoilState; - onlyMatchAtom: RecoilState; color: string; totalCount: number; modal: boolean; @@ -34,7 +33,6 @@ const Wrapper = ({ selectedValuesAtom, excludeAtom, isMatchingAtom, - onlyMatchAtom, modal, path, selectedCounts, @@ -44,7 +42,6 @@ const Wrapper = ({ const [selected, setSelected] = useRecoilState(selectedValuesAtom); const selectedSet = new Set(selected); const setExcluded = excludeAtom ? useSetRecoilState(excludeAtom) : null; - const setOnlyMatch = onlyMatchAtom ? useSetRecoilState(onlyMatchAtom) : null; const setIsMatching = isMatchingAtom ? useSetRecoilState(isMatchingAtom) : null; @@ -87,7 +84,6 @@ const Wrapper = ({ const initializeSettings = () => { setExcluded && setExcluded(false); - setOnlyMatch && setOnlyMatch(true); setIsMatching && setIsMatching(!nestedField); }; @@ -144,7 +140,6 @@ const Wrapper = ({ nestedField={nestedField} shouldNotShowExclude={shouldNotShowExclude} excludeAtom={excludeAtom} - onlyMatchAtom={onlyMatchAtom} isMatchingAtom={isMatchingAtom} valueName={name} color={color} diff --git a/app/packages/core/src/components/Filters/categoricalFilter/filterOption/FilterOption.tsx b/app/packages/core/src/components/Filters/categoricalFilter/filterOption/FilterOption.tsx index 8cb7239ff6..29d5afa561 100644 --- a/app/packages/core/src/components/Filters/categoricalFilter/filterOption/FilterOption.tsx +++ b/app/packages/core/src/components/Filters/categoricalFilter/filterOption/FilterOption.tsx @@ -1,27 +1,24 @@ -import React, { PropsWithChildren, useEffect } from "react"; -import styled from "styled-components"; -import { RecoilState, useRecoilState, useSetRecoilState } from "recoil"; import FilterAltIcon from "@mui/icons-material/FilterAlt"; import FilterAltOffIcon from "@mui/icons-material/FilterAltOff"; -import ImageIcon from "@mui/icons-material/Image"; import HideImageIcon from "@mui/icons-material/HideImage"; +import ImageIcon from "@mui/icons-material/Image"; import { IconButton } from "@mui/material"; -import { useSpring } from "framer-motion"; import Color from "color"; +import React, { useEffect } from "react"; +import { RecoilState, useRecoilState, useSetRecoilState } from "recoil"; +import styled from "styled-components"; -import { useOutsideClick } from "@fiftyone/state"; import { useTheme } from "@fiftyone/components/src/components/ThemeProvider"; import Tooltip from "@fiftyone/components/src/components/Tooltip"; +import { useOutsideClick } from "@fiftyone/state"; -import { PopoutDiv } from "../../../utils"; -import Item from "./FilterItem"; import { Popout } from "@fiftyone/components"; +import Item from "./FilterItem"; interface Props { nestedField: string | undefined; // nested ListFields only ("detections") shouldNotShowExclude: boolean; // for BooleanFields excludeAtom: RecoilState; - onlyMatchAtom: RecoilState; isMatchingAtom: RecoilState; valueName: string; color: string; @@ -132,7 +129,6 @@ const FilterOption: React.FC = ({ nestedField, shouldNotShowExclude, excludeAtom, - onlyMatchAtom, isMatchingAtom, }) => { const isLabelTag = path?.startsWith("_label_tags"); @@ -140,7 +136,6 @@ const FilterOption: React.FC = ({ const [open, setOpen] = React.useState(false); const [excluded, setExcluded] = useRecoilState(excludeAtom); - const setOnlyMatch = onlyMatchAtom ? useSetRecoilState(onlyMatchAtom) : null; const setIsMatching = isMatchingAtom ? useSetRecoilState(isMatchingAtom) : null; @@ -213,25 +208,21 @@ const FilterOption: React.FC = ({ const onSelectFilter = () => { setExcluded && setExcluded(false); setIsMatching && setIsMatching(false); - setOnlyMatch && setOnlyMatch(true); }; const onSelectNegativeFilter = () => { setExcluded && setExcluded(true); setIsMatching && setIsMatching(false); - setOnlyMatch && setOnlyMatch(false); }; const onSelectMatch = () => { setExcluded && setExcluded(false); setIsMatching && setIsMatching(true); - setOnlyMatch && setOnlyMatch(true); }; const onSelectNegativeMatch = () => { setExcluded && setExcluded(true); setIsMatching && setIsMatching(true); - setOnlyMatch && setOnlyMatch(true); }; const children = ( diff --git a/app/packages/core/src/plugins/SchemaIO/components/DropdownView.tsx b/app/packages/core/src/plugins/SchemaIO/components/DropdownView.tsx index 39d580f727..d9ad7d0ce7 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/DropdownView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/DropdownView.tsx @@ -1,15 +1,46 @@ +import { Box, ListItemText, MenuItem, Select, Typography } from "@mui/material"; import React from "react"; -import { Box, Typography, Select, MenuItem, ListItemText } from "@mui/material"; -import FieldWrapper from "./FieldWrapper"; -import autoFocus from "../utils/auto-focus"; import { getComponentProps } from "../utils"; +import autoFocus from "../utils/auto-focus"; +import AlertView from "./AlertView"; +import FieldWrapper from "./FieldWrapper"; + +const MULTI_SELECT_TYPES = ["string", "array"]; export default function DropdownView(props) { const { onChange, schema, path, data } = props; - const { view = {}, type } = schema; - const { choices, placeholder = "", readOnly } = view; + const { default: defaultValue, view = {}, type } = schema; + const { + choices, + multiple: multiSelect, + placeholder = "", + separator = ",", + readOnly, + } = view; + + if (multiSelect && !MULTI_SELECT_TYPES.includes(type)) + return ( + + ); - const multiple = type === "array"; + const isArrayType = type === "array"; + const multiple = multiSelect || isArrayType; + const fallbackDefaultValue = multiple ? [] : ""; + const rawDefaultValue = data ?? defaultValue ?? fallbackDefaultValue; + const computedDefaultValue = + multiple && !Array.isArray(rawDefaultValue) + ? rawDefaultValue.toString().split(separator) + : rawDefaultValue; const choiceLabels = choices.reduce((labels, choice) => { labels[choice.value] = choice.label; @@ -21,7 +52,7 @@ export default function DropdownView(props) {