Add option to disable create from dropdown (#1814)

* Convert config hooks to common context
* Add option to disable creating from dropdown
This commit is contained in:
WithoutPants
2021-10-11 17:45:58 +11:00
committed by GitHub
parent 46ae4581b8
commit b5381ff071
24 changed files with 269 additions and 130 deletions

View File

@@ -58,6 +58,11 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
cssEnabled
language
slideshowDelay
disabledDropdownCreate {
performer
tag
studio
}
handyKey
funscriptOffset
}

View File

@@ -188,6 +188,12 @@ type ConfigGeneralResult {
stashBoxes: [StashBox!]!
}
input ConfigDisableDropdownCreateInput {
performer: Boolean
tag: Boolean
studio: Boolean
}
input ConfigInterfaceInput {
"""Ordered list of items that should be shown in the menu"""
menuItems: [String!]
@@ -210,12 +216,20 @@ input ConfigInterfaceInput {
language: String
"""Slideshow Delay"""
slideshowDelay: Int
"""Set to true to disable creating new objects via the dropdown menus"""
disableDropdownCreate: ConfigDisableDropdownCreateInput
"""Handy Connection Key"""
handyKey: String
"""Funscript Time Offset"""
funscriptOffset: Int
}
type ConfigDisableDropdownCreate {
performer: Boolean!
tag: Boolean!
studio: Boolean!
}
type ConfigInterfaceResult {
"""Ordered list of items that should be shown in the menu"""
menuItems: [String!]
@@ -238,6 +252,8 @@ type ConfigInterfaceResult {
language: String
"""Slideshow Delay"""
slideshowDelay: Int
"""Fields are true if creating via dropdown menus are disabled"""
disabledDropdownCreate: ConfigDisableDropdownCreate!
"""Handy Connection Key"""
handyKey: String
"""Funscript Time Offset"""

View File

@@ -225,17 +225,19 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.ConfigInterfaceInput) (*models.ConfigInterfaceResult, error) {
c := config.GetInstance()
setBool := func(key string, v *bool) {
if v != nil {
c.Set(key, *v)
}
}
if input.MenuItems != nil {
c.Set(config.MenuItems, input.MenuItems)
}
if input.SoundOnPreview != nil {
c.Set(config.SoundOnPreview, *input.SoundOnPreview)
}
if input.WallShowTitle != nil {
c.Set(config.WallShowTitle, *input.WallShowTitle)
}
setBool(config.SoundOnPreview, input.SoundOnPreview)
setBool(config.WallShowTitle, input.WallShowTitle)
if input.WallPlayback != nil {
c.Set(config.WallPlayback, *input.WallPlayback)
@@ -245,13 +247,8 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
c.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration)
}
if input.AutostartVideo != nil {
c.Set(config.AutostartVideo, *input.AutostartVideo)
}
if input.ShowStudioAsText != nil {
c.Set(config.ShowStudioAsText, *input.ShowStudioAsText)
}
setBool(config.AutostartVideo, input.AutostartVideo)
setBool(config.ShowStudioAsText, input.ShowStudioAsText)
if input.Language != nil {
c.Set(config.Language, *input.Language)
@@ -269,8 +266,13 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
c.SetCSS(css)
if input.CSSEnabled != nil {
c.Set(config.CSSEnabled, *input.CSSEnabled)
setBool(config.CSSEnabled, input.CSSEnabled)
if input.DisableDropdownCreate != nil {
ddc := input.DisableDropdownCreate
setBool(config.DisableDropdownCreatePerformer, ddc.Performer)
setBool(config.DisableDropdownCreateStudio, ddc.Studio)
setBool(config.DisableDropdownCreateTag, ddc.Tag)
}
if input.HandyKey != nil {

View File

@@ -115,19 +115,20 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
scriptOffset := config.GetFunscriptOffset()
return &models.ConfigInterfaceResult{
MenuItems: menuItems,
SoundOnPreview: &soundOnPreview,
WallShowTitle: &wallShowTitle,
WallPlayback: &wallPlayback,
MaximumLoopDuration: &maximumLoopDuration,
AutostartVideo: &autostartVideo,
ShowStudioAsText: &showStudioAsText,
CSS: &css,
CSSEnabled: &cssEnabled,
Language: &language,
SlideshowDelay: &slideshowDelay,
HandyKey: &handyKey,
FunscriptOffset: &scriptOffset,
MenuItems: menuItems,
SoundOnPreview: &soundOnPreview,
WallShowTitle: &wallShowTitle,
WallPlayback: &wallPlayback,
MaximumLoopDuration: &maximumLoopDuration,
AutostartVideo: &autostartVideo,
ShowStudioAsText: &showStudioAsText,
CSS: &css,
CSSEnabled: &cssEnabled,
Language: &language,
SlideshowDelay: &slideshowDelay,
DisabledDropdownCreate: config.GetDisableDropdownCreate(),
HandyKey: &handyKey,
FunscriptOffset: &scriptOffset,
}
}

View File

@@ -135,6 +135,13 @@ const ShowStudioAsText = "show_studio_as_text"
const CSSEnabled = "cssEnabled"
const WallPlayback = "wall_playback"
const SlideshowDelay = "slideshow_delay"
const (
DisableDropdownCreatePerformer = "disable_dropdown_create.performer"
DisableDropdownCreateStudio = "disable_dropdown_create.studio"
DisableDropdownCreateTag = "disable_dropdown_create.tag"
)
const HandyKey = "handy_key"
const FunscriptOffset = "funscript_offset"
@@ -787,6 +794,17 @@ func (i *Instance) GetSlideshowDelay() int {
return viper.GetInt(SlideshowDelay)
}
func (i *Instance) GetDisableDropdownCreate() *models.ConfigDisableDropdownCreate {
i.Lock()
defer i.Unlock()
return &models.ConfigDisableDropdownCreate{
Performer: viper.GetBool(DisableDropdownCreatePerformer),
Studio: viper.GetBool(DisableDropdownCreateStudio),
Tag: viper.GetBool(DisableDropdownCreateTag),
}
}
func (i *Instance) GetCSSPath() string {
i.RLock()
defer i.RUnlock()

View File

@@ -31,6 +31,7 @@ import { Setup } from "./components/Setup/Setup";
import { Migrate } from "./components/Setup/Migrate";
import * as GQL from "./core/generated-graphql";
import { LoadingIndicator } from "./components/Shared";
import ConfigurationProvider from "./hooks/Config";
initPolyfills();
@@ -138,12 +139,17 @@ export const App: React.FC = () => {
return (
<ErrorBoundary>
<IntlProvider locale={language} messages={messages} formats={intlFormats}>
<ToastProvider>
<LightboxProvider>
{maybeRenderNavbar()}
<div className="main container-fluid">{renderContent()}</div>
</LightboxProvider>
</ToastProvider>
<ConfigurationProvider
configuration={config.data?.configuration}
loading={config.loading}
>
<ToastProvider>
<LightboxProvider>
{maybeRenderNavbar()}
<div className="main container-fluid">{renderContent()}</div>
</LightboxProvider>
</ToastProvider>
</ConfigurationProvider>
</IntlProvider>
</ErrorBoundary>
);

View File

@@ -1,2 +1,5 @@
### ✨ New Features
* Added interface options to disable creating performers/studios/tags from dropdown selectors. ([#1814](https://github.com/stashapp/stash/pull/1814))
### 🐛 Bug fixes
* Fix huge memory usage spike during clean task. ([#1805](https://github.com/stashapp/stash/pull/1805))

View File

@@ -2,7 +2,6 @@ import { Button, ButtonGroup } from "react-bootstrap";
import React from "react";
import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { useConfiguration } from "src/core/StashService";
import {
GridCard,
HoverPopover,
@@ -12,6 +11,7 @@ import {
} from "src/components/Shared";
import { PopoverCountButton } from "src/components/Shared/PopoverCountButton";
import { NavUtils, TextUtils } from "src/utils";
import { ConfigurationContext } from "src/hooks/Config";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
import { RatingBanner } from "../Shared/RatingBanner";
@@ -24,9 +24,8 @@ interface IProps {
}
export const GalleryCard: React.FC<IProps> = (props) => {
const config = useConfiguration();
const showStudioAsText =
config?.data?.configuration.interface.showStudioAsText ?? false;
const { configuration } = React.useContext(ConfigurationContext);
const showStudioAsText = configuration?.interface.showStudioAsText ?? false;
function maybeRenderScenePopoverButton() {
if (props.gallery.scenes.length === 0) return;

View File

@@ -13,8 +13,8 @@ import Mousetrap from "mousetrap";
import { SessionUtils } from "src/utils";
import { Icon } from "src/components/Shared";
import { ConfigurationContext } from "src/hooks/Config";
import { Manual } from "./Help/Manual";
import { useConfiguration } from "../core/StashService";
interface IMenuItem {
name: string;
@@ -120,7 +120,7 @@ const allMenuItems: IMenuItem[] = [
export const MainNavbar: React.FC = () => {
const history = useHistory();
const location = useLocation();
const { data: config, loading } = useConfiguration();
const { configuration, loading } = React.useContext(ConfigurationContext);
// Show all menu items by default, unless config says otherwise
const [menuItems, setMenuItems] = useState<IMenuItem[]>(allMenuItems);
@@ -129,7 +129,7 @@ export const MainNavbar: React.FC = () => {
const [showManual, setShowManual] = useState(false);
useEffect(() => {
const iCfg = config?.configuration?.interface;
const iCfg = configuration?.interface;
if (iCfg?.menuItems) {
setMenuItems(
allMenuItems.filter((menuItem) =>
@@ -137,7 +137,7 @@ export const MainNavbar: React.FC = () => {
)
);
}
}, [config]);
}, [configuration]);
// react-bootstrap typing bug
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -20,7 +20,6 @@ import {
usePerformerCreate,
useTagCreate,
queryScrapePerformerURL,
useConfiguration,
} from "src/core/StashService";
import {
Icon,
@@ -41,6 +40,7 @@ import {
genderToString,
stringToGender,
} from "src/utils/gender";
import { ConfigurationContext } from "src/hooks/Config";
import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
import PerformerScrapeModal from "./PerformerScrapeModal";
import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal";
@@ -88,7 +88,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
const [scrapedPerformer, setScrapedPerformer] = useState<
GQL.ScrapedPerformer | undefined
>();
const stashConfig = useConfiguration();
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
@@ -601,7 +601,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
if (!performer) {
return;
}
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? [];
const stashBoxes = stashConfig?.general.stashBoxes ?? [];
const popover = (
<Dropdown.Menu id="performer-scraper-popover">

View File

@@ -2,8 +2,8 @@
import React from "react";
import ReactJWPlayer from "react-jw-player";
import * as GQL from "src/core/generated-graphql";
import { useConfiguration } from "src/core/StashService";
import { JWUtils, ScreenUtils } from "src/utils";
import { ConfigurationContext } from "src/hooks/Config";
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
import { Interactive } from "../../utils/interactive";
@@ -366,16 +366,12 @@ export class ScenePlayerImpl extends React.Component<
export const ScenePlayer: React.FC<IScenePlayerProps> = (
props: IScenePlayerProps
) => {
const config = useConfiguration();
const { configuration } = React.useContext(ConfigurationContext);
return (
<ScenePlayerImpl
{...props}
config={
config.data && config.data.configuration
? config.data.configuration.interface
: undefined
}
config={configuration ? configuration.interface : undefined}
/>
);
};

View File

@@ -3,7 +3,6 @@ import { Button, ButtonGroup } from "react-bootstrap";
import { Link } from "react-router-dom";
import cx from "classnames";
import * as GQL from "src/core/generated-graphql";
import { useConfiguration } from "src/core/StashService";
import {
Icon,
TagLink,
@@ -13,6 +12,7 @@ import {
} from "src/components/Shared";
import { TextUtils } from "src/utils";
import { SceneQueue } from "src/models/sceneQueue";
import { ConfigurationContext } from "src/hooks/Config";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
import { GridCard } from "../Shared/GridCard";
import { RatingBanner } from "../Shared/RatingBanner";
@@ -80,15 +80,14 @@ interface ISceneCardProps {
export const SceneCard: React.FC<ISceneCardProps> = (
props: ISceneCardProps
) => {
const config = useConfiguration();
const { configuration } = React.useContext(ConfigurationContext);
// studio image is missing if it uses the default
const missingStudioImage = props.scene.studio?.image_path?.endsWith(
"?default=true"
);
const showStudioAsText =
missingStudioImage ||
(config?.data?.configuration.interface.showStudioAsText ?? false);
missingStudioImage || (configuration?.interface.showStudioAsText ?? false);
function maybeRenderSceneSpecsOverlay() {
return (
@@ -327,9 +326,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
image={props.scene.paths.screenshot ?? undefined}
video={props.scene.paths.preview ?? undefined}
isPortrait={isPortrait()}
soundActive={
config.data?.configuration?.interface?.soundOnPreview ?? false
}
soundActive={configuration?.interface?.soundOnPreview ?? false}
/>
<RatingBanner rating={props.scene.rating} />
{maybeRenderSceneSpecsOverlay()}

View File

@@ -18,7 +18,6 @@ import {
useListSceneScrapers,
useSceneUpdate,
mutateReloadScrapers,
useConfiguration,
queryScrapeSceneQueryFragment,
} from "src/core/StashService";
import {
@@ -35,6 +34,7 @@ import { ImageUtils, FormUtils, TextUtils, getStashIDs } from "src/utils";
import { MovieSelect } from "src/components/Shared/Select";
import { useFormik } from "formik";
import { Prompt } from "react-router";
import { ConfigurationContext } from "src/hooks/Config";
import { SceneMovieTable } from "./SceneMovieTable";
import { RatingStars } from "./RatingStars";
import { SceneScrapeDialog } from "./SceneScrapeDialog";
@@ -76,7 +76,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
string | undefined
>(scene.paths.screenshot ?? undefined);
const stashConfig = useConfiguration();
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
// Network state
const [isLoading, setIsLoading] = useState(false);
@@ -380,7 +380,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
}
function renderScrapeQueryMenu() {
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? [];
const stashBoxes = stashConfig?.general.stashBoxes ?? [];
if (stashBoxes.length === 0 && queryableScrapers.length === 0) return;
@@ -450,7 +450,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
};
function renderScraperMenu() {
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? [];
const stashBoxes = stashConfig?.general.stashBoxes ?? [];
return (
<DropdownButton

View File

@@ -1,13 +1,11 @@
import React, { useState, useEffect } from "react";
import { Form, Button, Collapse } from "react-bootstrap";
import {
mutateMetadataGenerate,
useConfiguration,
} from "src/core/StashService";
import { mutateMetadataGenerate } from "src/core/StashService";
import { Modal, Icon } from "src/components/Shared";
import { useToast } from "src/hooks";
import * as GQL from "src/core/generated-graphql";
import { useIntl } from "react-intl";
import { ConfigurationContext } from "src/hooks/Config";
interface ISceneGenerateDialogProps {
selectedIds: string[];
@@ -17,7 +15,7 @@ interface ISceneGenerateDialogProps {
export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
props: ISceneGenerateDialogProps
) => {
const { data, error, loading } = useConfiguration();
const { configuration } = React.useContext(ConfigurationContext);
const [sprites, setSprites] = useState(true);
const [phashes, setPhashes] = useState(true);
@@ -49,17 +47,16 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
const Toast = useToast();
useEffect(() => {
if (!data?.configuration) return;
if (!configuration) return;
const conf = data.configuration;
if (conf.general) {
setPreviewSegments(conf.general.previewSegments);
setPreviewSegmentDuration(conf.general.previewSegmentDuration);
setPreviewExcludeStart(conf.general.previewExcludeStart);
setPreviewExcludeEnd(conf.general.previewExcludeEnd);
setPreviewPreset(conf.general.previewPreset);
if (configuration.general) {
setPreviewSegments(configuration.general.previewSegments);
setPreviewSegmentDuration(configuration.general.previewSegmentDuration);
setPreviewExcludeStart(configuration.general.previewExcludeStart);
setPreviewExcludeEnd(configuration.general.previewExcludeEnd);
setPreviewPreset(configuration.general.previewPreset);
}
}, [data]);
}, [configuration]);
async function onGenerate() {
try {
@@ -90,15 +87,6 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
}
}
if (error) {
Toast.error(error);
props.onClose();
}
if (loading) {
return <></>;
}
return (
<Modal
show

View File

@@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
import { DurationInput, LoadingIndicator } from "src/components/Shared";
import { useConfiguration, useConfigureInterface } from "src/core/StashService";
import { useToast } from "src/hooks";
import * as GQL from "src/core/generated-graphql";
import { CheckboxGroup } from "./CheckboxGroup";
const allMenuItems = [
@@ -38,6 +39,10 @@ export const SettingsInterfacePanel: React.FC = () => {
const [language, setLanguage] = useState<string>("en");
const [handyKey, setHandyKey] = useState<string>();
const [funscriptOffset, setFunscriptOffset] = useState<number>(0);
const [
disableDropdownCreate,
setDisableDropdownCreate,
] = useState<GQL.ConfigDisableDropdownCreateInput>({});
const [updateInterfaceConfig] = useConfigureInterface({
menuItems: menuItemIds,
@@ -53,6 +58,7 @@ export const SettingsInterfacePanel: React.FC = () => {
slideshowDelay,
handyKey,
funscriptOffset,
disableDropdownCreate,
});
useEffect(() => {
@@ -70,6 +76,11 @@ export const SettingsInterfacePanel: React.FC = () => {
setSlideshowDelay(iCfg?.slideshowDelay ?? 5000);
setHandyKey(iCfg?.handyKey ?? "");
setFunscriptOffset(iCfg?.funscriptOffset ?? 0);
setDisableDropdownCreate({
performer: iCfg?.disabledDropdownCreate.performer,
studio: iCfg?.disabledDropdownCreate.studio,
tag: iCfg?.disabledDropdownCreate.tag,
});
}, [config]);
async function onSave() {
@@ -257,6 +268,64 @@ export const SettingsInterfacePanel: React.FC = () => {
</Form.Text>
</Form.Group>
<Form.Group>
<h5>{intl.formatMessage({ id: "config.ui.editing.heading" })}</h5>
<Form.Group>
<h6>
{intl.formatMessage({
id: "config.ui.editing.disable_dropdown_create.heading",
})}
</h6>
<Form.Check
id="disableDropdownCreate_performer"
checked={disableDropdownCreate.performer ?? false}
label={intl.formatMessage({
id: "performer",
})}
onChange={() => {
setDisableDropdownCreate({
...disableDropdownCreate,
performer: !disableDropdownCreate.performer ?? true,
});
}}
/>
<Form.Check
id="disableDropdownCreate_studio"
checked={disableDropdownCreate.studio ?? false}
label={intl.formatMessage({
id: "studio",
})}
onChange={() => {
setDisableDropdownCreate({
...disableDropdownCreate,
studio: !disableDropdownCreate.studio ?? true,
});
}}
/>
<Form.Check
id="disableDropdownCreate_tag"
checked={disableDropdownCreate.tag ?? false}
label={intl.formatMessage({
id: "tag",
})}
onChange={() => {
setDisableDropdownCreate({
...disableDropdownCreate,
tag: !disableDropdownCreate.tag ?? true,
});
}}
/>
<Form.Text className="text-muted">
{intl.formatMessage({
id: "config.ui.editing.disable_dropdown_create.description",
})}
</Form.Text>
</Form.Group>
</Form.Group>
<Form.Group>
<h5>{intl.formatMessage({ id: "config.ui.custom_css.heading" })}</h5>
<Form.Check

View File

@@ -1,9 +1,9 @@
import React, { useState } from "react";
import { Button, Col, Form, Row } from "react-bootstrap";
import { useIntl } from "react-intl";
import { useConfiguration } from "src/core/StashService";
import { Icon, Modal } from "src/components/Shared";
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
import { ConfigurationContext } from "src/hooks/Config";
interface IDirectorySelectionDialogProps {
onClose: (paths?: string[]) => void;
@@ -13,9 +13,9 @@ export const DirectorySelectionDialog: React.FC<IDirectorySelectionDialogProps>
props: IDirectorySelectionDialogProps
) => {
const intl = useIntl();
const { data } = useConfiguration();
const { configuration } = React.useContext(ConfigurationContext);
const libraryPaths = data?.configuration.general.stashes.map((s) => s.path);
const libraryPaths = configuration?.general.stashes.map((s) => s.path);
const [paths, setPaths] = useState<string[]>([]);
const [currentDirectory, setCurrentDirectory] = useState<string>("");

View File

@@ -24,6 +24,7 @@ import {
import { useToast } from "src/hooks";
import { TextUtils } from "src/utils";
import { SelectComponents } from "react-select/src/components";
import { ConfigurationContext } from "src/hooks/Config";
export type ValidTypes =
| GQL.SlimPerformerDataFragment
@@ -400,6 +401,10 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
const { data, loading } = useAllPerformersForFilter();
const [createPerformer] = usePerformerCreate();
const { configuration } = React.useContext(ConfigurationContext);
const defaultCreatable =
!configuration?.interface.disabledDropdownCreate.performer ?? true;
const performers = data?.allPerformers ?? [];
const onCreate = async (name: string) => {
@@ -416,7 +421,7 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
<FilterSelectComponent
{...props}
isMulti={props.isMulti ?? false}
creatable={props.creatable ?? true}
creatable={props.creatable ?? defaultCreatable}
onCreate={onCreate}
type="performers"
isLoading={loading}
@@ -436,6 +441,10 @@ export const StudioSelect: React.FC<
const { data, loading } = useAllStudiosForFilter();
const [createStudio] = useStudioCreate();
const { configuration } = React.useContext(ConfigurationContext);
const defaultCreatable =
!configuration?.interface.disabledDropdownCreate.studio ?? true;
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
const studios = useMemo(
() =>
@@ -542,7 +551,7 @@ export const StudioSelect: React.FC<
isLoading={loading}
items={studios}
placeholder={props.noSelectionString ?? "Select studio..."}
creatable={props.creatable ?? true}
creatable={props.creatable ?? defaultCreatable}
onCreate={onCreate}
/>
);
@@ -573,6 +582,10 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
const [createTag] = useTagCreate();
const placeholder = props.noSelectionString ?? "Select tags...";
const { configuration } = React.useContext(ConfigurationContext);
const defaultCreatable =
!configuration?.interface.disabledDropdownCreate.tag ?? true;
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
const tags = useMemo(
() => (data?.allTags ?? []).filter((tag) => !exclude.includes(tag.id)),
@@ -675,7 +688,7 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
components={{ Option: TagOption }}
isMulti={props.isMulti ?? false}
items={tags}
creatable={props.creatable ?? true}
creatable={props.creatable ?? defaultCreatable}
type="tags"
placeholder={placeholder}
isLoading={loading}

View File

@@ -6,10 +6,11 @@ import { useLocalForage } from "src/hooks";
import * as GQL from "src/core/generated-graphql";
import { LoadingIndicator } from "src/components/Shared";
import { stashBoxSceneQuery, useConfiguration } from "src/core/StashService";
import { stashBoxSceneQuery } from "src/core/StashService";
import { Manual } from "src/components/Help/Manual";
import { SceneQueue } from "src/models/sceneQueue";
import { ConfigurationContext } from "src/hooks/Config";
import Config from "./Config";
import { LOCAL_FORAGE_KEY, ITaggerConfig, initialConfig } from "./constants";
import { TaggerList } from "./TaggerList";
@@ -20,7 +21,7 @@ interface ITaggerProps {
}
export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
const stashConfig = useConfiguration();
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
const [{ data: config }, setConfig] = useLocalForage<ITaggerConfig>(
LOCAL_FORAGE_KEY,
initialConfig
@@ -63,20 +64,19 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
if (!config) return <LoadingIndicator />;
const savedEndpointIndex =
stashConfig.data?.configuration.general.stashBoxes.findIndex(
stashConfig?.general.stashBoxes.findIndex(
(s) => s.endpoint === config.selectedEndpoint
) ?? -1;
const selectedEndpointIndex =
savedEndpointIndex === -1 &&
stashConfig.data?.configuration.general.stashBoxes.length
savedEndpointIndex === -1 && stashConfig?.general.stashBoxes.length
? 0
: savedEndpointIndex;
const selectedEndpoint =
stashConfig.data?.configuration.general.stashBoxes[selectedEndpointIndex];
stashConfig?.general.stashBoxes[selectedEndpointIndex];
function getEndpointIndex(endpoint: string) {
return (
stashConfig.data?.configuration.general.stashBoxes.findIndex(
stashConfig?.general.stashBoxes.findIndex(
(s) => s.endpoint === endpoint
) ?? -1
);

View File

@@ -1,6 +1,6 @@
import React, { Dispatch, useState } from "react";
import { Badge, Button, Card, Collapse, Form } from "react-bootstrap";
import { useConfiguration } from "src/core/StashService";
import { ConfigurationContext } from "src/hooks/Config";
import { TextUtils } from "src/utils";
import { ITaggerConfig, PERFORMER_FIELDS } from "../constants";
@@ -13,7 +13,7 @@ interface IConfigProps {
}
const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
const stashConfig = useConfiguration();
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
const [showExclusionModal, setShowExclusionModal] = useState(false);
const excludedFields = config.excludedPerformerFields ?? [];
@@ -26,7 +26,7 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
});
};
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? [];
const stashBoxes = stashConfig?.general.stashBoxes ?? [];
const handleFieldSelect = (fields: string[]) => {
setConfig({ ...config, excludedPerformerFields: fields });
@@ -77,13 +77,11 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
onChange={handleInstanceSelect}
>
{!stashBoxes.length && <option>No instances found</option>}
{stashConfig.data?.configuration.general.stashBoxes.map(
(i) => (
<option value={i.endpoint} key={i.endpoint}>
{i.endpoint}
</option>
)
)}
{stashConfig?.general.stashBoxes.map((i) => (
<option value={i.endpoint} key={i.endpoint}>
{i.endpoint}
</option>
))}
</Form.Control>
</Form.Group>
</div>

View File

@@ -9,11 +9,11 @@ import * as GQL from "src/core/generated-graphql";
import { LoadingIndicator, Modal } from "src/components/Shared";
import {
stashBoxPerformerQuery,
useConfiguration,
useJobsSubscribe,
mutateStashBoxBatchPerformerTag,
} from "src/core/StashService";
import { Manual } from "src/components/Help/Manual";
import { ConfigurationContext } from "src/hooks/Config";
import StashSearchResult from "./StashSearchResult";
import PerformerConfig from "./Config";
@@ -491,7 +491,7 @@ interface ITaggerProps {
export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => {
const jobsSubscribe = useJobsSubscribe();
const stashConfig = useConfiguration();
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
const [{ data: config }, setConfig] = useLocalForage<ITaggerConfig>(
LOCAL_FORAGE_KEY,
initialConfig
@@ -524,16 +524,15 @@ export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => {
if (!config) return <LoadingIndicator />;
const savedEndpointIndex =
stashConfig.data?.configuration.general.stashBoxes.findIndex(
stashConfig?.general.stashBoxes.findIndex(
(s) => s.endpoint === config.selectedEndpoint
) ?? -1;
const selectedEndpointIndex =
savedEndpointIndex === -1 &&
stashConfig.data?.configuration.general.stashBoxes.length
savedEndpointIndex === -1 && stashConfig?.general.stashBoxes.length
? 0
: savedEndpointIndex;
const selectedEndpoint =
stashConfig.data?.configuration.general.stashBoxes[selectedEndpointIndex];
stashConfig?.general.stashBoxes[selectedEndpointIndex];
async function batchAdd(performerInput: string) {
if (performerInput && selectedEndpoint) {
@@ -641,7 +640,7 @@ export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => {
}}
isIdle={batchJobID === undefined}
config={config}
stashBoxes={stashConfig.data?.configuration.general.stashBoxes}
stashBoxes={stashConfig?.general.stashBoxes}
onBatchAdd={batchAdd}
onBatchUpdate={batchUpdate}
/>

View File

@@ -1,10 +1,10 @@
import React, { useRef, useState, useEffect } from "react";
import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { useConfiguration } from "src/core/StashService";
import { TextUtils, NavUtils } from "src/utils";
import cx from "classnames";
import { SceneQueue } from "src/models/sceneQueue";
import { ConfigurationContext } from "src/hooks/Config";
interface IWallItemProps {
index?: number;
@@ -105,10 +105,9 @@ const Preview: React.FC<{
export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
const [active, setActive] = useState(false);
const wallItem = useRef() as React.MutableRefObject<HTMLDivElement>;
const config = useConfiguration();
const { configuration: config } = React.useContext(ConfigurationContext);
const showTextContainer =
config.data?.configuration.interface.wallShowTitle ?? true;
const showTextContainer = config?.interface.wallShowTitle ?? true;
const previews = props.sceneMarker
? {
@@ -203,11 +202,7 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
<div className="wall-item">
<div className={`wall-item-container ${props.className}`} ref={wallItem}>
<Link onClick={clickHandler} to={linkSrc} className="wall-item-anchor">
<Preview
previews={previews}
config={config.data?.configuration}
active={active}
/>
<Preview previews={previews} config={config} active={active} />
{renderText()}
</Link>
</div>

View File

@@ -0,0 +1,28 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
interface IContext {
configuration?: GQL.ConfigDataFragment;
loading?: boolean;
}
export const ConfigurationContext = React.createContext<IContext>({});
export const ConfigurationProvider: React.FC<IContext> = ({
loading,
configuration,
children,
}) => {
return (
<ConfigurationContext.Provider
value={{
configuration,
loading,
}}
>
{children}
</ConfigurationContext.Provider>
);
};
export default ConfigurationProvider;

View File

@@ -15,9 +15,9 @@ import debounce from "lodash/debounce";
import { Icon, LoadingIndicator } from "src/components/Shared";
import { useInterval, usePageVisibility } from "src/hooks";
import { useConfiguration } from "src/core/StashService";
import { FormattedMessage, useIntl } from "react-intl";
import { DisplayMode, LightboxImage, ScrollMode } from "./LightboxImage";
import { ConfigurationContext } from "../Config";
const CLASSNAME = "Lightbox";
const CLASSNAME_HEADER = `${CLASSNAME}-header`;
@@ -93,11 +93,10 @@ export const LightboxComponent: React.FC<IProps> = ({
const allowNavigation = images.length > 1 || pageCallback;
const intl = useIntl();
const config = useConfiguration();
const { configuration: config } = React.useContext(ConfigurationContext);
const userSelectedSlideshowDelayOrDefault =
config?.data?.configuration.interface.slideshowDelay ??
DEFAULT_SLIDESHOW_DELAY;
config?.interface.slideshowDelay ?? DEFAULT_SLIDESHOW_DELAY;
// slideshowInterval is used for controlling the logic
// displaySlideshowInterval is for display purposes only

View File

@@ -327,6 +327,13 @@
"heading": "Custom CSS",
"option_label": "Custom CSS enabled"
},
"editing": {
"disable_dropdown_create": {
"heading": "Disable dropdown create",
"description": "Remove the ability to create new objects from the dropdown selectors"
},
"heading": "Editing"
},
"handy_connection_key": {
"description": "Handy connection key to use for interactive scenes.",
"heading": "Handy Connection Key"