Skip to content

Commit 9ef2169

Browse files
authored
Add edit scene markers dialog (#6239)
1 parent 1ec8d4a commit 9ef2169

File tree

4 files changed

+227
-0
lines changed

4 files changed

+227
-0
lines changed

ui/v2.5/graphql/mutations/scene-marker.graphql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ mutation SceneMarkerUpdate(
4444
}
4545
}
4646

47+
mutation BulkSceneMarkerUpdate($input: BulkSceneMarkerUpdateInput!) {
48+
bulkSceneMarkerUpdate(input: $input) {
49+
...SceneMarkerData
50+
}
51+
}
52+
4753
mutation SceneMarkerDestroy($id: ID!) {
4854
sceneMarkerDestroy(id: $id)
4955
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import React, { useEffect, useMemo, useState } from "react";
2+
import { Form } from "react-bootstrap";
3+
import { FormattedMessage, useIntl } from "react-intl";
4+
import { useBulkSceneMarkerUpdate } 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 {
10+
getAggregateState,
11+
getAggregateStateObject,
12+
} from "src/utils/bulkUpdate";
13+
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
14+
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
15+
import { TagSelect } from "../Shared/Select";
16+
17+
interface IListOperationProps {
18+
selected: GQL.SceneMarkerDataFragment[];
19+
onClose: (applied: boolean) => void;
20+
}
21+
22+
const scenemarkerFields = ["title"];
23+
24+
export const EditSceneMarkersDialog: React.FC<IListOperationProps> = (
25+
props: IListOperationProps
26+
) => {
27+
const intl = useIntl();
28+
const Toast = useToast();
29+
30+
const [updateInput, setUpdateInput] =
31+
useState<GQL.BulkSceneMarkerUpdateInput>({
32+
ids: props.selected.map((scenemarker) => {
33+
return scenemarker.id;
34+
}),
35+
});
36+
37+
const [tagIds, setTagIds] = useState<GQL.BulkUpdateIds>({
38+
mode: GQL.BulkUpdateIdMode.Add,
39+
});
40+
41+
const [updateSceneMarkers] = useBulkSceneMarkerUpdate();
42+
43+
// Network state
44+
const [isUpdating, setIsUpdating] = useState(false);
45+
46+
const aggregateState = useMemo(() => {
47+
const updateState: Partial<GQL.BulkSceneMarkerUpdateInput> = {};
48+
const state = props.selected;
49+
let updateTagIds: string[] = [];
50+
let first = true;
51+
52+
state.forEach((scenemarker: GQL.SceneMarkerDataFragment) => {
53+
getAggregateStateObject(
54+
updateState,
55+
scenemarker,
56+
scenemarkerFields,
57+
first
58+
);
59+
60+
// sceneMarker data fragment doesn't have primary_tag_id, so handle separately
61+
updateState.primary_tag_id = getAggregateState(
62+
updateState.primary_tag_id,
63+
scenemarker.primary_tag.id,
64+
first
65+
);
66+
67+
const thisTagIDs = (scenemarker.tags ?? []).map((p) => p.id).sort();
68+
69+
updateTagIds = getAggregateState(updateTagIds, thisTagIDs, first) ?? [];
70+
71+
first = false;
72+
});
73+
74+
return { state: updateState, tagIds: updateTagIds };
75+
}, [props.selected]);
76+
77+
// update initial state from aggregate
78+
useEffect(() => {
79+
setUpdateInput((current) => ({ ...current, ...aggregateState.state }));
80+
}, [aggregateState]);
81+
82+
function setUpdateField(input: Partial<GQL.BulkSceneMarkerUpdateInput>) {
83+
setUpdateInput((current) => ({ ...current, ...input }));
84+
}
85+
86+
function getSceneMarkerInput(): GQL.BulkSceneMarkerUpdateInput {
87+
const sceneMarkerInput: GQL.BulkSceneMarkerUpdateInput = {
88+
...updateInput,
89+
tag_ids: tagIds,
90+
};
91+
92+
return sceneMarkerInput;
93+
}
94+
95+
async function onSave() {
96+
setIsUpdating(true);
97+
try {
98+
await updateSceneMarkers({
99+
variables: {
100+
input: getSceneMarkerInput(),
101+
},
102+
});
103+
Toast.success(
104+
intl.formatMessage(
105+
{ id: "toast.updated_entity" },
106+
{
107+
entity: intl.formatMessage({ id: "markers" }).toLocaleLowerCase(),
108+
}
109+
)
110+
);
111+
props.onClose(true);
112+
} catch (e) {
113+
Toast.error(e);
114+
}
115+
setIsUpdating(false);
116+
}
117+
118+
function renderTextField(
119+
name: string,
120+
value: string | undefined | null,
121+
setter: (newValue: string | undefined) => void,
122+
area: boolean = false
123+
) {
124+
return (
125+
<Form.Group controlId={name}>
126+
<Form.Label>
127+
<FormattedMessage id={name} />
128+
</Form.Label>
129+
<BulkUpdateTextInput
130+
value={value === null ? "" : value ?? undefined}
131+
valueChanged={(newValue) => setter(newValue)}
132+
unsetDisabled={props.selected.length < 2}
133+
as={area ? "textarea" : undefined}
134+
/>
135+
</Form.Group>
136+
);
137+
}
138+
139+
function render() {
140+
return (
141+
<ModalComponent
142+
dialogClassName="edit-scenemarkers-dialog"
143+
show
144+
icon={faPencilAlt}
145+
header={intl.formatMessage(
146+
{ id: "actions.edit_entity" },
147+
{ entityType: intl.formatMessage({ id: "markers" }) }
148+
)}
149+
accept={{
150+
onClick: onSave,
151+
text: intl.formatMessage({ id: "actions.apply" }),
152+
}}
153+
cancel={{
154+
onClick: () => props.onClose(false),
155+
text: intl.formatMessage({ id: "actions.cancel" }),
156+
variant: "secondary",
157+
}}
158+
isRunning={isUpdating}
159+
>
160+
<Form>
161+
{renderTextField("title", updateInput.title, (newValue) =>
162+
setUpdateField({ title: newValue })
163+
)}
164+
165+
<Form.Group controlId="primary-tag">
166+
<Form.Label>
167+
<FormattedMessage id="primary_tag" />
168+
</Form.Label>
169+
<TagSelect
170+
onSelect={(t) => setUpdateField({ primary_tag_id: t[0]?.id })}
171+
ids={
172+
updateInput.primary_tag_id ? [updateInput.primary_tag_id] : []
173+
}
174+
/>
175+
</Form.Group>
176+
177+
<Form.Group controlId="tags">
178+
<Form.Label>
179+
<FormattedMessage id="tags" />
180+
</Form.Label>
181+
<MultiSet
182+
type="tags"
183+
disabled={isUpdating}
184+
onUpdate={(itemIDs) => setTagIds((v) => ({ ...v, ids: itemIDs }))}
185+
onSetMode={(newMode) =>
186+
setTagIds((v) => ({ ...v, mode: newMode }))
187+
}
188+
existingIds={aggregateState.tagIds ?? []}
189+
ids={tagIds.ids ?? []}
190+
mode={tagIds.mode}
191+
menuPortalTarget={document.body}
192+
/>
193+
</Form.Group>
194+
</Form>
195+
</ModalComponent>
196+
);
197+
}
198+
199+
return render();
200+
};

ui/v2.5/src/components/Scenes/SceneMarkerList.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { MarkerWallPanel } from "./SceneMarkerWallPanel";
1616
import { View } from "../List/views";
1717
import { SceneMarkerCardsGrid } from "./SceneMarkerCardsGrid";
1818
import { DeleteSceneMarkersDialog } from "./DeleteSceneMarkersDialog";
19+
import { EditSceneMarkersDialog } from "./EditSceneMarkersDialog";
1920

2021
function getItems(result: GQL.FindSceneMarkersQueryResult) {
2122
return result?.data?.findSceneMarkers?.scene_markers ?? [];
@@ -114,6 +115,15 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({
114115
}
115116
}
116117

118+
function renderEditDialog(
119+
selectedMarkers: GQL.SceneMarkerDataFragment[],
120+
onClose: (applied: boolean) => void
121+
) {
122+
return (
123+
<EditSceneMarkersDialog selected={selectedMarkers} onClose={onClose} />
124+
);
125+
}
126+
117127
function renderDeleteDialog(
118128
selectedSceneMarkers: GQL.SceneMarkerDataFragment[],
119129
onClose: (confirmed: boolean) => void
@@ -143,6 +153,7 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({
143153
otherOperations={otherOperations}
144154
addKeybinds={addKeybinds}
145155
renderContent={renderContent}
156+
renderEditDialog={renderEditDialog}
146157
renderDeleteDialog={renderDeleteDialog}
147158
/>
148159
</ItemListContext>

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,16 @@ export const useSceneMarkerUpdate = () =>
14861486
},
14871487
});
14881488

1489+
export const useBulkSceneMarkerUpdate = () =>
1490+
GQL.useBulkSceneMarkerUpdateMutation({
1491+
update(cache, result) {
1492+
if (!result.data?.bulkSceneMarkerUpdate) return;
1493+
1494+
evictTypeFields(cache, sceneMarkerMutationImpactedTypeFields);
1495+
evictQueries(cache, sceneMarkerMutationImpactedQueries);
1496+
},
1497+
});
1498+
14891499
export const useSceneMarkerDestroy = () =>
14901500
GQL.useSceneMarkerDestroyMutation({
14911501
update(cache, result, { variables }) {

0 commit comments

Comments
 (0)