From 677e817b42087fb439c7fc3819f309359d32c0c7 Mon Sep 17 00:00:00 2001 From: Yousif Yassi Date: Tue, 22 Apr 2025 14:38:03 -0400 Subject: [PATCH] Revert "feat: OPTIC-1733: Standardize Dropdown Components Using LSE selector (#7257)" This reverts commit ddb598a3e2f0aedb7d58055aa80b1937ee7f901f. --- label_studio/core/static/css/uikit.css | 2 +- .../actions/predictions_to_annotations.py | 3 +- .../components/Form/Elements/Input/Input.scss | 6 +- .../Form/Elements/Select/Select.jsx | 53 ++- .../Form/Elements/Select/Select.scss | 38 ++ .../src/components/Pagination/Pagination.scss | 8 +- .../src/components/Pagination/Pagination.tsx | 6 +- .../src/pages/CreateProject/Config/Config.jsx | 84 ++-- .../pages/CreateProject/Config/Config.scss | 2 - .../src/pages/CreateProject/CreateProject.jsx | 4 +- .../ModelVersionSelector.jsx | 5 +- .../src/pages/Settings/GeneralSettings.jsx | 4 +- .../MachineLearningSettings/Forms.jsx | 7 +- .../Settings/StorageSettings/StorageForm.jsx | 4 +- .../SampleDatasetSelect.tsx | 46 +- .../CellViews/Annotators/Annotators.jsx | 9 - .../Common/DatePicker/DatePicker.jsx | 27 +- .../Common/DatePicker/DatePicker.scss | 9 - .../Common/Dropdown/DropdownTrigger.jsx | 19 +- .../src/components/Common/FiltersPane.jsx | 3 - .../Common/Form/Elements/Select/Select.jsx | 47 +- .../Common/Form/Elements/Select/Select.scss | 36 ++ .../src/components/Common/Input/Input.jsx | 5 +- .../src/components/Common/Input/Input.scss | 1 + .../Common/Pagination/Pagination.tsx | 4 +- .../src/components/Common/Select/Select.jsx | 204 +++++++++ .../src/components/Common/Select/Select.scss | 101 +++++ .../DataManager/Toolbar/ActionsButton.jsx | 1 - .../src/components/Filters/FilterDropdown.jsx | 83 ++-- .../src/components/Filters/FilterInput.jsx | 2 +- .../Filters/FilterLine/FilterLine.jsx | 4 +- .../Filters/FilterLine/FilterLine.scss | 7 +- .../Filters/FilterLine/FilterOperation.jsx | 1 - .../src/components/Filters/Filters.scss | 2 +- .../src/components/Filters/types/Date.jsx | 2 +- .../src/components/Filters/types/List.jsx | 7 +- .../src/common/Pagination/Pagination.tsx | 20 +- web/libs/editor/src/common/Select/Select.scss | 148 +++++++ web/libs/editor/src/common/Select/Select.tsx | 293 ++++++++++++ .../editor/src/components/Filter/Filter.scss | 42 ++ .../editor/src/components/Filter/Filter.tsx | 132 ++++++ .../src/components/Filter/FilterDropdown.tsx | 53 +++ .../src/components/Filter/FilterInput.tsx | 36 ++ .../components/Filter/FilterInterfaces.tsx | 26 ++ .../src/components/Filter/FilterRow.scss | 25 ++ .../src/components/Filter/FilterRow.tsx | 118 +++++ .../Filter/__tests__/Filter.test.tsx | 244 ++++++++++ .../Filter/__tests__/FilterRow.test.tsx | 149 +++++++ .../Filter/__tests__/filter-utils.test.tsx | 284 ++++++++++++ .../src/components/Filter/filter-util.ts | 192 ++++++++ .../src/components/Filter/types/Boolean.jsx | 23 + .../src/components/Filter/types/Common.jsx | 14 + .../src/components/Filter/types/Number.jsx | 85 ++++ .../src/components/Filter/types/String.jsx | 50 +++ .../src/components/Filter/types/index.js | 4 + .../SidePanels/DetailsPanel/RegionEditor.tsx | 15 +- .../SidePanels/DetailsPanel/Relations.tsx | 19 +- .../src/components/Waveform/Waveform.jsx | 16 +- web/libs/editor/src/tags/control/Choices.jsx | 33 +- web/libs/editor/src/tags/control/DateTime.jsx | 44 +- .../tags/object/Paragraphs/AuthorFilter.jsx | 60 ++- .../editor/src/utils/__tests__/date.test.js | 12 +- .../tests/e2e/fragments/AtParagraphs.js | 29 +- .../editor/tests/e2e/tests/date-time.test.js | 17 +- .../tests/e2e/tests/paragraphs-filter.test.js | 7 +- .../integration/e2e/control_tags/choice.cy.ts | 2 +- .../taxonomy-mig-per-item.cy.ts | 1 - .../frontend-test/src/helpers/LSF/Choices.ts | 5 +- web/libs/storybook/select/select.stories.tsx | 167 ++----- web/libs/ui/package.json | 1 + web/libs/ui/src/assets/icons/search.svg | 6 +- web/libs/ui/src/index.ts | 1 - .../ui/src/lib/Tooltip/Tooltip.module.scss | 2 +- web/libs/ui/src/lib/label/label.module.scss | 5 +- web/libs/ui/src/lib/select/select.module.scss | 83 ---- web/libs/ui/src/lib/select/select.tsx | 418 ------------------ web/libs/ui/src/lib/select/types.ts | 94 ---- web/libs/ui/src/lib/toggle/toggle.tsx | 9 +- .../ui/src/shad/components/ui/command.tsx | 92 ---- web/libs/ui/src/shad/components/ui/dialog.tsx | 111 ----- .../ui/src/shad/components/ui/popover.tsx | 41 -- web/libs/ui/src/shad/components/ui/select.tsx | 150 +++++++ web/package.json | 5 +- web/yarn.lock | 258 +++++------ 84 files changed, 2981 insertions(+), 1506 deletions(-) create mode 100644 web/apps/labelstudio/src/components/Form/Elements/Select/Select.scss create mode 100644 web/libs/datamanager/src/components/Common/Form/Elements/Select/Select.scss create mode 100644 web/libs/datamanager/src/components/Common/Select/Select.jsx create mode 100644 web/libs/datamanager/src/components/Common/Select/Select.scss create mode 100644 web/libs/editor/src/common/Select/Select.scss create mode 100644 web/libs/editor/src/common/Select/Select.tsx create mode 100644 web/libs/editor/src/components/Filter/Filter.scss create mode 100644 web/libs/editor/src/components/Filter/Filter.tsx create mode 100644 web/libs/editor/src/components/Filter/FilterDropdown.tsx create mode 100644 web/libs/editor/src/components/Filter/FilterInput.tsx create mode 100644 web/libs/editor/src/components/Filter/FilterInterfaces.tsx create mode 100644 web/libs/editor/src/components/Filter/FilterRow.scss create mode 100644 web/libs/editor/src/components/Filter/FilterRow.tsx create mode 100644 web/libs/editor/src/components/Filter/__tests__/Filter.test.tsx create mode 100644 web/libs/editor/src/components/Filter/__tests__/FilterRow.test.tsx create mode 100644 web/libs/editor/src/components/Filter/__tests__/filter-utils.test.tsx create mode 100644 web/libs/editor/src/components/Filter/filter-util.ts create mode 100644 web/libs/editor/src/components/Filter/types/Boolean.jsx create mode 100644 web/libs/editor/src/components/Filter/types/Common.jsx create mode 100644 web/libs/editor/src/components/Filter/types/Number.jsx create mode 100644 web/libs/editor/src/components/Filter/types/String.jsx create mode 100644 web/libs/editor/src/components/Filter/types/index.js delete mode 100644 web/libs/ui/src/lib/select/select.module.scss delete mode 100644 web/libs/ui/src/lib/select/select.tsx delete mode 100644 web/libs/ui/src/lib/select/types.ts delete mode 100644 web/libs/ui/src/shad/components/ui/command.tsx delete mode 100644 web/libs/ui/src/shad/components/ui/dialog.tsx delete mode 100644 web/libs/ui/src/shad/components/ui/popover.tsx create mode 100644 web/libs/ui/src/shad/components/ui/select.tsx diff --git a/label_studio/core/static/css/uikit.css b/label_studio/core/static/css/uikit.css index d5d5d5341daa..07677d28e752 100644 --- a/label_studio/core/static/css/uikit.css +++ b/label_studio/core/static/css/uikit.css @@ -22,7 +22,7 @@ body { display: block; } .field--wide > *:not(:first-child) { - display: flex; + display: block; margin-top: 4px; width: 100%; box-sizing: border-box; diff --git a/label_studio/data_manager/actions/predictions_to_annotations.py b/label_studio/data_manager/actions/predictions_to_annotations.py index 26e62666db56..56964eb5c30d 100644 --- a/label_studio/data_manager/actions/predictions_to_annotations.py +++ b/label_studio/data_manager/actions/predictions_to_annotations.py @@ -91,7 +91,6 @@ def predictions_to_annotations_form(user, project): 'name': 'model_version', 'label': 'Choose predictions', 'options': versions, - 'value': first, } ], } @@ -107,7 +106,7 @@ def predictions_to_annotations_form(user, project): 'dialog': { 'title': 'Create Annotations From Predictions', 'text': 'Create annotations from predictions using selected predictions set ' - 'for each selected task. ' + 'for each selected task.' 'Your account will be assigned as an owner to those annotations. ', 'type': 'confirm', 'form': predictions_to_annotations_form, diff --git a/web/apps/labelstudio/src/components/Form/Elements/Input/Input.scss b/web/apps/labelstudio/src/components/Form/Elements/Input/Input.scss index 63ba912c0fbc..42b92837da95 100644 --- a/web/apps/labelstudio/src/components/Form/Elements/Input/Input.scss +++ b/web/apps/labelstudio/src/components/Form/Elements/Input/Input.scss @@ -1,10 +1,8 @@ .input-ls, .select-ls, .textarea-ls { - --input-size: 40px; - - height: var(--input-size); - min-height: var(--input-size); + height: 40px; + min-height: 40px; background: var(--sand_0); font-size: 16px; line-height: 22px; diff --git a/web/apps/labelstudio/src/components/Form/Elements/Select/Select.jsx b/web/apps/labelstudio/src/components/Form/Elements/Select/Select.jsx index bb65f28ad30f..59212fdaf5a7 100644 --- a/web/apps/labelstudio/src/components/Form/Elements/Select/Select.jsx +++ b/web/apps/labelstudio/src/components/Form/Elements/Select/Select.jsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { cn } from "../../../../utils/bem"; import { FormField } from "../../FormField"; import { default as Label } from "../Label/Label"; -import { Select as SelectUI } from "@humansignal/ui"; +import "./Select.scss"; const SelectOption = ({ value, label, disabled = false, hidden = false, ...props }) => { return ( @@ -24,23 +24,16 @@ const Select = ({ label, className, options, validate, required, skip, labelProp return groupedOptions; }, {}); + const renderOptions = (option) => { + return ; + }; + const classList = rootClass.mod({ ghost }).mix(className); useEffect(() => { setValue(initialValue); }, [initialValue]); - const selectOptions = useMemo(() => { - return Object.keys(grouped).flatMap((group) => { - return group === "NoGroup" - ? grouped[group] - : (grouped[group] = { - label: group, - children: grouped[group], - }); - }); - }, [grouped]); - const selectWrapper = ( {(ref) => { return ( - { - setValue(val); - props.onChange?.(val); - }} - ref={ref} - options={selectOptions} - /> +
+ +
); }}
diff --git a/web/apps/labelstudio/src/components/Form/Elements/Select/Select.scss b/web/apps/labelstudio/src/components/Form/Elements/Select/Select.scss new file mode 100644 index 000000000000..075251e0ace3 --- /dev/null +++ b/web/apps/labelstudio/src/components/Form/Elements/Select/Select.scss @@ -0,0 +1,38 @@ +.select-ls { + position: relative; + + &__list { + top: 0; + left: 0; + width: 100%; + height: 100%; + padding: 0 16px; + margin: 0; + border: none; + border-radius: 3px; + position: absolute; + background-color: transparent; + transition: box-shadow 80ms ease; + font-size: inherit; + border-right: 10px solid transparent; + } + + &_ghost { + margin-left: -10px; + border: 1px solid transparent; + + &__list { + padding: 0 5px; + } + + &:hover { + border: 1px solid var(--sand_300); + } + } +} + +select:disabled { + background: var(--sand_200); + color: var(--sand_500); + opacity: 1; +} diff --git a/web/apps/labelstudio/src/components/Pagination/Pagination.scss b/web/apps/labelstudio/src/components/Pagination/Pagination.scss index 18000bed00dc..83fe89e9e37e 100644 --- a/web/apps/labelstudio/src/components/Pagination/Pagination.scss +++ b/web/apps/labelstudio/src/components/Pagination/Pagination.scss @@ -1,7 +1,5 @@ .pagination-ls { - --pagination-height: 40px; - - height: var(--pagination-height); + height: 40px; display: inline-flex; align-items: center; @@ -116,12 +114,14 @@ &__input { width: 100px; - height: var(--pagination-height); + height: 38px; text-align: center; display: flex; align-items: center; justify-content: center; border: 1px solid #D9D9D9; + border-top: none; + border-bottom: none; background: var(--sand_100); margin: 1px 0; diff --git a/web/apps/labelstudio/src/components/Pagination/Pagination.tsx b/web/apps/labelstudio/src/components/Pagination/Pagination.tsx index 8589a8cec424..190807fcb424 100644 --- a/web/apps/labelstudio/src/components/Pagination/Pagination.tsx +++ b/web/apps/labelstudio/src/components/Pagination/Pagination.tsx @@ -11,7 +11,7 @@ import { import { Block, Elem } from "../../utils/bem"; import { clamp, isDefined } from "../../utils/helpers"; import { useValueTracker } from "../Form/Utils"; -import { Select } from "@humansignal/ui"; +import { Select } from "../Form/Elements"; import "./Pagination.scss"; import { useUpdateEffect } from "../../utils/hooks"; @@ -263,8 +263,8 @@ export const Pagination: FC = forwardRef( + ); case Boolean: @@ -246,7 +247,9 @@ const ConfigureColumn = ({ template, obj, columns }) => { template.render(); }; - const selectValue = (value) => { + const selectValue = (e) => { + const value = e.target.value; + if (value === "-") { setIsManual(true); return; @@ -275,40 +278,23 @@ const ConfigureColumn = ({ template, obj, columns }) => { } }; - const columnsList = useMemo(() => { - const cols = (columns ?? []).map((col) => { - return { - value: col, - label: col === DEFAULT_COLUMN ? "" : `$${col}`, - }; - }); - if (!columns?.length) { - cols.push({ value, label: "" }); - } - cols.push({ value: "-", label: "" }); - return cols; - }, [columns, DEFAULT_COLUMN, value]); - return ( - <> - } - +

+ Use {obj.tagName.toLowerCase()} + {template.objects > 1 && ` for ${obj.getAttribute("name")}`} + {" from "} + {columns?.length > 0 && columns[0] !== DEFAULT_COLUMN && "field "} + + {isManual && } +

); }; diff --git a/web/apps/labelstudio/src/pages/CreateProject/Config/Config.scss b/web/apps/labelstudio/src/pages/CreateProject/Config/Config.scss index ff3c35cd9e9e..6cb072596220 100644 --- a/web/apps/labelstudio/src/pages/CreateProject/Config/Config.scss +++ b/web/apps/labelstudio/src/pages/CreateProject/Config/Config.scss @@ -309,8 +309,6 @@ $scroll-width: 5px; margin-left: 8px; padding: 4px 8px; line-height: 1.4em; - height: 40px; - border-radius: var(--corner-radius-smaller); } select { diff --git a/web/apps/labelstudio/src/pages/CreateProject/CreateProject.jsx b/web/apps/labelstudio/src/pages/CreateProject/CreateProject.jsx index dd5639a54dd5..13159f68e0ea 100644 --- a/web/apps/labelstudio/src/pages/CreateProject/CreateProject.jsx +++ b/web/apps/labelstudio/src/pages/CreateProject/CreateProject.jsx @@ -1,4 +1,4 @@ -import { EnterpriseBadge, Select } from "@humansignal/ui"; +import { EnterpriseBadge } from "@humansignal/ui"; import React from "react"; import { useHistory } from "react-router"; import { Button, ToggleItems } from "../../components"; @@ -12,7 +12,7 @@ import "./CreateProject.scss"; import { ImportPage } from "./Import/Import"; import { useImportPage } from "./Import/useImportPage"; import { useDraftProject } from "./utils/useDraftProject"; -import { Input, TextArea } from "../../components/Form"; +import { Input, Select, TextArea } from "../../components/Form"; import { Caption } from "../../components/Caption/Caption"; import { FF_LSDV_E_297, isFF } from "../../utils/feature-flags"; import { createURL } from "../../components/HeidiTips/utils"; diff --git a/web/apps/labelstudio/src/pages/Settings/AnnotationSettings/ModelVersionSelector.jsx b/web/apps/labelstudio/src/pages/Settings/AnnotationSettings/ModelVersionSelector.jsx index a39468a1c1af..17692d6ebb25 100644 --- a/web/apps/labelstudio/src/pages/Settings/AnnotationSettings/ModelVersionSelector.jsx +++ b/web/apps/labelstudio/src/pages/Settings/AnnotationSettings/ModelVersionSelector.jsx @@ -82,10 +82,9 @@ export const ModelVersionSelector = ({ name={name} disabled={!versions.length && !models.length} value={version} - onChange={setVersion} + onChange={(e) => setVersion(e.target.value)} options={[...models, ...versions]} - placeholder={placeholder || "Please select model or predictions"} - isInProgress={loading} + placeholder={loading ? "Loading ..." : placeholder ? placeholder : "Please select model or predictions"} {...props} /> diff --git a/web/apps/labelstudio/src/pages/Settings/GeneralSettings.jsx b/web/apps/labelstudio/src/pages/Settings/GeneralSettings.jsx index e359fa99b240..3a21d733bdc5 100644 --- a/web/apps/labelstudio/src/pages/Settings/GeneralSettings.jsx +++ b/web/apps/labelstudio/src/pages/Settings/GeneralSettings.jsx @@ -1,7 +1,7 @@ -import { EnterpriseBadge, Select } from "@humansignal/ui"; +import { EnterpriseBadge } from "@humansignal/ui"; import { useCallback, useContext } from "react"; import { Button } from "../../components"; -import { Form, Input, TextArea } from "../../components/Form"; +import { Form, Input, Select, TextArea } from "../../components/Form"; import { RadioGroup } from "../../components/Form/Elements/RadioGroup/RadioGroup"; import { ProjectContext } from "../../providers/ProjectProvider"; import { Block, Elem } from "../../utils/bem"; diff --git a/web/apps/labelstudio/src/pages/Settings/MachineLearningSettings/Forms.jsx b/web/apps/labelstudio/src/pages/Settings/MachineLearningSettings/Forms.jsx index d782508239fc..c4a7a7b4bfae 100644 --- a/web/apps/labelstudio/src/pages/Settings/MachineLearningSettings/Forms.jsx +++ b/web/apps/labelstudio/src/pages/Settings/MachineLearningSettings/Forms.jsx @@ -6,7 +6,7 @@ import { Form, Input, Select, TextArea, Toggle } from "../../../components/Form" import "./MachineLearningSettings.scss"; const CustomBackendForm = ({ action, backend, project, onSubmit }) => { - const [selectedAuthMethod, setAuthMethod] = useState("NONE"); + const [selectedAuthMethod, setAuthMethod] = useState(""); const [, setMLError] = useState(); return ( @@ -38,8 +38,9 @@ const CustomBackendForm = ({ action, backend, project, onSubmit }) => { { label: "No Authentication", value: "NONE" }, { label: "Basic Authentication", value: "BASIC_AUTH" }, ]} - value={selectedAuthMethod} - onChange={setAuthMethod} + onChange={(e) => { + setAuthMethod(e.target.value); + }} /> diff --git a/web/apps/labelstudio/src/pages/Settings/StorageSettings/StorageForm.jsx b/web/apps/labelstudio/src/pages/Settings/StorageSettings/StorageForm.jsx index b57e8a3a91a1..2b03f8b25fbc 100644 --- a/web/apps/labelstudio/src/pages/Settings/StorageSettings/StorageForm.jsx +++ b/web/apps/labelstudio/src/pages/Settings/StorageSettings/StorageForm.jsx @@ -41,7 +41,9 @@ export const StorageForm = forwardRef(({ onSubmit, target, project, rootClass, s label: title, })), value: storage?.type ?? type, - onChange: setType, + onChange: (e) => { + setType(e.target.value); + }, }, ], }; diff --git a/web/libs/app-common/src/blocks/SampleDatasetSelect/SampleDatasetSelect.tsx b/web/libs/app-common/src/blocks/SampleDatasetSelect/SampleDatasetSelect.tsx index 52bd6099b653..f4f0ff5c6ba5 100644 --- a/web/libs/app-common/src/blocks/SampleDatasetSelect/SampleDatasetSelect.tsx +++ b/web/libs/app-common/src/blocks/SampleDatasetSelect/SampleDatasetSelect.tsx @@ -1,4 +1,4 @@ -import { Select } from "@humansignal/ui"; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger } from "@humansignal/shad/components/ui/select"; import { useCallback, useMemo } from "react"; type Sample = { @@ -30,41 +30,33 @@ export function SampleDatasetSelect({ [samples, onSampleApplied], ); - const options = useMemo(() => { - return samples.map((sample) => ({ - value: sample.url, - label: ( -
-
{sample.title}
-
{sample.description}
-
- ), - })); - }, [samples]); const onClick = () => { if ("__lsa" in window) { __lsa("sample.open"); } }; - const selectedValueRenderer = useCallback( - (option: any) => { - return samples.find((o) => o.url === option.value)?.title ?? option?.label; - }, - [samples], - ); - return (
or use a sample dataset - + + {title} + + + + {samples.map((sample) => ( + +
{sample.title}
+
{sample.description}
+
+ ))} +
+
+
); } diff --git a/web/libs/datamanager/src/components/CellViews/Annotators/Annotators.jsx b/web/libs/datamanager/src/components/CellViews/Annotators/Annotators.jsx index 1de781cc8e0d..3c48ec49666a 100644 --- a/web/libs/datamanager/src/components/CellViews/Annotators/Annotators.jsx +++ b/web/libs/datamanager/src/components/CellViews/Annotators/Annotators.jsx @@ -88,15 +88,6 @@ Annotators.FilterItem = UsersInjector(({ users, item }) => { ) : null; }); -Annotators.searchFilter = (option, queryString) => { - const user = DM.users.find((u) => u.id === option?.value); - return ( - user.id?.toString().toLowerCase().includes(queryString.toLowerCase()) || - user.email.toLowerCase().includes(queryString.toLowerCase()) || - user.displayName.toLowerCase().includes(queryString.toLowerCase()) - ); -}; - Annotators.filterable = true; Annotators.customOperators = [ { diff --git a/web/libs/datamanager/src/components/Common/DatePicker/DatePicker.jsx b/web/libs/datamanager/src/components/Common/DatePicker/DatePicker.jsx index e4a881639b30..b6f644e394b2 100644 --- a/web/libs/datamanager/src/components/Common/DatePicker/DatePicker.jsx +++ b/web/libs/datamanager/src/components/Common/DatePicker/DatePicker.jsx @@ -2,7 +2,7 @@ import { format, isMatch, isValid } from "date-fns"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { default as DP } from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; -import { BemWithSpecifiContext, cn } from "../../../utils/bem"; +import { BemWithSpecifiContext } from "../../../utils/bem"; import { isDefined } from "../../../utils/utils"; import { Dropdown } from "../Dropdown/Dropdown"; import Input from "../Input/Input"; @@ -106,21 +106,18 @@ export const DatePicker = ({ ref={dropdownRef} toggle={false} content={ -
- onChangeHandler(date)} - onSelect={(date) => onChangeHandler(date)} - monthsShown={2} - selectsRange={selectRange} - showTimeSelect={showTime} - inline - /> -
+ onChangeHandler(date)} + onSelect={(date) => onChangeHandler(date)} + monthsShown={2} + selectsRange={selectRange} + showTimeSelect={showTime} + inline + /> } - style={{ backgroundColor: "transparent", borderRadius: "1em" }} > div { - display: flex; - background-color: transparent; - } - } } diff --git a/web/libs/datamanager/src/components/Common/Dropdown/DropdownTrigger.jsx b/web/libs/datamanager/src/components/Common/Dropdown/DropdownTrigger.jsx index 995441040644..be0fa82df394 100644 --- a/web/libs/datamanager/src/components/Common/Dropdown/DropdownTrigger.jsx +++ b/web/libs/datamanager/src/components/Common/Dropdown/DropdownTrigger.jsx @@ -4,20 +4,7 @@ import { Dropdown } from "./DropdownComponent"; import { DropdownContext } from "./DropdownContext"; export const DropdownTrigger = React.forwardRef( - ( - { - tag, - children, - dropdown, - content, - toggle, - closeOnClickOutside = true, - disabled = false, - isChildValid = (element) => false, - ...props - }, - ref, - ) => { + ({ tag, children, dropdown, content, toggle, closeOnClickOutside = true, disabled = false, ...props }, ref) => { if (children.length > 2) throw new Error("Trigger can't contain more that one child and a dropdown"); const dropdownRef = ref ?? dropdown ?? React.useRef(); const triggerEL = React.Children.only(children); @@ -35,9 +22,9 @@ export const DropdownTrigger = React.forwardRef( return res || child.hasTarget(target); }, false); - return triggerClicked || dropdownClicked || childDropdownClicked || isChildValid(target); + return triggerClicked || dropdownClicked || childDropdownClicked; }, - [triggerRef, dropdownRef, isChildValid], + [triggerRef, dropdownRef], ); const handleClick = React.useCallback( diff --git a/web/libs/datamanager/src/components/Common/FiltersPane.jsx b/web/libs/datamanager/src/components/Common/FiltersPane.jsx index e8409e915a09..e299408c889e 100644 --- a/web/libs/datamanager/src/components/Common/FiltersPane.jsx +++ b/web/libs/datamanager/src/components/Common/FiltersPane.jsx @@ -58,9 +58,6 @@ export const FiltersPane = injector( disabled={sidebarEnabled} content={} openUpwardForShortViewport={false} - isChildValid={(ele) => { - return !!ele.closest("[data-radix-popper-content-wrapper]"); - }} > diff --git a/web/libs/datamanager/src/components/Common/Form/Elements/Select/Select.jsx b/web/libs/datamanager/src/components/Common/Form/Elements/Select/Select.jsx index 4b1a0d500701..b2d13c747773 100644 --- a/web/libs/datamanager/src/components/Common/Form/Elements/Select/Select.jsx +++ b/web/libs/datamanager/src/components/Common/Form/Elements/Select/Select.jsx @@ -2,7 +2,7 @@ import { cn } from "../../../../../utils/bem"; import { FormField } from "../../FormField"; import { useValueTracker } from "../../Utils"; import { default as Label } from "../Label/Label"; -import { Select as SelectUI } from "@humansignal/ui"; +import "./Select.scss"; const Select = ({ label, @@ -20,6 +20,8 @@ const Select = ({ const rootClass = cn("form-select"); const [value, setValue] = useValueTracker(props.value, defaultValue); + const classList = rootClass.mod({ ghost, size }).mix(className); + const selectWrapper = ( {({ ref }) => { return ( - { - setValue(val); - props.onChange?.(val); - }} - className={rootClass.elem("list").toString()} - options={options?.toJSON ? options.toJSON() : options} - size={size} - /> +
+ +
); }}
diff --git a/web/libs/datamanager/src/components/Common/Form/Elements/Select/Select.scss b/web/libs/datamanager/src/components/Common/Form/Elements/Select/Select.scss new file mode 100644 index 000000000000..57c1e84aab95 --- /dev/null +++ b/web/libs/datamanager/src/components/Common/Form/Elements/Select/Select.scss @@ -0,0 +1,36 @@ +.form-select { + position: relative; + + &__list { + top: 0; + left: 0; + width: 100%; + height: 100%; + padding: 0 16px; + margin: 0; + border: none; + border-radius: 3px; + position: absolute; + background-color: transparent; + transition: box-shadow 80ms ease; + font-size: inherit; + border-right: 10px solid transparent; + + .form-select_size_small & { + padding: 0 8px; + } + } + + &_ghost { + margin-left: -10px; + border: 1px solid transparent; + + & .form-select__list { + padding: 0 5px; + } + + &:hover { + border: 1px solid var(--sand_300); + } + } +} diff --git a/web/libs/datamanager/src/components/Common/Input/Input.jsx b/web/libs/datamanager/src/components/Common/Input/Input.jsx index 45a022809048..57b4e4bca90a 100644 --- a/web/libs/datamanager/src/components/Common/Input/Input.jsx +++ b/web/libs/datamanager/src/components/Common/Input/Input.jsx @@ -1,10 +1,9 @@ import React from "react"; -import { clsx } from "clsx"; import { cn } from "../../../utils/bem"; import "./Input.scss"; -const Input = React.forwardRef(({ className, size, rawClassName, ...props }, ref) => { - const classList = clsx(cn("input-dm").mod({ size }).mix(className).toString(), rawClassName); +const Input = React.forwardRef(({ className, size, ...props }, ref) => { + const classList = cn("input-dm").mod({ size }).mix(className); return ; }); diff --git a/web/libs/datamanager/src/components/Common/Input/Input.scss b/web/libs/datamanager/src/components/Common/Input/Input.scss index 630d6a1635a8..8efe20772f53 100644 --- a/web/libs/datamanager/src/components/Common/Input/Input.scss +++ b/web/libs/datamanager/src/components/Common/Input/Input.scss @@ -1,5 +1,6 @@ .input-dm, .textarea-dm { + height: 32px; width: 100%; background: #FAFAFA; font-size: 14px; diff --git a/web/libs/datamanager/src/components/Common/Pagination/Pagination.tsx b/web/libs/datamanager/src/components/Common/Pagination/Pagination.tsx index 9799709b4e75..9a374b5a94ba 100644 --- a/web/libs/datamanager/src/components/Common/Pagination/Pagination.tsx +++ b/web/libs/datamanager/src/components/Common/Pagination/Pagination.tsx @@ -299,8 +299,8 @@ export const Pagination: FC = forwardRef( size={size} value={pageSize} options={pageSizeOptions.map((v) => ({ label: `${v} per page`, value: v }))} - onChange={(val: any) => { - const newPageSize = Number.parseInt(val); + onChange={(e: any) => { + const newPageSize = Number.parseInt(e.target.value); setPageSize(newPageSize); diff --git a/web/libs/datamanager/src/components/Common/Select/Select.jsx b/web/libs/datamanager/src/components/Common/Select/Select.jsx new file mode 100644 index 000000000000..badac82a5d12 --- /dev/null +++ b/web/libs/datamanager/src/components/Common/Select/Select.jsx @@ -0,0 +1,204 @@ +import { + Children, + cloneElement, + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { shallowEqualArrays } from "shallow-equal"; +import { BemWithSpecifiContext } from "../../../utils/bem"; +import { isDefined } from "../../../utils/utils"; +import { Dropdown } from "../Dropdown/Dropdown"; +import { IconChevronDown } from "@humansignal/icons"; +import "./Select.scss"; + +const SelectContext = createContext(); +const { Block, Elem } = BemWithSpecifiContext(); + +const findSelectedChild = (children, value) => { + return Children.toArray(children).reduce((res, child) => { + if (res !== null) return res; + + if (child.type.displayName === "Select.Option") { + if (child.props.value === value) { + res = child; + } else if (Array.isArray(value) && value.length === 1) { + res = findSelectedChild(children, value[0]); + } + } else if (child.type.displayName === "Select.OptGroup") { + res = findSelectedChild(child.props.children, value); + } + + return res; + }, null); +}; + +export const Select = ({ value, defaultValue, size, children, onChange, style, multiple, tabIndex = 0 }) => { + const dropdown = useRef(); + const rootRef = useRef(); + const [currentValue, setCurrentValue] = useState(multiple ? [].concat(value ?? []).flat(10) : value); + const [focused, setFocused] = useState(); + + const options = Children.toArray(children); + + const setValue = (newValue) => { + let updatedValue = newValue; + + if (multiple) { + if (currentValue.includes(newValue)) { + updatedValue = currentValue.filter((v) => v !== newValue); + } else { + updatedValue = [...currentValue, newValue].flat(10); + } + } + + setCurrentValue(updatedValue); + return updatedValue; + }; + + const context = { + currentValue, + focused, + multiple, + setCurrentValue(value) { + const newValue = setValue(value); + + onChange?.(newValue); + + if (multiple !== true) { + dropdown.current?.close(); + } + }, + }; + + const selected = useMemo(() => { + if (multiple && currentValue?.length > 1) { + return <>Multiple values selected; + } + + const foundChild = findSelectedChild(children, defaultValue ?? currentValue); + + const result = foundChild?.props?.children; + + return result ? cloneElement(<>{result}) : null; + }, [currentValue, defaultValue, children, value]); + + const focusItem = (i) => { + setFocused(options[i ?? 0].props.value); + }; + + const focusNext = useCallback( + (direction) => { + const selectedIndex = options.findIndex((c) => c.props.value === focused); + let nextIndex = selectedIndex === -1 ? 0 : selectedIndex + direction; + + if (nextIndex >= options.length) { + nextIndex = 0; + } else if (nextIndex < 0) { + nextIndex = options.length - 1; + } + + focusItem(nextIndex); + }, + [focused], + ); + + const handleKeyboard = (e) => { + if (document.activeElement !== rootRef.current) { + return; + } + + if (["ArrowDown", "ArrowUp"].includes(e.key)) { + if (dropdown?.current.visible) { + focusNext(e.key === "ArrowDown" ? 1 : -1); + } else { + dropdown.current?.open(); + focusItem(); + } + } else if ((e.code === "Space" || e.code === "Enter") && isDefined(focused)) { + context.setCurrentValue(focused); + } + }; + + useEffect(() => { + if (multiple) { + if (shallowEqualArrays(value ?? [], currentValue ?? []) === false) { + context.setCurrentValue(value?.flat?.(10) ?? []); + } + } else if (value !== currentValue) { + context.setCurrentValue(value); + } + }, [value, multiple]); + + return ( + + + {children}
} + onToggle={(visible) => { + if (!visible) setFocused(null); + }} + > + + {selected ?? "Select value"} + + + + + + + + ); +}; +Select.displayName = "Select"; + +Select.Option = ({ value, children, style }) => { + const { setCurrentValue, multiple, currentValue, focused } = useContext(SelectContext); + + const isSelected = useMemo(() => { + const option = String(value); + + if (multiple) { + return currentValue.map((v) => String(v)).includes(option); + } + return option === String(currentValue); + }, [value, focused, currentValue]); + + const isFocused = useMemo(() => { + return String(value) === String(focused); + }, [value, focused]); + + return ( + { + e.stopPropagation(); + setCurrentValue(value); + }} + style={style} + > + {children} + + ); +}; +Select.Option.displayName = "Select.Option"; + +Select.OptGroup = ({ label, children, style }) => { + return ( + + {label} + {children} + + ); +}; +Select.OptGroup.displayName = "Select.OptGroup"; diff --git a/web/libs/datamanager/src/components/Common/Select/Select.scss b/web/libs/datamanager/src/components/Common/Select/Select.scss new file mode 100644 index 000000000000..e0c0f89f390d --- /dev/null +++ b/web/libs/datamanager/src/components/Common/Select/Select.scss @@ -0,0 +1,101 @@ +@use 'sass:color'; + +.select-dm { + height: 40px; + background: #FAFAFA; + font-size: 16px; + line-height: 22px; + border: 1px solid var(--sand_300); + box-sizing: border-box; + border-radius: 5px; + cursor: pointer; + + &_size { + &_compact { + height: 32px; + } + + &_small { + height: 24px; + font-size: 12px; + } + } + + &__list { + width: max-content; + } + + &__selected { + width: 100%; + padding: 0 7px; + min-width: 60px; + display: flex; + font-weight: 500; + align-items: center; + } + + &__value { + width: max-content; + min-width: 30px; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__option { + cursor: pointer; + padding: 4px 16px; + + &:hover, + &_focused { + background-color: #f7f7f7; + } + + &_selected { + color: white; + background-color: var(--accent_color); + } + + &_selected:hover, + &_selected.select-dm__option_focused { + background-color: hsl(var(--accent_color), 10%); + color: var(--accent_color); + } + } + + &__icon { + margin-left: 5px; + position: relative; + + svg { + width: 16px; + height: 16px; + } + } + + &__optgroup { + &-list { + width: max-content; + } + + &-label { + padding: 4px 16px; + color: rgb(var(--black-raw) / 60%); + } + } + + &__optgroup-list &__option { + padding-left: 24px; + } + + &_disabled { + pointer-events: none; + } + + &:focus { + outline: none; + box-shadow: 0 0 0 6px rgb(var(--accent_color-raw) / 20%), inset 0 -1px 0 var(--black_10), inset 0 0 0 1px var(--black_15), inset 0 0 0 1px rgb(var(--accent_color-raw) / 20%); + border-color: rgb(var(--accent_color-raw) / 20%); + } +} diff --git a/web/libs/datamanager/src/components/DataManager/Toolbar/ActionsButton.jsx b/web/libs/datamanager/src/components/DataManager/Toolbar/ActionsButton.jsx index 4f51cac738b7..c8db8de4088f 100644 --- a/web/libs/datamanager/src/components/DataManager/Toolbar/ActionsButton.jsx +++ b/web/libs/datamanager/src/components/DataManager/Toolbar/ActionsButton.jsx @@ -50,7 +50,6 @@ export const ActionsButton = injector( store.SDK.invoke("actionDialogOk", action.id, { body }); store.invokeAction(action.id, { body }); }, - closeOnClickOutside: false, }); } else { store.invokeAction(action.id); diff --git a/web/libs/datamanager/src/components/Filters/FilterDropdown.jsx b/web/libs/datamanager/src/components/Filters/FilterDropdown.jsx index cb6ad53ee7fd..3073bc659580 100644 --- a/web/libs/datamanager/src/components/Filters/FilterDropdown.jsx +++ b/web/libs/datamanager/src/components/Filters/FilterDropdown.jsx @@ -1,6 +1,40 @@ import { observer } from "mobx-react"; -import { Select } from "../Common/Form"; -import { useCallback, useMemo } from "react"; +import { IconChevronDown } from "@humansignal/icons"; +import { Icon } from "../Common/Icon/Icon"; +import { Select } from "../Common/Select/Select"; +import { Tag } from "../Common/Tag/Tag"; + +const TagRender = + (items) => + ({ label, ...rest }) => { + const color = items.find((el) => el.value === rest.value)?.color; + + return ( + +
{label}
+
+ ); + }; + +const renderOptions = (OptionRender) => (item) => { + const value = item.value ?? item; + const label = item.label ?? item.title ?? value; + const key = `${item.id}-${value}-${label}`; + + if (item.options) { + return ( + + {item.options.map(renderOptions(OptionRender))} + + ); + } + + return ( + + {OptionRender ? : label} + + ); +}; export const FilterDropdown = observer( ({ @@ -15,44 +49,33 @@ export const FilterDropdown = observer( optionRender, dropdownClassName, outputFormat, - searchFilter, }) => { - const parseItems = useCallback( - (item) => { - const OptionVisuals = optionRender; - const option = - typeof item === "string" || typeof item === "number" - ? { label: , value: item, original: item } - : { - ...item, - label: item?.original?.field?.parent ? ( - - ) : ( - (item?.title ?? item?.label ?? item?.name) - ), - value: item?.value ?? item, - children: item?.options?.map(parseItems), - }; - return option; - }, - [optionRender], - ); - const options = useMemo(() => items.map(parseItems), [items, parseItems]); - return ( ); }, ); diff --git a/web/libs/datamanager/src/components/Filters/FilterInput.jsx b/web/libs/datamanager/src/components/Filters/FilterInput.jsx index bfb100bde73a..6dbf7bef0554 100644 --- a/web/libs/datamanager/src/components/Filters/FilterInput.jsx +++ b/web/libs/datamanager/src/components/Filters/FilterInput.jsx @@ -11,7 +11,7 @@ export const FilterInput = ({ value, type, onChange, placeholder, schema, style return ( } + icon={} /> diff --git a/web/libs/datamanager/src/components/Filters/FilterLine/FilterLine.scss b/web/libs/datamanager/src/components/Filters/FilterLine/FilterLine.scss index 46a254d36957..2e0eb7f82c8f 100644 --- a/web/libs/datamanager/src/components/Filters/FilterLine/FilterLine.scss +++ b/web/libs/datamanager/src/components/Filters/FilterLine/FilterLine.scss @@ -10,16 +10,15 @@ grid-template-columns: 70px 110px 100px 120px 24px; &__remove { - height: 100%; + width: 24px; + height: 24px; display: flex; align-items: center; justify-content: center; .button-dm { - height: 24px; - width: 24px; - padding: 0; flex: none; + padding: 0; } } diff --git a/web/libs/datamanager/src/components/Filters/FilterLine/FilterOperation.jsx b/web/libs/datamanager/src/components/Filters/FilterLine/FilterOperation.jsx index a3d6c3cf92af..de1e6b4cd99b 100644 --- a/web/libs/datamanager/src/components/Filters/FilterLine/FilterOperation.jsx +++ b/web/libs/datamanager/src/components/Filters/FilterLine/FilterOperation.jsx @@ -78,7 +78,6 @@ export const FilterOperation = observer(({ filter, field, operator, value }) => filter={filter} value={value} onChange={onChange} - size="small" /> diff --git a/web/libs/datamanager/src/components/Filters/Filters.scss b/web/libs/datamanager/src/components/Filters/Filters.scss index 38806abfb2a5..390a8882d863 100644 --- a/web/libs/datamanager/src/components/Filters/Filters.scss +++ b/web/libs/datamanager/src/components/Filters/Filters.scss @@ -61,7 +61,7 @@ &_withFilters { display: grid; - grid-template-columns: 65px min-content min-content 1fr min-content; + grid-template-columns: 65px min-content min-content 1fr 24px; grid-gap: 3px 4px; } } diff --git a/web/libs/datamanager/src/components/Filters/types/Date.jsx b/web/libs/datamanager/src/components/Filters/types/Date.jsx index 6bf99795a5d2..46b35bf53681 100644 --- a/web/libs/datamanager/src/components/Filters/types/Date.jsx +++ b/web/libs/datamanager/src/components/Filters/types/Date.jsx @@ -38,7 +38,7 @@ export const DateTimeInput = observer(({ value, range, time, onChange }) => { }, [range, value]); return ( - + ); }); diff --git a/web/libs/datamanager/src/components/Filters/types/List.jsx b/web/libs/datamanager/src/components/Filters/types/List.jsx index f803fa824485..62ac1f1a8922 100644 --- a/web/libs/datamanager/src/components/Filters/types/List.jsx +++ b/web/libs/datamanager/src/components/Filters/types/List.jsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react"; import { FilterDropdown } from "../FilterDropdown"; // import { Common } from "./Common"; -export const VariantSelect = observer(({ filter, schema, onChange, multiple, value, placeholder }) => { +export const VariantSelect = observer(({ filter, schema, onChange, multiple, value }) => { if (!schema) return <>; const { items } = schema; @@ -14,9 +14,10 @@ export const VariantSelect = observer(({ filter, schema, onChange, multiple, val })(); const FilterItem = filter.cellView?.FilterItem; + return ( onChange(value)} - placeholder={placeholder ?? "Select value"} /> ); }); diff --git a/web/libs/editor/src/common/Pagination/Pagination.tsx b/web/libs/editor/src/common/Pagination/Pagination.tsx index 71d8878bc4c8..f62d1a2ce2f0 100644 --- a/web/libs/editor/src/common/Pagination/Pagination.tsx +++ b/web/libs/editor/src/common/Pagination/Pagination.tsx @@ -1,8 +1,7 @@ -import { type ChangeEvent, type FC, forwardRef, type KeyboardEvent, useCallback, useMemo, useState } from "react"; +import { type ChangeEvent, type FC, forwardRef, type KeyboardEvent, useCallback, useState } from "react"; import { Hotkey } from "../../core/Hotkey"; import { useHotkey } from "../../hooks/useHotkey"; import { Block, Elem } from "../../utils/bem"; -import { Select } from "@humansignal/ui"; import "./Pagination.scss"; interface PaginationProps { @@ -57,14 +56,15 @@ export const Pagination: FC = forwardRef( onChange?.(1, e.currentTarget.value); }; - const options = useMemo(() => { + const renderOptions = () => { return pageSizeOptions.map((obj: number, index: number) => { - return { - value: obj, - label: `${obj} per page`, - }; + return ( + + ); }); - }, [pageSizeOptions]); + }; return ( @@ -149,7 +149,9 @@ export const Pagination: FC = forwardRef( {pageSizeSelectable && ( - + {renderOptions()} + )} diff --git a/web/libs/editor/src/common/Select/Select.scss b/web/libs/editor/src/common/Select/Select.scss new file mode 100644 index 000000000000..de352a0a2e06 --- /dev/null +++ b/web/libs/editor/src/common/Select/Select.scss @@ -0,0 +1,148 @@ +.select { + --select-surface-color: var(--sand_0); + + height: 40px; + background-color: var(--select-surface-color); + font-size: 16px; + line-height: 22px; + border: 1px solid var(--sand_300); + box-sizing: border-box; + border-radius: 5px; + cursor: pointer; + + &_surface { + &_emphasis { + --select-surface-color: var(--sand_0); + } + } + + &_size { + &_compact { + height: 32px; + border-radius: 3px; + } + + &_small { + height: 24px; + font-size: 12px; + } + } + + &__list { + width: max-content; + } + + &__dropdown { + &:not(.dropdown__trigger) { + max-height: 280px; + overflow: auto; + + .select__option { + padding-inline: 12px; + } + } + } + + &__dropdown_variant_rounded { + &:not(.dropdown__trigger) { + border-radius: 4px; + padding-bottom: 8px; + margin-top: 4px; + } + } + + &__selected { + width: 100%; + padding: 0 7px; + min-width: 60px; + display: flex; + font-weight: 500; + align-items: center; + height: 100%; + } + + &__value { + width: max-content; + min-width: 30px; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__option { + cursor: pointer; + padding: 4px 16px; + + &:hover, + &_focused { + background-color: var(--sand_0); + } + + &_selected { + color: var(--sand_0); + background-color: var(--primary_link); + } + + &_selected:hover, + &_selected.select_focused { + background-color: var(--grape_0); + } + } + + &__icon { + width: 16px; + height: 16px; + margin-left: 5px; + position: relative; + + &::before, + &::after { + width: 6px; + height: 6px; + display: block; + position: absolute; + content: ""; + border: 1px solid var(--primary_link); + border-right: none; + border-bottom: none; + } + + &::before { + top: 0; + left: 50%; + transform: translate(-50%, 50%) rotate(45deg); + } + + &::after { + bottom: 0; + left: 50%; + transform: translate(-50%, -50%) rotate(-135deg); + } + } + + &__optgroup { + &-list { + width: max-content; + } + + &-label { + padding: 4px 16px; + color:var(--sand_700); + } + } + + &__optgroup-list &__option { + padding-left: 24px; + } + + &_disabled { + pointer-events: none; + } + + &:focus { + outline: none; + box-shadow: 0 0 0 6px var(--grape_100), inset 0 -1px 0 rgb(0 0 0 / 10%), inset 0 0 0 1px rgb(0 0 0 / 15%), inset 0 0 0 1px var(--grape_100); + border-color: var(--primary_link); + } +} \ No newline at end of file diff --git a/web/libs/editor/src/common/Select/Select.tsx b/web/libs/editor/src/common/Select/Select.tsx new file mode 100644 index 000000000000..4a79a99cea45 --- /dev/null +++ b/web/libs/editor/src/common/Select/Select.tsx @@ -0,0 +1,293 @@ +import { + Children, + cloneElement, + createContext, + type CSSProperties, + type FC, + type KeyboardEvent, + type MouseEvent, + type ReactChild, + type ReactFragment, + type ReactNode, + type ReactPortal, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { BemWithSpecifiContext, cn } from "../../utils/bem"; +import { shallowEqualArrays } from "shallow-equal"; +import { isDefined } from "../../utils/utilities"; +import { Dropdown } from "../Dropdown/Dropdown"; +import "./Select.scss"; +import { FF_DEV_2669, isFF } from "../../utils/feature-flags"; + +type FoundChild = ReactChild | ReactFragment | ReactPortal; + +interface SelectProps { + placeholder?: ReactNode; + value?: string | string[]; + defaultValue?: string | string[]; + size?: "normal" | "medium" | "small"; + style?: CSSProperties; + variant?: "base" | "rounded"; + surface?: "base" | "emphasis"; + multiple?: boolean; + renderMultipleSelected?: (value: string[]) => ReactNode; + tabIndex?: number; + onChange?: (newValue?: string | string[]) => void; + dataTestid?: string; +} + +interface SelectComponent extends FC { + Option: FC; + OptGroup: FC; +} + +interface SelectContextProps { + multiple?: boolean; + focused?: string | boolean | null; + currentValue?: string | string[] | null; + setCurrentValue: (value?: string | string[]) => void; +} + +const SelectContext = createContext({ + multiple: false, + focused: false, + currentValue: [], + setCurrentValue() {}, +}); + +const { Block, Elem } = BemWithSpecifiContext(); + +const findSelectedChild = (children: ReactNode, value?: string | string[]): FoundChild | null => { + return Children.toArray(children).reduce((res, child) => { + if (res !== null) return res; + + const { type, props } = child as any; + + if (type.displayName === "Select.Option") { + if (props.value === value) { + res = child; + } else if (Array.isArray(value) && value.length === 1) { + res = findSelectedChild(children, value[0]); + } + } else if (type.displayName === "Select.OptGroup") { + res = findSelectedChild(props.children, value); + } + + return res; + }, null); +}; + +export const Select: SelectComponent = ({ + value, + defaultValue, + size, + children, + style, + multiple, + renderMultipleSelected, + onChange, + variant, + surface, + dataTestid, + tabIndex = 0, + placeholder = "Select value", +}) => { + const dropdown = useRef(); + const rootRef = useRef(); + const [currentValue, setCurrentValue] = useState(multiple ? ([] as string[]).concat(value ?? []).flat(10) : value); + const [focused, setFocused] = useState(); + + const options = Children.toArray(children).filter((child: any) => { + // toArray is returning incorrect types which don't have type.displayName or props, but the actual child does. + return child.type.displayName === "Select.Option" && !child.props.exclude; + }); + + const setValue = (newValue?: string | string[]) => { + let updatedValue: string | string[] | undefined = newValue; + + if (multiple && Array.isArray(currentValue) && newValue) { + if (!Array.isArray(newValue) && currentValue.includes(newValue)) { + updatedValue = currentValue.filter((v) => v !== newValue); + } else { + updatedValue = [...currentValue, newValue].flat(10); + } + } + + setCurrentValue(updatedValue); + return updatedValue; + }; + + const context: SelectContextProps = { + currentValue, + focused, + multiple, + setCurrentValue(value) { + const newValue = setValue(value); + + onChange?.(newValue); + + if (multiple !== true) { + dropdown.current?.close(); + } + }, + }; + + const selected = useMemo(() => { + if (isFF(FF_DEV_2669) && multiple && renderMultipleSelected) { + return renderMultipleSelected(Array.isArray(currentValue) ? currentValue : [currentValue || ""]); + } + if (multiple && Array.isArray(currentValue) && currentValue?.length > 1) { + return <>Multiple values selected; + } + + const foundChild = findSelectedChild(children, defaultValue ?? currentValue) as any; + + const result = foundChild?.props?.children; + + return result ? cloneElement(<>{result}) : null; + }, [currentValue, defaultValue, children, value, renderMultipleSelected]); + + const focusItem = (i?: number) => { + const child = options[i ?? 0] as any; + + setFocused(child.props.value); + }; + + const focusNext = useCallback( + (direction) => { + const selectedIndex = options.findIndex((c: any) => c.props.value === focused); + + let nextIndex = selectedIndex === -1 ? 0 : selectedIndex + direction; + + if (nextIndex >= options.length) { + nextIndex = 0; + } else if (nextIndex < 0) { + nextIndex = options.length - 1; + } + + focusItem(nextIndex); + }, + [focused], + ); + + const handleKeyboard = (e: KeyboardEvent) => { + if (document.activeElement !== rootRef.current) { + return; + } + + if (["ArrowDown", "ArrowUp"].includes(e.key)) { + if (dropdown?.current.visible) { + focusNext(e.key === "ArrowDown" ? 1 : -1); + } else { + dropdown.current?.open(); + focusItem(); + } + } else if ((e.code === "Space" || e.code === "Enter") && isDefined(focused)) { + context.setCurrentValue(focused); + } + }; + + useEffect(() => { + if (multiple && Array.isArray(value) && Array.isArray(currentValue)) { + if (shallowEqualArrays(value ?? [], currentValue ?? []) === false) { + context.setCurrentValue(value?.flat?.(10) ?? []); + } + } else if (value !== currentValue) { + context.setCurrentValue(value); + } + }, [value, multiple]); + + return ( + + + {children}} + onToggle={(visible: boolean) => { + if (!visible) setFocused(null); + }} + > + + {selected ?? placeholder} + + + + + + ); +}; +Select.displayName = "Select"; + +interface SelectOptionProps { + value?: string; + style?: CSSProperties; + exclude?: boolean; +} + +const SelectOption: FC = ({ value, children, style }) => { + const { setCurrentValue, multiple, currentValue, focused } = useContext(SelectContext); + + const isSelected = useMemo(() => { + const option = String(value); + + if (multiple && Array.isArray(currentValue)) { + return currentValue.map((v) => String(v)).includes(option); + } + return option === String(currentValue); + }, [value, focused, currentValue]); + + const isFocused = useMemo(() => { + return String(value) === String(focused); + }, [value, focused]); + + return ( + ) => { + e.stopPropagation(); + setCurrentValue(value); + }} + style={style} + > + {children} + + ); +}; + +SelectOption.displayName = "Select.Option"; + +interface SelectioOptGroupProps { + label?: JSX.Element | string; + style?: CSSProperties; +} + +const SelectOptGroup: FC = ({ label, children, style }) => { + return ( + + {label} + {children} + + ); +}; + +SelectOptGroup.displayName = "Select.OptGroup"; + +Select.Option = SelectOption; +Select.OptGroup = SelectOptGroup; diff --git a/web/libs/editor/src/components/Filter/Filter.scss b/web/libs/editor/src/components/Filter/Filter.scss new file mode 100644 index 000000000000..64b55fa7078c --- /dev/null +++ b/web/libs/editor/src/components/Filter/Filter.scss @@ -0,0 +1,42 @@ +.filter { + padding: 10px; + + &__empty { + margin-bottom: 10px; + font-size: 14px; + color: var(--sand_900); + width: 220px; + } +} + +.filter-button { + display: flex; + height: 24px; + padding: 0 6px 0 2px; + cursor: pointer; + align-items: center; + border-radius: 4px; + + &:active, + &_active { + background: var(--sand_200); + } + + &__icon { + align-items: center; + display: flex; + } + + &__filter-length { + font-size: 11px; + font-weight: 500; + text-align: center; + color: var(--grape_700); + width: 15px; + height: 20px; + line-height: 21px; + border-radius: 2px; + background: var(--grape_100); + margin-left: 3px; + } +} \ No newline at end of file diff --git a/web/libs/editor/src/components/Filter/Filter.tsx b/web/libs/editor/src/components/Filter/Filter.tsx new file mode 100644 index 000000000000..6eafe0de8a0c --- /dev/null +++ b/web/libs/editor/src/components/Filter/Filter.tsx @@ -0,0 +1,132 @@ +import { Block, Elem } from "../../utils/bem"; +import { Dropdown } from "../../common/Dropdown/Dropdown"; + +import { type FC, useCallback, useEffect, useMemo, useState } from "react"; +import { Button } from "../../common/Button/Button"; +import { IconFilter } from "@humansignal/icons"; + +import "./Filter.scss"; +import type { FilterInterface, FilterListInterface } from "./FilterInterfaces"; +import { FilterRow } from "./FilterRow"; +import { FilterItems } from "./filter-util"; +import { FF_DEV_3873, isFF } from "../../utils/feature-flags"; + +export const Filter: FC = ({ availableFilters, filterData, onChange, animated = true }) => { + const [filterList, setFilterList] = useState([]); + const [active, setActive] = useState(false); + + useEffect(() => { + onChange(FilterItems(filterData, filterList)); + }, [filterData]); + + const addNewFilterListItem = useCallback(() => { + setFilterList((filterList) => [ + ...filterList, + { + field: availableFilters[0]?.label ?? "", + logic: "and", + operation: "", + value: "", + path: "", + }, + ]); + }, [setFilterList, availableFilters]); + + const onChangeRow = useCallback( + (index: number, { field, operation, value, path, logic }: Partial) => { + setFilterList((oldList) => { + const newList = [...oldList]; + + newList[index] = { + ...newList[index], + field: field ?? newList[index].field, + operation: operation ?? newList[index].operation, + logic: logic ?? newList[index].logic, + value: value ?? newList[index].value, + path: path ?? newList[index].path, + }; + + onChange(FilterItems(filterData, newList)); + + return newList; + }); + }, + [setFilterList, filterData], + ); + + const onDeleteRow = useCallback( + (index: number) => { + setFilterList((oldList) => { + const newList = [...oldList]; + + newList.splice(index, 1); + + if (newList[0]) { + newList[0].logic = "and"; + } + + onChange(FilterItems(filterData, newList)); + + return newList; + }); + }, + [setFilterList, filterData], + ); + + const renderFilterList = useMemo(() => { + return filterList.map(({ field, operation, logic, value }, index) => ( + + + + )); + }, [filterList, availableFilters, onDeleteRow, onChangeRow]); + + const renderFilter = useMemo(() => { + return ( + + {filterList.length > 0 ? renderFilterList : No filters applied} + + + ); + }, [filterList, renderFilterList, addNewFilterListItem]); + + const onToggle = useCallback((isOpen: boolean) => { + setActive(isOpen); + }, []); + + return ( + + + + + + + Filter + + {filterList.length > 0 && ( + + {filterList.length} + + )} + + + ); +}; diff --git a/web/libs/editor/src/components/Filter/FilterDropdown.tsx b/web/libs/editor/src/components/Filter/FilterDropdown.tsx new file mode 100644 index 000000000000..0bc8efb3506a --- /dev/null +++ b/web/libs/editor/src/components/Filter/FilterDropdown.tsx @@ -0,0 +1,53 @@ +import type { FC } from "react"; +import { Select } from "../../common/Select/Select"; + +interface FilterDropdownInterface { + items: any[]; + onChange: (value: any) => void; + value?: string | string[] | undefined; + placeholder?: string; + defaultValue?: string | string[] | undefined; + optionRender?: any; + dataTestid?: string; + style?: any; +} + +const renderOptions = (item: any, index: number) => { + const value = item.key ?? item.label; + const key = index; + + return ( + + {item.label} + + ); +}; + +export const FilterDropdown: FC = ({ + placeholder, + defaultValue, + items, + style, + dataTestid, + value, + onChange, +}) => { + return ( + + ); +}; diff --git a/web/libs/editor/src/components/Filter/FilterInput.tsx b/web/libs/editor/src/components/Filter/FilterInput.tsx new file mode 100644 index 000000000000..8393e64fa3ac --- /dev/null +++ b/web/libs/editor/src/components/Filter/FilterInput.tsx @@ -0,0 +1,36 @@ +import React, { type FC } from "react"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import Input from "../../common/Input/Input"; + +interface FilterInputInterface { + value: string | number | undefined; + type: string; + onChange: (value: any) => void; + placeholder?: string; + schema?: any; + style?: any; +} + +export const FilterInput: FC = ({ value, type, onChange, placeholder, schema, style }) => { + const inputRef = React.useRef(); + const onChangeHandler = () => { + const value = inputRef.current?.value ?? inputRef.current?.input?.value; + + onChange(value); + }; + + return ( + + ); +}; diff --git a/web/libs/editor/src/components/Filter/FilterInterfaces.tsx b/web/libs/editor/src/components/Filter/FilterInterfaces.tsx new file mode 100644 index 000000000000..4963f69dbcbd --- /dev/null +++ b/web/libs/editor/src/components/Filter/FilterInterfaces.tsx @@ -0,0 +1,26 @@ +export enum Logic { + and = "And", + or = "Or", +} + +export interface FilterInterface { + availableFilters: AvailableFiltersInterface[]; + onChange: (filter: any) => void; + filterData: any; + + animated?: boolean; +} + +export interface FilterListInterface { + field?: string | string[] | undefined; + operation?: string | string[] | undefined; + value?: any; + path?: string; + logic?: "and" | "or"; +} + +export interface AvailableFiltersInterface { + label: string; + path: string; + type: "Boolean" | "Common" | "Number" | "String" | string; +} diff --git a/web/libs/editor/src/components/Filter/FilterRow.scss b/web/libs/editor/src/components/Filter/FilterRow.scss new file mode 100644 index 000000000000..7e4a70955dec --- /dev/null +++ b/web/libs/editor/src/components/Filter/FilterRow.scss @@ -0,0 +1,25 @@ +.filter-row { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 8px; + + &__title-row { + width: 60px; + text-align: right; + } + + &__delete { + cursor: pointer; + height: 16px; + } + + &__column { + display: flex; + margin-right: 8px; + + input { + height: 24px !important; + } + } +} \ No newline at end of file diff --git a/web/libs/editor/src/components/Filter/FilterRow.tsx b/web/libs/editor/src/components/Filter/FilterRow.tsx new file mode 100644 index 000000000000..4c42ac7a2b56 --- /dev/null +++ b/web/libs/editor/src/components/Filter/FilterRow.tsx @@ -0,0 +1,118 @@ +import { type FC, useEffect, useState } from "react"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import * as FilterInputs from "./types"; + +import { Block, Elem } from "../../utils/bem"; +import { FilterDropdown } from "./FilterDropdown"; + +import "./FilterRow.scss"; +import { type FilterListInterface, Logic } from "./FilterInterfaces"; +import { isDefined } from "../../utils/utilities"; +import { IconDelete } from "@humansignal/icons"; + +interface FilterRowInterface extends FilterListInterface { + availableFilters: any; + index: number; + onChange: (index: number, obj: any) => void; + onDelete: (index: number) => void; +} + +const logicItems = Object.entries(Logic).map(([key, label]) => ({ key, label })); + +export const FilterRow: FC = ({ + field, + operation, + value, + logic, + availableFilters, + index, + onChange, + onDelete, +}) => { + const [_selectedField, setSelectedField] = useState(0); + const [_selectedOperation, setSelectedOperation] = useState(-1); + const [_inputComponent, setInputComponent] = useState(null); + + useEffect(() => { + onChange(index, { field: availableFilters[_selectedField].label, path: availableFilters[_selectedField].path }); + }, [_selectedField]); + + useEffect(() => { + const _operationItems = FilterInputs?.[availableFilters[_selectedField].type]; + const _operation = _operationItems.findIndex((item: any) => (item.key ?? item.label) === _selectedOperation); + + if (!isDefined(_operation) || _operation < 0) return; + const _filterInputs = FilterInputs?.[availableFilters[_selectedField].type][_operation]; + + onChange(index, { operation: _filterInputs?.key }); + setInputComponent(_filterInputs?.input); + }, [_selectedOperation, _selectedField]); + + return ( + + + {index === 0 ? ( + Where + ) : ( + { + onChange(index, { logic: value }); + }} + /> + )} + + + { + setSelectedField(availableFilters.findIndex((item: any) => (item.key ?? item.label) === value)); + + onChange(index, { value: null }); + }} + /> + + + { + setSelectedOperation(value); + }} + /> + + + {_inputComponent && operation !== "empty" && ( + { + onChange(index, { value }); + }} + /> + )} + + + { + onDelete(index); + }} + data-testid={`delete-row-${index}`} + name={"delete"} + > + + + + + ); +}; diff --git a/web/libs/editor/src/components/Filter/__tests__/Filter.test.tsx b/web/libs/editor/src/components/Filter/__tests__/Filter.test.tsx new file mode 100644 index 000000000000..b3a4d725f103 --- /dev/null +++ b/web/libs/editor/src/components/Filter/__tests__/Filter.test.tsx @@ -0,0 +1,244 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { Filter } from "../Filter"; + +describe("Filter", () => { + const mockOnChange = jest.fn(); + const filterData = [ + { + labelName: "AirPlane", + }, + { + labelName: "Car", + }, + { + labelName: "AirCar", + }, + ]; + + test("Validate if filter is rendering", () => { + const filter = render( + , + ); + + const whereText = filter.getByText("Filter"); + + expect(whereText).toBeDefined(); + }); + + test("Should delete row when delete button is clicked", () => { + const filter = render( + , + ); + + const FilterButton = filter.getByText("Filter"); + + fireEvent.click(FilterButton); + + const AddButton = filter.getByText("Add Filter"); + + fireEvent.click(AddButton); + fireEvent.click(AddButton); + + const selectBox = filter.getByTestId("logic-dropdown"); + + expect(selectBox.textContent).toBe("And"); + + fireEvent.click(selectBox); + fireEvent.click(screen.getByText("Or")); + + expect(selectBox.textContent).toBe("Or"); + + fireEvent.click(screen.getByTestId("delete-row-1")); + + expect(filter.getAllByTestId("filter-row")).toHaveLength(1); + }); + + test("Should filter the content", () => { + let filteredContent: any; + + const filter = render( + { + filteredContent = value; + }} + filterData={filterData} + availableFilters={[ + { + label: "Annotation results", + path: "labelName", + type: "String", + }, + { + label: "Confidence score", + path: "score", + type: "Number", + }, + ]} + />, + ); + + const FilterButton = filter.getByText("Filter"); + + fireEvent.click(FilterButton); + + expect(screen.getByText("No filters applied")).toBeDefined(); + + const AddButton = filter.getByText("Add Filter"); + + fireEvent.click(AddButton); + + const fieldDropdown = filter.getByTestId("field-dropdown"); + const operationDropdown = filter.getByTestId("operation-dropdown"); + + fireEvent.click(operationDropdown); + fireEvent.click(screen.getByText("not contains")); + + const filterInput = filter.getByTestId("filter-input"); + + expect(filterInput).toBeDefined(); + + expect(fieldDropdown.textContent).toBe("Annotation results"); + expect(operationDropdown.textContent).toBe("not contains"); + + fireEvent.change(filterInput, { target: { value: "Plane" } }); + + expect(filteredContent).toStrictEqual([{ labelName: "Car" }, { labelName: "AirCar" }]); + }); + + test("Should hide dropdown filter", async () => { + const filter = render( + , + ); + + const FilterButton = await filter.getByText("Filter"); + + fireEvent.click(FilterButton); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + const dropdown = await filter.getByTestId("dropdown"); + + expect(dropdown.classList.contains("dm-visible")).toBe(true); + + const AddButton = await filter.getByText("Add Filter"); + + fireEvent.click(AddButton); + + fireEvent.click(FilterButton); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(dropdown.classList.contains("dm-before-appear")).toBe(false); + expect(dropdown.classList.contains("dm-visible")).toBe(false); + expect(dropdown.classList.contains("dm-before-disappear")).toBe(false); + }); + + test("Should show filter length badge", () => { + const filter = render( + , + ); + + const FilterButton = filter.getByText("Filter"); + + fireEvent.click(FilterButton); + + expect(screen.getByText("No filters applied")).toBeDefined(); + + const AddButton = filter.getByText("Add Filter"); + + fireEvent.click(AddButton); + fireEvent.click(AddButton); + + const filterLength = filter.getByTestId("filter-length"); + + expect(filterLength.textContent).toBe("2"); + }); + + test("Filter button should be selected", () => { + const filter = render( + , + ); + + const FilterButton = filter.getByTestId("filter-button"); + + fireEvent.click(FilterButton); + + expect(FilterButton.classList.contains("dm-filter-button_active")).toBe(true); + }); +}); diff --git a/web/libs/editor/src/components/Filter/__tests__/FilterRow.test.tsx b/web/libs/editor/src/components/Filter/__tests__/FilterRow.test.tsx new file mode 100644 index 000000000000..75c88334c004 --- /dev/null +++ b/web/libs/editor/src/components/Filter/__tests__/FilterRow.test.tsx @@ -0,0 +1,149 @@ +import { FilterRow } from "../FilterRow"; +import { fireEvent, render, screen } from "@testing-library/react"; + +describe("FilterRow", () => { + const mockOnChange = jest.fn(); + const mockOnDelete = jest.fn(); + + test('should display "Where" when index is 0', () => { + const filter = render( + , + ); + + const whereText = filter.getByText("Where"); + + expect(whereText).toBeDefined(); + }); + + test("should display select box when index is 1 or more", () => { + const filter = render( + , + ); + + const selectBox = filter.getByTestId("logic-dropdown"); + + expect(selectBox.textContent).toBe("And"); + + fireEvent.click(screen.getByTestId("logic-dropdown")); + fireEvent.click(screen.getByText("Or")); + + expect(selectBox.textContent).toBe("Or"); + }); + + test("should display select box when index is 1 or more", () => { + const filter = render( + , + ); + + const selectBox = filter.getByTestId("logic-dropdown"); + + expect(selectBox.textContent).toBe("And"); + + fireEvent.click(selectBox); + fireEvent.click(screen.getByText("Or")); + + expect(selectBox.textContent).toBe("Or"); + }); + + test("select and fill fields", () => { + const filter = render( + , + ); + + const fieldDropdown = filter.getByTestId("field-dropdown"); + const operationDropdown = filter.getByTestId("operation-dropdown"); + + expect(fieldDropdown).toBeDefined(); + fireEvent.click(fieldDropdown); + fireEvent.click(screen.getByText("Annotation results")); + fireEvent.click(operationDropdown); + fireEvent.click(screen.getByText("not contains")); + + const filterInput = filter.getByTestId("filter-input"); + + expect(filterInput).toBeDefined(); + + expect(fieldDropdown.textContent).toBe("Annotation results"); + expect(operationDropdown.textContent).toBe("not contains"); + }); +}); diff --git a/web/libs/editor/src/components/Filter/__tests__/filter-utils.test.tsx b/web/libs/editor/src/components/Filter/__tests__/filter-utils.test.tsx new file mode 100644 index 000000000000..a25f4fbc0bc5 --- /dev/null +++ b/web/libs/editor/src/components/Filter/__tests__/filter-utils.test.tsx @@ -0,0 +1,284 @@ +import { FilterItemsByOperation } from "../filter-util"; + +describe("FilterItems", () => { + const items = [ + { + item: { + name: "Car", + value: 25, + }, + }, + { + item: { + name: "AirPlane", + value: 30, + }, + }, + { + item: { + name: "Car Flower", + value: 40, + }, + }, + ]; + + test("should filter items that contain the specified value", () => { + const filterItem = { operation: "contains", path: "item.name", value: "Car" }; + const filteredItems = FilterItemsByOperation(items, filterItem); + + expect(filteredItems).toEqual([ + { + item: { + name: "Car", + value: 25, + }, + }, + { + item: { + name: "Car Flower", + value: 40, + }, + }, + ]); + }); + + test("should filter items that do not contain the specified value", () => { + const filterItem = { operation: "not_contains", path: "item.name", value: "Car" }; + const filteredItems = FilterItemsByOperation(items, filterItem); + + expect(filteredItems).toEqual([ + { + item: { + name: "AirPlane", + value: 30, + }, + }, + ]); + }); + + test("should filter items that value is between the specified values", () => { + const filterItem = { operation: "in", path: "item.value", value: { min: 26, max: 35 } }; + const filteredItems = FilterItemsByOperation(items, filterItem); + + expect(filteredItems).toEqual([ + { + item: { + name: "AirPlane", + value: 30, + }, + }, + ]); + }); + + test("should filter items that value is not between the specified values", () => { + const filterItem = { operation: "not_in", path: "item.value", value: { min: 26, max: 35 } }; + const filteredItems = FilterItemsByOperation(items, filterItem); + + expect(filteredItems).toEqual([ + { + item: { + name: "Car", + value: 25, + }, + }, + { + item: { + name: "Car Flower", + value: 40, + }, + }, + ]); + }); + + test("should filter items that value match with regex specified value", () => { + const filterItem = { operation: "regex", path: "item.name", value: "[C-O]" }; + const filteredItems = FilterItemsByOperation(items, filterItem); + + expect(filteredItems).toEqual([ + { + item: { + name: "Car", + value: 25, + }, + }, + { + item: { + name: "Car Flower", + value: 40, + }, + }, + ]); + }); + + test("should filter items that value is empty", () => { + const filterItem = { operation: "empty", path: "item.name", value: "" }; + const filteredItems = FilterItemsByOperation( + [ + { + item: { + name: "Car", + value: 25, + }, + }, + { + item: { + name: "", + value: 30, + }, + }, + { + item: { + name: null, + value: 40, + }, + }, + ], + filterItem, + ); + + console.log(filteredItems); + + expect(filteredItems).toEqual([ + { + item: { + name: "", + value: 30, + }, + }, + { + item: { + name: null, + value: 40, + }, + }, + ]); + }); + + test("should return all items when value is empty", () => { + const filterItem = { operation: "contains", path: "item.name", value: "" }; + const filteredItems = FilterItemsByOperation(items, filterItem); + + expect(filteredItems).toEqual(items); + }); + + test("should filter items that have a greater value than the specified value ", () => { + const filterItem = { operation: "greater", path: "item.value", value: "25" }; + const filteredItems = FilterItemsByOperation(items, filterItem); + + expect(filteredItems).toEqual([ + { + item: { + name: "AirPlane", + value: 30, + }, + }, + { + item: { + name: "Car Flower", + value: 40, + }, + }, + ]); + }); + + test("should filter items that have a less value than the specified value ", () => { + const filterItem = { operation: "less", path: "item.value", value: "40" }; + const filteredItems = FilterItemsByOperation(items, filterItem); + + expect(filteredItems).toEqual([ + { + item: { + name: "Car", + value: 25, + }, + }, + { + item: { + name: "AirPlane", + value: 30, + }, + }, + ]); + }); + + test("should filter items that have a less or equal value than the specified value ", () => { + const filterItem = { operation: "less_or_equal", path: "item.value", value: "30" }; + const filteredItems = FilterItemsByOperation(items, filterItem); + + expect(filteredItems).toEqual([ + { + item: { + name: "Car", + value: 25, + }, + }, + { + item: { + name: "AirPlane", + value: 30, + }, + }, + ]); + }); + + test("should filter items that have a greater or equal value than the specified value ", () => { + const filterItem = { operation: "greater_or_equal", path: "item.value", value: "30" }; + const filteredItems = FilterItemsByOperation(items, filterItem); + + expect(filteredItems).toEqual([ + { + item: { + name: "AirPlane", + value: 30, + }, + }, + { + item: { + name: "Car Flower", + value: 40, + }, + }, + ]); + }); + + test("should return all items when operation is invalid", () => { + const filterItem = { operation: "invalid_operation", path: "item.name", value: "Doe" }; + const filteredItems = FilterItemsByOperation(items, filterItem); + + expect(filteredItems).toEqual(items); + }); + + test("should filter items that is equal as the specified value", () => { + const filterItem = { operation: "equal", path: "item.value", value: "25" }; + const filteredItems = FilterItemsByOperation(items, filterItem); + + expect(filteredItems).toEqual([ + { + item: { + name: "Car", + value: 25, + }, + }, + ]); + }); + + test("should filter items that is not equal as the specified value", () => { + const filterItem = { operation: "not_equal", path: "item.value", value: "25" }; + const filteredItems = FilterItemsByOperation(items, filterItem); + + expect(filteredItems).toEqual([ + { + item: { + name: "AirPlane", + value: 30, + }, + }, + { + item: { + name: "Car Flower", + value: 40, + }, + }, + ]); + }); +}); diff --git a/web/libs/editor/src/components/Filter/filter-util.ts b/web/libs/editor/src/components/Filter/filter-util.ts new file mode 100644 index 000000000000..0041f7c2bed8 --- /dev/null +++ b/web/libs/editor/src/components/Filter/filter-util.ts @@ -0,0 +1,192 @@ +import type { FilterListInterface } from "./FilterInterfaces"; +import { isDefined } from "../../utils/utilities"; + +export const FilterItemsByOperation = (items: any[], filterItem: FilterListInterface) => { + if ((!filterItem.value || filterItem.value === "") && filterItem.operation !== "empty") return items; + + switch (filterItem.operation) { + case "contains": + return contains(items, filterItem); + case "not_contains": + return notcontains(items, filterItem); + case "in": + return between(items, filterItem); + case "not_in": + return notbetween(items, filterItem); + case "regex": + return regex(items, filterItem); + case "empty": + return empty(items, filterItem); + case "greater": + return greater(items, filterItem); + case "less": + return less(items, filterItem); + case "less_or_equal": + return lessOrEqual(items, filterItem); + case "greater_or_equal": + return greaterOrEqual(items, filterItem); + case "equal": + return equal(items, filterItem); + case "not_equal": + return notequal(items, filterItem); + default: + return items; + } +}; + +export const FilterItems = (items: any[], filterList: FilterListInterface[]) => { + const _filteredList = [[...items]]; + + for (let i = 0; i < filterList.length; i++) { + if (!filterList[i].value && filterList[i].operation !== "empty") continue; + + if (filterList[i].logic === "and") { + // 0 is equal to AND, 1 is equal to OR + _filteredList[_filteredList.length - 1] = FilterItemsByOperation( + _filteredList[_filteredList.length - 1], + filterList[i], + ); + } else { + _filteredList.push(FilterItemsByOperation(items, filterList[i])); + } + } + + return _filteredList.flat(1).reduce((unique, item) => (unique.includes(item) ? unique : [...unique, item]), []); +}; + +const contains = (items: any[], filterItem: FilterListInterface) => { + if (isDefined(filterItem.value)) { + return items.filter((obj) => { + const item = getFilteredPath(filterItem.path, obj); + + return item?.toLowerCase().includes(filterItem.value.toLowerCase()); + }); + } + return items; +}; + +const notcontains = (items: any[], filterItem: FilterListInterface) => { + if (isDefined(filterItem.value)) { + return items.filter((obj) => { + const item = getFilteredPath(filterItem.path, obj); + + return !item?.toLowerCase().includes(filterItem.value.toLowerCase()); + }); + } + return items; +}; + +const greater = (items: any[], filterItem: FilterListInterface) => { + if (isDefined(filterItem.value)) { + return items.filter((obj) => { + const item = getFilteredPath(filterItem.path, obj); + + return item > filterItem.value; + }); + } + return items; +}; + +const greaterOrEqual = (items: any[], filterItem: FilterListInterface) => { + if (isDefined(filterItem.value)) { + return items.filter((obj) => { + const item = getFilteredPath(filterItem.path, obj); + + return item >= filterItem.value; + }); + } + return items; +}; + +const less = (items: any[], filterItem: FilterListInterface) => { + if (isDefined(filterItem.value)) { + return items.filter((obj) => { + const item = getFilteredPath(filterItem.path, obj); + + return item < filterItem.value; + }); + } + return items; +}; + +const lessOrEqual = (items: any[], filterItem: FilterListInterface) => { + if (isDefined(filterItem.value)) { + return items.filter((obj) => { + const item = getFilteredPath(filterItem.path, obj); + + return item <= filterItem.value; + }); + } + return items; +}; + +const equal = (items: any[], filterItem: FilterListInterface) => { + if (isDefined(filterItem.value)) { + return items.filter((obj) => { + const item = getFilteredPath(filterItem.path, obj); + + return item?.toString().toLowerCase() === filterItem.value?.toString().toLowerCase(); + }); + } + return items; +}; + +const notequal = (items: any[], filterItem: FilterListInterface) => { + if (isDefined(filterItem.value)) { + return items.filter((obj) => { + const item = getFilteredPath(filterItem.path, obj); + + return item?.toString().toLowerCase() !== filterItem.value?.toLowerCase(); + }); + } + return items; +}; + +const between = (items: any[], filterItem: FilterListInterface) => { + if (isDefined(filterItem.value)) { + return items.filter((obj) => { + const item = getFilteredPath(filterItem.path, obj); + + return filterItem.value.min <= item && item <= filterItem.value.max; + }); + } + return items; +}; + +const notbetween = (items: any[], filterItem: FilterListInterface) => { + if (isDefined(filterItem.value)) { + return items.filter((obj) => { + const item = getFilteredPath(filterItem.path, obj); + + return item <= filterItem.value.min || filterItem.value.max <= item; + }); + } + return items; +}; + +const regex = (items: any[], filterItem: FilterListInterface) => { + try { + return items.filter((obj) => { + const item = getFilteredPath(filterItem.path, obj); + const regex = new RegExp(filterItem.value, "g"); + + return item.match(regex); + }); + } catch (e) { + return items; + } +}; + +const empty = (items: any[], filterItem: FilterListInterface) => { + return items.filter((obj) => { + const item = getFilteredPath(filterItem.path, obj); + + return item === "" || !item || item === null || item === undefined || item === "blank"; + }); +}; + +const getFilteredPath = (path: string | string[], items: any[], separator = ".") => { + const properties = Array.isArray(path) ? path : path.split(separator); + + return properties.reduce((prev, curr) => prev?.[curr], items); +}; diff --git a/web/libs/editor/src/components/Filter/types/Boolean.jsx b/web/libs/editor/src/components/Filter/types/Boolean.jsx new file mode 100644 index 000000000000..4b708df12bad --- /dev/null +++ b/web/libs/editor/src/components/Filter/types/Boolean.jsx @@ -0,0 +1,23 @@ +import { FilterDropdown } from "../FilterDropdown"; +import { observer } from "mobx-react"; + +const BaseInput = observer((props) => ( + { + props.onChange(!value); + }} + items={[ + { label: "true", key: true }, + { label: "false", key: false }, + ]} + /> +)); + +export const BooleanFilter = [ + { + key: "equal", + label: "is", + valueType: "single", + input: BaseInput, + }, +]; diff --git a/web/libs/editor/src/components/Filter/types/Common.jsx b/web/libs/editor/src/components/Filter/types/Common.jsx new file mode 100644 index 000000000000..68724ef95e37 --- /dev/null +++ b/web/libs/editor/src/components/Filter/types/Common.jsx @@ -0,0 +1,14 @@ +import { FilterDropdown } from "../FilterDropdown"; +import { observer } from "mobx-react"; + +const BaseInput = observer((props) => ( + props.onChange(value)} items={[{ label: "yes" }, { label: "no" }]} /> +)); + +export const Common = [ + { + key: "empty", + label: "is empty", + input: BaseInput, + }, +]; diff --git a/web/libs/editor/src/components/Filter/types/Number.jsx b/web/libs/editor/src/components/Filter/types/Number.jsx new file mode 100644 index 000000000000..bc08372ab973 --- /dev/null +++ b/web/libs/editor/src/components/Filter/types/Number.jsx @@ -0,0 +1,85 @@ +import { observer } from "mobx-react"; +import { FilterInput } from "../FilterInput"; +import { Common } from "./Common"; + +const NumberInput = observer((props) => { + return ; +}); + +const RangeInput = observer((props) => { + const min = props.value?.min ?? null; + const max = props.value?.max ?? null; + + const onValueChange = (newValue) => { + console.log({ newValue }); + props.onChange(newValue); + }; + + const onChangeMin = (newValue) => { + onValueChange({ min: Number(newValue), max }); + }; + + const onChangeMax = (newValue) => { + onValueChange({ min, max: Number(newValue) }); + }; + + return ( + <> + + and + + + ); +}); + +export const NumberFilter = [ + { + key: "equal", + label: "=", + valueType: "single", + input: NumberInput, + }, + { + key: "not_equal", + label: "≠", + valueType: "single", + input: NumberInput, + }, + { + key: "less", + label: "<", + valueType: "single", + input: NumberInput, + }, + { + key: "greater", + label: ">", + valueType: "single", + input: NumberInput, + }, + { + key: "less_or_equal", + label: "≤", + valueType: "single", + input: NumberInput, + }, + { + key: "greater_or_equal", + label: "≥", + valueType: "single", + input: NumberInput, + }, + { + key: "in", + label: "is between", + valueType: "range", + input: RangeInput, + }, + { + key: "not_in", + label: "not between", + valueType: "range", + input: RangeInput, + }, + ...Common, +]; diff --git a/web/libs/editor/src/components/Filter/types/String.jsx b/web/libs/editor/src/components/Filter/types/String.jsx new file mode 100644 index 000000000000..e2c38af17a22 --- /dev/null +++ b/web/libs/editor/src/components/Filter/types/String.jsx @@ -0,0 +1,50 @@ +import { observer } from "mobx-react"; +import { FilterInput } from "../FilterInput"; +import { Common } from "./Common"; + +const BaseInput = observer((props) => { + return ( + + ); +}); + +export const StringFilter = [ + { + key: "contains", + label: "contains", + valueType: "single", + input: BaseInput, + }, + { + key: "not_contains", + label: "not contains", + valueType: "single", + input: BaseInput, + }, + { + key: "regex", + label: "regex", + valueType: "single", + input: BaseInput, + }, + { + key: "equal", + label: "equal", + valueType: "single", + input: BaseInput, + }, + { + key: "not_equal", + label: "not equal", + valueType: "single", + input: BaseInput, + }, + ...Common, +]; diff --git a/web/libs/editor/src/components/Filter/types/index.js b/web/libs/editor/src/components/Filter/types/index.js new file mode 100644 index 000000000000..22306a9663e2 --- /dev/null +++ b/web/libs/editor/src/components/Filter/types/index.js @@ -0,0 +1,4 @@ +export { BooleanFilter as Boolean } from "./Boolean"; +export { Common } from "./Common"; +export { NumberFilter as Number } from "./Number"; +export { StringFilter as Image, StringFilter as String } from "./String"; diff --git a/web/libs/editor/src/components/SidePanels/DetailsPanel/RegionEditor.tsx b/web/libs/editor/src/components/SidePanels/DetailsPanel/RegionEditor.tsx index b6129699d764..38939ae5383a 100644 --- a/web/libs/editor/src/components/SidePanels/DetailsPanel/RegionEditor.tsx +++ b/web/libs/editor/src/components/SidePanels/DetailsPanel/RegionEditor.tsx @@ -13,7 +13,7 @@ import { useState, } from "react"; import { IconPropertyAngle } from "@humansignal/icons"; -import { Checkbox, Select } from "@humansignal/ui"; +import { Checkbox } from "@humansignal/ui"; import { Block, Elem, useBEM } from "../../../utils/bem"; import { FF_DEV_2715, isFF } from "../../../utils/feature-flags"; import { TimeDurationControl } from "../../TimeDurationControl/TimeDurationControl"; @@ -189,12 +189,17 @@ const RegionProperty: FC = ({ property, label, region }) => onChange={(v) => onChangeHandler(Number(v))} /> ) : options ? ( - ) : null} diff --git a/web/libs/editor/src/components/SidePanels/DetailsPanel/Relations.tsx b/web/libs/editor/src/components/SidePanels/DetailsPanel/Relations.tsx index c3dbaaf418ec..2baf51341acc 100644 --- a/web/libs/editor/src/components/SidePanels/DetailsPanel/Relations.tsx +++ b/web/libs/editor/src/components/SidePanels/DetailsPanel/Relations.tsx @@ -13,7 +13,7 @@ import { Button } from "../../../common/Button/Button"; import { Block, Elem } from "../../../utils/bem"; import { wrapArray } from "../../../utils/utilities"; import { RegionItem } from "./RegionItem"; -import { Select } from "@humansignal/ui"; +import { Select } from "antd"; import "./Relations.scss"; const RealtionsComponent: FC = ({ relationStore }) => { @@ -143,7 +143,7 @@ const RelationMeta: FC = observer(({ relation }) => { const { children, choice } = control; const selectionMode = useMemo(() => { - return choice === "multiple"; + return choice === "multiple" ? "multiple" : undefined; }, [choice]); const onChange = useCallback( @@ -154,21 +154,22 @@ const RelationMeta: FC = observer(({ relation }) => { }, [relation], ); - const options = useMemo( - () => children.map((c: any) => ({ value: c.value, style: { background: c.background } })), - [children], - ); return ( ); }); diff --git a/web/libs/editor/src/components/Waveform/Waveform.jsx b/web/libs/editor/src/components/Waveform/Waveform.jsx index e2ac3618f5ac..6f92db5bbefd 100644 --- a/web/libs/editor/src/components/Waveform/Waveform.jsx +++ b/web/libs/editor/src/components/Waveform/Waveform.jsx @@ -7,15 +7,14 @@ import TimelinePlugin from "wavesurfer.js/dist/plugin/wavesurfer.timeline.min.js import WaveSurfer from "wavesurfer.js"; import styles from "./Waveform.module.scss"; import globalStyles from "../../styles/global.module.scss"; -import { Col, Row, Slider } from "antd"; +import { Col, Row, Select, Slider } from "antd"; import { SoundOutlined } from "@ant-design/icons"; import defaultMessages from "../../utils/messages"; import { Hotkey } from "../../core/Hotkey"; -import { Select, Tooltip } from "@humansignal/ui"; +import { Tooltip } from "@humansignal/ui"; const MIN_ZOOM_Y = 1; const MAX_ZOOM_Y = 50; -const SPEEDS = ["0.5", "0.75", "1.0", "1.25", "1.5", "2.0"].map((v) => ({ value: +v, label: `Speed ${v}` })); /** * Use formatTimeCallback to style the notch labels as you wish, such @@ -481,6 +480,8 @@ export default class Waveform extends React.Component { }; render() { + const speeds = ["0.5", "0.75", "1.0", "1.25", "1.5", "2.0"]; + return (
@@ -566,8 +567,13 @@ export default class Waveform extends React.Component { style={{ width: "100%" }} defaultValue={this.state.speed} onChange={this.onChangeSpeed} - options={SPEEDS} - /> + > + {speeds.map((speed) => ( + + Speed {speed} + + ))} + )} diff --git a/web/libs/editor/src/tags/control/Choices.jsx b/web/libs/editor/src/tags/control/Choices.jsx index 6530eaa1b915..1ab4208570dc 100644 --- a/web/libs/editor/src/tags/control/Choices.jsx +++ b/web/libs/editor/src/tags/control/Choices.jsx @@ -1,3 +1,4 @@ +import { Select } from "antd"; import { observer } from "mobx-react"; import { types } from "mobx-state-tree"; @@ -20,11 +21,12 @@ import DynamicChildrenMixin from "../../mixins/DynamicChildrenMixin"; import { FF_LSDV_4583, isFF } from "../../utils/feature-flags"; import { ReadOnlyControlMixin } from "../../mixins/ReadOnlyMixin"; import SelectedChoiceMixin from "../../mixins/SelectedChoiceMixin"; +import { HintTooltip } from "../../components/Taxonomy/Taxonomy"; import ClassificationBase from "./ClassificationBase"; import PerItemMixin from "../../mixins/PerItem"; import Infomodal from "../../components/Infomodal/Infomodal"; -import { useMemo } from "react"; -import { Select, Tooltip } from "@humansignal/ui"; + +const { Option } = Select; /** * The `Choices` tag is used to create a group of choices, with radio buttons or checkboxes. It can be used for single or multi-class classification. Also, it is used for advanced classification tasks where annotators can choose one or multiple answers. @@ -251,25 +253,11 @@ const ChoicesModel = types.compose( ); const ChoicesSelectLayout = observer(({ item }) => { - const options = useMemo( - () => - item.tiedChildren.map((i) => ({ - value: i._value, - label: ( - - - {i._value} - - - ), - })), - [item.tiedChildren], - ); return ( ); }); diff --git a/web/libs/editor/src/tags/control/DateTime.jsx b/web/libs/editor/src/tags/control/DateTime.jsx index 0bb17e550a3d..8c627ffc0f8f 100644 --- a/web/libs/editor/src/tags/control/DateTime.jsx +++ b/web/libs/editor/src/tags/control/DateTime.jsx @@ -15,7 +15,6 @@ import { ReadOnlyControlMixin } from "../../mixins/ReadOnlyMixin"; import ClassificationBase from "./ClassificationBase"; import PerItemMixin from "../../mixins/PerItem"; import { FF_LSDV_4583, isFF } from "../../utils/feature-flags"; -import { Select } from "@humansignal/ui"; const FORMAT_FULL = "%Y-%m-%dT%H:%M"; const FORMAT_DATE = "%Y-%m-%d"; @@ -177,7 +176,7 @@ const Model = types const date = new Date(); const getYear = (minmax) => { if (minmax === "current") return date.getFullYear(); - if (minmax.length === 4) return Number.parseInt(minmax); + if (minmax.length === 4) return minmax; return self.parseDateTime(minmax)?.getFullYear(); }; const minYear = getYear(self.min ?? "2000"); @@ -254,13 +253,13 @@ const Model = types } }, - onMonthChange(val) { - self.month = +val || undefined; + onMonthChange(e) { + self.month = +e.target.value || undefined; self.updateResult(); }, - onYearChange(val) { - self.year = +val || undefined; + onYearChange(e) { + self.year = +e.target.value || undefined; self.updateResult(); }, @@ -339,6 +338,7 @@ const HtxDateTime = inject("store")( const visibleStyle = item.perRegionVisible() ? { margin: "0 0 1em" } : { display: "none" }; const visual = { style: { width: "auto", marginRight: "4px", borderColor: item.isValid ? undefined : "red" }, + className: "ant-input", }; const [minTime, maxTime] = [item.min, item.max].map((s) => s?.match(/\d?\d:\d\d/)?.[0]); const [dateInputValue, setDateInputValue] = useState(""); @@ -367,28 +367,36 @@ const HtxDateTime = inject("store")( return (
{item.showMonth && ( - )} {item.showYear && ( - )} {item.showDate && ( { const itemStyle = { border: `2px solid ${Utils.Colors.convertToRGBA(ColorScheme.make_color({ seed: name })[0])}` }; - if (name === "all") { - return <>Show all authors; - } - return ( { export const AuthorFilter = observer(({ item, onChange }) => { const placeholder = useMemo(() => Show all authors, []); - const initialValue = "all"; - const options = useMemo(() => { - const authorOptions = item._value - .reduce((all, v) => (all.includes(v[item.namekey]) ? all : [...all, v[item.namekey]]), []) - .sort() - .map((name) => ({ - value: name, - label: , - })); - return [{ value: initialValue, label: , children: authorOptions }]; - }, [item._value, item.namekey, initialValue]); - + const value = item.filterByAuthor; + const options = useMemo( + () => item._value.reduce((all, v) => (all.includes(v[item.namekey]) ? all : [...all, v[item.namekey]]), []).sort(), + [item._value, item.namekey], + ); + const filteredOptions = item.searchAuthor + ? options.filter((o) => o.toLowerCase().includes(item.searchAuthor.toLowerCase())) + : options; const onFilterChange = useCallback( (next) => { - const nextVal = next?.value ?? next; // ensure this is cleared if any action promoting an empty value change is made - if (!nextVal || nextVal?.includes("all")) { + if (!next || next?.includes(null)) { item.setAuthorFilter([]); - } else if (nextVal) { - item.setAuthorFilter(nextVal); + } else { + item.setAuthorFilter(next); } onChange?.(); @@ -67,12 +58,33 @@ export const AuthorFilter = observer(({ item, onChange }) => {
item.setAuthorSearch(e.target.value)} + /> +
+ + Show all authors + + {filteredOptions.map((name) => ( + + + + ))} +
); }); diff --git a/web/libs/editor/src/utils/__tests__/date.test.js b/web/libs/editor/src/utils/__tests__/date.test.js index 5f981467aec4..233b2429cc33 100644 --- a/web/libs/editor/src/utils/__tests__/date.test.js +++ b/web/libs/editor/src/utils/__tests__/date.test.js @@ -9,32 +9,28 @@ describe("Helper function prettyDate", () => { }); test("Yesterday", () => { - const today = new Date(); - const testing = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate())); + const testing = new Date(); const resultDate = new Date(testing.setDate(testing.getDate() - 1)); expect(prettyDate(resultDate.toISOString())).toBe("Yesterday"); }); test("2 days ago", () => { - const today = new Date(); - const testing = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate())); + const testing = new Date(); const resultDate = new Date(testing.setDate(testing.getDate() - 2)); expect(prettyDate(resultDate.toISOString())).toBe("2 days ago"); }); test("2 weeks ago", () => { - const today = new Date(); - const testing = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate())); + const testing = new Date(); const resultDate = new Date(testing.setDate(testing.getDate() - 14)); expect(prettyDate(resultDate.toISOString())).toBe("2 weeks ago"); }); test("100 days ago", () => { - const today = new Date(); - const testing = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate())); + const testing = new Date(); const resultDate = new Date(testing.setDate(testing.getDate() - 100)); expect(prettyDate(resultDate.toISOString())).toBe("100 days ago"); diff --git a/web/libs/editor/tests/e2e/fragments/AtParagraphs.js b/web/libs/editor/tests/e2e/fragments/AtParagraphs.js index 85f3be3e8eea..9a89ba2e6cab 100644 --- a/web/libs/editor/tests/e2e/fragments/AtParagraphs.js +++ b/web/libs/editor/tests/e2e/fragments/AtParagraphs.js @@ -3,7 +3,7 @@ const Helpers = require("../tests/helpers"); module.exports = { _rootSelector: ".lsf-paragraphs", - _filterSelector: "button[data-testid*='select-trigger']", + _filterSelector: ".lsf-select__value", _phraseSelector: "[class^='phrase--']", _phraseDialoguetextSelector: "[class^='dialoguetext--']", @@ -36,30 +36,11 @@ module.exports = { }, clickFilter(...authors) { - // Open dropdown and wait for it to appear - I.click(locate(this._filterSelector)); - I.wait(0.5); - // For the new select component, we need to select each author - // and the dropdown is managed automatically + I.click(this.locate(this._filterSelector)); for (const author of authors) { - // We may or may not have a search field depending on number of options - const hasSearchField = I.executeScript(() => { - return !!document.querySelector("input[data-testid='select-search-field']"); - }); - - if (hasSearchField) { - // Try to search if field is available - I.fillField(locate("input[data-testid='select-search-field']"), author); - I.wait(0.5); - } - - // Select the author option - I.click(locate(`div[data-testid='select-option-${author}']`)); - I.wait(0.5); + I.fillField("search_author", author); + I.click(locate(".lsf-select__option").withText(author)); } - - // Close any open dropdown - I.pressKey("Escape"); - I.wait(1); // Wait for UI to update after filter change + I.click(this.locate(this._filterSelector)); }, }; diff --git a/web/libs/editor/tests/e2e/tests/date-time.test.js b/web/libs/editor/tests/e2e/tests/date-time.test.js index 8f678c16b429..fe7b88b63377 100644 --- a/web/libs/editor/tests/e2e/tests/date-time.test.js +++ b/web/libs/editor/tests/e2e/tests/date-time.test.js @@ -5,11 +5,6 @@ const { serialize, selectText } = require("./helpers"); Feature("Date Time"); const config = ` -
Select text to see related smaller DateTime controls for every region