Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ui/v2.5/graphql/mutations/scene-marker.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ mutation SceneMarkerUpdate(
}
}

mutation BulkSceneMarkerUpdate($input: BulkSceneMarkerUpdateInput!) {
bulkSceneMarkerUpdate(input: $input) {
...SceneMarkerData
}
}

mutation SceneMarkerDestroy($id: ID!) {
sceneMarkerDestroy(id: $id)
}
Expand Down
200 changes: 200 additions & 0 deletions ui/v2.5/src/components/Scenes/EditSceneMarkersDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import React, { useEffect, useMemo, useState } from "react";
import { Form } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl";
import { useBulkSceneMarkerUpdate } 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 {
getAggregateState,
getAggregateStateObject,
} from "src/utils/bulkUpdate";
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
import { TagSelect } from "../Shared/Select";

interface IListOperationProps {
selected: GQL.SceneMarkerDataFragment[];
onClose: (applied: boolean) => void;
}

const scenemarkerFields = ["title"];

export const EditSceneMarkersDialog: React.FC<IListOperationProps> = (
props: IListOperationProps
) => {
const intl = useIntl();
const Toast = useToast();

const [updateInput, setUpdateInput] =
useState<GQL.BulkSceneMarkerUpdateInput>({
ids: props.selected.map((scenemarker) => {
return scenemarker.id;
}),
});

const [tagIds, setTagIds] = useState<GQL.BulkUpdateIds>({
mode: GQL.BulkUpdateIdMode.Add,
});

const [updateSceneMarkers] = useBulkSceneMarkerUpdate();

// Network state
const [isUpdating, setIsUpdating] = useState(false);

const aggregateState = useMemo(() => {
const updateState: Partial<GQL.BulkSceneMarkerUpdateInput> = {};
const state = props.selected;
let updateTagIds: string[] = [];
let first = true;

state.forEach((scenemarker: GQL.SceneMarkerDataFragment) => {
getAggregateStateObject(
updateState,
scenemarker,
scenemarkerFields,
first
);

// sceneMarker data fragment doesn't have primary_tag_id, so handle separately
updateState.primary_tag_id = getAggregateState(
updateState.primary_tag_id,
scenemarker.primary_tag.id,
first
);

const thisTagIDs = (scenemarker.tags ?? []).map((p) => p.id).sort();

updateTagIds = getAggregateState(updateTagIds, thisTagIDs, 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.BulkSceneMarkerUpdateInput>) {
setUpdateInput((current) => ({ ...current, ...input }));
}

function getSceneMarkerInput(): GQL.BulkSceneMarkerUpdateInput {
const sceneMarkerInput: GQL.BulkSceneMarkerUpdateInput = {
...updateInput,
tag_ids: tagIds,
};

return sceneMarkerInput;
}

async function onSave() {
setIsUpdating(true);
try {
await updateSceneMarkers({
variables: {
input: getSceneMarkerInput(),
},
});
Toast.success(
intl.formatMessage(
{ id: "toast.updated_entity" },
{
entity: intl.formatMessage({ id: "markers" }).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-scenemarkers-dialog"
show
icon={faPencilAlt}
header={intl.formatMessage(
{ id: "actions.edit_entity" },
{ entityType: intl.formatMessage({ id: "markers" }) }
)}
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>
{renderTextField("title", updateInput.title, (newValue) =>
setUpdateField({ title: newValue })
)}

<Form.Group controlId="primary-tag">
<Form.Label>
<FormattedMessage id="primary_tag" />
</Form.Label>
<TagSelect
onSelect={(t) => setUpdateField({ primary_tag_id: t[0]?.id })}
ids={
updateInput.primary_tag_id ? [updateInput.primary_tag_id] : []
}
/>
</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>
</Form>
</ModalComponent>
);
}

return render();
};
11 changes: 11 additions & 0 deletions ui/v2.5/src/components/Scenes/SceneMarkerList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { MarkerWallPanel } from "./SceneMarkerWallPanel";
import { View } from "../List/views";
import { SceneMarkerCardsGrid } from "./SceneMarkerCardsGrid";
import { DeleteSceneMarkersDialog } from "./DeleteSceneMarkersDialog";
import { EditSceneMarkersDialog } from "./EditSceneMarkersDialog";

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

function renderEditDialog(
selectedMarkers: GQL.SceneMarkerDataFragment[],
onClose: (applied: boolean) => void
) {
return (
<EditSceneMarkersDialog selected={selectedMarkers} onClose={onClose} />
);
}

function renderDeleteDialog(
selectedSceneMarkers: GQL.SceneMarkerDataFragment[],
onClose: (confirmed: boolean) => void
Expand Down Expand Up @@ -143,6 +153,7 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({
otherOperations={otherOperations}
addKeybinds={addKeybinds}
renderContent={renderContent}
renderEditDialog={renderEditDialog}
renderDeleteDialog={renderDeleteDialog}
/>
</ItemListContext>
Expand Down
10 changes: 10 additions & 0 deletions ui/v2.5/src/core/StashService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,16 @@ export const useSceneMarkerUpdate = () =>
},
});

export const useBulkSceneMarkerUpdate = () =>
GQL.useBulkSceneMarkerUpdateMutation({
update(cache, result) {
if (!result.data?.bulkSceneMarkerUpdate) return;

evictTypeFields(cache, sceneMarkerMutationImpactedTypeFields);
evictQueries(cache, sceneMarkerMutationImpactedQueries);
},
});

export const useSceneMarkerDestroy = () =>
GQL.useSceneMarkerDestroyMutation({
update(cache, result, { variables }) {
Expand Down