Skip to content

Commit 1ec8d4a

Browse files
authored
Add edit studios dialog (#6238)
1 parent 15db2da commit 1ec8d4a

File tree

5 files changed

+272
-0
lines changed

5 files changed

+272
-0
lines changed

ui/v2.5/graphql/mutations/studio.graphql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ mutation StudioUpdate($input: StudioUpdateInput!) {
1010
}
1111
}
1212

13+
mutation BulkStudioUpdate($input: BulkStudioUpdateInput!) {
14+
bulkStudioUpdate(input: $input) {
15+
...StudioData
16+
}
17+
}
18+
1319
mutation StudioDestroy($id: ID!) {
1420
studioDestroy(input: { id: $id })
1521
}

ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Icon } from "./Icon";
77
interface IBulkUpdateTextInputProps extends FormControlProps {
88
valueChanged: (value: string | undefined) => void;
99
unsetDisabled?: boolean;
10+
as?: React.ElementType;
1011
}
1112

1213
export const BulkUpdateTextInput: React.FC<IBulkUpdateTextInputProps> = ({
@@ -24,6 +25,7 @@ export const BulkUpdateTextInput: React.FC<IBulkUpdateTextInputProps> = ({
2425
{...props}
2526
className="input-control"
2627
type="text"
28+
as={props.as}
2729
value={props.value ?? ""}
2830
placeholder={
2931
props.value === undefined
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import React, { useEffect, useMemo, useState } from "react";
2+
import { Col, Form, Row } from "react-bootstrap";
3+
import { FormattedMessage, useIntl } from "react-intl";
4+
import { useBulkStudioUpdate } from "src/core/StashService";
5+
import * as GQL from "src/core/generated-graphql";
6+
import { ModalComponent } from "../Shared/Modal";
7+
import { useToast } from "src/hooks/Toast";
8+
import { MultiSet } from "../Shared/MultiSet";
9+
import { RatingSystem } from "../Shared/Rating/RatingSystem";
10+
import {
11+
getAggregateInputValue,
12+
getAggregateState,
13+
getAggregateStateObject,
14+
} from "src/utils/bulkUpdate";
15+
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
16+
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
17+
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
18+
import * as FormUtils from "src/utils/form";
19+
import { StudioSelect } from "../Shared/Select";
20+
21+
interface IListOperationProps {
22+
selected: GQL.SlimStudioDataFragment[];
23+
onClose: (applied: boolean) => void;
24+
}
25+
26+
const studioFields = ["favorite", "rating100", "details", "ignore_auto_tag"];
27+
28+
export const EditStudiosDialog: React.FC<IListOperationProps> = (
29+
props: IListOperationProps
30+
) => {
31+
const intl = useIntl();
32+
const Toast = useToast();
33+
34+
const [updateInput, setUpdateInput] = useState<GQL.BulkStudioUpdateInput>({
35+
ids: props.selected.map((studio) => {
36+
return studio.id;
37+
}),
38+
});
39+
40+
const [tagIds, setTagIds] = useState<GQL.BulkUpdateIds>({
41+
mode: GQL.BulkUpdateIdMode.Add,
42+
});
43+
44+
const [updateStudios] = useBulkStudioUpdate();
45+
46+
// Network state
47+
const [isUpdating, setIsUpdating] = useState(false);
48+
49+
const aggregateState = useMemo(() => {
50+
const updateState: Partial<GQL.BulkStudioUpdateInput> = {};
51+
const state = props.selected;
52+
let updateTagIds: string[] = [];
53+
let first = true;
54+
55+
state.forEach((studio: GQL.SlimStudioDataFragment) => {
56+
getAggregateStateObject(updateState, studio, studioFields, first);
57+
58+
// studio data fragment doesn't have parent_id, so handle separately
59+
updateState.parent_id = getAggregateState(
60+
updateState.parent_id,
61+
studio.parent_studio?.id,
62+
first
63+
);
64+
65+
const studioTagIDs = (studio.tags ?? []).map((p) => p.id).sort();
66+
67+
updateTagIds = getAggregateState(updateTagIds, studioTagIDs, first) ?? [];
68+
69+
first = false;
70+
});
71+
72+
return { state: updateState, tagIds: updateTagIds };
73+
}, [props.selected]);
74+
75+
// update initial state from aggregate
76+
useEffect(() => {
77+
setUpdateInput((current) => ({ ...current, ...aggregateState.state }));
78+
}, [aggregateState]);
79+
80+
function setUpdateField(input: Partial<GQL.BulkStudioUpdateInput>) {
81+
setUpdateInput((current) => ({ ...current, ...input }));
82+
}
83+
84+
function getStudioInput(): GQL.BulkStudioUpdateInput {
85+
const studioInput: GQL.BulkStudioUpdateInput = {
86+
...updateInput,
87+
tag_ids: tagIds,
88+
};
89+
90+
// we don't have unset functionality for the rating star control
91+
// so need to determine if we are setting a rating or not
92+
studioInput.rating100 = getAggregateInputValue(
93+
updateInput.rating100,
94+
aggregateState.state.rating100
95+
);
96+
97+
return studioInput;
98+
}
99+
100+
async function onSave() {
101+
setIsUpdating(true);
102+
try {
103+
await updateStudios({
104+
variables: {
105+
input: getStudioInput(),
106+
},
107+
});
108+
Toast.success(
109+
intl.formatMessage(
110+
{ id: "toast.updated_entity" },
111+
{
112+
entity: intl.formatMessage({ id: "studios" }).toLocaleLowerCase(),
113+
}
114+
)
115+
);
116+
props.onClose(true);
117+
} catch (e) {
118+
Toast.error(e);
119+
}
120+
setIsUpdating(false);
121+
}
122+
123+
function renderTextField(
124+
name: string,
125+
value: string | undefined | null,
126+
setter: (newValue: string | undefined) => void,
127+
area: boolean = false
128+
) {
129+
return (
130+
<Form.Group controlId={name}>
131+
<Form.Label>
132+
<FormattedMessage id={name} />
133+
</Form.Label>
134+
<BulkUpdateTextInput
135+
value={value === null ? "" : value ?? undefined}
136+
valueChanged={(newValue) => setter(newValue)}
137+
unsetDisabled={props.selected.length < 2}
138+
as={area ? "textarea" : undefined}
139+
/>
140+
</Form.Group>
141+
);
142+
}
143+
144+
function render() {
145+
return (
146+
<ModalComponent
147+
dialogClassName="edit-studios-dialog"
148+
show
149+
icon={faPencilAlt}
150+
header={intl.formatMessage(
151+
{ id: "actions.edit_entity" },
152+
{ entityType: intl.formatMessage({ id: "studios" }) }
153+
)}
154+
accept={{
155+
onClick: onSave,
156+
text: intl.formatMessage({ id: "actions.apply" }),
157+
}}
158+
cancel={{
159+
onClick: () => props.onClose(false),
160+
text: intl.formatMessage({ id: "actions.cancel" }),
161+
variant: "secondary",
162+
}}
163+
isRunning={isUpdating}
164+
>
165+
<Form.Group controlId="parent-studio" as={Row}>
166+
{FormUtils.renderLabel({
167+
title: intl.formatMessage({ id: "parent_studio" }),
168+
})}
169+
<Col xs={9}>
170+
<StudioSelect
171+
onSelect={(items) =>
172+
setUpdateField({
173+
parent_id: items.length > 0 ? items[0]?.id : undefined,
174+
})
175+
}
176+
ids={updateInput.parent_id ? [updateInput.parent_id] : []}
177+
isDisabled={isUpdating}
178+
menuPortalTarget={document.body}
179+
/>
180+
</Col>
181+
</Form.Group>
182+
<Form.Group controlId="rating" as={Row}>
183+
{FormUtils.renderLabel({
184+
title: intl.formatMessage({ id: "rating" }),
185+
})}
186+
<Col xs={9}>
187+
<RatingSystem
188+
value={updateInput.rating100}
189+
onSetRating={(value) =>
190+
setUpdateField({ rating100: value ?? undefined })
191+
}
192+
disabled={isUpdating}
193+
/>
194+
</Col>
195+
</Form.Group>
196+
<Form>
197+
<Form.Group controlId="favorite">
198+
<IndeterminateCheckbox
199+
setChecked={(checked) => setUpdateField({ favorite: checked })}
200+
checked={updateInput.favorite ?? undefined}
201+
label={intl.formatMessage({ id: "favourite" })}
202+
/>
203+
</Form.Group>
204+
205+
<Form.Group controlId="tags">
206+
<Form.Label>
207+
<FormattedMessage id="tags" />
208+
</Form.Label>
209+
<MultiSet
210+
type="tags"
211+
disabled={isUpdating}
212+
onUpdate={(itemIDs) => setTagIds((v) => ({ ...v, ids: itemIDs }))}
213+
onSetMode={(newMode) =>
214+
setTagIds((v) => ({ ...v, mode: newMode }))
215+
}
216+
existingIds={aggregateState.tagIds ?? []}
217+
ids={tagIds.ids ?? []}
218+
mode={tagIds.mode}
219+
menuPortalTarget={document.body}
220+
/>
221+
</Form.Group>
222+
223+
{renderTextField(
224+
"details",
225+
updateInput.details,
226+
(newValue) => setUpdateField({ details: newValue }),
227+
true
228+
)}
229+
230+
<Form.Group controlId="ignore-auto-tags">
231+
<IndeterminateCheckbox
232+
label={intl.formatMessage({ id: "ignore_auto_tag" })}
233+
setChecked={(checked) =>
234+
setUpdateField({ ignore_auto_tag: checked })
235+
}
236+
checked={updateInput.ignore_auto_tag ?? undefined}
237+
/>
238+
</Form.Group>
239+
</Form>
240+
</ModalComponent>
241+
);
242+
}
243+
244+
return render();
245+
};

ui/v2.5/src/components/Studios/StudioList.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog";
1717
import { StudioTagger } from "../Tagger/studios/StudioTagger";
1818
import { StudioCardGrid } from "./StudioCardGrid";
1919
import { View } from "../List/views";
20+
import { EditStudiosDialog } from "./EditStudiosDialog";
2021

2122
function getItems(result: GQL.FindStudiosQueryResult) {
2223
return result?.data?.findStudios?.studios ?? [];
@@ -161,6 +162,13 @@ export const StudioList: React.FC<IStudioList> = ({
161162
);
162163
}
163164

165+
function renderEditDialog(
166+
selectedStudios: GQL.SlimStudioDataFragment[],
167+
onClose: (applied: boolean) => void
168+
) {
169+
return <EditStudiosDialog selected={selectedStudios} onClose={onClose} />;
170+
}
171+
164172
function renderDeleteDialog(
165173
selectedStudios: GQL.SlimStudioDataFragment[],
166174
onClose: (confirmed: boolean) => void
@@ -193,6 +201,7 @@ export const StudioList: React.FC<IStudioList> = ({
193201
otherOperations={otherOperations}
194202
addKeybinds={addKeybinds}
195203
renderContent={renderContent}
204+
renderEditDialog={renderEditDialog}
196205
renderDeleteDialog={renderDeleteDialog}
197206
/>
198207
</ItemListContext>

ui/v2.5/src/core/StashService.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1907,6 +1907,16 @@ export const useStudioUpdate = () =>
19071907
},
19081908
});
19091909

1910+
export const useBulkStudioUpdate = () =>
1911+
GQL.useBulkStudioUpdateMutation({
1912+
update(cache, result) {
1913+
if (!result.data?.bulkStudioUpdate) return;
1914+
1915+
evictTypeFields(cache, studioMutationImpactedTypeFields);
1916+
evictQueries(cache, studioMutationImpactedQueries);
1917+
},
1918+
});
1919+
19101920
export const useStudioDestroy = (input: GQL.StudioDestroyInput) =>
19111921
GQL.useStudioDestroyMutation({
19121922
variables: input,

0 commit comments

Comments
 (0)