Various UI fixes (#1502)

* Set/unset existing ids when moving to/from set
* Refactor rating banner
* Fix overlapping in multi set
* Prevent UI crash on bad hierarchical input value
This commit is contained in:
WithoutPants
2021-06-16 12:17:54 +10:00
committed by GitHub
parent b55715775d
commit 45f4a5ba81
14 changed files with 95 additions and 160 deletions

View File

@@ -27,10 +27,12 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
setPerformerMode,
] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [performerIds, setPerformerIds] = useState<string[]>();
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
GQL.BulkUpdateIdMode.Add
);
const [tagIds, setTagIds] = useState<string[]>();
const [existingTagIds, setExistingTagIds] = useState<string[]>();
const [organized, setOrganized] = useState<boolean | undefined>();
const [updateGalleries] = useBulkGalleryUpdate();
@@ -279,16 +281,11 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
setRating(updateRating);
setStudioId(updateStudioID);
if (performerMode === GQL.BulkUpdateIdMode.Set) {
setPerformerIds(updatePerformerIds);
}
if (tagMode === GQL.BulkUpdateIdMode.Set) {
setTagIds(updateTagIds);
}
setExistingPerformerIds(updatePerformerIds);
setExistingTagIds(updateTagIds);
setOrganized(updateOrganized);
}, [props.selected, performerMode, tagMode]);
}, [props.selected]);
useEffect(() => {
if (checkboxRef.current) {
@@ -301,12 +298,15 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
ids: string[] | undefined
) {
let mode = GQL.BulkUpdateIdMode.Add;
let existingIds: string[] | undefined = [];
switch (type) {
case "performers":
mode = performerMode;
existingIds = existingPerformerIds;
break;
case "tags":
mode = tagMode;
existingIds = existingTagIds;
break;
}
@@ -314,8 +314,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
<MultiSet
type={type}
disabled={isUpdating}
onUpdate={(items) => {
const itemIDs = items.map((i) => i.id);
onUpdate={(itemIDs) => {
switch (type) {
case "performers":
setPerformerIds(itemIDs);
@@ -335,6 +334,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
break;
}
}}
existingIds={existingIds ?? []}
ids={ids ?? []}
mode={mode}
/>

View File

@@ -7,6 +7,7 @@ import { useConfiguration } from "src/core/StashService";
import { GridCard, HoverPopover, Icon, TagLink } from "src/components/Shared";
import { TextUtils } from "src/utils";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
import { RatingBanner } from "../Shared/RatingBanner";
interface IProps {
gallery: GQL.SlimGalleryDataFragment;
@@ -114,21 +115,6 @@ export const GalleryCard: React.FC<IProps> = (props) => {
}
}
function maybeRenderRatingBanner() {
if (!props.gallery.rating) {
return;
}
return (
<div
className={`rating-banner ${
props.gallery.rating ? `rating-${props.gallery.rating}` : ""
}`}
>
RATING: {props.gallery.rating}
</div>
);
}
return (
<GridCard
className={`gallery-card zoom-${props.zoomIndex}`}
@@ -148,7 +134,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
src={`${props.gallery.cover.paths.thumbnail}`}
/>
) : undefined}
{maybeRenderRatingBanner()}
<RatingBanner rating={props.gallery.rating} />
</>
}
overlays={maybeRenderSceneStudioOverlay()}

View File

@@ -27,10 +27,12 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
setPerformerMode,
] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [performerIds, setPerformerIds] = useState<string[]>();
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
GQL.BulkUpdateIdMode.Add
);
const [tagIds, setTagIds] = useState<string[]>();
const [existingTagIds, setExistingTagIds] = useState<string[]>();
const [organized, setOrganized] = useState<boolean | undefined>();
const [updateImages] = useBulkImageUpdate();
@@ -275,13 +277,8 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
setRating(updateRating);
setStudioId(updateStudioID);
if (performerMode === GQL.BulkUpdateIdMode.Set) {
setPerformerIds(updatePerformerIds);
}
if (tagMode === GQL.BulkUpdateIdMode.Set) {
setTagIds(updateTagIds);
}
setExistingPerformerIds(updatePerformerIds);
setExistingTagIds(updateTagIds);
setOrganized(updateOrganized);
}, [props.selected, performerMode, tagMode]);
@@ -296,12 +293,15 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
ids: string[] | undefined
) {
let mode = GQL.BulkUpdateIdMode.Add;
let existingIds: string[] | undefined = [];
switch (type) {
case "performers":
mode = performerMode;
existingIds = existingPerformerIds;
break;
case "tags":
mode = tagMode;
existingIds = existingTagIds;
break;
}
@@ -309,8 +309,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
<MultiSet
type={type}
disabled={isUpdating}
onUpdate={(items) => {
const itemIDs = items.map((i) => i.id);
onUpdate={(itemIDs) => {
switch (type) {
case "performers":
setPerformerIds(itemIDs);
@@ -330,6 +329,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
break;
}
}}
existingIds={existingIds ?? []}
ids={ids ?? []}
mode={mode}
/>

View File

@@ -6,6 +6,7 @@ import { Icon, TagLink, HoverPopover, SweatDrops } from "src/components/Shared";
import { TextUtils } from "src/utils";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
import { GridCard } from "../Shared/GridCard";
import { RatingBanner } from "../Shared/RatingBanner";
interface IImageCardProps {
image: GQL.SlimImageDataFragment;
@@ -18,21 +19,6 @@ interface IImageCardProps {
export const ImageCard: React.FC<IImageCardProps> = (
props: IImageCardProps
) => {
function maybeRenderRatingBanner() {
if (!props.image.rating) {
return;
}
return (
<div
className={`rating-banner ${
props.image.rating ? `rating-${props.image.rating}` : ""
}`}
>
RATING: {props.image.rating}
</div>
);
}
function maybeRenderTagPopoverButton() {
if (props.image.tags.length <= 0) return;
@@ -130,7 +116,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
src={props.image.paths.thumbnail ?? ""}
/>
</div>
{maybeRenderRatingBanner()}
<RatingBanner rating={props.image.rating} />
</>
}
popovers={maybeRenderPopoverButtonGroup()}

View File

@@ -2,6 +2,7 @@ import React, { FunctionComponent } from "react";
import { FormattedPlural } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import { GridCard } from "src/components/Shared";
import { RatingBanner } from "../Shared/RatingBanner";
interface IProps {
movie: GQL.MovieDataFragment;
@@ -12,21 +13,6 @@ interface IProps {
}
export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
function maybeRenderRatingBanner() {
if (!props.movie.rating) {
return;
}
return (
<div
className={`rating-banner ${
props.movie.rating ? `rating-${props.movie.rating}` : ""
}`}
>
RATING: {props.movie.rating}
</div>
);
}
function maybeRenderSceneNumber() {
if (!props.sceneIndex) {
return (
@@ -57,7 +43,7 @@ export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
alt={props.movie.name ?? ""}
src={props.movie.front_image_path ?? ""}
/>
{maybeRenderRatingBanner()}
<RatingBanner rating={props.movie.rating} />
</>
}
details={maybeRenderSceneNumber()}

View File

@@ -25,6 +25,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
GQL.BulkUpdateIdMode.Add
);
const [tagIds, setTagIds] = useState<string[]>();
const [existingTagIds, setExistingTagIds] = useState<string[]>();
const [favorite, setFavorite] = useState<boolean | undefined>();
const [updatePerformers] = useBulkPerformerUpdate(getPerformerInput());
@@ -178,9 +179,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
}
});
if (tagMode === GQL.BulkUpdateIdMode.Set) {
setTagIds(updateTagIds);
}
setExistingTagIds(updateTagIds);
setFavorite(updateFavorite);
setRating(updateRating);
}, [props.selected, tagMode]);
@@ -191,42 +190,6 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
}
}, [favorite, checkboxRef]);
function renderMultiSelect(
type: "performers" | "tags",
ids: string[] | undefined
) {
let mode = GQL.BulkUpdateIdMode.Add;
switch (type) {
case "tags":
mode = tagMode;
break;
}
return (
<MultiSet
type={type}
disabled={isUpdating}
onUpdate={(items) => {
const itemIDs = items.map((i) => i.id);
switch (type) {
case "tags":
setTagIds(itemIDs);
break;
}
}}
onSetMode={(newMode) => {
switch (type) {
case "tags":
setTagMode(newMode);
break;
}
}}
ids={ids ?? []}
mode={mode}
/>
);
}
function cycleFavorite() {
if (favorite) {
setFavorite(undefined);
@@ -271,7 +234,15 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
<Form.Label>
<FormattedMessage id="tags" />
</Form.Label>
{renderMultiSelect("tags", tagIds)}
<MultiSet
type="tags"
disabled={isUpdating}
onUpdate={(itemIDs) => setTagIds(itemIDs)}
onSetMode={(newMode) => setTagMode(newMode)}
existingIds={existingTagIds ?? []}
ids={tagIds ?? []}
mode={tagMode}
/>
</Form.Group>
<Form.Group controlId="favorite">

View File

@@ -27,10 +27,12 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
setPerformerMode,
] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [performerIds, setPerformerIds] = useState<string[]>();
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
GQL.BulkUpdateIdMode.Add
);
const [tagIds, setTagIds] = useState<string[]>();
const [existingTagIds, setExistingTagIds] = useState<string[]>();
const [organized, setOrganized] = useState<boolean | undefined>();
const [updateScenes] = useBulkSceneUpdate(getSceneInput());
@@ -271,13 +273,8 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
setRating(updateRating);
setStudioId(updateStudioID);
if (performerMode === GQL.BulkUpdateIdMode.Set) {
setPerformerIds(updatePerformerIds);
}
if (tagMode === GQL.BulkUpdateIdMode.Set) {
setTagIds(updateTagIds);
}
setExistingPerformerIds(updatePerformerIds);
setExistingTagIds(updateTagIds);
setOrganized(updateOrganized);
}, [props.selected, performerMode, tagMode]);
@@ -292,12 +289,15 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
ids: string[] | undefined
) {
let mode = GQL.BulkUpdateIdMode.Add;
let existingIds: string[] | undefined = [];
switch (type) {
case "performers":
mode = performerMode;
existingIds = existingPerformerIds;
break;
case "tags":
mode = tagMode;
existingIds = existingTagIds;
break;
}
@@ -305,8 +305,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
<MultiSet
type={type}
disabled={isUpdating}
onUpdate={(items) => {
const itemIDs = items.map((i) => i.id);
onUpdate={(itemIDs) => {
switch (type) {
case "performers":
setPerformerIds(itemIDs);
@@ -327,6 +326,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
}
}}
ids={ids ?? []}
existingIds={existingIds ?? []}
mode={mode}
/>
);
@@ -404,7 +404,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
<Form.Group controlId="tags">
<Form.Label>
<FormattedMessage id="performers" />
<FormattedMessage id="tags" />
</Form.Label>
{renderMultiSelect("tags", tagIds)}
</Form.Group>

View File

@@ -15,6 +15,7 @@ import { TextUtils } from "src/utils";
import { SceneQueue } from "src/models/sceneQueue";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
import { GridCard } from "../Shared/GridCard";
import { RatingBanner } from "../Shared/RatingBanner";
interface IScenePreviewProps {
isPortrait: boolean;
@@ -89,21 +90,6 @@ export const SceneCard: React.FC<ISceneCardProps> = (
missingStudioImage ||
(config?.data?.configuration.interface.showStudioAsText ?? false);
function maybeRenderRatingBanner() {
if (!props.scene.rating) {
return;
}
return (
<div
className={`rating-banner ${
props.scene.rating ? `rating-${props.scene.rating}` : ""
}`}
>
RATING: {props.scene.rating}
</div>
);
}
function maybeRenderSceneSpecsOverlay() {
return (
<div className="scene-specs-overlay">
@@ -333,7 +319,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
config.data?.configuration?.interface?.soundOnPreview ?? false
}
/>
{maybeRenderRatingBanner()}
<RatingBanner rating={props.scene.rating} />
{maybeRenderSceneSpecsOverlay()}
</>
}

View File

@@ -13,10 +13,11 @@ type ValidTypes =
interface IMultiSetProps {
type: "performers" | "studios" | "tags";
existingIds?: string[];
ids?: string[];
mode: GQL.BulkUpdateIdMode;
disabled?: boolean;
onUpdate: (items: ValidTypes[]) => void;
onUpdate: (ids: string[]) => void;
onSetMode: (mode: GQL.BulkUpdateIdMode) => void;
}
@@ -31,7 +32,7 @@ const MultiSet: React.FunctionComponent<IMultiSetProps> = (
];
function onUpdate(items: ValidTypes[]) {
props.onUpdate(items);
props.onUpdate(items.map((i) => i.id));
}
function getModeText(mode: GQL.BulkUpdateIdMode) {
@@ -51,13 +52,32 @@ const MultiSet: React.FunctionComponent<IMultiSetProps> = (
}
}
function onSetMode(mode: GQL.BulkUpdateIdMode) {
if (mode === props.mode) {
return;
}
// if going to Set, set the existing ids
if (mode === GQL.BulkUpdateIdMode.Set && props.existingIds) {
props.onUpdate(props.existingIds);
// if going from Set, wipe the ids
} else if (
mode !== GQL.BulkUpdateIdMode.Set &&
props.mode === GQL.BulkUpdateIdMode.Set
) {
props.onUpdate([]);
}
props.onSetMode(mode);
}
function renderModeButton(mode: GQL.BulkUpdateIdMode) {
return (
<Button
variant="primary"
active={props.mode === mode}
size="sm"
onClick={() => props.onSetMode(mode)}
onClick={() => onSetMode(mode)}
disabled={props.disabled}
>
{getModeText(mode)}
@@ -66,7 +86,7 @@ const MultiSet: React.FunctionComponent<IMultiSetProps> = (
}
return (
<div>
<div className="multi-set">
<ButtonGroup className="button-group-above">
{modes.map((m) => renderModeButton(m))}
</ButtonGroup>

View File

@@ -0,0 +1,15 @@
import React from "react";
import { FormattedMessage } from "react-intl";
interface IProps {
rating?: number | null;
}
export const RatingBanner: React.FC<IProps> = ({ rating }) =>
rating ? (
<div className={`rating-banner rating-${rating}`}>
<FormattedMessage id="rating" />: {rating}
</div>
) : (
<></>
);

View File

@@ -76,11 +76,9 @@
}
}
.multi-set > div.input-group-prepend + div {
flex: 1 1;
margin-bottom: 0;
min-width: 0;
position: relative;
// z-index gets set on button groups for some reason
.multi-set .btn-group > button.btn {
z-index: auto;
}
.folder-list {

View File

@@ -5,6 +5,7 @@ import { NavUtils } from "src/utils";
import { GridCard } from "src/components/Shared";
import { ButtonGroup } from "react-bootstrap";
import { PopoverCountButton } from "../Shared/PopoverCountButton";
import { RatingBanner } from "../Shared/RatingBanner";
interface IProps {
studio: GQL.StudioDataFragment;
@@ -45,21 +46,6 @@ function maybeRenderChildren(studio: GQL.StudioDataFragment) {
}
}
function maybeRenderRatingBanner(studio: GQL.StudioDataFragment) {
if (!studio.rating) {
return;
}
return (
<div
className={`rating-banner ${
studio.rating ? `rating-${studio.rating}` : ""
}`}
>
RATING: {studio.rating}
</div>
);
}
export const StudioCard: React.FC<IProps> = ({
studio,
hideParent,
@@ -135,7 +121,7 @@ export const StudioCard: React.FC<IProps> = ({
<>
{maybeRenderParent(studio, hideParent)}
{maybeRenderChildren(studio)}
{maybeRenderRatingBanner(studio)}
<RatingBanner rating={studio.rating} />
{maybeRenderPopoverButtonGroup()}
</>
}

View File

@@ -362,6 +362,7 @@ div.dropdown-menu {
position: absolute;
text-align: center;
text-size-adjust: none;
text-transform: uppercase;
top: 14px;
transform: rotate(-36deg);
}

View File

@@ -341,14 +341,14 @@ export abstract class IHierarchicalLabeledIdCriterion extends Criterion<IHierarc
protected toCriterionInput(): HierarchicalMultiCriterionInput {
return {
value: this.value.items.map((v) => v.id),
value: (this.value.items ?? []).map((v) => v.id),
modifier: this.modifier,
depth: this.value.depth,
};
}
public getLabelValue(): string {
const labels = this.value.items.map((v) => v.label).join(", ");
const labels = (this.value.items ?? []).map((v) => v.label).join(", ");
if (this.value.depth === 0) {
return labels;