consolidate search and filtering

This commit is contained in:
Brandon Thomas committed 2023-01-27 04:23:51 -05:00
1 parent b73aaab255
commit c82a6dd07c
10 files changed
+288 -158

No files matched your search

@@ -0,0 +1,14 @@
import React, { Fragment } from "react";
import { components } from "react-select";
// Problem - search label isn't present when searching, confusing users that don't realize it's
// a text search box.
// Ticket: https://github.com/JedWatson/react-select/issues/1351
// Solution from: https://codesandbox.io/s/hide-selected-on-focus-react-select-n05yi2?file=/src/App.js:531-774
export function FixedSingleValueSelectOption(props: any) {
const { children, ...rest } = props;
const { selectProps } = props;
if (selectProps.menuIsOpen) return <Fragment></Fragment>;
return <components.SingleValue {...rest}>{children}</components.SingleValue>;
}
@@ -30,7 +30,6 @@ import {
ListTtsCategoriesIsOk,
} from "@storyteller/components/src/api/category/ListTtsCategories";
import { TtsCategoryType } from "../../../../../AppWrapper";
import { SelectSearch } from "./search/SelectSearch";
import { LanguageNotice } from "./notices/LanguageNotice";
import { Language } from "@storyteller/components/src/i18n/Language";
import { TwitchTtsNotice } from "./notices/TwitchTtsNotice";
@@ -39,8 +38,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faArrowRight,
faBarsStaggered,
faChevronDown,
faChevronUp,
faDeleteLeft,
faGlobe,
faVolumeHigh,
@@ -69,7 +66,7 @@ import { WebUrl } from "../../../../../common/WebUrl";
import { usePrefixedDocumentTitle } from "../../../../../common/UsePrefixedDocumentTitle";
import { RatingButtons } from "../../../_common/ratings/RatingButtons";
import { RatingStats } from "../../../_common/ratings/RatingStats";
import { ExploreVoicesModal } from "./explore/ExploreVoicesModal";
import { SearchOmnibar } from "./search/SearchOmnibar";
export interface EnqueueJobResponsePayload {
success: boolean;
@@ -150,12 +147,6 @@ function TtsModelListPage(props: Props) {
return () => clearTimeout(timeout);
}, []);
const [isOpen, setIsOpen] = useState(false);
const handleClickExplore = () => {
setIsOpen(!isOpen);
};
let {
setTtsModels,
setAllTtsCategories,
@@ -522,82 +513,29 @@ function TtsModelListPage(props: Props) {
onSubmit={handleFormSubmit}
>
{/* Explore Rollout */}
<div
className={`sliding-content ${
isOpen ? "open pb-4" : "closed"
}`}
>
<ExploreVoicesModal
allTtsCategories={props.allTtsCategories}
allTtsModels={props.ttsModels}
allTtsCategoriesByTokenMap={
props.allTtsCategoriesByTokenMap
}
allTtsModelsByTokenMap={props.allTtsModelsByTokenMap}
ttsModelsByCategoryToken={props.ttsModelsByCategoryToken}
dropdownCategories={props.dropdownCategories}
setDropdownCategories={props.setDropdownCategories}
selectedCategories={props.selectedCategories}
setSelectedCategories={props.setSelectedCategories}
maybeSelectedTtsModel={props.maybeSelectedTtsModel}
setMaybeSelectedTtsModel={props.setMaybeSelectedTtsModel}
selectedTtsLanguageScope={props.selectedTtsLanguageScope}
setSelectedTtsLanguageScope={
props.setSelectedTtsLanguageScope
}
/>
</div>
<div className="pb-4">
<div className="d-flex gap-2">
<label className="sub-title">
{t("tts.TtsModelListPage.form.searchBarLabel")}
</label>
{/*<a href="/" className="ms-1">
<FontAwesomeIcon icon={faShuffle} />
</a>*/}
</div>
<SearchOmnibar
allTtsCategories={props.allTtsCategories}
allTtsModels={props.ttsModels}
allTtsCategoriesByTokenMap={props.allTtsCategoriesByTokenMap}
allTtsModelsByTokenMap={props.allTtsModelsByTokenMap}
ttsModelsByCategoryToken={props.ttsModelsByCategoryToken}
dropdownCategories={props.dropdownCategories}
setDropdownCategories={props.setDropdownCategories}
selectedCategories={props.selectedCategories}
setSelectedCategories={props.setSelectedCategories}
maybeSelectedTtsModel={props.maybeSelectedTtsModel}
setMaybeSelectedTtsModel={props.setMaybeSelectedTtsModel}
selectedTtsLanguageScope={props.selectedTtsLanguageScope}
setSelectedTtsLanguageScope={props.setSelectedTtsLanguageScope}
/>
<div className="d-flex flex-column flex-lg-row gap-3 zi-2">
<div className="flex-grow-1">
<SelectSearch
allTtsCategories={props.allTtsCategories}
allTtsModels={props.ttsModels}
allTtsModelsByTokenMap={props.allTtsModelsByTokenMap}
dropdownCategories={props.dropdownCategories}
setDropdownCategories={props.setDropdownCategories}
selectedCategories={props.selectedCategories}
setSelectedCategories={props.setSelectedCategories}
maybeSelectedTtsModel={props.maybeSelectedTtsModel}
setMaybeSelectedTtsModel={
props.setMaybeSelectedTtsModel
}
selectedTtsLanguageScope={
props.selectedTtsLanguageScope
}
/>
</div>
{}
<button
onClick={handleClickExplore}
// onClick={() => {
// Analytics.ttsOpenExploreVoicesModal();
// }}
className="btn btn-primary rounded-50"
type="button"
>
<FontAwesomeIcon
icon={isOpen ? faChevronUp : faChevronDown}
className="me-2"
/>
{t(
"tts.TtsModelListPage.exploreModal.exploreModalOpenButton"
)}
</button>
</div>
</div>
{/*
EXPLORE OMNIBAR GOES HERE TODO
*/}
{directViewLink}
@@ -0,0 +1,117 @@
import React, { useState } from "react";
import { t } from "i18next";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { TtsCategoryType } from "../../../../../../AppWrapper";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
import { ExploreVoicesTray } from "./components/ExploreVoicesTray";
import { faChevronDown, faChevronUp } from "@fortawesome/pro-duotone-svg-icons";
import { ScopedVoiceModelOptions } from "./components/ScopedVoiceModelOptions";
import { VoiceCountLabel } from "./components/VoiceCountLabel";
import { Analytics } from "../../../../../../common/Analytics";
interface Props {
allTtsCategories: TtsCategoryType[];
allTtsModels: TtsModelListItem[];
allTtsCategoriesByTokenMap: Map<string, TtsCategoryType>;
allTtsModelsByTokenMap: Map<string, TtsModelListItem>;
ttsModelsByCategoryToken: Map<string, Set<TtsModelListItem>>;
dropdownCategories: TtsCategoryType[][];
setDropdownCategories: (dropdownCategories: TtsCategoryType[][]) => void;
selectedCategories: TtsCategoryType[];
setSelectedCategories: (selectedCategories: TtsCategoryType[]) => void;
maybeSelectedTtsModel?: TtsModelListItem;
setMaybeSelectedTtsModel: (maybeSelectedTtsModel: TtsModelListItem) => void;
selectedTtsLanguageScope: string;
setSelectedTtsLanguageScope: (selectedTtsLanguageScope: string) => void;
}
export function SearchOmnibar(props: Props) {
const [isOpen, setIsOpen] = useState(false);
const handleClickExplore = () => {
setIsOpen(!isOpen);
};
const canSearchVoices = !isOpen; // TODO: Perhaps only set false on mobile.
return (
<>
<div
className={`sliding-content ${
isOpen ? "open pb-4" : "closed"
}`}
>
<ExploreVoicesTray
allTtsCategories={props.allTtsCategories}
allTtsModels={props.allTtsModels}
allTtsCategoriesByTokenMap={
props.allTtsCategoriesByTokenMap
}
allTtsModelsByTokenMap={props.allTtsModelsByTokenMap}
ttsModelsByCategoryToken={props.ttsModelsByCategoryToken}
dropdownCategories={props.dropdownCategories}
setDropdownCategories={props.setDropdownCategories}
selectedCategories={props.selectedCategories}
setSelectedCategories={props.setSelectedCategories}
maybeSelectedTtsModel={props.maybeSelectedTtsModel}
setMaybeSelectedTtsModel={props.setMaybeSelectedTtsModel}
selectedTtsLanguageScope={props.selectedTtsLanguageScope}
setSelectedTtsLanguageScope={
props.setSelectedTtsLanguageScope
}
/>
</div>
<div className="pb-4">
<VoiceCountLabel
allTtsModels={props.allTtsModels}
ttsModelsByCategoryToken={props.ttsModelsByCategoryToken}
selectedCategories={props.selectedCategories}
selectedTtsLanguageScope={props.selectedTtsLanguageScope}
/>
<div className="d-flex flex-column flex-lg-row gap-3 zi-2">
<div className="flex-grow-1">
<ScopedVoiceModelOptions
allTtsCategories={props.allTtsCategories}
allTtsModels={props.allTtsModels}
allTtsCategoriesByTokenMap={props.allTtsCategoriesByTokenMap}
allTtsModelsByTokenMap={props.allTtsModelsByTokenMap}
ttsModelsByCategoryToken={props.ttsModelsByCategoryToken}
dropdownCategories={props.dropdownCategories}
selectedCategories={props.selectedCategories}
maybeSelectedTtsModel={props.maybeSelectedTtsModel}
setMaybeSelectedTtsModel={props.setMaybeSelectedTtsModel}
selectedTtsLanguageScope={props.selectedTtsLanguageScope}
canSearchVoices={canSearchVoices}
/>
</div>
{}
<button
onClick={() => {
Analytics.ttsOpenExploreVoicesModal();
handleClickExplore();
}}
className="btn btn-primary rounded-50"
type="button"
>
<FontAwesomeIcon
icon={isOpen ? faChevronUp : faChevronDown}
className="me-2"
/>
{t(
"tts.TtsModelListPage.exploreModal.exploreModalOpenButton"
)}
</button>
</div>
</div>
</>
)
}
@@ -1,13 +1,14 @@
import React, { Fragment } from "react";
import React from "react";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
import { TtsCategoryType } from "../../../../../../AppWrapper";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import Select, { ActionMeta, createFilter, components } from "react-select";
import Select, { ActionMeta, createFilter } from "react-select";
import Option from "react-select";
import { SearchFieldClass } from "./SearchFieldClass";
import { FastReactSelectOption } from "../../../../_common/FastReactSelectOption";
import { FastReactSelectOption } from "../../../../_common/react_select/FastReactSelectOption";
import { Analytics } from "../../../../../../common/Analytics";
import { FixedSingleValueSelectOption } from "../../../../_common/react_select/FixedSingleValueSelectOption";
interface Props {
allTtsCategories: TtsCategoryType[];
@@ -92,21 +93,9 @@ export function SelectSearch(props: Props) {
// NB: The following settings improve upon performance.
// See: https://github.com/JedWatson/react-select/issues/3128
filterOption={createFilter({ignoreAccents: false})}
components={{SingleValue: FixedSingleValue, Option: FastReactSelectOption} as any}
components={{SingleValue: FixedSingleValueSelectOption, Option: FastReactSelectOption} as any}
/>
</div>
</>
);
}
// Problem - search label isn't present when searching, confusing users that don't realize it's
// a text search box.
// Ticket: https://github.com/JedWatson/react-select/issues/1351
// Solution from: https://codesandbox.io/s/hide-selected-on-focus-react-select-n05yi2?file=/src/App.js:531-774
function FixedSingleValue(props: any) {
const { children, ...rest } = props;
const { selectProps } = props;
if (selectProps.menuIsOpen) return <Fragment></Fragment>;
return <components.SingleValue {...rest}>{children}</components.SingleValue>;
}
@@ -1,11 +1,10 @@
import React from "react";
import { faEraser } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { LanguageOptions } from "./LanguageOptions";
import { TtsCategoryType } from "../../../../../../AppWrapper";
import { LanguageOptions } from "../filters/LanguageOptions";
import { TtsCategoryType } from "../../../../../../../AppWrapper";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
import { CategoryOptions } from "./CategoryOptions";
import { ScopedVoiceModelOptions } from "./ScopedVoiceModelOptions";
import { CategoryOptions } from "../filters/CategoryOptions";
import { t } from "i18next";
interface Props {
@@ -29,7 +28,7 @@ interface Props {
setSelectedTtsLanguageScope: (selectedTtsLanguageScope: string) => void;
}
export function ExploreVoicesModal(props: Props) {
export function ExploreVoicesTray(props: Props) {
const {
allTtsCategories,
allTtsCategoriesByTokenMap,
@@ -132,20 +131,6 @@ export function ExploreVoicesModal(props: Props) {
/>
</div>
</div>
<div className="mt-4">
<ScopedVoiceModelOptions
allTtsCategories={props.allTtsCategories}
allTtsModels={props.allTtsModels}
allTtsCategoriesByTokenMap={props.allTtsCategoriesByTokenMap}
allTtsModelsByTokenMap={props.allTtsModelsByTokenMap}
ttsModelsByCategoryToken={props.ttsModelsByCategoryToken}
dropdownCategories={props.dropdownCategories}
selectedCategories={props.selectedCategories}
maybeSelectedTtsModel={props.maybeSelectedTtsModel}
setMaybeSelectedTtsModel={props.setMaybeSelectedTtsModel}
selectedTtsLanguageScope={props.selectedTtsLanguageScope}
/>
</div>
</div>
);
}
@@ -2,12 +2,12 @@ import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMicrophone } from "@fortawesome/free-solid-svg-icons";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
import { TtsCategoryType } from "../../../../../../AppWrapper";
import { Trans } from "react-i18next";
import { TtsCategoryType } from "../../../../../../../AppWrapper";
import Select, { createFilter } from "react-select";
import { SearchFieldClass } from "../search/SearchFieldClass";
import { FastReactSelectOption } from "../../../../_common/FastReactSelectOption";
import { Analytics } from "../../../../../../common/Analytics";
import { SearchFieldClass } from "../SearchFieldClass";
import { FastReactSelectOption } from "../../../../../_common/react_select/FastReactSelectOption";
import { Analytics } from "../../../../../../../common/Analytics";
import { FixedSingleValueSelectOption } from "../../../../../_common/react_select/FixedSingleValueSelectOption";
interface Props {
allTtsCategories: TtsCategoryType[];
@@ -24,6 +24,8 @@ interface Props {
setMaybeSelectedTtsModel: (maybeSelectedTtsModel: TtsModelListItem) => void;
selectedTtsLanguageScope: string;
canSearchVoices: boolean;
}
export function ScopedVoiceModelOptions(props: Props) {
@@ -88,8 +90,6 @@ export function ScopedVoiceModelOptions(props: Props) {
};
}
const voiceCount = options.length;
let isLoading = false;
if (props.allTtsModels.length === 0) {
@@ -111,40 +111,56 @@ export function ScopedVoiceModelOptions(props: Props) {
};
}
let select;
if (props.canSearchVoices) {
select = (
<Select
value={selectedOption} // Controlled components use "value" instead of "defaultValue".
options={options}
classNames={SearchFieldClass}
onChange={handleChange}
onMenuOpen={() => { Analytics.ttsOpenPrimaryVoiceSelectMenu() } }
isLoading={isLoading}
isSearchable={true}
// NB: The following settings improve upon performance.
// See: https://github.com/JedWatson/react-select/issues/3128
filterOption={createFilter({ignoreAccents: false})}
components={{SingleValue: FixedSingleValueSelectOption, Option: FastReactSelectOption} as any}
/>
);
} else {
select = (
<Select
value={selectedOption}
options={options}
classNames={SearchFieldClass}
onChange={handleChange}
onMenuOpen={() => {
Analytics.ttsOpenScopedVoiceSelectMenu();
}}
isLoading={isLoading}
// On mobile, we don't want the onscreen keyboard to take up half the UI.
autoFocus={false}
isSearchable={false}
// NB: The following settings improve upon performance.
// See: https://github.com/JedWatson/react-select/issues/3128
filterOption={createFilter({ ignoreAccents: false })}
components={{ Option: FastReactSelectOption } as any}
/>
);
}
return (
<>
<div className="col">
<label className="sub-title">
<Trans
i18nKey="tts.TtsModelListPage.form.voicesLabel"
count={voiceCount}
>
Voice ({voiceCount} to choose from)
</Trans>
</label>
<div className="input-icon-search">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faMicrophone} />
</span>
<Select
value={selectedOption}
options={options}
classNames={SearchFieldClass}
onChange={handleChange}
onMenuOpen={() => {
Analytics.ttsOpenScopedVoiceSelectMenu();
}}
isLoading={isLoading}
// On mobile, we don't want the onscreen keyboard to take up half the UI.
autoFocus={false}
isSearchable={false}
// NB: The following settings improve upon performance.
// See: https://github.com/JedWatson/react-select/issues/3128
filterOption={createFilter({ ignoreAccents: false })}
components={{ Option: FastReactSelectOption } as any}
/>
{select}
</div>
</div>
</>
@@ -0,0 +1,71 @@
import React from "react";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
import { TtsCategoryType } from "../../../../../../../AppWrapper";
import { Trans } from "react-i18next";
interface Props {
allTtsModels: TtsModelListItem[];
ttsModelsByCategoryToken: Map<string, Set<TtsModelListItem>>;
selectedCategories: TtsCategoryType[];
selectedTtsLanguageScope: string;
}
// NB/TODO: This duplicates the work of a sister component, but it was the fastest way
// to hack this in without passing callbacks around or moving calculation higher up the
// component tree.
export function VoiceCountLabel(props: Props) {
const {
allTtsModels,
ttsModelsByCategoryToken,
selectedCategories,
} = props;
const leafiestCategory = selectedCategories[selectedCategories.length - 1];
let leafiestCategoryModels: Array<TtsModelListItem> = [];
if (leafiestCategory !== undefined) {
leafiestCategoryModels = Array.from(
ttsModelsByCategoryToken.get(leafiestCategory.category_token) || new Set()
);
} else {
leafiestCategoryModels = Array.from(new Set(allTtsModels));
}
let options = leafiestCategoryModels
.filter((ttsModel) => {
// Scope to currently selected language
if (props.selectedTtsLanguageScope === "*") {
return true; // NB: Sentinel value of "*" means all languages.
}
return (
ttsModel.ietf_primary_language_subtag === props.selectedTtsLanguageScope
);
})
.map((ttsModel) => {
return true; // NB: We're only counting voices.
});
const voiceCount = options.length;
return (
<>
<div className="d-flex gap-2">
<label className="sub-title">
<Trans
i18nKey="tts.TtsModelListPage.form.voicesLabel"
count={voiceCount}
>
Voice ({voiceCount} to choose from)
</Trans>
</label>
{/*<a href="/" className="ms-1">
<FontAwesomeIcon icon={faShuffle} />
</a>*/}
</div>
</>
);
}
@@ -5,11 +5,11 @@ import {
faTags,
} from "@fortawesome/free-solid-svg-icons";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
import { TtsCategoryType } from "../../../../../../AppWrapper";
import { TtsCategoryType } from "../../../../../../../AppWrapper";
//import { useTranslation } from "react-i18next";
import Select from "react-select";
import { SearchFieldClass } from "../search/SearchFieldClass";
import { Analytics } from "../../../../../../common/Analytics";
import { SearchFieldClass } from "../SearchFieldClass";
import { Analytics } from "../../../../../../../common/Analytics";
import { t } from "i18next";
interface Props {
@@ -2,9 +2,9 @@ import React from "react";
import { faGlobe } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Select from "react-select";
import { SearchFieldClass } from "../search/SearchFieldClass";
import { AVAILABLE_TTS_LANGUAGE_CATEGORY_MAP } from "../../../../../../_i18n/AvailableLanguageMap";
import { Analytics } from "../../../../../../common/Analytics";
import { SearchFieldClass } from "../SearchFieldClass";
import { AVAILABLE_TTS_LANGUAGE_CATEGORY_MAP } from "../../../../../../../_i18n/AvailableLanguageMap";
import { Analytics } from "../../../../../../../common/Analytics";
import { t } from "i18next";
interface Props {