mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Add batch delete for performers/tags/studios/movies (#1053)
* Add batch delete for performers/tags/studios/movies * Fix ListFilter styling bug
This commit is contained in:
@@ -25,3 +25,7 @@ mutation MovieUpdate($input: MovieUpdateInput!) {
|
|||||||
mutation MovieDestroy($id: ID!) {
|
mutation MovieDestroy($id: ID!) {
|
||||||
movieDestroy(input: { id: $id })
|
movieDestroy(input: { id: $id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation MoviesDestroy($ids: [ID!]!) {
|
||||||
|
moviesDestroy(ids: $ids)
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,3 +55,7 @@ mutation PerformerUpdate(
|
|||||||
mutation PerformerDestroy($id: ID!) {
|
mutation PerformerDestroy($id: ID!) {
|
||||||
performerDestroy(input: { id: $id })
|
performerDestroy(input: { id: $id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation PerformersDestroy($ids: [ID!]!) {
|
||||||
|
performersDestroy(ids: $ids)
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,3 +21,7 @@ mutation StudioUpdate(
|
|||||||
mutation StudioDestroy($id: ID!) {
|
mutation StudioDestroy($id: ID!) {
|
||||||
studioDestroy(input: { id: $id })
|
studioDestroy(input: { id: $id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation StudiosDestroy($ids: [ID!]!) {
|
||||||
|
studiosDestroy(ids: $ids)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ mutation TagDestroy($id: ID!) {
|
|||||||
tagDestroy(input: { id: $id })
|
tagDestroy(input: { id: $id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation TagsDestroy($ids: [ID!]!) {
|
||||||
|
tagsDestroy(ids: $ids)
|
||||||
|
}
|
||||||
|
|
||||||
mutation TagUpdate($input: TagUpdateInput!) {
|
mutation TagUpdate($input: TagUpdateInput!) {
|
||||||
tagUpdate(input: $input) {
|
tagUpdate(input: $input) {
|
||||||
...TagData
|
...TagData
|
||||||
|
|||||||
@@ -175,18 +175,22 @@ type Mutation {
|
|||||||
performerCreate(input: PerformerCreateInput!): Performer
|
performerCreate(input: PerformerCreateInput!): Performer
|
||||||
performerUpdate(input: PerformerUpdateInput!): Performer
|
performerUpdate(input: PerformerUpdateInput!): Performer
|
||||||
performerDestroy(input: PerformerDestroyInput!): Boolean!
|
performerDestroy(input: PerformerDestroyInput!): Boolean!
|
||||||
|
performersDestroy(ids: [ID!]!): Boolean!
|
||||||
|
|
||||||
studioCreate(input: StudioCreateInput!): Studio
|
studioCreate(input: StudioCreateInput!): Studio
|
||||||
studioUpdate(input: StudioUpdateInput!): Studio
|
studioUpdate(input: StudioUpdateInput!): Studio
|
||||||
studioDestroy(input: StudioDestroyInput!): Boolean!
|
studioDestroy(input: StudioDestroyInput!): Boolean!
|
||||||
|
studiosDestroy(ids: [ID!]!): Boolean!
|
||||||
|
|
||||||
movieCreate(input: MovieCreateInput!): Movie
|
movieCreate(input: MovieCreateInput!): Movie
|
||||||
movieUpdate(input: MovieUpdateInput!): Movie
|
movieUpdate(input: MovieUpdateInput!): Movie
|
||||||
movieDestroy(input: MovieDestroyInput!): Boolean!
|
movieDestroy(input: MovieDestroyInput!): Boolean!
|
||||||
|
moviesDestroy(ids: [ID!]!): Boolean!
|
||||||
|
|
||||||
tagCreate(input: TagCreateInput!): Tag
|
tagCreate(input: TagCreateInput!): Tag
|
||||||
tagUpdate(input: TagUpdateInput!): Tag
|
tagUpdate(input: TagUpdateInput!): Tag
|
||||||
tagDestroy(input: TagDestroyInput!): Boolean!
|
tagDestroy(input: TagDestroyInput!): Boolean!
|
||||||
|
tagsDestroy(ids: [ID!]!): Boolean!
|
||||||
|
|
||||||
"""Change general configuration options"""
|
"""Change general configuration options"""
|
||||||
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
|
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
|
||||||
|
|||||||
@@ -222,3 +222,18 @@ func (r *mutationResolver) MovieDestroy(ctx context.Context, input models.MovieD
|
|||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) MoviesDestroy(ctx context.Context, ids []string) (bool, error) {
|
||||||
|
qb := models.NewMovieQueryBuilder()
|
||||||
|
tx := database.DB.MustBeginTx(ctx, nil)
|
||||||
|
for _, id := range ids {
|
||||||
|
if err := qb.Destroy(id, tx); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -242,3 +242,18 @@ func (r *mutationResolver) PerformerDestroy(ctx context.Context, input models.Pe
|
|||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) PerformersDestroy(ctx context.Context, ids []string) (bool, error) {
|
||||||
|
qb := models.NewPerformerQueryBuilder()
|
||||||
|
tx := database.DB.MustBeginTx(ctx, nil)
|
||||||
|
for _, id := range ids {
|
||||||
|
if err := qb.Destroy(id, tx); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -182,3 +182,18 @@ func (r *mutationResolver) StudioDestroy(ctx context.Context, input models.Studi
|
|||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) StudiosDestroy(ctx context.Context, ids []string) (bool, error) {
|
||||||
|
qb := models.NewStudioQueryBuilder()
|
||||||
|
tx := database.DB.MustBeginTx(ctx, nil)
|
||||||
|
for _, id := range ids {
|
||||||
|
if err := qb.Destroy(id, tx); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -152,3 +152,19 @@ func (r *mutationResolver) TagDestroy(ctx context.Context, input models.TagDestr
|
|||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) TagsDestroy(ctx context.Context, ids []string) (bool, error) {
|
||||||
|
qb := models.NewTagQueryBuilder()
|
||||||
|
tx := database.DB.MustBeginTx(ctx, nil)
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
if err := qb.Destroy(id, tx); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
* Allow configuration of visible navbar items.
|
* Allow configuration of visible navbar items.
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Add batch deleting of performers, tags, studios, and movies.
|
||||||
* Reset cache after scan/clean to ensure scenes are updated.
|
* Reset cache after scan/clean to ensure scenes are updated.
|
||||||
* Add more video/image resolution tags.
|
* Add more video/image resolution tags.
|
||||||
* Add option to strip file extension from scene title when populating from scanning task.
|
* Add option to strip file extension from scene title when populating from scanning task.
|
||||||
|
|||||||
@@ -76,11 +76,13 @@ export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
|
|||||||
</p>
|
</p>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
|
id="delete-image"
|
||||||
checked={deleteFile}
|
checked={deleteFile}
|
||||||
label="Delete file"
|
label="Delete file"
|
||||||
onChange={() => setDeleteFile(!deleteFile)}
|
onChange={() => setDeleteFile(!deleteFile)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
|
id="delete-image-generated"
|
||||||
checked={deleteGenerated}
|
checked={deleteGenerated}
|
||||||
label="Delete generated supporting files"
|
label="Delete generated supporting files"
|
||||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||||
|
|||||||
@@ -426,29 +426,25 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderSelectedButtons() {
|
function maybeRenderSelectedButtons() {
|
||||||
if (props.itemsSelected) {
|
if (props.itemsSelected && (props.onEdit || props.onDelete)) {
|
||||||
return (
|
return (
|
||||||
<>
|
<ButtonGroup className="ml-2">
|
||||||
{props.onEdit ? (
|
{props.onEdit && (
|
||||||
<ButtonGroup className="mr-1">
|
<OverlayTrigger overlay={<Tooltip id="edit">Edit</Tooltip>}>
|
||||||
<OverlayTrigger overlay={<Tooltip id="edit">Edit</Tooltip>}>
|
<Button variant="secondary" onClick={onEdit}>
|
||||||
<Button variant="secondary" onClick={onEdit}>
|
<Icon icon="pencil-alt" />
|
||||||
<Icon icon="pencil-alt" />
|
</Button>
|
||||||
</Button>
|
</OverlayTrigger>
|
||||||
</OverlayTrigger>
|
)}
|
||||||
</ButtonGroup>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
{props.onDelete ? (
|
{props.onDelete && (
|
||||||
<ButtonGroup className="mr-1">
|
<OverlayTrigger overlay={<Tooltip id="delete">Delete</Tooltip>}>
|
||||||
<OverlayTrigger overlay={<Tooltip id="delete">Delete</Tooltip>}>
|
<Button variant="danger" onClick={onDelete}>
|
||||||
<Button variant="danger" onClick={onDelete}>
|
<Icon icon="trash" />
|
||||||
<Icon icon="trash" />
|
</Button>
|
||||||
</Button>
|
</OverlayTrigger>
|
||||||
</OverlayTrigger>
|
)}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
) : undefined}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -456,8 +452,8 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
function render() {
|
function render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonToolbar className="align-items-center justify-content-center">
|
<ButtonToolbar className="align-items-center justify-content-center mb-2">
|
||||||
<div className="my-1 d-flex">
|
<div className="d-flex">
|
||||||
<InputGroup className="mr-2 flex-grow-1">
|
<InputGroup className="mr-2 flex-grow-1">
|
||||||
<FormControl
|
<FormControl
|
||||||
ref={queryRef}
|
ref={queryRef}
|
||||||
@@ -530,18 +526,15 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
))}
|
))}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
|
||||||
<ButtonGroup className="mx-3 my-1">
|
{maybeRenderSelectedButtons()}
|
||||||
{maybeRenderSelectedButtons()}
|
|
||||||
{renderMore()}
|
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
<ButtonGroup className="my-1">
|
<div className="mx-2">{renderMore()}</div>
|
||||||
{renderDisplayModeOptions()}
|
|
||||||
</ButtonGroup>
|
<ButtonGroup>{renderDisplayModeOptions()}</ButtonGroup>
|
||||||
{maybeRenderZoom()}
|
{maybeRenderZoom()}
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
|
|
||||||
<div className="d-flex justify-content-center mt-1">
|
<div className="d-flex justify-content-center">
|
||||||
{renderFilterTags()}
|
{renderFilterTags()}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { FindMoviesQueryResult } from "src/core/generated-graphql";
|
import { useHistory } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
FindMoviesQueryResult,
|
||||||
|
SlimMovieDataFragment,
|
||||||
|
} from "src/core/generated-graphql";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { queryFindMovies } from "src/core/StashService";
|
import { queryFindMovies, useMoviesDestroy } from "src/core/StashService";
|
||||||
import { showWhenSelected, useMoviesList } from "src/hooks/ListHook";
|
import { showWhenSelected, useMoviesList } from "src/hooks/ListHook";
|
||||||
import { useHistory } from "react-router-dom";
|
import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
|
||||||
import { MovieCard } from "./MovieCard";
|
import { MovieCard } from "./MovieCard";
|
||||||
import { ExportDialog } from "../Shared/ExportDialog";
|
|
||||||
|
|
||||||
export const MovieList: React.FC = () => {
|
export const MovieList: React.FC = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -44,12 +47,26 @@ export const MovieList: React.FC = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderDeleteDialog = (
|
||||||
|
selectedMovies: SlimMovieDataFragment[],
|
||||||
|
onClose: (confirmed: boolean) => void
|
||||||
|
) => (
|
||||||
|
<DeleteEntityDialog
|
||||||
|
selected={selectedMovies}
|
||||||
|
onClose={onClose}
|
||||||
|
singularEntity="movie"
|
||||||
|
pluralEntity="movies"
|
||||||
|
destroyMutation={useMoviesDestroy}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const listData = useMoviesList({
|
const listData = useMoviesList({
|
||||||
renderContent,
|
renderContent,
|
||||||
addKeybinds,
|
addKeybinds,
|
||||||
otherOperations,
|
otherOperations,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
persistState: true,
|
persistState: true,
|
||||||
|
renderDeleteDialog,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function viewRandom(
|
async function viewRandom(
|
||||||
|
|||||||
@@ -2,13 +2,19 @@ import _ from "lodash";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { FindPerformersQueryResult } from "src/core/generated-graphql";
|
import {
|
||||||
import { queryFindPerformers } from "src/core/StashService";
|
FindPerformersQueryResult,
|
||||||
|
SlimPerformerDataFragment,
|
||||||
|
} from "src/core/generated-graphql";
|
||||||
|
import {
|
||||||
|
queryFindPerformers,
|
||||||
|
usePerformersDestroy,
|
||||||
|
} from "src/core/StashService";
|
||||||
import { usePerformersList } from "src/hooks";
|
import { usePerformersList } from "src/hooks";
|
||||||
import { showWhenSelected } from "src/hooks/ListHook";
|
import { showWhenSelected } from "src/hooks/ListHook";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { ExportDialog } from "src/components/Shared/ExportDialog";
|
import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
|
||||||
import { PerformerCard } from "./PerformerCard";
|
import { PerformerCard } from "./PerformerCard";
|
||||||
import { PerformerListTable } from "./PerformerListTable";
|
import { PerformerListTable } from "./PerformerListTable";
|
||||||
|
|
||||||
@@ -76,12 +82,26 @@ export const PerformerList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderDeleteDialog = (
|
||||||
|
selectedPerformers: SlimPerformerDataFragment[],
|
||||||
|
onClose: (confirmed: boolean) => void
|
||||||
|
) => (
|
||||||
|
<DeleteEntityDialog
|
||||||
|
selected={selectedPerformers}
|
||||||
|
onClose={onClose}
|
||||||
|
singularEntity="performer"
|
||||||
|
pluralEntity="performers"
|
||||||
|
destroyMutation={usePerformersDestroy}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const listData = usePerformersList({
|
const listData = usePerformersList({
|
||||||
otherOperations,
|
otherOperations,
|
||||||
renderContent,
|
renderContent,
|
||||||
addKeybinds,
|
addKeybinds,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
persistState: true,
|
persistState: true,
|
||||||
|
renderDeleteDialog,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getRandom(
|
async function getRandom(
|
||||||
|
|||||||
@@ -76,11 +76,13 @@ export const DeleteScenesDialog: React.FC<IDeleteSceneDialogProps> = (
|
|||||||
</p>
|
</p>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
|
id="delete-file"
|
||||||
checked={deleteFile}
|
checked={deleteFile}
|
||||||
label="Delete file"
|
label="Delete file"
|
||||||
onChange={() => setDeleteFile(!deleteFile)}
|
onChange={() => setDeleteFile(!deleteFile)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
|
id="delete-generated"
|
||||||
checked={deleteGenerated}
|
checked={deleteGenerated}
|
||||||
label="Delete generated supporting files"
|
label="Delete generated supporting files"
|
||||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||||
|
|||||||
@@ -68,13 +68,18 @@ export const SceneList: React.FC<ISceneList> = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderDeleteDialog = (
|
||||||
|
selectedScenes: SlimSceneDataFragment[],
|
||||||
|
onClose: (confirmed: boolean) => void
|
||||||
|
) => <DeleteScenesDialog selected={selectedScenes} onClose={onClose} />;
|
||||||
|
|
||||||
const listData = useScenesList({
|
const listData = useScenesList({
|
||||||
zoomable: true,
|
zoomable: true,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
otherOperations,
|
otherOperations,
|
||||||
renderContent,
|
renderContent,
|
||||||
renderEditDialog: renderEditScenesDialog,
|
renderEditDialog: renderEditScenesDialog,
|
||||||
renderDeleteDialog: renderDeleteScenesDialog,
|
renderDeleteDialog,
|
||||||
filterHook,
|
filterHook,
|
||||||
addKeybinds,
|
addKeybinds,
|
||||||
persistState,
|
persistState,
|
||||||
@@ -166,17 +171,6 @@ export const SceneList: React.FC<ISceneList> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDeleteScenesDialog(
|
|
||||||
selectedScenes: SlimSceneDataFragment[],
|
|
||||||
onClose: (confirmed: boolean) => void
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DeleteScenesDialog selected={selectedScenes} onClose={onClose} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSceneCard(
|
function renderSceneCard(
|
||||||
scene: SlimSceneDataFragment,
|
scene: SlimSceneDataFragment,
|
||||||
selectedIds: Set<string>,
|
selectedIds: Set<string>,
|
||||||
|
|||||||
123
ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx
Normal file
123
ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import { FetchResult } from "@apollo/client";
|
||||||
|
|
||||||
|
import { Modal } from "src/components/Shared";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
|
||||||
|
interface IDeletionEntity {
|
||||||
|
id: string;
|
||||||
|
name?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DestroyMutation = (input: {
|
||||||
|
ids: string[];
|
||||||
|
}) => [() => Promise<FetchResult>, {}];
|
||||||
|
|
||||||
|
interface IDeleteEntityDialogProps {
|
||||||
|
selected: IDeletionEntity[];
|
||||||
|
onClose: (confirmed: boolean) => void;
|
||||||
|
singularEntity: string;
|
||||||
|
pluralEntity: string;
|
||||||
|
destroyMutation: DestroyMutation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
deleteHeader: {
|
||||||
|
id: "delete-header",
|
||||||
|
defaultMessage:
|
||||||
|
"Delete {count, plural, =1 {{singularEntity}} other {{pluralEntity}}}",
|
||||||
|
},
|
||||||
|
deleteToast: {
|
||||||
|
id: "delete-toast",
|
||||||
|
defaultMessage:
|
||||||
|
"Deleted {count, plural, =1 {{singularEntity}} other {{pluralEntity}}}",
|
||||||
|
},
|
||||||
|
deleteMessage: {
|
||||||
|
id: "delete-message",
|
||||||
|
defaultMessage:
|
||||||
|
"Are you sure you want to delete {count, plural, =1 {this {singularEntity}} other {these {pluralEntity}}}?",
|
||||||
|
},
|
||||||
|
overflowMessage: {
|
||||||
|
id: "overflow-message",
|
||||||
|
defaultMessage:
|
||||||
|
"...and {count} other {count, plural, =1 {{ singularEntity}} other {{ pluralEntity }}}.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const DeleteEntityDialog: React.FC<IDeleteEntityDialogProps> = ({
|
||||||
|
selected,
|
||||||
|
onClose,
|
||||||
|
singularEntity,
|
||||||
|
pluralEntity,
|
||||||
|
destroyMutation,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const Toast = useToast();
|
||||||
|
const [deleteEntities] = destroyMutation({ ids: selected.map((p) => p.id) });
|
||||||
|
const count = selected.length;
|
||||||
|
|
||||||
|
// Network state
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
|
async function onDelete() {
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await deleteEntities();
|
||||||
|
Toast.success({
|
||||||
|
content: intl.formatMessage(messages.deleteToast, {
|
||||||
|
count,
|
||||||
|
singularEntity,
|
||||||
|
pluralEntity,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
setIsDeleting(false);
|
||||||
|
onClose(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show
|
||||||
|
icon="trash-alt"
|
||||||
|
header={intl.formatMessage(messages.deleteHeader, {
|
||||||
|
count,
|
||||||
|
singularEntity,
|
||||||
|
pluralEntity,
|
||||||
|
})}
|
||||||
|
accept={{ variant: "danger", onClick: onDelete, text: "Delete" }}
|
||||||
|
cancel={{
|
||||||
|
onClick: () => onClose(false),
|
||||||
|
text: "Cancel",
|
||||||
|
variant: "secondary",
|
||||||
|
}}
|
||||||
|
isRunning={isDeleting}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
values={{ count, singularEntity, pluralEntity }}
|
||||||
|
{...messages.deleteMessage}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{selected.slice(0, 10).map((s) => (
|
||||||
|
<li>{s.name}</li>
|
||||||
|
))}
|
||||||
|
{selected.length > 10 && (
|
||||||
|
<FormattedMessage
|
||||||
|
values={{
|
||||||
|
count: selected.length - 10,
|
||||||
|
singularEntity,
|
||||||
|
pluralEntity,
|
||||||
|
}}
|
||||||
|
{...messages.overflowMessage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteEntityDialog;
|
||||||
@@ -24,3 +24,5 @@ export { default as ErrorMessage } from "./ErrorMessage";
|
|||||||
export { default as TruncatedText } from "./TruncatedText";
|
export { default as TruncatedText } from "./TruncatedText";
|
||||||
export { BasicCard } from "./BasicCard";
|
export { BasicCard } from "./BasicCard";
|
||||||
export { RatingStars } from "./RatingStars";
|
export { RatingStars } from "./RatingStars";
|
||||||
|
export { ExportDialog } from "./ExportDialog";
|
||||||
|
export { default as DeleteEntityDialog } from "./DeleteEntityDialog";
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import React, { useState } from "react";
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { FindStudiosQueryResult } from "src/core/generated-graphql";
|
import {
|
||||||
|
FindStudiosQueryResult,
|
||||||
|
SlimStudioDataFragment,
|
||||||
|
} from "src/core/generated-graphql";
|
||||||
import { useStudiosList } from "src/hooks";
|
import { useStudiosList } from "src/hooks";
|
||||||
import { showWhenSelected } from "src/hooks/ListHook";
|
import { showWhenSelected } from "src/hooks/ListHook";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { queryFindStudios } from "src/core/StashService";
|
import { queryFindStudios, useStudiosDestroy } from "src/core/StashService";
|
||||||
import { ExportDialog } from "../Shared/ExportDialog";
|
import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
|
||||||
import { StudioCard } from "./StudioCard";
|
import { StudioCard } from "./StudioCard";
|
||||||
|
|
||||||
interface IStudioList {
|
interface IStudioList {
|
||||||
@@ -109,6 +112,19 @@ export const StudioList: React.FC<IStudioList> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderDeleteDialog = (
|
||||||
|
selectedStudios: SlimStudioDataFragment[],
|
||||||
|
onClose: (confirmed: boolean) => void
|
||||||
|
) => (
|
||||||
|
<DeleteEntityDialog
|
||||||
|
selected={selectedStudios}
|
||||||
|
onClose={onClose}
|
||||||
|
singularEntity="studio"
|
||||||
|
pluralEntity="studios"
|
||||||
|
destroyMutation={useStudiosDestroy}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const listData = useStudiosList({
|
const listData = useStudiosList({
|
||||||
renderContent,
|
renderContent,
|
||||||
filterHook,
|
filterHook,
|
||||||
@@ -116,6 +132,7 @@ export const StudioList: React.FC<IStudioList> = ({
|
|||||||
otherOperations,
|
otherOperations,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
persistState: !fromParent,
|
persistState: !fromParent,
|
||||||
|
renderDeleteDialog,
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderStudios(
|
function renderStudios(
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import {
|
|||||||
queryFindTags,
|
queryFindTags,
|
||||||
mutateMetadataAutoTag,
|
mutateMetadataAutoTag,
|
||||||
useTagDestroy,
|
useTagDestroy,
|
||||||
|
useTagsDestroy,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { FormattedNumber } from "react-intl";
|
import { FormattedNumber } from "react-intl";
|
||||||
import { NavUtils } from "src/utils";
|
import { NavUtils } from "src/utils";
|
||||||
import { Icon, Modal } from "src/components/Shared";
|
import { Icon, Modal, DeleteEntityDialog } from "src/components/Shared";
|
||||||
import { TagCard } from "./TagCard";
|
import { TagCard } from "./TagCard";
|
||||||
import { ExportDialog } from "../Shared/ExportDialog";
|
import { ExportDialog } from "../Shared/ExportDialog";
|
||||||
|
|
||||||
@@ -121,6 +122,19 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderDeleteDialog = (
|
||||||
|
selectedTags: GQL.TagDataFragment[],
|
||||||
|
onClose: (confirmed: boolean) => void
|
||||||
|
) => (
|
||||||
|
<DeleteEntityDialog
|
||||||
|
selected={selectedTags}
|
||||||
|
onClose={onClose}
|
||||||
|
singularEntity="tag"
|
||||||
|
pluralEntity="tags"
|
||||||
|
destroyMutation={useTagsDestroy}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const listData = useTagsList({
|
const listData = useTagsList({
|
||||||
renderContent,
|
renderContent,
|
||||||
filterHook,
|
filterHook,
|
||||||
@@ -130,6 +144,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
|||||||
zoomable: true,
|
zoomable: true,
|
||||||
defaultZoomIndex: 0,
|
defaultZoomIndex: 0,
|
||||||
persistState: true,
|
persistState: true,
|
||||||
|
renderDeleteDialog,
|
||||||
});
|
});
|
||||||
|
|
||||||
function getDeleteTagInput() {
|
function getDeleteTagInput() {
|
||||||
|
|||||||
@@ -309,6 +309,18 @@ export const usePerformerDestroy = () =>
|
|||||||
update: deleteCache(performerMutationImpactedQueries),
|
update: deleteCache(performerMutationImpactedQueries),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const usePerformersDestroy = (
|
||||||
|
variables: GQL.PerformersDestroyMutationVariables
|
||||||
|
) =>
|
||||||
|
GQL.usePerformersDestroyMutation({
|
||||||
|
variables,
|
||||||
|
refetchQueries: getQueryNames([
|
||||||
|
GQL.FindPerformersDocument,
|
||||||
|
GQL.AllPerformersForFilterDocument,
|
||||||
|
]),
|
||||||
|
update: deleteCache(performerMutationImpactedQueries),
|
||||||
|
});
|
||||||
|
|
||||||
const sceneMutationImpactedQueries = [
|
const sceneMutationImpactedQueries = [
|
||||||
GQL.FindPerformerDocument,
|
GQL.FindPerformerDocument,
|
||||||
GQL.FindPerformersDocument,
|
GQL.FindPerformersDocument,
|
||||||
@@ -562,6 +574,12 @@ export const useStudioDestroy = (input: GQL.StudioDestroyInput) =>
|
|||||||
update: deleteCache(studioMutationImpactedQueries),
|
update: deleteCache(studioMutationImpactedQueries),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useStudiosDestroy = (input: GQL.StudiosDestroyMutationVariables) =>
|
||||||
|
GQL.useStudiosDestroyMutation({
|
||||||
|
variables: input,
|
||||||
|
update: deleteCache(studioMutationImpactedQueries),
|
||||||
|
});
|
||||||
|
|
||||||
export const movieMutationImpactedQueries = [
|
export const movieMutationImpactedQueries = [
|
||||||
GQL.FindSceneDocument,
|
GQL.FindSceneDocument,
|
||||||
GQL.FindScenesDocument,
|
GQL.FindScenesDocument,
|
||||||
@@ -589,6 +607,12 @@ export const useMovieDestroy = (input: GQL.MovieDestroyInput) =>
|
|||||||
update: deleteCache(movieMutationImpactedQueries),
|
update: deleteCache(movieMutationImpactedQueries),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useMoviesDestroy = (input: GQL.MoviesDestroyMutationVariables) =>
|
||||||
|
GQL.useMoviesDestroyMutation({
|
||||||
|
variables: input,
|
||||||
|
update: deleteCache(movieMutationImpactedQueries),
|
||||||
|
});
|
||||||
|
|
||||||
export const tagMutationImpactedQueries = [
|
export const tagMutationImpactedQueries = [
|
||||||
GQL.FindSceneDocument,
|
GQL.FindSceneDocument,
|
||||||
GQL.FindScenesDocument,
|
GQL.FindScenesDocument,
|
||||||
@@ -622,6 +646,12 @@ export const useTagDestroy = (input: GQL.TagDestroyInput) =>
|
|||||||
update: deleteCache(tagMutationImpactedQueries),
|
update: deleteCache(tagMutationImpactedQueries),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useTagsDestroy = (input: GQL.TagsDestroyMutationVariables) =>
|
||||||
|
GQL.useTagsDestroyMutation({
|
||||||
|
variables: input,
|
||||||
|
update: deleteCache(tagMutationImpactedQueries),
|
||||||
|
});
|
||||||
|
|
||||||
export const useConfigureGeneral = (input: GQL.ConfigGeneralInput) =>
|
export const useConfigureGeneral = (input: GQL.ConfigGeneralInput) =>
|
||||||
GQL.useConfigureGeneralMutation({
|
GQL.useConfigureGeneralMutation({
|
||||||
variables: { input },
|
variables: { input },
|
||||||
|
|||||||
Reference in New Issue
Block a user