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!) {
|
mutation StudioDestroy($id: ID!) {
|
||||||
studioDestroy(input: { id: $id })
|
studioDestroy(input: { id: $id })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Icon } from "./Icon";
|
|||||||
interface IBulkUpdateTextInputProps extends FormControlProps {
|
interface IBulkUpdateTextInputProps extends FormControlProps {
|
||||||
valueChanged: (value: string | undefined) => void;
|
valueChanged: (value: string | undefined) => void;
|
||||||
unsetDisabled?: boolean;
|
unsetDisabled?: boolean;
|
||||||
|
as?: React.ElementType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BulkUpdateTextInput: React.FC<IBulkUpdateTextInputProps> = ({
|
export const BulkUpdateTextInput: React.FC<IBulkUpdateTextInputProps> = ({
|
||||||
@@ -24,6 +25,7 @@ export const BulkUpdateTextInput: React.FC<IBulkUpdateTextInputProps> = ({
|
|||||||
{...props}
|
{...props}
|
||||||
className="input-control"
|
className="input-control"
|
||||||
type="text"
|
type="text"
|
||||||
|
as={props.as}
|
||||||
value={props.value ?? ""}
|
value={props.value ?? ""}
|
||||||
placeholder={
|
placeholder={
|
||||||
props.value === undefined
|
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 { StudioTagger } from "../Tagger/studios/StudioTagger";
|
||||||
import { StudioCardGrid } from "./StudioCardGrid";
|
import { StudioCardGrid } from "./StudioCardGrid";
|
||||||
import { View } from "../List/views";
|
import { View } from "../List/views";
|
||||||
|
import { EditStudiosDialog } from "./EditStudiosDialog";
|
||||||
|
|
||||||
function getItems(result: GQL.FindStudiosQueryResult) {
|
function getItems(result: GQL.FindStudiosQueryResult) {
|
||||||
return result?.data?.findStudios?.studios ?? [];
|
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(
|
function renderDeleteDialog(
|
||||||
selectedStudios: GQL.SlimStudioDataFragment[],
|
selectedStudios: GQL.SlimStudioDataFragment[],
|
||||||
onClose: (confirmed: boolean) => void
|
onClose: (confirmed: boolean) => void
|
||||||
@@ -193,6 +201,7 @@ export const StudioList: React.FC<IStudioList> = ({
|
|||||||
otherOperations={otherOperations}
|
otherOperations={otherOperations}
|
||||||
addKeybinds={addKeybinds}
|
addKeybinds={addKeybinds}
|
||||||
renderContent={renderContent}
|
renderContent={renderContent}
|
||||||
|
renderEditDialog={renderEditDialog}
|
||||||
renderDeleteDialog={renderDeleteDialog}
|
renderDeleteDialog={renderDeleteDialog}
|
||||||
/>
|
/>
|
||||||
</ItemListContext>
|
</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) =>
|
export const useStudioDestroy = (input: GQL.StudioDestroyInput) =>
|
||||||
GQL.useStudioDestroyMutation({
|
GQL.useStudioDestroyMutation({
|
||||||
variables: input,
|
variables: input,
|
||||||
|
|||||||
Reference in New Issue
Block a user