mirror of
https://github.com/stashapp/stash.git
synced 2026-04-20 14:04:51 +02:00
Refactor bulk edit dialogs (#6647)
* Add BulkUpdateDateInput * Refactor edit scenes dialog * Improve bulk date input styling * Make fields inline in edit performers dialog * Refactor edit images dialog * Refactor edit galleries dialog * Add date and synopsis to bulk update group input * Refactor edit groups dialog * Change edit dialog titles to 'Edit x entities' * Update styling of bulk fields to be consistent with other UI * Rename BulkUpdateTextInput to generic BulkUpdate We'll collect other bulk inputs here * Add and use BulkUpdateFormGroup * Handle null dates correctly * Add date clear button and validation
This commit is contained in:
parent
300e7edb75
commit
b8bd8953f7
20 changed files with 1253 additions and 1155 deletions
|
|
@ -99,6 +99,8 @@ input BulkGroupUpdateInput {
|
|||
ids: [ID!]
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
date: String
|
||||
synopsis: String
|
||||
studio_id: ID
|
||||
director: String
|
||||
urls: BulkUpdateStrings
|
||||
|
|
|
|||
|
|
@ -227,6 +227,12 @@ func (r *mutationResolver) GroupUpdate(ctx context.Context, input GroupUpdateInp
|
|||
func groupPartialFromBulkGroupUpdateInput(translator changesetTranslator, input BulkGroupUpdateInput) (ret models.GroupPartial, err error) {
|
||||
updatedGroup := models.NewGroupPartial()
|
||||
|
||||
updatedGroup.Date, err = translator.optionalDate(input.Date, "date")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting date: %w", err)
|
||||
return
|
||||
}
|
||||
updatedGroup.Synopsis = translator.optionalString(input.Synopsis, "synopsis")
|
||||
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedGroup.Director = translator.optionalString(input.Director, "director")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,100 +1,129 @@
|
|||
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 React, { useEffect, useMemo, useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
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 { useToast } from "src/hooks/Toast";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateInputIDs,
|
||||
getAggregateInputValue,
|
||||
getAggregatePerformerIds,
|
||||
getAggregateRating,
|
||||
getAggregateStudioId,
|
||||
getAggregateStateObject,
|
||||
getAggregateTagIds,
|
||||
getAggregateStudioId,
|
||||
getAggregateSceneIds,
|
||||
} from "src/utils/bulkUpdate";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||
import { BulkUpdateFormGroup, BulkUpdateTextInput } from "../Shared/BulkUpdate";
|
||||
import { BulkUpdateDateInput } from "../Shared/DateInput";
|
||||
import { getDateError } from "src/utils/yup";
|
||||
|
||||
interface IListOperationProps {
|
||||
selected: GQL.SlimGalleryDataFragment[];
|
||||
onClose: (applied: boolean) => void;
|
||||
}
|
||||
|
||||
const galleryFields = [
|
||||
"code",
|
||||
"rating100",
|
||||
"details",
|
||||
"organized",
|
||||
"photographer",
|
||||
"date",
|
||||
];
|
||||
|
||||
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 [updateInput, setUpdateInput] = useState<GQL.BulkGalleryUpdateInput>({
|
||||
ids: props.selected.map((gallery) => {
|
||||
return gallery.id;
|
||||
}),
|
||||
});
|
||||
|
||||
const [performerIds, setPerformerIds] = useState<GQL.BulkUpdateIds>({
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
const [tagIds, setTagIds] = useState<GQL.BulkUpdateIds>({
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
const [sceneIds, setSceneIds] = useState<GQL.BulkUpdateIds>({
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
|
||||
const unsetDisabled = props.selected.length < 2;
|
||||
|
||||
const [dateError, setDateError] = useState<string | undefined>();
|
||||
|
||||
const [updateGalleries] = useBulkGalleryUpdate();
|
||||
|
||||
// Network state
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
const checkboxRef = React.createRef<HTMLInputElement>();
|
||||
const aggregateState = useMemo(() => {
|
||||
const updateState: Partial<GQL.BulkGalleryUpdateInput> = {};
|
||||
const state = props.selected;
|
||||
updateState.studio_id = getAggregateStudioId(props.selected);
|
||||
const updateTagIds = getAggregateTagIds(props.selected);
|
||||
const updatePerformerIds = getAggregatePerformerIds(props.selected);
|
||||
const updateSceneIds = getAggregateSceneIds(props.selected);
|
||||
let first = true;
|
||||
|
||||
state.forEach((gallery: GQL.SlimGalleryDataFragment) => {
|
||||
getAggregateStateObject(updateState, gallery, galleryFields, first);
|
||||
first = false;
|
||||
});
|
||||
|
||||
return {
|
||||
state: updateState,
|
||||
tagIds: updateTagIds,
|
||||
performerIds: updatePerformerIds,
|
||||
sceneIds: updateSceneIds,
|
||||
};
|
||||
}, [props.selected]);
|
||||
|
||||
// update initial state from aggregate
|
||||
useEffect(() => {
|
||||
setUpdateInput((current) => ({ ...current, ...aggregateState.state }));
|
||||
}, [aggregateState]);
|
||||
|
||||
useEffect(() => {
|
||||
setDateError(getDateError(updateInput.date ?? "", intl));
|
||||
}, [updateInput.date, intl]);
|
||||
|
||||
function setUpdateField(input: Partial<GQL.BulkGalleryUpdateInput>) {
|
||||
setUpdateInput((current) => ({ ...current, ...input }));
|
||||
}
|
||||
|
||||
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;
|
||||
}),
|
||||
...updateInput,
|
||||
tag_ids: tagIds,
|
||||
performer_ids: performerIds,
|
||||
scene_ids: sceneIds,
|
||||
};
|
||||
|
||||
galleryInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
|
||||
galleryInput.studio_id = getAggregateInputValue(
|
||||
studioId,
|
||||
aggregateStudioId
|
||||
// we don't have unset functionality for the rating star control
|
||||
// so need to determine if we are setting a rating or not
|
||||
galleryInput.rating100 = getAggregateInputValue(
|
||||
updateInput.rating100,
|
||||
aggregateState.state.rating100
|
||||
);
|
||||
|
||||
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(),
|
||||
},
|
||||
});
|
||||
await updateGalleries({ variables: { input: getGalleryInput() } });
|
||||
Toast.success(
|
||||
intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
|
|
@ -110,129 +139,13 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||
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" },
|
||||
{ id: "dialogs.edit_entity_count_title" },
|
||||
{
|
||||
count: props?.selected?.length ?? 1,
|
||||
singularEntity: intl.formatMessage({ id: "gallery" }),
|
||||
|
|
@ -243,6 +156,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||
onClick: onSave,
|
||||
text: intl.formatMessage({ id: "actions.apply" }),
|
||||
}}
|
||||
disabled={isUpdating || !!dateError}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(false),
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
|
|
@ -251,55 +165,119 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||
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>
|
||||
<BulkUpdateFormGroup name="rating">
|
||||
<RatingSystem
|
||||
value={updateInput.rating100}
|
||||
onSetRating={(value) =>
|
||||
setUpdateField({ rating100: value ?? undefined })
|
||||
}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="performers">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="performers" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("performers", performerIds)}
|
||||
</Form.Group>
|
||||
<BulkUpdateFormGroup name="scene_code">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.code}
|
||||
valueChanged={(newValue) => setUpdateField({ code: newValue })}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="date">
|
||||
<BulkUpdateDateInput
|
||||
value={updateInput.date}
|
||||
valueChanged={(newValue) => setUpdateField({ date: newValue })}
|
||||
unsetDisabled={unsetDisabled}
|
||||
error={dateError}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="tags" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("tags", tagIds)}
|
||||
</Form.Group>
|
||||
<BulkUpdateFormGroup name="photographer">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.photographer}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ photographer: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="studio">
|
||||
<StudioSelect
|
||||
onSelect={(items) =>
|
||||
setUpdateField({
|
||||
studio_id: items.length > 0 ? items[0]?.id : undefined,
|
||||
})
|
||||
}
|
||||
ids={updateInput.studio_id ? [updateInput.studio_id] : []}
|
||||
isDisabled={isUpdating}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="performers" inline={false}>
|
||||
<MultiSet
|
||||
type={"performers"}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => {
|
||||
setPerformerIds((c) => ({ ...c, ids: itemIDs }));
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
setPerformerIds((c) => ({ ...c, mode: newMode }));
|
||||
}}
|
||||
ids={performerIds.ids ?? []}
|
||||
existingIds={aggregateState.performerIds}
|
||||
mode={performerIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="scenes" inline={false}>
|
||||
<MultiSet
|
||||
type={"scenes"}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => {
|
||||
setSceneIds((c) => ({ ...c, ids: itemIDs }));
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
setSceneIds((c) => ({ ...c, mode: newMode }));
|
||||
}}
|
||||
ids={sceneIds.ids ?? []}
|
||||
existingIds={aggregateState.sceneIds}
|
||||
mode={sceneIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="tags" inline={false}>
|
||||
<MultiSet
|
||||
type={"tags"}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => {
|
||||
setTagIds((c) => ({ ...c, ids: itemIDs }));
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
setTagIds((c) => ({ ...c, mode: newMode }));
|
||||
}}
|
||||
ids={tagIds.ids ?? []}
|
||||
existingIds={aggregateState.tagIds}
|
||||
mode={tagIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="details" inline={false}>
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.details}
|
||||
valueChanged={(newValue) => setUpdateField({ details: newValue })}
|
||||
unsetDisabled={unsetDisabled}
|
||||
as="textarea"
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="organized">
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
<IndeterminateCheckbox
|
||||
label={intl.formatMessage({ id: "organized" })}
|
||||
checked={organized}
|
||||
ref={checkboxRef}
|
||||
onChange={() => cycleOrganized()}
|
||||
setChecked={(checked) => setUpdateField({ organized: checked })}
|
||||
checked={updateInput.organized ?? undefined}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Form, Col, Row } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useBulkGroupUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { StudioSelect } from "../Shared/Select";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { MultiSet } from "../Shared/MultiSet";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import * as FormUtils from "src/utils/form";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateIds,
|
||||
getAggregateInputIDs,
|
||||
getAggregateInputValue,
|
||||
getAggregateRating,
|
||||
getAggregateStudioId,
|
||||
getAggregateStateObject,
|
||||
getAggregateTagIds,
|
||||
getAggregateStudioId,
|
||||
getAggregateIds,
|
||||
} from "src/utils/bulkUpdate";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { MultiSet } from "../Shared/MultiSet";
|
||||
import { ContainingGroupsMultiSet } from "./ContainingGroupsMultiSet";
|
||||
import { BulkUpdateFormGroup, BulkUpdateTextInput } from "../Shared/BulkUpdate";
|
||||
import { BulkUpdateDateInput } from "../Shared/DateInput";
|
||||
import { IRelatedGroupEntry } from "./GroupDetails/RelatedGroupTable";
|
||||
import { ContainingGroupsMultiSet } from "./ContainingGroupsMultiSet";
|
||||
import { getDateError } from "src/utils/yup";
|
||||
|
||||
interface IListOperationProps {
|
||||
selected: GQL.ListGroupDataFragment[];
|
||||
|
|
@ -67,50 +67,86 @@ function getAggregateContainingGroupInput(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const groupFields = ["rating100", "synopsis", "director", "date"];
|
||||
|
||||
export const EditGroupsDialog: React.FC<IListOperationProps> = (
|
||||
props: IListOperationProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [rating100, setRating] = useState<number | undefined>();
|
||||
const [studioId, setStudioId] = useState<string | undefined>();
|
||||
const [director, setDirector] = useState<string | undefined>();
|
||||
|
||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||
GQL.BulkUpdateIdMode.Add
|
||||
);
|
||||
const [tagIds, setTagIds] = useState<string[]>();
|
||||
const [existingTagIds, setExistingTagIds] = useState<string[]>();
|
||||
const [updateInput, setUpdateInput] = useState<GQL.BulkGroupUpdateInput>({
|
||||
ids: props.selected.map((group) => {
|
||||
return group.id;
|
||||
}),
|
||||
});
|
||||
|
||||
const [tagIds, setTagIds] = useState<GQL.BulkUpdateIds>({
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
const [containingGroupsMode, setGroupMode] =
|
||||
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||
const [containingGroups, setGroups] = useState<IRelatedGroupEntry[]>();
|
||||
const [existingContainingGroups, setExistingContainingGroups] =
|
||||
useState<IRelatedGroupEntry[]>();
|
||||
|
||||
const [updateGroups] = useBulkGroupUpdate(getGroupInput());
|
||||
const unsetDisabled = props.selected.length < 2;
|
||||
|
||||
const [updateGroups] = useBulkGroupUpdate();
|
||||
|
||||
const [dateError, setDateError] = useState<string | undefined>();
|
||||
|
||||
// Network state
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
function getGroupInput(): GQL.BulkGroupUpdateInput {
|
||||
const aggregateRating = getAggregateRating(props.selected);
|
||||
const aggregateStudioId = getAggregateStudioId(props.selected);
|
||||
const aggregateTagIds = getAggregateTagIds(props.selected);
|
||||
const aggregateState = useMemo(() => {
|
||||
const updateState: Partial<GQL.BulkGroupUpdateInput> = {};
|
||||
const state = props.selected;
|
||||
updateState.studio_id = getAggregateStudioId(props.selected);
|
||||
const updateTagIds = getAggregateTagIds(props.selected);
|
||||
const aggregateGroups = getAggregateContainingGroups(props.selected);
|
||||
let first = true;
|
||||
|
||||
state.forEach((group: GQL.ListGroupDataFragment) => {
|
||||
getAggregateStateObject(updateState, group, groupFields, first);
|
||||
first = false;
|
||||
});
|
||||
|
||||
return {
|
||||
state: updateState,
|
||||
tagIds: updateTagIds,
|
||||
containingGroups: aggregateGroups,
|
||||
};
|
||||
}, [props.selected]);
|
||||
|
||||
// update initial state from aggregate
|
||||
useEffect(() => {
|
||||
setUpdateInput((current) => ({ ...current, ...aggregateState.state }));
|
||||
}, [aggregateState]);
|
||||
|
||||
useEffect(() => {
|
||||
setDateError(getDateError(updateInput.date ?? "", intl));
|
||||
}, [updateInput.date, intl]);
|
||||
|
||||
function setUpdateField(input: Partial<GQL.BulkGroupUpdateInput>) {
|
||||
setUpdateInput((current) => ({ ...current, ...input }));
|
||||
}
|
||||
|
||||
function getGroupInput(): GQL.BulkGroupUpdateInput {
|
||||
const groupInput: GQL.BulkGroupUpdateInput = {
|
||||
ids: props.selected.map((group) => group.id),
|
||||
director,
|
||||
...updateInput,
|
||||
tag_ids: tagIds,
|
||||
};
|
||||
|
||||
groupInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
|
||||
groupInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
||||
groupInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
|
||||
// we don't have unset functionality for the rating star control
|
||||
// so need to determine if we are setting a rating or not
|
||||
groupInput.rating100 = getAggregateInputValue(
|
||||
updateInput.rating100,
|
||||
aggregateState.state.rating100
|
||||
);
|
||||
|
||||
groupInput.containing_groups = getAggregateContainingGroupInput(
|
||||
containingGroupsMode,
|
||||
containingGroups,
|
||||
aggregateGroups
|
||||
aggregateState.containingGroups
|
||||
);
|
||||
|
||||
return groupInput;
|
||||
|
|
@ -119,13 +155,11 @@ export const EditGroupsDialog: React.FC<IListOperationProps> = (
|
|||
async function onSave() {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
await updateGroups();
|
||||
await updateGroups({ variables: { input: getGroupInput() } });
|
||||
Toast.success(
|
||||
intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{
|
||||
entity: intl.formatMessage({ id: "groups" }).toLocaleLowerCase(),
|
||||
}
|
||||
{ entity: intl.formatMessage({ id: "groups" }).toLocaleLowerCase() }
|
||||
)
|
||||
);
|
||||
props.onClose(true);
|
||||
|
|
@ -135,67 +169,24 @@ export const EditGroupsDialog: React.FC<IListOperationProps> = (
|
|||
setIsUpdating(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const state = props.selected;
|
||||
let updateRating: number | undefined;
|
||||
let updateStudioId: string | undefined;
|
||||
let updateTagIds: string[] = [];
|
||||
let updateContainingGroupIds: IRelatedGroupEntry[] = [];
|
||||
let updateDirector: string | undefined;
|
||||
let first = true;
|
||||
|
||||
state.forEach((group: GQL.ListGroupDataFragment) => {
|
||||
const groupTagIDs = (group.tags ?? []).map((p) => p.id).sort();
|
||||
const groupContainingGroupIDs = (group.containing_groups ?? []).sort(
|
||||
(a, b) => a.group.id.localeCompare(b.group.id)
|
||||
);
|
||||
|
||||
if (first) {
|
||||
first = false;
|
||||
updateRating = group.rating100 ?? undefined;
|
||||
updateStudioId = group.studio?.id ?? undefined;
|
||||
updateTagIds = groupTagIDs;
|
||||
updateContainingGroupIds = groupContainingGroupIDs;
|
||||
updateDirector = group.director ?? undefined;
|
||||
} else {
|
||||
if (group.rating100 !== updateRating) {
|
||||
updateRating = undefined;
|
||||
}
|
||||
if (group.studio?.id !== updateStudioId) {
|
||||
updateStudioId = undefined;
|
||||
}
|
||||
if (group.director !== updateDirector) {
|
||||
updateDirector = undefined;
|
||||
}
|
||||
if (!isEqual(groupTagIDs, updateTagIds)) {
|
||||
updateTagIds = [];
|
||||
}
|
||||
if (!isEqual(groupContainingGroupIDs, updateContainingGroupIds)) {
|
||||
updateTagIds = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setRating(updateRating);
|
||||
setStudioId(updateStudioId);
|
||||
setExistingTagIds(updateTagIds);
|
||||
setExistingContainingGroups(updateContainingGroupIds);
|
||||
setDirector(updateDirector);
|
||||
}, [props.selected]);
|
||||
|
||||
function render() {
|
||||
return (
|
||||
<ModalComponent
|
||||
show
|
||||
icon={faPencilAlt}
|
||||
header={intl.formatMessage(
|
||||
{ id: "actions.edit_entity" },
|
||||
{ entityType: intl.formatMessage({ id: "groups" }) }
|
||||
{ id: "dialogs.edit_entity_count_title" },
|
||||
{
|
||||
count: props?.selected?.length ?? 1,
|
||||
singularEntity: intl.formatMessage({ id: "group" }),
|
||||
pluralEntity: intl.formatMessage({ id: "groups" }),
|
||||
}
|
||||
)}
|
||||
accept={{
|
||||
onClick: onSave,
|
||||
text: intl.formatMessage({ id: "actions.apply" }),
|
||||
}}
|
||||
disabled={isUpdating || !!dateError}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(false),
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
|
|
@ -204,74 +195,90 @@ export const EditGroupsDialog: React.FC<IListOperationProps> = (
|
|||
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="containing-groups">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="containing_groups" />
|
||||
</Form.Label>
|
||||
<BulkUpdateFormGroup name="rating">
|
||||
<RatingSystem
|
||||
value={updateInput.rating100}
|
||||
onSetRating={(value) =>
|
||||
setUpdateField({ rating100: value ?? undefined })
|
||||
}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="date">
|
||||
<BulkUpdateDateInput
|
||||
value={updateInput.date}
|
||||
valueChanged={(newValue) => setUpdateField({ date: newValue })}
|
||||
unsetDisabled={unsetDisabled}
|
||||
error={dateError}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="director">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.director}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ director: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="studio">
|
||||
<StudioSelect
|
||||
onSelect={(items) =>
|
||||
setUpdateField({
|
||||
studio_id: items.length > 0 ? items[0]?.id : undefined,
|
||||
})
|
||||
}
|
||||
ids={updateInput.studio_id ? [updateInput.studio_id] : []}
|
||||
isDisabled={isUpdating}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup
|
||||
name="containing-groups"
|
||||
messageId="containing_groups"
|
||||
inline={false}
|
||||
>
|
||||
<ContainingGroupsMultiSet
|
||||
disabled={isUpdating}
|
||||
onUpdate={(v) => setGroups(v)}
|
||||
onSetMode={(newMode) => setGroupMode(newMode)}
|
||||
existingValue={existingContainingGroups ?? []}
|
||||
existingValue={aggregateState.containingGroups ?? []}
|
||||
value={containingGroups ?? []}
|
||||
mode={containingGroupsMode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="director">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="director" />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
className="input-control"
|
||||
type="text"
|
||||
value={director}
|
||||
onChange={(event) => setDirector(event.currentTarget.value)}
|
||||
placeholder={intl.formatMessage({ id: "director" })}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="tags" />
|
||||
</Form.Label>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="tags" inline={false}>
|
||||
<MultiSet
|
||||
type="tags"
|
||||
type={"tags"}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => setTagIds(itemIDs)}
|
||||
onSetMode={(newMode) => setTagMode(newMode)}
|
||||
existingIds={existingTagIds ?? []}
|
||||
ids={tagIds ?? []}
|
||||
mode={tagMode}
|
||||
onUpdate={(itemIDs) => {
|
||||
setTagIds((c) => ({ ...c, ids: itemIDs }));
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
setTagIds((c) => ({ ...c, mode: newMode }));
|
||||
}}
|
||||
ids={tagIds.ids ?? []}
|
||||
existingIds={aggregateState.tagIds}
|
||||
mode={tagIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</Form.Group>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="synopsis" inline={false}>
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.synopsis}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ synopsis: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
as="textarea"
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
</Form>
|
||||
</ModalComponent>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,96 +1,121 @@
|
|||
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 React, { useEffect, useMemo, useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useBulkImageUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StudioSelect } from "src/components/Shared/Select";
|
||||
import { ModalComponent } from "src/components/Shared/Modal";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import * as FormUtils from "src/utils/form";
|
||||
import { StudioSelect } from "../Shared/Select";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { MultiSet } from "../Shared/MultiSet";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateGalleryIds,
|
||||
getAggregateInputIDs,
|
||||
getAggregateInputValue,
|
||||
getAggregatePerformerIds,
|
||||
getAggregateRating,
|
||||
getAggregateStudioId,
|
||||
getAggregateStateObject,
|
||||
getAggregateTagIds,
|
||||
getAggregateStudioId,
|
||||
getAggregateGalleryIds,
|
||||
} from "src/utils/bulkUpdate";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||
import { BulkUpdateFormGroup, BulkUpdateTextInput } from "../Shared/BulkUpdate";
|
||||
import { BulkUpdateDateInput } from "../Shared/DateInput";
|
||||
import { getDateError } from "src/utils/yup";
|
||||
|
||||
interface IListOperationProps {
|
||||
selected: GQL.SlimImageDataFragment[];
|
||||
onClose: (applied: boolean) => void;
|
||||
}
|
||||
|
||||
const imageFields = [
|
||||
"code",
|
||||
"rating100",
|
||||
"details",
|
||||
"organized",
|
||||
"photographer",
|
||||
"date",
|
||||
];
|
||||
|
||||
export const EditImagesDialog: 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 [updateInput, setUpdateInput] = useState<GQL.BulkImageUpdateInput>({
|
||||
ids: props.selected.map((image) => {
|
||||
return image.id;
|
||||
}),
|
||||
});
|
||||
|
||||
const [galleryMode, setGalleryMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||
GQL.BulkUpdateIdMode.Add
|
||||
);
|
||||
const [galleryIds, setGalleryIds] = useState<string[]>();
|
||||
const [existingGalleryIds, setExistingGalleryIds] = useState<string[]>();
|
||||
const [performerIds, setPerformerIds] = useState<GQL.BulkUpdateIds>({
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
const [tagIds, setTagIds] = useState<GQL.BulkUpdateIds>({
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
const [galleryIds, setGalleryIds] = useState<GQL.BulkUpdateIds>({
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
|
||||
const [organized, setOrganized] = useState<boolean | undefined>();
|
||||
const unsetDisabled = props.selected.length < 2;
|
||||
|
||||
const [dateError, setDateError] = useState<string | undefined>();
|
||||
|
||||
const [updateImages] = useBulkImageUpdate();
|
||||
|
||||
// Network state
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
const checkboxRef = React.createRef<HTMLInputElement>();
|
||||
const aggregateState = useMemo(() => {
|
||||
const updateState: Partial<GQL.BulkImageUpdateInput> = {};
|
||||
const state = props.selected;
|
||||
updateState.studio_id = getAggregateStudioId(props.selected);
|
||||
const updateTagIds = getAggregateTagIds(props.selected);
|
||||
const updatePerformerIds = getAggregatePerformerIds(props.selected);
|
||||
const updateGalleryIds = getAggregateGalleryIds(props.selected);
|
||||
let first = true;
|
||||
|
||||
state.forEach((image: GQL.SlimImageDataFragment) => {
|
||||
getAggregateStateObject(updateState, image, imageFields, first);
|
||||
first = false;
|
||||
});
|
||||
|
||||
return {
|
||||
state: updateState,
|
||||
tagIds: updateTagIds,
|
||||
performerIds: updatePerformerIds,
|
||||
galleryIds: updateGalleryIds,
|
||||
};
|
||||
}, [props.selected]);
|
||||
|
||||
// update initial state from aggregate
|
||||
useEffect(() => {
|
||||
setUpdateInput((current) => ({ ...current, ...aggregateState.state }));
|
||||
}, [aggregateState]);
|
||||
|
||||
useEffect(() => {
|
||||
setDateError(getDateError(updateInput.date ?? "", intl));
|
||||
}, [updateInput.date, intl]);
|
||||
|
||||
function setUpdateField(input: Partial<GQL.BulkImageUpdateInput>) {
|
||||
setUpdateInput((current) => ({ ...current, ...input }));
|
||||
}
|
||||
|
||||
function getImageInput(): GQL.BulkImageUpdateInput {
|
||||
// need to determine what we are actually setting on each image
|
||||
const aggregateRating = getAggregateRating(props.selected);
|
||||
const aggregateStudioId = getAggregateStudioId(props.selected);
|
||||
const aggregatePerformerIds = getAggregatePerformerIds(props.selected);
|
||||
const aggregateTagIds = getAggregateTagIds(props.selected);
|
||||
const aggregateGalleryIds = getAggregateGalleryIds(props.selected);
|
||||
|
||||
const imageInput: GQL.BulkImageUpdateInput = {
|
||||
ids: props.selected.map((image) => {
|
||||
return image.id;
|
||||
}),
|
||||
...updateInput,
|
||||
tag_ids: tagIds,
|
||||
performer_ids: performerIds,
|
||||
gallery_ids: galleryIds,
|
||||
};
|
||||
|
||||
imageInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
|
||||
imageInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
||||
|
||||
imageInput.performer_ids = getAggregateInputIDs(
|
||||
performerMode,
|
||||
performerIds,
|
||||
aggregatePerformerIds
|
||||
// we don't have unset functionality for the rating star control
|
||||
// so need to determine if we are setting a rating or not
|
||||
imageInput.rating100 = getAggregateInputValue(
|
||||
updateInput.rating100,
|
||||
aggregateState.state.rating100
|
||||
);
|
||||
imageInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
|
||||
imageInput.gallery_ids = getAggregateInputIDs(
|
||||
galleryMode,
|
||||
galleryIds,
|
||||
aggregateGalleryIds
|
||||
);
|
||||
|
||||
if (organized !== undefined) {
|
||||
imageInput.organized = organized;
|
||||
}
|
||||
|
||||
return imageInput;
|
||||
}
|
||||
|
|
@ -98,11 +123,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||
async function onSave() {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
await updateImages({
|
||||
variables: {
|
||||
input: getImageInput(),
|
||||
},
|
||||
});
|
||||
await updateImages({ variables: { input: getImageInput() } });
|
||||
Toast.success(
|
||||
intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
|
|
@ -116,86 +137,13 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||
setIsUpdating(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const state = props.selected;
|
||||
let updateRating: number | undefined;
|
||||
let updateStudioID: string | undefined;
|
||||
let updatePerformerIds: string[] = [];
|
||||
let updateTagIds: string[] = [];
|
||||
let updateGalleryIds: string[] = [];
|
||||
let updateOrganized: boolean | undefined;
|
||||
let first = true;
|
||||
|
||||
state.forEach((image: GQL.SlimImageDataFragment) => {
|
||||
const imageRating = image.rating100;
|
||||
const imageStudioID = image?.studio?.id;
|
||||
const imagePerformerIDs = (image.performers ?? [])
|
||||
.map((p) => p.id)
|
||||
.sort();
|
||||
const imageTagIDs = (image.tags ?? []).map((p) => p.id).sort();
|
||||
const imageGalleryIDs = (image.galleries ?? []).map((p) => p.id).sort();
|
||||
|
||||
if (first) {
|
||||
updateRating = imageRating ?? undefined;
|
||||
updateStudioID = imageStudioID;
|
||||
updatePerformerIds = imagePerformerIDs;
|
||||
updateTagIds = imageTagIDs;
|
||||
updateGalleryIds = imageGalleryIDs;
|
||||
updateOrganized = image.organized;
|
||||
first = false;
|
||||
} else {
|
||||
if (imageRating !== updateRating) {
|
||||
updateRating = undefined;
|
||||
}
|
||||
if (imageStudioID !== updateStudioID) {
|
||||
updateStudioID = undefined;
|
||||
}
|
||||
if (!isEqual(imagePerformerIDs, updatePerformerIds)) {
|
||||
updatePerformerIds = [];
|
||||
}
|
||||
if (!isEqual(imageTagIDs, updateTagIds)) {
|
||||
updateTagIds = [];
|
||||
}
|
||||
if (!isEqual(imageGalleryIDs, updateGalleryIds)) {
|
||||
updateGalleryIds = [];
|
||||
}
|
||||
if (image.organized !== updateOrganized) {
|
||||
updateOrganized = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setRating(updateRating);
|
||||
setStudioId(updateStudioID);
|
||||
setExistingPerformerIds(updatePerformerIds);
|
||||
setExistingTagIds(updateTagIds);
|
||||
setExistingGalleryIds(updateGalleryIds);
|
||||
setOrganized(updateOrganized);
|
||||
}, [props.selected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (checkboxRef.current) {
|
||||
checkboxRef.current.indeterminate = organized === undefined;
|
||||
}
|
||||
}, [organized, checkboxRef]);
|
||||
|
||||
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" },
|
||||
{ id: "dialogs.edit_entity_count_title" },
|
||||
{
|
||||
count: props?.selected?.length ?? 1,
|
||||
singularEntity: intl.formatMessage({ id: "image" }),
|
||||
|
|
@ -206,6 +154,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||
onClick: onSave,
|
||||
text: intl.formatMessage({ id: "actions.apply" }),
|
||||
}}
|
||||
disabled={isUpdating || !!dateError}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(false),
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
|
|
@ -214,89 +163,120 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||
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>
|
||||
<MultiSet
|
||||
type="performers"
|
||||
<BulkUpdateFormGroup name="rating">
|
||||
<RatingSystem
|
||||
value={updateInput.rating100}
|
||||
onSetRating={(value) =>
|
||||
setUpdateField({ rating100: value ?? undefined })
|
||||
}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => setPerformerIds(itemIDs)}
|
||||
onSetMode={(newMode) => setPerformerMode(newMode)}
|
||||
existingIds={existingPerformerIds ?? []}
|
||||
ids={performerIds ?? []}
|
||||
mode={performerMode}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="scene_code">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.code}
|
||||
valueChanged={(newValue) => setUpdateField({ code: newValue })}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="date">
|
||||
<BulkUpdateDateInput
|
||||
value={updateInput.date}
|
||||
valueChanged={(newValue) => setUpdateField({ date: newValue })}
|
||||
unsetDisabled={unsetDisabled}
|
||||
error={dateError}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="photographer">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.photographer}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ photographer: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="studio">
|
||||
<StudioSelect
|
||||
onSelect={(items) =>
|
||||
setUpdateField({
|
||||
studio_id: items.length > 0 ? items[0]?.id : undefined,
|
||||
})
|
||||
}
|
||||
ids={updateInput.studio_id ? [updateInput.studio_id] : []}
|
||||
isDisabled={isUpdating}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</Form.Group>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="tags" />
|
||||
</Form.Label>
|
||||
<BulkUpdateFormGroup name="performers" inline={false}>
|
||||
<MultiSet
|
||||
type="tags"
|
||||
type={"performers"}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => setTagIds(itemIDs)}
|
||||
onSetMode={(newMode) => setTagMode(newMode)}
|
||||
existingIds={existingTagIds ?? []}
|
||||
ids={tagIds ?? []}
|
||||
mode={tagMode}
|
||||
onUpdate={(itemIDs) => {
|
||||
setPerformerIds((c) => ({ ...c, ids: itemIDs }));
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
setPerformerIds((c) => ({ ...c, mode: newMode }));
|
||||
}}
|
||||
ids={performerIds.ids ?? []}
|
||||
existingIds={aggregateState.performerIds}
|
||||
mode={performerIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</Form.Group>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="galleries">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="galleries" />
|
||||
</Form.Label>
|
||||
<BulkUpdateFormGroup name="galleries" inline={false}>
|
||||
<MultiSet
|
||||
type="galleries"
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => setGalleryIds(itemIDs)}
|
||||
onSetMode={(newMode) => setGalleryMode(newMode)}
|
||||
existingIds={existingGalleryIds ?? []}
|
||||
ids={galleryIds ?? []}
|
||||
mode={galleryMode}
|
||||
onUpdate={(itemIDs) => {
|
||||
setGalleryIds((c) => ({ ...c, ids: itemIDs }));
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
setGalleryIds((c) => ({ ...c, mode: newMode }));
|
||||
}}
|
||||
ids={galleryIds.ids ?? []}
|
||||
existingIds={aggregateState.galleryIds}
|
||||
mode={galleryIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</Form.Group>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="tags" inline={false}>
|
||||
<MultiSet
|
||||
type={"tags"}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => {
|
||||
setTagIds((c) => ({ ...c, ids: itemIDs }));
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
setTagIds((c) => ({ ...c, mode: newMode }));
|
||||
}}
|
||||
ids={tagIds.ids ?? []}
|
||||
existingIds={aggregateState.tagIds}
|
||||
mode={tagIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="details" inline={false}>
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.details}
|
||||
valueChanged={(newValue) => setUpdateField({ details: newValue })}
|
||||
unsetDisabled={unsetDisabled}
|
||||
as="textarea"
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="organized">
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
<IndeterminateCheckbox
|
||||
label={intl.formatMessage({ id: "organized" })}
|
||||
checked={organized}
|
||||
ref={checkboxRef}
|
||||
onChange={() => cycleOrganized()}
|
||||
setChecked={(checked) => setUpdateField({ organized: checked })}
|
||||
checked={updateInput.organized ?? undefined}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Col, Form, Row } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useBulkPerformerUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
|
|
@ -23,12 +23,13 @@ import {
|
|||
stringToCircumcised,
|
||||
} from "src/utils/circumcised";
|
||||
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
||||
import { BulkUpdateFormGroup, BulkUpdateTextInput } from "../Shared/BulkUpdate";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import * as FormUtils from "src/utils/form";
|
||||
import { CountrySelect } from "../Shared/CountrySelect";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import cx from "classnames";
|
||||
import { BulkUpdateDateInput } from "../Shared/DateInput";
|
||||
import { getDateError } from "src/utils/yup";
|
||||
|
||||
interface IListOperationProps {
|
||||
selected: GQL.SlimPerformerDataFragment[];
|
||||
|
|
@ -75,17 +76,30 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
const [aggregateState, setAggregateState] =
|
||||
useState<GQL.BulkPerformerUpdateInput>({});
|
||||
// height and weight needs conversion to/from number
|
||||
const [height, setHeight] = useState<string | undefined>();
|
||||
const [weight, setWeight] = useState<string | undefined>();
|
||||
const [penis_length, setPenisLength] = useState<string | undefined>();
|
||||
const [height, setHeight] = useState<string | undefined | null>();
|
||||
const [weight, setWeight] = useState<string | undefined | null>();
|
||||
const [penis_length, setPenisLength] = useState<string | undefined | null>();
|
||||
const [updateInput, setUpdateInput] = useState<GQL.BulkPerformerUpdateInput>(
|
||||
{}
|
||||
);
|
||||
const genderOptions = [""].concat(genderStrings);
|
||||
const circumcisedOptions = [""].concat(circumcisedStrings);
|
||||
|
||||
const unsetDisabled = props.selected.length < 2;
|
||||
|
||||
const [updatePerformers] = useBulkPerformerUpdate(getPerformerInput());
|
||||
|
||||
const [birthdateError, setBirthdateError] = useState<string | undefined>();
|
||||
const [deathDateError, setDeathDateError] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
setBirthdateError(getDateError(updateInput.birthdate ?? "", intl));
|
||||
}, [updateInput.birthdate, intl]);
|
||||
|
||||
useEffect(() => {
|
||||
setDeathDateError(getDateError(updateInput.death_date ?? "", intl));
|
||||
}, [updateInput.death_date, intl]);
|
||||
|
||||
// Network state
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
|
|
@ -121,14 +135,14 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
);
|
||||
|
||||
if (height !== undefined) {
|
||||
performerInput.height_cm = parseFloat(height);
|
||||
performerInput.height_cm = height === null ? null : parseFloat(height);
|
||||
}
|
||||
if (weight !== undefined) {
|
||||
performerInput.weight = parseFloat(weight);
|
||||
performerInput.weight = weight === null ? null : parseFloat(weight);
|
||||
}
|
||||
|
||||
if (penis_length !== undefined) {
|
||||
performerInput.penis_length = parseFloat(penis_length);
|
||||
performerInput.penis_length =
|
||||
penis_length === null ? null : parseFloat(penis_length);
|
||||
}
|
||||
|
||||
return performerInput;
|
||||
|
|
@ -205,25 +219,6 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
setUpdateInput(updateState);
|
||||
}, [props.selected]);
|
||||
|
||||
function renderTextField(
|
||||
name: string,
|
||||
value: string | undefined | null,
|
||||
setter: (newValue: string | undefined) => void
|
||||
) {
|
||||
return (
|
||||
<Form.Group controlId={name} data-field={name}>
|
||||
<Form.Label>
|
||||
<FormattedMessage id={name} />
|
||||
</Form.Label>
|
||||
<BulkUpdateTextInput
|
||||
value={value === null ? "" : value ?? undefined}
|
||||
valueChanged={(newValue) => setter(newValue)}
|
||||
unsetDisabled={props.selected.length < 2}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
function render() {
|
||||
// sfw class needs to be set because it is outside body
|
||||
|
||||
|
|
@ -235,13 +230,18 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
show
|
||||
icon={faPencilAlt}
|
||||
header={intl.formatMessage(
|
||||
{ id: "actions.edit_entity" },
|
||||
{ entityType: intl.formatMessage({ id: "performers" }) }
|
||||
{ id: "dialogs.edit_entity_count_title" },
|
||||
{
|
||||
count: props?.selected?.length ?? 1,
|
||||
singularEntity: intl.formatMessage({ id: "performer" }),
|
||||
pluralEntity: intl.formatMessage({ id: "performers" }),
|
||||
}
|
||||
)}
|
||||
accept={{
|
||||
onClick: onSave,
|
||||
text: intl.formatMessage({ id: "actions.apply" }),
|
||||
}}
|
||||
disabled={isUpdating || !!birthdateError || !!deathDateError}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(false),
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
|
|
@ -249,11 +249,8 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
}}
|
||||
isRunning={isUpdating}
|
||||
>
|
||||
<Form.Group controlId="rating" as={Row} data-field={name}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<Form>
|
||||
<BulkUpdateFormGroup name="rating">
|
||||
<RatingSystem
|
||||
value={updateInput.rating100}
|
||||
onSetRating={(value) =>
|
||||
|
|
@ -261,9 +258,8 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="favorite">
|
||||
<IndeterminateCheckbox
|
||||
setChecked={(checked) => setUpdateField({ favorite: checked })}
|
||||
|
|
@ -272,10 +268,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="gender" />
|
||||
</Form.Label>
|
||||
<BulkUpdateFormGroup name="gender">
|
||||
<Form.Control
|
||||
as="select"
|
||||
className="input-control"
|
||||
|
|
@ -292,51 +285,105 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
{renderTextField("disambiguation", updateInput.disambiguation, (v) =>
|
||||
setUpdateField({ disambiguation: v })
|
||||
)}
|
||||
{renderTextField("birthdate", updateInput.birthdate, (v) =>
|
||||
setUpdateField({ birthdate: v })
|
||||
)}
|
||||
{renderTextField("death_date", updateInput.death_date, (v) =>
|
||||
setUpdateField({ death_date: v })
|
||||
)}
|
||||
<BulkUpdateFormGroup name="disambiguation">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.disambiguation}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ disambiguation: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="country" />
|
||||
</Form.Label>
|
||||
<BulkUpdateFormGroup name="birthdate">
|
||||
<BulkUpdateDateInput
|
||||
value={updateInput.birthdate}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ birthdate: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
error={birthdateError}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="death_date">
|
||||
<BulkUpdateDateInput
|
||||
value={updateInput.death_date}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ death_date: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
error={deathDateError}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="country">
|
||||
<CountrySelect
|
||||
value={updateInput.country ?? ""}
|
||||
onChange={(v) => setUpdateField({ country: v })}
|
||||
showFlag
|
||||
/>
|
||||
</Form.Group>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
{renderTextField("ethnicity", updateInput.ethnicity, (v) =>
|
||||
setUpdateField({ ethnicity: v })
|
||||
)}
|
||||
{renderTextField("hair_color", updateInput.hair_color, (v) =>
|
||||
setUpdateField({ hair_color: v })
|
||||
)}
|
||||
{renderTextField("eye_color", updateInput.eye_color, (v) =>
|
||||
setUpdateField({ eye_color: v })
|
||||
)}
|
||||
{renderTextField("height", height, (v) => setHeight(v))}
|
||||
{renderTextField("weight", weight, (v) => setWeight(v))}
|
||||
{renderTextField("measurements", updateInput.measurements, (v) =>
|
||||
setUpdateField({ measurements: v })
|
||||
)}
|
||||
{renderTextField("penis_length", penis_length, (v) =>
|
||||
setPenisLength(v)
|
||||
)}
|
||||
<BulkUpdateFormGroup name="ethnicity">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.ethnicity}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ ethnicity: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="hair_color">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.hair_color}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ hair_color: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="eye_color">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.eye_color}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ eye_color: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="height">
|
||||
<BulkUpdateTextInput
|
||||
value={height}
|
||||
valueChanged={(newValue) => setHeight(newValue)}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="weight">
|
||||
<BulkUpdateTextInput
|
||||
value={weight}
|
||||
valueChanged={(newValue) => setWeight(newValue)}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="measurements">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.measurements}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ measurements: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="penis_length">
|
||||
<BulkUpdateTextInput
|
||||
value={penis_length}
|
||||
valueChanged={(newValue) => setPenisLength(newValue)}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group data-field="circumcised">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="circumcised" />
|
||||
</Form.Label>
|
||||
<BulkUpdateFormGroup name="circumcised">
|
||||
<Form.Control
|
||||
as="select"
|
||||
className="input-control"
|
||||
|
|
@ -353,43 +400,68 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
{renderTextField("fake_tits", updateInput.fake_tits, (v) =>
|
||||
setUpdateField({ fake_tits: v })
|
||||
)}
|
||||
{renderTextField("tattoos", updateInput.tattoos, (v) =>
|
||||
setUpdateField({ tattoos: v })
|
||||
)}
|
||||
{renderTextField("piercings", updateInput.piercings, (v) =>
|
||||
setUpdateField({ piercings: v })
|
||||
)}
|
||||
{renderTextField(
|
||||
"career_start",
|
||||
updateInput.career_start?.toString(),
|
||||
(v) => setUpdateField({ career_start: v ? parseInt(v) : undefined })
|
||||
)}
|
||||
{renderTextField(
|
||||
"career_end",
|
||||
updateInput.career_end?.toString(),
|
||||
(v) => setUpdateField({ career_end: v ? parseInt(v) : undefined })
|
||||
)}
|
||||
<BulkUpdateFormGroup name="fake_tits">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.fake_tits}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ fake_tits: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="tattoos">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.tattoos}
|
||||
valueChanged={(newValue) => setUpdateField({ tattoos: newValue })}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="piercings">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.piercings}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ piercings: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="career_start">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.career_start?.toString()}
|
||||
valueChanged={(v) =>
|
||||
setUpdateField({ career_start: v ? parseInt(v) : undefined })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="career_end">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.career_end?.toString()}
|
||||
valueChanged={(v) =>
|
||||
setUpdateField({ career_end: v ? parseInt(v) : undefined })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="tags" />
|
||||
</Form.Label>
|
||||
<BulkUpdateFormGroup name="tags" inline={false}>
|
||||
<MultiSet
|
||||
type="tags"
|
||||
type={"tags"}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => setTagIds({ ...tagIds, ids: itemIDs })}
|
||||
onSetMode={(newMode) => setTagIds({ ...tagIds, mode: newMode })}
|
||||
existingIds={existingTagIds ?? []}
|
||||
onUpdate={(itemIDs) => {
|
||||
setTagIds((c) => ({ ...c, ids: itemIDs }));
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
setTagIds((c) => ({ ...c, mode: newMode }));
|
||||
}}
|
||||
ids={tagIds.ids ?? []}
|
||||
existingIds={existingTagIds}
|
||||
mode={tagIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</Form.Group>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="ignore-auto-tags">
|
||||
<IndeterminateCheckbox
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useBulkSceneMarkerUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
|
|
@ -10,7 +10,7 @@ import {
|
|||
getAggregateState,
|
||||
getAggregateStateObject,
|
||||
} from "src/utils/bulkUpdate";
|
||||
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
||||
import { BulkUpdateFormGroup, BulkUpdateTextInput } from "../Shared/BulkUpdate";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { TagSelect } from "../Shared/Select";
|
||||
|
||||
|
|
@ -38,6 +38,8 @@ export const EditSceneMarkersDialog: React.FC<IListOperationProps> = (
|
|||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
|
||||
const unsetDisabled = props.selected.length < 2;
|
||||
|
||||
const [updateSceneMarkers] = useBulkSceneMarkerUpdate();
|
||||
|
||||
// Network state
|
||||
|
|
@ -115,27 +117,6 @@ export const EditSceneMarkersDialog: React.FC<IListOperationProps> = (
|
|||
setIsUpdating(false);
|
||||
}
|
||||
|
||||
function renderTextField(
|
||||
name: string,
|
||||
value: string | undefined | null,
|
||||
setter: (newValue: string | undefined) => void,
|
||||
area: boolean = false
|
||||
) {
|
||||
return (
|
||||
<Form.Group controlId={name}>
|
||||
<Form.Label>
|
||||
<FormattedMessage id={name} />
|
||||
</Form.Label>
|
||||
<BulkUpdateTextInput
|
||||
value={value === null ? "" : value ?? undefined}
|
||||
valueChanged={(newValue) => setter(newValue)}
|
||||
unsetDisabled={props.selected.length < 2}
|
||||
as={area ? "textarea" : undefined}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
function render() {
|
||||
return (
|
||||
<ModalComponent
|
||||
|
|
@ -143,8 +124,12 @@ export const EditSceneMarkersDialog: React.FC<IListOperationProps> = (
|
|||
show
|
||||
icon={faPencilAlt}
|
||||
header={intl.formatMessage(
|
||||
{ id: "actions.edit_entity" },
|
||||
{ entityType: intl.formatMessage({ id: "markers" }) }
|
||||
{ id: "dialogs.edit_entity_count_title" },
|
||||
{
|
||||
count: props?.selected?.length ?? 1,
|
||||
singularEntity: intl.formatMessage({ id: "marker" }),
|
||||
pluralEntity: intl.formatMessage({ id: "markers" }),
|
||||
}
|
||||
)}
|
||||
accept={{
|
||||
onClick: onSave,
|
||||
|
|
@ -158,39 +143,39 @@ export const EditSceneMarkersDialog: React.FC<IListOperationProps> = (
|
|||
isRunning={isUpdating}
|
||||
>
|
||||
<Form>
|
||||
{renderTextField("title", updateInput.title, (newValue) =>
|
||||
setUpdateField({ title: newValue })
|
||||
)}
|
||||
<BulkUpdateFormGroup name="title">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.title}
|
||||
valueChanged={(newValue) => setUpdateField({ title: newValue })}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="primary-tag">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="primary_tag" />
|
||||
</Form.Label>
|
||||
<BulkUpdateFormGroup name="primary-tag" messageId="primary_tag">
|
||||
<TagSelect
|
||||
onSelect={(t) => setUpdateField({ primary_tag_id: t[0]?.id })}
|
||||
ids={
|
||||
updateInput.primary_tag_id ? [updateInput.primary_tag_id] : []
|
||||
}
|
||||
/>
|
||||
</Form.Group>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="tags" />
|
||||
</Form.Label>
|
||||
<BulkUpdateFormGroup name="tags" inline={false}>
|
||||
<MultiSet
|
||||
type="tags"
|
||||
type={"tags"}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => setTagIds((v) => ({ ...v, ids: itemIDs }))}
|
||||
onSetMode={(newMode) =>
|
||||
setTagIds((v) => ({ ...v, mode: newMode }))
|
||||
}
|
||||
existingIds={aggregateState.tagIds ?? []}
|
||||
onUpdate={(itemIDs) => {
|
||||
setTagIds((c) => ({ ...c, ids: itemIDs }));
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
setTagIds((c) => ({ ...c, mode: newMode }));
|
||||
}}
|
||||
ids={tagIds.ids ?? []}
|
||||
existingIds={aggregateState.tagIds ?? []}
|
||||
mode={tagIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</Form.Group>
|
||||
</BulkUpdateFormGroup>
|
||||
</Form>
|
||||
</ModalComponent>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,93 +1,121 @@
|
|||
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 React, { useEffect, useMemo, useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useBulkSceneUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StudioSelect } from "../Shared/Select";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { MultiSet } from "../Shared/MultiSet";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import * as FormUtils from "src/utils/form";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateInputIDs,
|
||||
getAggregateInputValue,
|
||||
getAggregateGroupIds,
|
||||
getAggregatePerformerIds,
|
||||
getAggregateRating,
|
||||
getAggregateStudioId,
|
||||
getAggregateStateObject,
|
||||
getAggregateTagIds,
|
||||
getAggregateStudioId,
|
||||
} from "src/utils/bulkUpdate";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||
import { BulkUpdateFormGroup, BulkUpdateTextInput } from "../Shared/BulkUpdate";
|
||||
import { BulkUpdateDateInput } from "../Shared/DateInput";
|
||||
import { getDateError } from "src/utils/yup";
|
||||
|
||||
interface IListOperationProps {
|
||||
selected: GQL.SlimSceneDataFragment[];
|
||||
onClose: (applied: boolean) => void;
|
||||
}
|
||||
|
||||
const sceneFields = [
|
||||
"code",
|
||||
"rating100",
|
||||
"details",
|
||||
"organized",
|
||||
"director",
|
||||
"date",
|
||||
];
|
||||
|
||||
export const EditScenesDialog: 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 [groupMode, setGroupMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||
GQL.BulkUpdateIdMode.Add
|
||||
);
|
||||
const [groupIds, setGroupIds] = useState<string[]>();
|
||||
const [existingGroupIds, setExistingGroupIds] = useState<string[]>();
|
||||
const [organized, setOrganized] = useState<boolean | undefined>();
|
||||
|
||||
const [updateScenes] = useBulkSceneUpdate(getSceneInput());
|
||||
const [updateInput, setUpdateInput] = useState<GQL.BulkSceneUpdateInput>({
|
||||
ids: props.selected.map((scene) => {
|
||||
return scene.id;
|
||||
}),
|
||||
});
|
||||
|
||||
const [dateError, setDateError] = useState<string | undefined>();
|
||||
|
||||
const [performerIds, setPerformerIds] = useState<GQL.BulkUpdateIds>({
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
const [tagIds, setTagIds] = useState<GQL.BulkUpdateIds>({
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
const [groupIds, setGroupIds] = useState<GQL.BulkUpdateIds>({
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
|
||||
const unsetDisabled = props.selected.length < 2;
|
||||
|
||||
const [updateScenes] = useBulkSceneUpdate();
|
||||
|
||||
// Network state
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
const checkboxRef = React.createRef<HTMLInputElement>();
|
||||
const aggregateState = useMemo(() => {
|
||||
const updateState: Partial<GQL.BulkSceneUpdateInput> = {};
|
||||
const state = props.selected;
|
||||
updateState.studio_id = getAggregateStudioId(props.selected);
|
||||
const updateTagIds = getAggregateTagIds(props.selected);
|
||||
const updatePerformerIds = getAggregatePerformerIds(props.selected);
|
||||
const updateGroupIds = getAggregateGroupIds(props.selected);
|
||||
let first = true;
|
||||
|
||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
||||
getAggregateStateObject(updateState, scene, sceneFields, first);
|
||||
first = false;
|
||||
});
|
||||
|
||||
return {
|
||||
state: updateState,
|
||||
tagIds: updateTagIds,
|
||||
performerIds: updatePerformerIds,
|
||||
groupIds: updateGroupIds,
|
||||
};
|
||||
}, [props.selected]);
|
||||
|
||||
// update initial state from aggregate
|
||||
useEffect(() => {
|
||||
setUpdateInput((current) => ({ ...current, ...aggregateState.state }));
|
||||
}, [aggregateState]);
|
||||
|
||||
useEffect(() => {
|
||||
setDateError(getDateError(updateInput.date ?? "", intl));
|
||||
}, [updateInput.date, intl]);
|
||||
|
||||
function setUpdateField(input: Partial<GQL.BulkSceneUpdateInput>) {
|
||||
setUpdateInput((current) => ({ ...current, ...input }));
|
||||
}
|
||||
|
||||
function getSceneInput(): GQL.BulkSceneUpdateInput {
|
||||
// need to determine what we are actually setting on each scene
|
||||
const aggregateRating = getAggregateRating(props.selected);
|
||||
const aggregateStudioId = getAggregateStudioId(props.selected);
|
||||
const aggregatePerformerIds = getAggregatePerformerIds(props.selected);
|
||||
const aggregateTagIds = getAggregateTagIds(props.selected);
|
||||
const aggregateGroupIds = getAggregateGroupIds(props.selected);
|
||||
|
||||
const sceneInput: GQL.BulkSceneUpdateInput = {
|
||||
ids: props.selected.map((scene) => {
|
||||
return scene.id;
|
||||
}),
|
||||
...updateInput,
|
||||
tag_ids: tagIds,
|
||||
performer_ids: performerIds,
|
||||
group_ids: groupIds,
|
||||
};
|
||||
|
||||
sceneInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
|
||||
sceneInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
||||
|
||||
sceneInput.performer_ids = getAggregateInputIDs(
|
||||
performerMode,
|
||||
performerIds,
|
||||
aggregatePerformerIds
|
||||
// we don't have unset functionality for the rating star control
|
||||
// so need to determine if we are setting a rating or not
|
||||
sceneInput.rating100 = getAggregateInputValue(
|
||||
updateInput.rating100,
|
||||
aggregateState.state.rating100
|
||||
);
|
||||
sceneInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
|
||||
sceneInput.group_ids = getAggregateInputIDs(
|
||||
groupMode,
|
||||
groupIds,
|
||||
aggregateGroupIds
|
||||
);
|
||||
|
||||
if (organized !== undefined) {
|
||||
sceneInput.organized = organized;
|
||||
}
|
||||
|
||||
return sceneInput;
|
||||
}
|
||||
|
|
@ -95,7 +123,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||
async function onSave() {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
await updateScenes();
|
||||
await updateScenes({ variables: { input: getSceneInput() } });
|
||||
Toast.success(
|
||||
intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
|
|
@ -109,145 +137,13 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||
setIsUpdating(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const state = props.selected;
|
||||
let updateRating: number | undefined;
|
||||
let updateStudioID: string | undefined;
|
||||
let updatePerformerIds: string[] = [];
|
||||
let updateTagIds: string[] = [];
|
||||
let updateGroupIds: string[] = [];
|
||||
let updateOrganized: boolean | undefined;
|
||||
let first = true;
|
||||
|
||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
||||
const sceneRating = scene.rating100;
|
||||
const sceneStudioID = scene?.studio?.id;
|
||||
const scenePerformerIDs = (scene.performers ?? [])
|
||||
.map((p) => p.id)
|
||||
.sort();
|
||||
const sceneTagIDs = (scene.tags ?? []).map((p) => p.id).sort();
|
||||
const sceneGroupIDs = (scene.groups ?? []).map((m) => m.group.id).sort();
|
||||
|
||||
if (first) {
|
||||
updateRating = sceneRating ?? undefined;
|
||||
updateStudioID = sceneStudioID;
|
||||
updatePerformerIds = scenePerformerIDs;
|
||||
updateTagIds = sceneTagIDs;
|
||||
updateGroupIds = sceneGroupIDs;
|
||||
first = false;
|
||||
updateOrganized = scene.organized;
|
||||
} else {
|
||||
if (sceneRating !== updateRating) {
|
||||
updateRating = undefined;
|
||||
}
|
||||
if (sceneStudioID !== updateStudioID) {
|
||||
updateStudioID = undefined;
|
||||
}
|
||||
if (!isEqual(scenePerformerIDs, updatePerformerIds)) {
|
||||
updatePerformerIds = [];
|
||||
}
|
||||
if (!isEqual(sceneTagIDs, updateTagIds)) {
|
||||
updateTagIds = [];
|
||||
}
|
||||
if (!isEqual(sceneGroupIDs, updateGroupIds)) {
|
||||
updateGroupIds = [];
|
||||
}
|
||||
if (scene.organized !== updateOrganized) {
|
||||
updateOrganized = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setRating(updateRating);
|
||||
setStudioId(updateStudioID);
|
||||
setExistingPerformerIds(updatePerformerIds);
|
||||
setExistingTagIds(updateTagIds);
|
||||
setExistingGroupIds(updateGroupIds);
|
||||
setOrganized(updateOrganized);
|
||||
}, [props.selected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (checkboxRef.current) {
|
||||
checkboxRef.current.indeterminate = organized === undefined;
|
||||
}
|
||||
}, [organized, checkboxRef]);
|
||||
|
||||
function renderMultiSelect(
|
||||
type: "performers" | "tags" | "groups",
|
||||
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;
|
||||
case "groups":
|
||||
mode = groupMode;
|
||||
existingIds = existingGroupIds;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<MultiSet
|
||||
type={type}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => {
|
||||
switch (type) {
|
||||
case "performers":
|
||||
setPerformerIds(itemIDs);
|
||||
break;
|
||||
case "tags":
|
||||
setTagIds(itemIDs);
|
||||
break;
|
||||
case "groups":
|
||||
setGroupIds(itemIDs);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
switch (type) {
|
||||
case "performers":
|
||||
setPerformerMode(newMode);
|
||||
break;
|
||||
case "tags":
|
||||
setTagMode(newMode);
|
||||
break;
|
||||
case "groups":
|
||||
setGroupMode(newMode);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
ids={ids ?? []}
|
||||
existingIds={existingIds ?? []}
|
||||
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" },
|
||||
{ id: "dialogs.edit_entity_count_title" },
|
||||
{
|
||||
count: props?.selected?.length ?? 1,
|
||||
singularEntity: intl.formatMessage({ id: "scene" }),
|
||||
|
|
@ -258,6 +154,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||
onClick: onSave,
|
||||
text: intl.formatMessage({ id: "actions.apply" }),
|
||||
}}
|
||||
disabled={isUpdating || !!dateError}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(false),
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
|
|
@ -266,62 +163,121 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||
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>
|
||||
<BulkUpdateFormGroup name="rating">
|
||||
<RatingSystem
|
||||
value={updateInput.rating100}
|
||||
onSetRating={(value) =>
|
||||
setUpdateField({ rating100: value ?? undefined })
|
||||
}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="performers">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="performers" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("performers", performerIds)}
|
||||
</Form.Group>
|
||||
<BulkUpdateFormGroup name="scene_code">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.code}
|
||||
valueChanged={(newValue) => setUpdateField({ code: newValue })}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="tags" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("tags", tagIds)}
|
||||
</Form.Group>
|
||||
<BulkUpdateFormGroup name="date">
|
||||
<BulkUpdateDateInput
|
||||
value={updateInput.date}
|
||||
valueChanged={(newValue) => setUpdateField({ date: newValue })}
|
||||
unsetDisabled={unsetDisabled}
|
||||
error={dateError}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="groups">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="groups" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("groups", groupIds)}
|
||||
</Form.Group>
|
||||
<BulkUpdateFormGroup name="director">
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.director}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ director: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="studio">
|
||||
<StudioSelect
|
||||
onSelect={(items) =>
|
||||
setUpdateField({
|
||||
studio_id: items.length > 0 ? items[0]?.id : undefined,
|
||||
})
|
||||
}
|
||||
ids={updateInput.studio_id ? [updateInput.studio_id] : []}
|
||||
isDisabled={isUpdating}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="performers" inline={false}>
|
||||
<MultiSet
|
||||
type={"performers"}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => {
|
||||
setPerformerIds((c) => ({ ...c, ids: itemIDs }));
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
setPerformerIds((c) => ({ ...c, mode: newMode }));
|
||||
}}
|
||||
ids={performerIds.ids ?? []}
|
||||
existingIds={aggregateState.performerIds}
|
||||
mode={performerIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="groups" inline={false}>
|
||||
<MultiSet
|
||||
type={"groups"}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => {
|
||||
setGroupIds((c) => ({ ...c, ids: itemIDs }));
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
setGroupIds((c) => ({ ...c, mode: newMode }));
|
||||
}}
|
||||
ids={groupIds.ids ?? []}
|
||||
existingIds={aggregateState.groupIds}
|
||||
mode={groupIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="tags" inline={false}>
|
||||
<MultiSet
|
||||
type={"tags"}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => {
|
||||
setTagIds((c) => ({ ...c, ids: itemIDs }));
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
setTagIds((c) => ({ ...c, mode: newMode }));
|
||||
}}
|
||||
ids={tagIds.ids ?? []}
|
||||
existingIds={aggregateState.tagIds}
|
||||
mode={tagIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<BulkUpdateFormGroup name="details" inline={false}>
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.details}
|
||||
valueChanged={(newValue) => setUpdateField({ details: newValue })}
|
||||
unsetDisabled={unsetDisabled}
|
||||
as="textarea"
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="organized">
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
<IndeterminateCheckbox
|
||||
label={intl.formatMessage({ id: "organized" })}
|
||||
checked={organized}
|
||||
ref={checkboxRef}
|
||||
onChange={() => cycleOrganized()}
|
||||
setChecked={(checked) => setUpdateField({ organized: checked })}
|
||||
checked={updateInput.organized ?? undefined}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
|
|
|
|||
89
ui/v2.5/src/components/Shared/BulkUpdate.tsx
Normal file
89
ui/v2.5/src/components/Shared/BulkUpdate.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
||||
import React from "react";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
FormControlProps,
|
||||
InputGroup,
|
||||
Row,
|
||||
} from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Icon } from "./Icon";
|
||||
import * as FormUtils from "src/utils/form";
|
||||
|
||||
interface IBulkUpdateTextInputProps extends Omit<FormControlProps, "value"> {
|
||||
valueChanged: (value: string | null | undefined) => void;
|
||||
value: string | null | undefined;
|
||||
unsetDisabled?: boolean;
|
||||
as?: React.ElementType;
|
||||
}
|
||||
|
||||
export const BulkUpdateTextInput: React.FC<IBulkUpdateTextInputProps> = ({
|
||||
valueChanged,
|
||||
unsetDisabled,
|
||||
...props
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const value = props.value === null ? "" : props.value ?? undefined;
|
||||
const unset = value === undefined;
|
||||
|
||||
const placeholderValue = unset
|
||||
? `<${intl.formatMessage({ id: "existing_value" })}>`
|
||||
: value === ""
|
||||
? `<${intl.formatMessage({ id: "empty_value" })}>`
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<InputGroup className="bulk-update-text-input">
|
||||
<Form.Control
|
||||
{...props}
|
||||
className="text-input"
|
||||
type="text"
|
||||
as={props.as}
|
||||
value={value ?? ""}
|
||||
placeholder={placeholderValue}
|
||||
onChange={(event) => valueChanged(event.currentTarget.value)}
|
||||
/>
|
||||
<InputGroup.Append>
|
||||
{!unsetDisabled ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => valueChanged(undefined)}
|
||||
title={intl.formatMessage({ id: "actions.unset" })}
|
||||
disabled={unset}
|
||||
>
|
||||
<Icon icon={faBan} />
|
||||
</Button>
|
||||
) : undefined}
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const BulkUpdateFormGroup: React.FC<{
|
||||
name: string;
|
||||
messageId?: string;
|
||||
inline?: boolean;
|
||||
}> = ({ name, messageId = name, inline = true, children }) => {
|
||||
if (inline) {
|
||||
return (
|
||||
<Form.Group controlId={name} data-field={name} as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: <FormattedMessage id={messageId} />,
|
||||
})}
|
||||
<Col xs={9}>{children}</Col>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Group controlId={name} data-field={name}>
|
||||
<Form.Label>
|
||||
<FormattedMessage id={messageId} />
|
||||
</Form.Label>
|
||||
{children}
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
||||
import React from "react";
|
||||
import { Button, Form, FormControlProps, InputGroup } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
interface IBulkUpdateTextInputProps extends FormControlProps {
|
||||
valueChanged: (value: string | undefined) => void;
|
||||
unsetDisabled?: boolean;
|
||||
as?: React.ElementType;
|
||||
}
|
||||
|
||||
export const BulkUpdateTextInput: React.FC<IBulkUpdateTextInputProps> = ({
|
||||
valueChanged,
|
||||
unsetDisabled,
|
||||
...props
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const unsetClassName = props.value === undefined ? "unset" : "";
|
||||
|
||||
return (
|
||||
<InputGroup className={`bulk-update-text-input ${unsetClassName}`}>
|
||||
<Form.Control
|
||||
{...props}
|
||||
className="input-control"
|
||||
type="text"
|
||||
as={props.as}
|
||||
value={props.value ?? ""}
|
||||
placeholder={
|
||||
props.value === undefined
|
||||
? `<${intl.formatMessage({ id: "existing_value" })}>`
|
||||
: undefined
|
||||
}
|
||||
onChange={(event) => valueChanged(event.currentTarget.value)}
|
||||
/>
|
||||
{!unsetDisabled ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => valueChanged(undefined)}
|
||||
title={intl.formatMessage({ id: "actions.unset" })}
|
||||
>
|
||||
<Icon icon={faBan} />
|
||||
</Button>
|
||||
) : undefined}
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
|
@ -8,14 +8,20 @@ import { Icon } from "./Icon";
|
|||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import { useIntl } from "react-intl";
|
||||
import { PatchComponent } from "src/patch";
|
||||
import { faBan, faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
interface IProps {
|
||||
groupClassName?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
value: string;
|
||||
isTime?: boolean;
|
||||
onValueChange(value: string): void;
|
||||
placeholder?: string;
|
||||
placeholderOverride?: string;
|
||||
error?: string;
|
||||
appendBefore?: React.ReactNode;
|
||||
appendAfter?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ShowPickerButton = forwardRef<
|
||||
|
|
@ -32,6 +38,11 @@ const ShowPickerButton = forwardRef<
|
|||
const _DateInput: React.FC<IProps> = (props: IProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const {
|
||||
groupClassName = "date-input-group",
|
||||
className = "date-input text-input",
|
||||
} = props;
|
||||
|
||||
const date = useMemo(() => {
|
||||
const toDate = props.isTime
|
||||
? TextUtils.stringToFuzzyDateTime
|
||||
|
|
@ -70,34 +81,108 @@ const _DateInput: React.FC<IProps> = (props: IProps) => {
|
|||
}
|
||||
}
|
||||
|
||||
const placeholderText = intl.formatMessage({
|
||||
const formatHint = intl.formatMessage({
|
||||
id: props.isTime ? "datetime_format" : "date_format",
|
||||
});
|
||||
|
||||
const placeholderText = props.placeholder
|
||||
? `${props.placeholder} (${formatHint})`
|
||||
: formatHint;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InputGroup hasValidation>
|
||||
<Form.Control
|
||||
className="date-input text-input"
|
||||
disabled={props.disabled}
|
||||
value={props.value}
|
||||
onChange={(e) => props.onValueChange(e.currentTarget.value)}
|
||||
placeholder={
|
||||
!props.disabled
|
||||
? props.placeholder
|
||||
? `${props.placeholder} (${placeholderText})`
|
||||
: placeholderText
|
||||
: undefined
|
||||
}
|
||||
isInvalid={!!props.error}
|
||||
/>
|
||||
<InputGroup.Append>{maybeRenderButton()}</InputGroup.Append>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{props.error}
|
||||
</Form.Control.Feedback>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<InputGroup hasValidation className={groupClassName}>
|
||||
<Form.Control
|
||||
className={className}
|
||||
disabled={props.disabled}
|
||||
value={props.value}
|
||||
onChange={(e) => props.onValueChange(e.currentTarget.value)}
|
||||
placeholder={
|
||||
!props.disabled
|
||||
? props.placeholderOverride ?? placeholderText
|
||||
: undefined
|
||||
}
|
||||
isInvalid={!!props.error}
|
||||
/>
|
||||
<InputGroup.Append>
|
||||
{props.appendBefore}
|
||||
{maybeRenderButton()}
|
||||
{props.appendAfter}
|
||||
</InputGroup.Append>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{props.error}
|
||||
</Form.Control.Feedback>
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const DateInput = PatchComponent("DateInput", _DateInput);
|
||||
|
||||
interface IBulkUpdateDateInputProps
|
||||
extends Omit<IProps, "onValueChange" | "value"> {
|
||||
value: string | null | undefined;
|
||||
valueChanged: (value: string | null | undefined) => void;
|
||||
unsetDisabled?: boolean;
|
||||
as?: React.ElementType;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const BulkUpdateDateInput: React.FC<IBulkUpdateDateInputProps> = ({
|
||||
valueChanged,
|
||||
unsetDisabled,
|
||||
...props
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const unset = props.value === undefined;
|
||||
|
||||
const unsetButton = !unsetDisabled ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => valueChanged(undefined)}
|
||||
title={intl.formatMessage({ id: "actions.unset" })}
|
||||
disabled={unset}
|
||||
>
|
||||
<Icon icon={faBan} />
|
||||
</Button>
|
||||
) : undefined;
|
||||
|
||||
const clearButton =
|
||||
props.value !== null ? (
|
||||
<Button
|
||||
className="minimal"
|
||||
variant="secondary"
|
||||
onClick={() => valueChanged(null)}
|
||||
title={intl.formatMessage({ id: "actions.clear" })}
|
||||
>
|
||||
<Icon icon={faTimes} />
|
||||
</Button>
|
||||
) : undefined;
|
||||
|
||||
const placeholderValue =
|
||||
props.value === null
|
||||
? `<${intl.formatMessage({ id: "empty_value" })}>`
|
||||
: props.value === undefined
|
||||
? `<${intl.formatMessage({ id: "existing_value" })}>`
|
||||
: undefined;
|
||||
|
||||
function outValue(v: string | undefined) {
|
||||
if (v === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
return (
|
||||
<DateInput
|
||||
{...props}
|
||||
value={props.value ?? ""}
|
||||
placeholderOverride={placeholderValue}
|
||||
onValueChange={(v) => valueChanged(outValue(v))}
|
||||
groupClassName="bulk-update-date-input"
|
||||
className="date-input text-input"
|
||||
appendBefore={clearButton}
|
||||
appendAfter={unsetButton}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ import { PerformerIDSelect } from "../Performers/PerformerSelect";
|
|||
import { StudioIDSelect } from "../Studios/StudioSelect";
|
||||
import { TagIDSelect } from "../Tags/TagSelect";
|
||||
import { GroupIDSelect } from "../Groups/GroupSelect";
|
||||
import { SceneIDSelect } from "../Scenes/SceneSelect";
|
||||
|
||||
interface IMultiSetProps {
|
||||
type: "performers" | "studios" | "tags" | "groups" | "galleries";
|
||||
type: "performers" | "studios" | "tags" | "groups" | "galleries" | "scenes";
|
||||
existingIds?: string[];
|
||||
ids?: string[];
|
||||
mode: GQL.BulkUpdateIdMode;
|
||||
|
|
@ -89,6 +90,17 @@ const Select: React.FC<IMultiSetProps> = (props) => {
|
|||
menuPortalTarget={props.menuPortalTarget}
|
||||
/>
|
||||
);
|
||||
case "scenes":
|
||||
return (
|
||||
<SceneIDSelect
|
||||
isDisabled={disabled}
|
||||
isMulti
|
||||
isClearable={false}
|
||||
onSelect={onUpdate}
|
||||
ids={props.ids ?? []}
|
||||
menuPortalTarget={props.menuPortalTarget}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<FilterSelect
|
||||
|
|
|
|||
|
|
@ -494,30 +494,10 @@ button.collapse-button {
|
|||
}
|
||||
}
|
||||
|
||||
.bulk-update-text-input {
|
||||
button {
|
||||
background-color: $secondary;
|
||||
color: $text-muted;
|
||||
font-size: $btn-font-size-sm;
|
||||
margin: $btn-padding-y $btn-padding-x;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 4;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active,
|
||||
&:not(:disabled):not(.disabled):active,
|
||||
&:not(:disabled):not(.disabled):active:focus {
|
||||
background-color: $secondary;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.unset button {
|
||||
visibility: hidden;
|
||||
.bulk-update-date-input {
|
||||
.react-datepicker-wrapper .btn {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1268,3 +1248,8 @@ input[type="range"].double-range-slider-max {
|
|||
margin-left: 0.25rem;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
// general styling for appended minimal button to input group
|
||||
.text-input + .input-group-append .btn.minimal {
|
||||
background-color: $textfield-bg;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Col, Form, Row } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useBulkStudioUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
|
|
@ -13,9 +13,8 @@ import {
|
|||
getAggregateStateObject,
|
||||
} from "src/utils/bulkUpdate";
|
||||
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
||||
import { BulkUpdateFormGroup, BulkUpdateTextInput } from "../Shared/BulkUpdate";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import * as FormUtils from "src/utils/form";
|
||||
import { StudioSelect } from "../Shared/Select";
|
||||
|
||||
interface IListOperationProps {
|
||||
|
|
@ -47,6 +46,8 @@ export const EditStudiosDialog: React.FC<IListOperationProps> = (
|
|||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
|
||||
const unsetDisabled = props.selected.length < 2;
|
||||
|
||||
const [updateStudios] = useBulkStudioUpdate();
|
||||
|
||||
// Network state
|
||||
|
|
@ -126,27 +127,6 @@ export const EditStudiosDialog: React.FC<IListOperationProps> = (
|
|||
setIsUpdating(false);
|
||||
}
|
||||
|
||||
function renderTextField(
|
||||
name: string,
|
||||
value: string | undefined | null,
|
||||
setter: (newValue: string | undefined) => void,
|
||||
area: boolean = false
|
||||
) {
|
||||
return (
|
||||
<Form.Group controlId={name}>
|
||||
<Form.Label>
|
||||
<FormattedMessage id={name} />
|
||||
</Form.Label>
|
||||
<BulkUpdateTextInput
|
||||
value={value === null ? "" : value ?? undefined}
|
||||
valueChanged={(newValue) => setter(newValue)}
|
||||
unsetDisabled={props.selected.length < 2}
|
||||
as={area ? "textarea" : undefined}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
function render() {
|
||||
return (
|
||||
<ModalComponent
|
||||
|
|
@ -154,8 +134,12 @@ export const EditStudiosDialog: React.FC<IListOperationProps> = (
|
|||
show
|
||||
icon={faPencilAlt}
|
||||
header={intl.formatMessage(
|
||||
{ id: "actions.edit_entity" },
|
||||
{ entityType: intl.formatMessage({ id: "studios" }) }
|
||||
{ id: "dialogs.edit_entity_count_title" },
|
||||
{
|
||||
count: props?.selected?.length ?? 1,
|
||||
singularEntity: intl.formatMessage({ id: "studio" }),
|
||||
pluralEntity: intl.formatMessage({ id: "studios" }),
|
||||
}
|
||||
)}
|
||||
accept={{
|
||||
onClick: onSave,
|
||||
|
|
@ -168,11 +152,8 @@ export const EditStudiosDialog: React.FC<IListOperationProps> = (
|
|||
}}
|
||||
isRunning={isUpdating}
|
||||
>
|
||||
<Form.Group controlId="parent-studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "parent_studio" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<Form>
|
||||
<BulkUpdateFormGroup name="parent-studio" messageId="parent_studio">
|
||||
<StudioSelect
|
||||
onSelect={(items) =>
|
||||
setUpdateField({
|
||||
|
|
@ -183,13 +164,8 @@ export const EditStudiosDialog: React.FC<IListOperationProps> = (
|
|||
isDisabled={isUpdating}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
</BulkUpdateFormGroup>
|
||||
<BulkUpdateFormGroup name="rating">
|
||||
<RatingSystem
|
||||
value={updateInput.rating100}
|
||||
onSetRating={(value) =>
|
||||
|
|
@ -197,9 +173,8 @@ export const EditStudiosDialog: React.FC<IListOperationProps> = (
|
|||
}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="favorite">
|
||||
<IndeterminateCheckbox
|
||||
setChecked={(checked) => setUpdateField({ favorite: checked })}
|
||||
|
|
@ -208,30 +183,31 @@ export const EditStudiosDialog: React.FC<IListOperationProps> = (
|
|||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="tags" />
|
||||
</Form.Label>
|
||||
<BulkUpdateFormGroup name="tags" inline={false}>
|
||||
<MultiSet
|
||||
type="tags"
|
||||
type={"tags"}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => setTagIds((v) => ({ ...v, ids: itemIDs }))}
|
||||
onSetMode={(newMode) =>
|
||||
setTagIds((v) => ({ ...v, mode: newMode }))
|
||||
}
|
||||
existingIds={aggregateState.tagIds ?? []}
|
||||
onUpdate={(itemIDs) => {
|
||||
setTagIds((c) => ({ ...c, ids: itemIDs }));
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
setTagIds((c) => ({ ...c, mode: newMode }));
|
||||
}}
|
||||
ids={tagIds.ids ?? []}
|
||||
existingIds={aggregateState.tagIds}
|
||||
mode={tagIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</Form.Group>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
{renderTextField(
|
||||
"details",
|
||||
updateInput.details,
|
||||
(newValue) => setUpdateField({ details: newValue }),
|
||||
true
|
||||
)}
|
||||
<BulkUpdateFormGroup name="details" inline={false}>
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.details}
|
||||
valueChanged={(newValue) => setUpdateField({ details: newValue })}
|
||||
unsetDisabled={unsetDisabled}
|
||||
as="textarea"
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Form.Group controlId="ignore-auto-tags">
|
||||
<IndeterminateCheckbox
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
getAggregateStateObject,
|
||||
} from "src/utils/bulkUpdate";
|
||||
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
||||
import { BulkUpdateFormGroup, BulkUpdateTextInput } from "../Shared/BulkUpdate";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
function Tags(props: {
|
||||
|
|
@ -85,6 +85,8 @@ export const EditTagsDialog: React.FC<IListOperationProps> = (
|
|||
|
||||
const [updateInput, setUpdateInput] = useState<GQL.BulkTagUpdateInput>({});
|
||||
|
||||
const unsetDisabled = props.selected.length < 2;
|
||||
|
||||
const [updateTags] = useBulkTagUpdate(getTagInput());
|
||||
|
||||
// Network state
|
||||
|
|
@ -153,33 +155,18 @@ export const EditTagsDialog: React.FC<IListOperationProps> = (
|
|||
setUpdateInput(updateState);
|
||||
}, [props.selected]);
|
||||
|
||||
function renderTextField(
|
||||
name: string,
|
||||
value: string | undefined | null,
|
||||
setter: (newValue: string | undefined) => void
|
||||
) {
|
||||
return (
|
||||
<Form.Group controlId={name}>
|
||||
<Form.Label>
|
||||
<FormattedMessage id={name} />
|
||||
</Form.Label>
|
||||
<BulkUpdateTextInput
|
||||
value={value === null ? "" : value ?? undefined}
|
||||
valueChanged={(newValue) => setter(newValue)}
|
||||
unsetDisabled={props.selected.length < 2}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalComponent
|
||||
dialogClassName="edit-tags-dialog"
|
||||
show
|
||||
icon={faPencilAlt}
|
||||
header={intl.formatMessage(
|
||||
{ id: "actions.edit_entity" },
|
||||
{ entityType: intl.formatMessage({ id: "tags" }) }
|
||||
{ id: "dialogs.edit_entity_count_title" },
|
||||
{
|
||||
count: props?.selected?.length ?? 1,
|
||||
singularEntity: intl.formatMessage({ id: "tag" }),
|
||||
pluralEntity: intl.formatMessage({ id: "tags" }),
|
||||
}
|
||||
)}
|
||||
accept={{
|
||||
onClick: onSave,
|
||||
|
|
@ -201,9 +188,16 @@ export const EditTagsDialog: React.FC<IListOperationProps> = (
|
|||
/>
|
||||
</Form.Group>
|
||||
|
||||
{renderTextField("description", updateInput.description, (v) =>
|
||||
setUpdateField({ description: v })
|
||||
)}
|
||||
<BulkUpdateFormGroup name="description" inline={false}>
|
||||
<BulkUpdateTextInput
|
||||
value={updateInput.description}
|
||||
valueChanged={(newValue) =>
|
||||
setUpdateField({ description: newValue })
|
||||
}
|
||||
unsetDisabled={unsetDisabled}
|
||||
as="textarea"
|
||||
/>
|
||||
</BulkUpdateFormGroup>
|
||||
|
||||
<Tags
|
||||
isUpdating={isUpdating}
|
||||
|
|
|
|||
|
|
@ -625,9 +625,8 @@ export const useSceneUpdate = () =>
|
|||
},
|
||||
});
|
||||
|
||||
export const useBulkSceneUpdate = (input: GQL.BulkSceneUpdateInput) =>
|
||||
export const useBulkSceneUpdate = () =>
|
||||
GQL.useBulkSceneUpdateMutation({
|
||||
variables: { input },
|
||||
update(cache, result) {
|
||||
if (!result.data?.bulkSceneUpdate) return;
|
||||
|
||||
|
|
@ -1403,9 +1402,8 @@ export const useGroupUpdate = () =>
|
|||
},
|
||||
});
|
||||
|
||||
export const useBulkGroupUpdate = (input: GQL.BulkGroupUpdateInput) =>
|
||||
export const useBulkGroupUpdate = () =>
|
||||
GQL.useBulkGroupUpdateMutation({
|
||||
variables: { input },
|
||||
update(cache, result) {
|
||||
if (!result.data?.bulkGroupUpdate) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -985,6 +985,7 @@
|
|||
"delete_object_title": "Delete {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||
"dont_show_until_updated": "Don't show until next update",
|
||||
"edit_entity_title": "Edit {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||
"edit_entity_count_title": "Edit {count} {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||
"export_include_related_objects": "Include related objects in export",
|
||||
"export_title": "Export",
|
||||
"imagewall": {
|
||||
|
|
@ -1147,6 +1148,7 @@
|
|||
"warmth": "Warmth"
|
||||
},
|
||||
"empty_server": "Add some scenes to your server to view recommendations on this page.",
|
||||
"empty_value": "empty",
|
||||
"errors": {
|
||||
"custom_fields": {
|
||||
"duplicate_field": "Field name must be unique",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ export function getAggregateTagIds(state: { tags: IHasID[] }[]) {
|
|||
return getAggregateIds(sortedLists);
|
||||
}
|
||||
|
||||
export function getAggregateSceneIds(state: { scenes: IHasID[] }[]) {
|
||||
const sortedLists = state.map((o) => o.scenes.map((oo) => oo.id).sort());
|
||||
return getAggregateIds(sortedLists);
|
||||
}
|
||||
|
||||
interface IGroup {
|
||||
group: IHasID;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ function getLabelProps(labelProps?: FormLabelProps) {
|
|||
}
|
||||
|
||||
export function renderLabel(options: {
|
||||
title: string;
|
||||
title: React.ReactNode;
|
||||
labelProps?: FormLabelProps;
|
||||
}) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -92,6 +92,37 @@ export function yupUniqueStringList(intl: IntlShape) {
|
|||
});
|
||||
}
|
||||
|
||||
export function validateDateString(value?: string) {
|
||||
if (!value) return true;
|
||||
// Allow YYYY, YYYY-MM, or YYYY-MM-DD formats
|
||||
if (!value.match(/^\d{4}(-\d{2}(-\d{2})?)?$/)) return false;
|
||||
// Validate the date components
|
||||
const parts = value.split("-");
|
||||
const year = parseInt(parts[0], 10);
|
||||
if (year < 1 || year > 9999) return false;
|
||||
if (parts.length >= 2) {
|
||||
const month = parseInt(parts[1], 10);
|
||||
if (month < 1 || month > 12) return false;
|
||||
}
|
||||
if (parts.length === 3) {
|
||||
const day = parseInt(parts[2], 10);
|
||||
if (day < 1 || day > 31) return false;
|
||||
// Full date - validate it parses correctly
|
||||
if (Number.isNaN(Date.parse(value))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getDateError(
|
||||
value: string | undefined | null,
|
||||
intl: IntlShape
|
||||
) {
|
||||
if (validateDateString(value ?? "")) return undefined;
|
||||
return intl
|
||||
.formatMessage({ id: "validation.date_invalid_form" })
|
||||
.replace("${path}", intl.formatMessage({ id: "date" }));
|
||||
}
|
||||
|
||||
export function yupDateString(intl: IntlShape) {
|
||||
return yup
|
||||
.string()
|
||||
|
|
@ -99,24 +130,7 @@ export function yupDateString(intl: IntlShape) {
|
|||
.test({
|
||||
name: "date",
|
||||
test(value) {
|
||||
if (!value) return true;
|
||||
// Allow YYYY, YYYY-MM, or YYYY-MM-DD formats
|
||||
if (!value.match(/^\d{4}(-\d{2}(-\d{2})?)?$/)) return false;
|
||||
// Validate the date components
|
||||
const parts = value.split("-");
|
||||
const year = parseInt(parts[0], 10);
|
||||
if (year < 1 || year > 9999) return false;
|
||||
if (parts.length >= 2) {
|
||||
const month = parseInt(parts[1], 10);
|
||||
if (month < 1 || month > 12) return false;
|
||||
}
|
||||
if (parts.length === 3) {
|
||||
const day = parseInt(parts[2], 10);
|
||||
if (day < 1 || day > 31) return false;
|
||||
// Full date - validate it parses correctly
|
||||
if (Number.isNaN(Date.parse(value))) return false;
|
||||
}
|
||||
return true;
|
||||
return validateDateString(value);
|
||||
},
|
||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue