diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 06244ea1a..caf9e0bdb 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -30,6 +30,7 @@ "react-router-bootstrap": "^0.25.0", "react-router-dom": "^5.1.2", "react-scripts": "3.3.0", + "react-select": "^3.0.8", "subscriptions-transport-ws": "^0.9.16", "video.js": "^7.6.0" }, @@ -60,6 +61,7 @@ "@types/react-dom": "16.9.4", "@types/react-router-bootstrap": "^0.24.5", "@types/react-router-dom": "5.1.3", + "@types/react-select": "^3.0.8", "@types/video.js": "^7.2.11", "eslint": "^6.7.2", "graphql-code-generator": "0.18.2", diff --git a/ui/v2.5/src/.index.scss.swp b/ui/v2.5/src/.index.scss.swp deleted file mode 100644 index f3efde583..000000000 Binary files a/ui/v2.5/src/.index.scss.swp and /dev/null differ diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 8bf99a7a5..a5a457ea1 100755 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -11,6 +11,7 @@ import { Stats } from "./components/Stats"; import Studios from "./components/Studios/Studios"; import Tags from "./components/Tags/Tags"; import { SceneFilenameParser } from "./components/scenes/SceneFilenameParser"; +import { ToastProvider } from './components/Shared/Toast'; import { library } from '@fortawesome/fontawesome-svg-core' import { fas } from '@fortawesome/free-solid-svg-icons' @@ -23,20 +24,22 @@ export const App: React.FC = () => (
-
- - - - {/* */} - - - - - - - - -
+ +
+ + + + {/* */} + + + + + + + + +
+
); diff --git a/ui/v2.5/src/components/list/AddFilter.tsx b/ui/v2.5/src/components/list/AddFilter.tsx index ee44e7cfd..8cb0237f3 100644 --- a/ui/v2.5/src/components/list/AddFilter.tsx +++ b/ui/v2.5/src/components/list/AddFilter.tsx @@ -10,7 +10,7 @@ import { StudiosCriterion } from "../../models/list-filter/criteria/studios"; import { TagsCriterion } from "../../models/list-filter/criteria/tags"; import { makeCriteria } from "../../models/list-filter/criteria/utils"; import { ListFilterModel } from "../../models/list-filter/filter"; -import { FilterMultiSelect } from "../select/FilterMultiSelect"; +import { FilterSelect } from "../select/FilterSelect"; interface IAddFilterProps { onAddCriterion: (criterion: Criterion, oldId?: string) => void; @@ -113,27 +113,24 @@ export const AddFilter: React.FC = (props: IAddFilterProps) => } if (Array.isArray(criterion.value)) { - let type: "performers" | "studios" | "tags" | "" = ""; + let type: "performers" | "studios" | "tags"; if (criterion instanceof PerformersCriterion) { type = "performers"; } else if (criterion instanceof StudiosCriterion) { type = "studios"; } else if (criterion instanceof TagsCriterion) { type = "tags"; + } else { + return; } - if (type === "") { - return (<>todo); - } else { - return ( - criterion.value = items.map((i) => ({id: i.id, label: i.name!}))} - openOnKeyDown={true} - initialIds={criterion.value.map((labeled: any) => labeled.id)} - /> - ); - } + return ( + criterion.value = items.map((i) => ({id: i.id, label: i.name!}))} + initialIds={criterion.value.map((labeled: any) => labeled.id)} + /> + ); } else { if (criterion.options) { defaultValue.current = criterion.value; diff --git a/ui/v2.5/src/components/scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/scenes/SceneDetails/SceneEditPanel.tsx index e446da27a..d5c223ed3 100644 --- a/ui/v2.5/src/components/scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/scenes/SceneDetails/SceneEditPanel.tsx @@ -3,8 +3,7 @@ import * as GQL from "../../../core/generated-graphql"; import { StashService } from "../../../core/StashService"; import { ErrorUtils } from "../../../utils/errors"; import { ToastUtils } from "../../../utils/toasts"; -import { FilterMultiSelect } from "../../select/FilterMultiSelect"; -import { FilterSelect } from "../../select/FilterSelect"; +import { FilterSelect, StudioSelect } from "../../select/FilterSelect"; import { ValidGalleriesSelect } from "../../select/ValidGalleriesSelect"; import { ImageUtils } from "../../../utils/image"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -129,11 +128,12 @@ export const SceneEditPanel: React.FC = (props: IProps) => { props.onDelete(); } - function renderMultiSelect(type: "performers" | "tags", initialIds: string[] | undefined) { + function renderMultiSelect(type: "performers" | "tags", initialIds: string[] = []) { return ( - { + isMulti={true} + onSelect={(items) => { const ids = items.map((i) => i.id); switch (type) { case "performers": setPerformerIds(ids); break; @@ -352,10 +352,9 @@ export const SceneEditPanel: React.FC = (props: IProps) => { Studio - setStudioId(item ? item.id : undefined)} - initialId={studioId} + items.length && setStudioId(items[0]?.id)} + initialIds={studioId ? [studioId] : []} /> diff --git a/ui/v2.5/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx b/ui/v2.5/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx index 80c497ac4..c5caef146 100644 --- a/ui/v2.5/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx +++ b/ui/v2.5/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx @@ -4,8 +4,7 @@ import React, { CSSProperties, FunctionComponent, useState } from "react"; import * as GQL from "../../../core/generated-graphql"; import { StashService } from "../../../core/StashService"; import { TextUtils } from "../../../utils/text"; -import { FilterMultiSelect } from "../../select/FilterMultiSelect"; -import { FilterSelect } from "../../select/FilterSelect"; +import { TagSelect } from "../../select/FilterSelect"; import { MarkerTitleSuggest } from "../../select/MarkerTitleSuggest"; import { WallPanel } from "../../Wall/WallPanel"; import { SceneHelpers } from "../helpers"; @@ -149,19 +148,18 @@ export const SceneMarkersPanel: FunctionComponent = (pr } function renderPrimaryTagField(fieldProps: FieldProps) { return ( - fieldProps.form.setFieldValue("primaryTagId", tag ? tag.id : undefined)} - initialId={!!editingMarker ? editingMarker.primary_tag.id : undefined} + fieldProps.form.setFieldValue("primaryTagId", tags[0]?.id)} + initialIds={editingMarker ? [editingMarker.primary_tag.id] : []} /> ); } function renderTagsField(fieldProps: FieldProps) { return ( - fieldProps.form.setFieldValue("tagIds", tags.map((tag) => tag.id))} - initialIds={!!editingMarker ? fieldProps.form.values.tagIds : undefined} + fieldProps.form.setFieldValue("tagIds", tags.map((tag) => tag.id))} + initialIds={editingMarker ? fieldProps.form.values.tagIds : []} /> ); } diff --git a/ui/v2.5/src/components/scenes/SceneFilenameParser.tsx b/ui/v2.5/src/components/scenes/SceneFilenameParser.tsx index 24b3c8a7b..5ba313de9 100644 --- a/ui/v2.5/src/components/scenes/SceneFilenameParser.tsx +++ b/ui/v2.5/src/components/scenes/SceneFilenameParser.tsx @@ -9,8 +9,7 @@ import _ from "lodash"; import { ToastUtils } from "../../utils/toasts"; import { ErrorUtils } from "../../utils/errors"; import { Pagination } from "../list/Pagination"; -import { FilterMultiSelect } from "../select/FilterMultiSelect"; -import { FilterSelect } from "../select/FilterSelect"; +import { FilterSelect, StudioSelect } from "../select/FilterSelect"; class ParserResult { public value: Maybe; @@ -760,10 +759,11 @@ export const SceneFilenameParser: React.FC = () => { function renderNewMultiSelect(type: "performers" | "tags", props : ISceneParserFieldProps, onChange : (value : any) => void) { return ( - { + isMulti={true} + onSelect={(items) => { const ids = items.map((i) => i.id); onChange(ids); }} @@ -782,12 +782,11 @@ export const SceneFilenameParser: React.FC = () => { function renderNewStudioSelect(props : ISceneParserFieldProps, onChange : (value : any) => void) { return ( - onChange(item ? item.id : undefined)} - initialId={props.parserResult.value} + onSelect={(items) => onChange(items[0]?.id)} + initialIds={props.parserResult.value ? [props.parserResult.value] : []} /> ); } diff --git a/ui/v2.5/src/components/scenes/SceneSelectedOptions.tsx b/ui/v2.5/src/components/scenes/SceneSelectedOptions.tsx index 5d60d0c6b..23b7d7f41 100644 --- a/ui/v2.5/src/components/scenes/SceneSelectedOptions.tsx +++ b/ui/v2.5/src/components/scenes/SceneSelectedOptions.tsx @@ -1,8 +1,7 @@ import _ from "lodash"; import { Button, ButtonGroup, Form, Spinner } from 'react-bootstrap'; import React, { useEffect, useState } from "react"; -import { FilterSelect } from "../select/FilterSelect"; -import { FilterMultiSelect } from "../select/FilterMultiSelect"; +import { FilterSelect, StudioSelect } from "../select/FilterSelect"; import { StashService } from "../../core/StashService"; import * as GQL from "../../core/generated-graphql"; import { ErrorUtils } from "../../utils/errors"; @@ -236,16 +235,17 @@ export const SceneSelectedOptions: React.FC = (props: IList function renderMultiSelect(type: "performers" | "tags", initialIds: string[] | undefined) { return ( - { + isMulti={true} + onSelect={(items) => { const ids = items.map((i) => i.id); switch (type) { case "performers": setPerformerIds(ids); break; case "tags": setTagIds(ids); break; } }} - initialIds={initialIds} + initialIds={initialIds ?? []} /> ); } @@ -268,10 +268,9 @@ export const SceneSelectedOptions: React.FC = (props: IList Studio - setStudioId(item ? item.id : undefined)} - initialId={studioId} + setStudioId(items[0]?.id)} + initialIds={studioId ? [studioId] : []} /> diff --git a/ui/v2.5/src/components/select/FilterSelect.tsx b/ui/v2.5/src/components/select/FilterSelect.tsx index 7f285c40b..b3d07eb1a 100644 --- a/ui/v2.5/src/components/select/FilterSelect.tsx +++ b/ui/v2.5/src/components/select/FilterSelect.tsx @@ -1,117 +1,146 @@ -import * as React from "react"; -import { Button } from 'react-bootstrap'; +import React, { useState } from "react"; +import Select, { ValueType } from 'react-select'; +import CreatableSelect from 'react-select/creatable'; -import { ISelectProps, ItemPredicate, ItemRenderer, Select } from "@blueprintjs/select"; +import { ErrorUtils } from "../../utils/errors"; import * as GQL from "../../core/generated-graphql"; import { StashService } from "../../core/StashService"; -import { HTMLInputProps } from "../../models"; - -const InternalPerformerSelect = Select.ofType(); -const InternalTagSelect = Select.ofType(); -const InternalStudioSelect = Select.ofType(); +import useToast from '../Shared/Toast'; type ValidTypes = GQL.AllPerformersForFilterAllPerformers | GQL.AllTagsForFilterAllTags | GQL.AllStudiosForFilterAllStudios; +type Option = { value:string, label:string }; -interface IProps extends HTMLInputProps { - type: "performers" | "studios" | "tags"; - initialId?: string; +interface ITypeProps { + type: 'performers' | 'studios' | 'tags'; +} +interface IFilterProps { + initialIds: string[]; + onSelect: (item: ValidTypes[]) => void; noSelectionString?: string; - onSelectItem: (item: ValidTypes | undefined) => void; + className?: string; + isMulti?: boolean; +} +interface ISelectProps { + className?: string; + items: Option[]; + selectedOptions?: Option[]; + creatable?: boolean; + onCreateOption?: (value: string) => void; + isLoading: boolean; + onChange: (item: ValueType