mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Add edit studios dialog (#6238)
This commit is contained in:
@@ -10,6 +10,12 @@ mutation StudioUpdate($input: StudioUpdateInput!) {
|
||||
}
|
||||
}
|
||||
|
||||
mutation BulkStudioUpdate($input: BulkStudioUpdateInput!) {
|
||||
bulkStudioUpdate(input: $input) {
|
||||
...StudioData
|
||||
}
|
||||
}
|
||||
|
||||
mutation StudioDestroy($id: ID!) {
|
||||
studioDestroy(input: { id: $id })
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Icon } from "./Icon";
|
||||
interface IBulkUpdateTextInputProps extends FormControlProps {
|
||||
valueChanged: (value: string | undefined) => void;
|
||||
unsetDisabled?: boolean;
|
||||
as?: React.ElementType;
|
||||
}
|
||||
|
||||
export const BulkUpdateTextInput: React.FC<IBulkUpdateTextInputProps> = ({
|
||||
@@ -24,6 +25,7 @@ export const BulkUpdateTextInput: React.FC<IBulkUpdateTextInputProps> = ({
|
||||
{...props}
|
||||
className="input-control"
|
||||
type="text"
|
||||
as={props.as}
|
||||
value={props.value ?? ""}
|
||||
placeholder={
|
||||
props.value === undefined
|
||||
|
||||
245
ui/v2.5/src/components/Studios/EditStudiosDialog.tsx
Normal file
245
ui/v2.5/src/components/Studios/EditStudiosDialog.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Col, Form, Row } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useBulkStudioUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { MultiSet } from "../Shared/MultiSet";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateInputValue,
|
||||
getAggregateState,
|
||||
getAggregateStateObject,
|
||||
} from "src/utils/bulkUpdate";
|
||||
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import * as FormUtils from "src/utils/form";
|
||||
import { StudioSelect } from "../Shared/Select";
|
||||
|
||||
interface IListOperationProps {
|
||||
selected: GQL.SlimStudioDataFragment[];
|
||||
onClose: (applied: boolean) => void;
|
||||
}
|
||||
|
||||
const studioFields = ["favorite", "rating100", "details", "ignore_auto_tag"];
|
||||
|
||||
export const EditStudiosDialog: React.FC<IListOperationProps> = (
|
||||
props: IListOperationProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const [updateInput, setUpdateInput] = useState<GQL.BulkStudioUpdateInput>({
|
||||
ids: props.selected.map((studio) => {
|
||||
return studio.id;
|
||||
}),
|
||||
});
|
||||
|
||||
const [tagIds, setTagIds] = useState<GQL.BulkUpdateIds>({
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
|
||||
const [updateStudios] = useBulkStudioUpdate();
|
||||
|
||||
// Network state
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
const aggregateState = useMemo(() => {
|
||||
const updateState: Partial<GQL.BulkStudioUpdateInput> = {};
|
||||
const state = props.selected;
|
||||
let updateTagIds: string[] = [];
|
||||
let first = true;
|
||||
|
||||
state.forEach((studio: GQL.SlimStudioDataFragment) => {
|
||||
getAggregateStateObject(updateState, studio, studioFields, first);
|
||||
|
||||
// studio data fragment doesn't have parent_id, so handle separately
|
||||
updateState.parent_id = getAggregateState(
|
||||
updateState.parent_id,
|
||||
studio.parent_studio?.id,
|
||||
first
|
||||
);
|
||||
|
||||
const studioTagIDs = (studio.tags ?? []).map((p) => p.id).sort();
|
||||
|
||||
updateTagIds = getAggregateState(updateTagIds, studioTagIDs, first) ?? [];
|
||||
|
||||
first = false;
|
||||
});
|
||||
|
||||
return { state: updateState, tagIds: updateTagIds };
|
||||
}, [props.selected]);
|
||||
|
||||
// update initial state from aggregate
|
||||
useEffect(() => {
|
||||
setUpdateInput((current) => ({ ...current, ...aggregateState.state }));
|
||||
}, [aggregateState]);
|
||||
|
||||
function setUpdateField(input: Partial<GQL.BulkStudioUpdateInput>) {
|
||||
setUpdateInput((current) => ({ ...current, ...input }));
|
||||
}
|
||||
|
||||
function getStudioInput(): GQL.BulkStudioUpdateInput {
|
||||
const studioInput: GQL.BulkStudioUpdateInput = {
|
||||
...updateInput,
|
||||
tag_ids: tagIds,
|
||||
};
|
||||
|
||||
// we don't have unset functionality for the rating star control
|
||||
// so need to determine if we are setting a rating or not
|
||||
studioInput.rating100 = getAggregateInputValue(
|
||||
updateInput.rating100,
|
||||
aggregateState.state.rating100
|
||||
);
|
||||
|
||||
return studioInput;
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
await updateStudios({
|
||||
variables: {
|
||||
input: getStudioInput(),
|
||||
},
|
||||
});
|
||||
Toast.success(
|
||||
intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{
|
||||
entity: intl.formatMessage({ id: "studios" }).toLocaleLowerCase(),
|
||||
}
|
||||
)
|
||||
);
|
||||
props.onClose(true);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
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
|
||||
dialogClassName="edit-studios-dialog"
|
||||
show
|
||||
icon={faPencilAlt}
|
||||
header={intl.formatMessage(
|
||||
{ id: "actions.edit_entity" },
|
||||
{ entityType: intl.formatMessage({ id: "studios" }) }
|
||||
)}
|
||||
accept={{
|
||||
onClick: onSave,
|
||||
text: intl.formatMessage({ id: "actions.apply" }),
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(false),
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
isRunning={isUpdating}
|
||||
>
|
||||
<Form.Group controlId="parent-studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "parent_studio" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<StudioSelect
|
||||
onSelect={(items) =>
|
||||
setUpdateField({
|
||||
parent_id: items.length > 0 ? items[0]?.id : undefined,
|
||||
})
|
||||
}
|
||||
ids={updateInput.parent_id ? [updateInput.parent_id] : []}
|
||||
isDisabled={isUpdating}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingSystem
|
||||
value={updateInput.rating100}
|
||||
onSetRating={(value) =>
|
||||
setUpdateField({ rating100: value ?? undefined })
|
||||
}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form>
|
||||
<Form.Group controlId="favorite">
|
||||
<IndeterminateCheckbox
|
||||
setChecked={(checked) => setUpdateField({ favorite: checked })}
|
||||
checked={updateInput.favorite ?? undefined}
|
||||
label={intl.formatMessage({ id: "favourite" })}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="tags" />
|
||||
</Form.Label>
|
||||
<MultiSet
|
||||
type="tags"
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => setTagIds((v) => ({ ...v, ids: itemIDs }))}
|
||||
onSetMode={(newMode) =>
|
||||
setTagIds((v) => ({ ...v, mode: newMode }))
|
||||
}
|
||||
existingIds={aggregateState.tagIds ?? []}
|
||||
ids={tagIds.ids ?? []}
|
||||
mode={tagIds.mode}
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
{renderTextField(
|
||||
"details",
|
||||
updateInput.details,
|
||||
(newValue) => setUpdateField({ details: newValue }),
|
||||
true
|
||||
)}
|
||||
|
||||
<Form.Group controlId="ignore-auto-tags">
|
||||
<IndeterminateCheckbox
|
||||
label={intl.formatMessage({ id: "ignore_auto_tag" })}
|
||||
setChecked={(checked) =>
|
||||
setUpdateField({ ignore_auto_tag: checked })
|
||||
}
|
||||
checked={updateInput.ignore_auto_tag ?? undefined}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</ModalComponent>
|
||||
);
|
||||
}
|
||||
|
||||
return render();
|
||||
};
|
||||
@@ -17,6 +17,7 @@ import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog";
|
||||
import { StudioTagger } from "../Tagger/studios/StudioTagger";
|
||||
import { StudioCardGrid } from "./StudioCardGrid";
|
||||
import { View } from "../List/views";
|
||||
import { EditStudiosDialog } from "./EditStudiosDialog";
|
||||
|
||||
function getItems(result: GQL.FindStudiosQueryResult) {
|
||||
return result?.data?.findStudios?.studios ?? [];
|
||||
@@ -161,6 +162,13 @@ export const StudioList: React.FC<IStudioList> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function renderEditDialog(
|
||||
selectedStudios: GQL.SlimStudioDataFragment[],
|
||||
onClose: (applied: boolean) => void
|
||||
) {
|
||||
return <EditStudiosDialog selected={selectedStudios} onClose={onClose} />;
|
||||
}
|
||||
|
||||
function renderDeleteDialog(
|
||||
selectedStudios: GQL.SlimStudioDataFragment[],
|
||||
onClose: (confirmed: boolean) => void
|
||||
@@ -193,6 +201,7 @@ export const StudioList: React.FC<IStudioList> = ({
|
||||
otherOperations={otherOperations}
|
||||
addKeybinds={addKeybinds}
|
||||
renderContent={renderContent}
|
||||
renderEditDialog={renderEditDialog}
|
||||
renderDeleteDialog={renderDeleteDialog}
|
||||
/>
|
||||
</ItemListContext>
|
||||
|
||||
@@ -1907,6 +1907,16 @@ export const useStudioUpdate = () =>
|
||||
},
|
||||
});
|
||||
|
||||
export const useBulkStudioUpdate = () =>
|
||||
GQL.useBulkStudioUpdateMutation({
|
||||
update(cache, result) {
|
||||
if (!result.data?.bulkStudioUpdate) return;
|
||||
|
||||
evictTypeFields(cache, studioMutationImpactedTypeFields);
|
||||
evictQueries(cache, studioMutationImpactedQueries);
|
||||
},
|
||||
});
|
||||
|
||||
export const useStudioDestroy = (input: GQL.StudioDestroyInput) =>
|
||||
GQL.useStudioDestroyMutation({
|
||||
variables: input,
|
||||
|
||||
Reference in New Issue
Block a user