Files
stash/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx
2024-09-11 16:12:18 +10:00

312 lines
9.0 KiB
TypeScript

import React, { useEffect, useState } from "react";
import { Form, Col, Row } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl";
import isEqual from "lodash-es/isEqual";
import { useBulkGalleryUpdate } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql";
import { StudioSelect } from "../Shared/Select";
import { ModalComponent } from "../Shared/Modal";
import { useToast } from "src/hooks/Toast";
import * as FormUtils from "src/utils/form";
import { MultiSet } from "../Shared/MultiSet";
import { RatingSystem } from "../Shared/Rating/RatingSystem";
import {
getAggregateInputIDs,
getAggregateInputValue,
getAggregatePerformerIds,
getAggregateRating,
getAggregateStudioId,
getAggregateTagIds,
} from "src/utils/bulkUpdate";
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
interface IListOperationProps {
selected: GQL.SlimGalleryDataFragment[];
onClose: (applied: boolean) => void;
}
export const EditGalleriesDialog: React.FC<IListOperationProps> = (
props: IListOperationProps
) => {
const intl = useIntl();
const Toast = useToast();
const [rating100, setRating] = useState<number>();
const [studioId, setStudioId] = useState<string>();
const [performerMode, 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();
// Network state
const [isUpdating, setIsUpdating] = useState(false);
const checkboxRef = React.createRef<HTMLInputElement>();
function getGalleryInput(): GQL.BulkGalleryUpdateInput {
// need to determine what we are actually setting on each gallery
const aggregateRating = getAggregateRating(props.selected);
const aggregateStudioId = getAggregateStudioId(props.selected);
const aggregatePerformerIds = getAggregatePerformerIds(props.selected);
const aggregateTagIds = getAggregateTagIds(props.selected);
const galleryInput: GQL.BulkGalleryUpdateInput = {
ids: props.selected.map((gallery) => {
return gallery.id;
}),
};
galleryInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
galleryInput.studio_id = getAggregateInputValue(
studioId,
aggregateStudioId
);
galleryInput.performer_ids = getAggregateInputIDs(
performerMode,
performerIds,
aggregatePerformerIds
);
galleryInput.tag_ids = getAggregateInputIDs(
tagMode,
tagIds,
aggregateTagIds
);
if (organized !== undefined) {
galleryInput.organized = organized;
}
return galleryInput;
}
async function onSave() {
setIsUpdating(true);
try {
await updateGalleries({
variables: {
input: getGalleryInput(),
},
});
Toast.success(
intl.formatMessage(
{ id: "toast.updated_entity" },
{
entity: intl.formatMessage({ id: "galleries" }).toLocaleLowerCase(),
}
)
);
props.onClose(true);
} catch (e) {
Toast.error(e);
}
setIsUpdating(false);
}
useEffect(() => {
const state = props.selected;
let updateRating: number | undefined;
let updateStudioID: string | undefined;
let updatePerformerIds: string[] = [];
let updateTagIds: string[] = [];
let updateOrganized: boolean | undefined;
let first = true;
state.forEach((gallery: GQL.SlimGalleryDataFragment) => {
const galleryRating = gallery.rating100;
const GalleriestudioID = gallery?.studio?.id;
const galleryPerformerIDs = (gallery.performers ?? [])
.map((p) => p.id)
.sort();
const galleryTagIDs = (gallery.tags ?? []).map((p) => p.id).sort();
if (first) {
updateRating = galleryRating ?? undefined;
updateStudioID = GalleriestudioID;
updatePerformerIds = galleryPerformerIDs;
updateTagIds = galleryTagIDs;
updateOrganized = gallery.organized;
first = false;
} else {
if (galleryRating !== updateRating) {
updateRating = undefined;
}
if (GalleriestudioID !== updateStudioID) {
updateStudioID = undefined;
}
if (!isEqual(galleryPerformerIDs, updatePerformerIds)) {
updatePerformerIds = [];
}
if (!isEqual(galleryTagIDs, updateTagIds)) {
updateTagIds = [];
}
if (gallery.organized !== updateOrganized) {
updateOrganized = undefined;
}
}
});
setRating(updateRating);
setStudioId(updateStudioID);
setExistingPerformerIds(updatePerformerIds);
setExistingTagIds(updateTagIds);
setOrganized(updateOrganized);
}, [props.selected]);
useEffect(() => {
if (checkboxRef.current) {
checkboxRef.current.indeterminate = organized === undefined;
}
}, [organized, checkboxRef]);
function renderMultiSelect(
type: "performers" | "tags",
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;
}
return (
<MultiSet
type={type}
disabled={isUpdating}
onUpdate={(itemIDs) => {
switch (type) {
case "performers":
setPerformerIds(itemIDs);
break;
case "tags":
setTagIds(itemIDs);
break;
}
}}
onSetMode={(newMode) => {
switch (type) {
case "performers":
setPerformerMode(newMode);
break;
case "tags":
setTagMode(newMode);
break;
}
}}
existingIds={existingIds ?? []}
ids={ids ?? []}
mode={mode}
menuPortalTarget={document.body}
/>
);
}
function cycleOrganized() {
if (organized) {
setOrganized(undefined);
} else if (organized === undefined) {
setOrganized(false);
} else {
setOrganized(true);
}
}
function render() {
return (
<ModalComponent
show
icon={faPencilAlt}
header={intl.formatMessage(
{ id: "dialogs.edit_entity_title" },
{
count: props?.selected?.length ?? 1,
singularEntity: intl.formatMessage({ id: "gallery" }),
pluralEntity: intl.formatMessage({ id: "galleries" }),
}
)}
accept={{
onClick: onSave,
text: intl.formatMessage({ id: "actions.apply" }),
}}
cancel={{
onClick: () => props.onClose(false),
text: intl.formatMessage({ id: "actions.cancel" }),
variant: "secondary",
}}
isRunning={isUpdating}
>
<Form>
<Form.Group controlId="rating" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "rating" }),
})}
<Col xs={9}>
<RatingSystem
value={rating100}
onSetRating={(value) => setRating(value ?? undefined)}
disabled={isUpdating}
/>
</Col>
</Form.Group>
<Form.Group controlId="studio" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "studio" }),
})}
<Col xs={9}>
<StudioSelect
onSelect={(items) =>
setStudioId(items.length > 0 ? items[0]?.id : undefined)
}
ids={studioId ? [studioId] : []}
isDisabled={isUpdating}
menuPortalTarget={document.body}
/>
</Col>
</Form.Group>
<Form.Group controlId="performers">
<Form.Label>
<FormattedMessage id="performers" />
</Form.Label>
{renderMultiSelect("performers", performerIds)}
</Form.Group>
<Form.Group controlId="tags">
<Form.Label>
<FormattedMessage id="tags" />
</Form.Label>
{renderMultiSelect("tags", tagIds)}
</Form.Group>
<Form.Group controlId="organized">
<Form.Check
type="checkbox"
label={intl.formatMessage({ id: "organized" })}
checked={organized}
ref={checkboxRef}
onChange={() => cycleOrganized()}
/>
</Form.Group>
</Form>
</ModalComponent>
);
}
return render();
};