diff --git a/ui/v2.5/graphql/mutations/scene-marker.graphql b/ui/v2.5/graphql/mutations/scene-marker.graphql index 766e318fc..a2162f799 100644 --- a/ui/v2.5/graphql/mutations/scene-marker.graphql +++ b/ui/v2.5/graphql/mutations/scene-marker.graphql @@ -44,6 +44,12 @@ mutation SceneMarkerUpdate( } } +mutation BulkSceneMarkerUpdate($input: BulkSceneMarkerUpdateInput!) { + bulkSceneMarkerUpdate(input: $input) { + ...SceneMarkerData + } +} + mutation SceneMarkerDestroy($id: ID!) { sceneMarkerDestroy(id: $id) } diff --git a/ui/v2.5/src/components/Scenes/EditSceneMarkersDialog.tsx b/ui/v2.5/src/components/Scenes/EditSceneMarkersDialog.tsx new file mode 100644 index 000000000..bb1d8067b --- /dev/null +++ b/ui/v2.5/src/components/Scenes/EditSceneMarkersDialog.tsx @@ -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 = ( + props: IListOperationProps +) => { + const intl = useIntl(); + const Toast = useToast(); + + const [updateInput, setUpdateInput] = + useState({ + ids: props.selected.map((scenemarker) => { + return scenemarker.id; + }), + }); + + const [tagIds, setTagIds] = useState({ + mode: GQL.BulkUpdateIdMode.Add, + }); + + const [updateSceneMarkers] = useBulkSceneMarkerUpdate(); + + // Network state + const [isUpdating, setIsUpdating] = useState(false); + + const aggregateState = useMemo(() => { + const updateState: Partial = {}; + 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) { + 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 ( + + + + + setter(newValue)} + unsetDisabled={props.selected.length < 2} + as={area ? "textarea" : undefined} + /> + + ); + } + + function render() { + return ( + props.onClose(false), + text: intl.formatMessage({ id: "actions.cancel" }), + variant: "secondary", + }} + isRunning={isUpdating} + > +
+ {renderTextField("title", updateInput.title, (newValue) => + setUpdateField({ title: newValue }) + )} + + + + + + setUpdateField({ primary_tag_id: t[0]?.id })} + ids={ + updateInput.primary_tag_id ? [updateInput.primary_tag_id] : [] + } + /> + + + + + + + setTagIds((v) => ({ ...v, ids: itemIDs }))} + onSetMode={(newMode) => + setTagIds((v) => ({ ...v, mode: newMode })) + } + existingIds={aggregateState.tagIds ?? []} + ids={tagIds.ids ?? []} + mode={tagIds.mode} + menuPortalTarget={document.body} + /> + +
+
+ ); + } + + return render(); +}; diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx index 94eb6e133..dbe6e2e23 100644 --- a/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx @@ -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 ?? []; @@ -114,6 +115,15 @@ export const SceneMarkerList: React.FC = ({ } } + function renderEditDialog( + selectedMarkers: GQL.SceneMarkerDataFragment[], + onClose: (applied: boolean) => void + ) { + return ( + + ); + } + function renderDeleteDialog( selectedSceneMarkers: GQL.SceneMarkerDataFragment[], onClose: (confirmed: boolean) => void @@ -143,6 +153,7 @@ export const SceneMarkerList: React.FC = ({ otherOperations={otherOperations} addKeybinds={addKeybinds} renderContent={renderContent} + renderEditDialog={renderEditDialog} renderDeleteDialog={renderDeleteDialog} /> diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index be5fb4dbe..100f25199 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -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 }) {