From cfe263683790f765d3223e007f2a4ecc3947d46b Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 15 Aug 2019 17:32:57 +1000 Subject: [PATCH 1/5] Add delete scene button --- graphql/documents/mutations/scene.graphql | 4 + graphql/schema/schema.graphql | 1 + graphql/schema/types/scene.graphql | 5 + pkg/api/resolver_mutation_scene.go | 60 ++++++++++- pkg/models/generated_exec.go | 101 ++++++++++++++++++ pkg/models/generated_models.go | 5 + pkg/models/querybuilder_joins.go | 46 ++++++++ pkg/models/querybuilder_scene.go | 8 +- .../components/scenes/SceneDetails/Scene.tsx | 7 +- .../scenes/SceneDetails/SceneEditPanel.tsx | 47 ++++++++ ui/v2/src/core/StashService.ts | 4 + ui/v2/src/core/generated-graphql.tsx | 35 +++++- 12 files changed, 317 insertions(+), 6 deletions(-) diff --git a/graphql/documents/mutations/scene.graphql b/graphql/documents/mutations/scene.graphql index aa7cea886..d2b1a96a7 100644 --- a/graphql/documents/mutations/scene.graphql +++ b/graphql/documents/mutations/scene.graphql @@ -24,4 +24,8 @@ mutation SceneUpdate( }) { ...SceneData } +} + +mutation SceneDestroy($id: ID!, $delete_file: Boolean) { + sceneDestroy(input: {id: $id, delete_file: $delete_file}) } \ No newline at end of file diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index 862da0847..80c473069 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -72,6 +72,7 @@ type Query { type Mutation { sceneUpdate(input: SceneUpdateInput!): Scene + sceneDestroy(input: SceneDestroyInput!): Boolean! sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker diff --git a/graphql/schema/types/scene.graphql b/graphql/schema/types/scene.graphql index bf9a55fd2..ce9f8fecb 100644 --- a/graphql/schema/types/scene.graphql +++ b/graphql/schema/types/scene.graphql @@ -53,6 +53,11 @@ input SceneUpdateInput { tag_ids: [ID!] } +input SceneDestroyInput { + id: ID! + delete_file: Boolean +} + type FindScenesResultType { count: Int! scenes: [Scene!]! diff --git a/pkg/api/resolver_mutation_scene.go b/pkg/api/resolver_mutation_scene.go index 3cf379d41..221cf92bf 100644 --- a/pkg/api/resolver_mutation_scene.go +++ b/pkg/api/resolver_mutation_scene.go @@ -3,10 +3,13 @@ package api import ( "context" "database/sql" - "github.com/stashapp/stash/pkg/database" - "github.com/stashapp/stash/pkg/models" + "os" "strconv" "time" + + "github.com/stashapp/stash/pkg/database" + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/models" ) func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (*models.Scene, error) { @@ -101,6 +104,59 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp return scene, nil } +func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) { + qb := models.NewSceneQueryBuilder() + jqb := models.NewJoinsQueryBuilder() + tx := database.DB.MustBeginTx(ctx, nil) + + sceneID, _ := strconv.Atoi(input.ID) + + scene, err := qb.Find(sceneID) + if err != nil { + _ = tx.Rollback() + return false, err + } + + if err := jqb.DestroyScenesTags(sceneID, tx); err != nil { + _ = tx.Rollback() + return false, err + } + + if err := jqb.DestroyPerformersScenes(sceneID, tx); err != nil { + _ = tx.Rollback() + return false, err + } + + if err := jqb.DestroyScenesMarkers(sceneID, tx); err != nil { + _ = tx.Rollback() + return false, err + } + + if err := jqb.DestroyScenesGalleries(sceneID, tx); err != nil { + _ = tx.Rollback() + return false, err + } + + if err := qb.Destroy(input.ID, tx); err != nil { + _ = tx.Rollback() + return false, err + } + if err := tx.Commit(); err != nil { + return false, err + } + + // if delete file is true, then delete the file as well + // if it fails, just log a message + if input.DeleteFile != nil && *input.DeleteFile { + err = os.Remove(scene.Path) + if err != nil { + logger.Warnf("Could not delete file %s: %s", scene.Path, err.Error()) + } + } + + return true, nil +} + func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input models.SceneMarkerCreateInput) (*models.SceneMarker, error) { primaryTagID, _ := strconv.Atoi(input.PrimaryTagID) sceneID, _ := strconv.Atoi(input.SceneID) diff --git a/pkg/models/generated_exec.go b/pkg/models/generated_exec.go index 3964dd1c9..3a99f83e1 100644 --- a/pkg/models/generated_exec.go +++ b/pkg/models/generated_exec.go @@ -109,6 +109,7 @@ type ComplexityRoot struct { ConfigureGeneral func(childComplexity int, input ConfigGeneralInput) int PerformerCreate func(childComplexity int, input PerformerCreateInput) int PerformerUpdate func(childComplexity int, input PerformerUpdateInput) int + SceneDestroy func(childComplexity int, input SceneDestroyInput) int SceneMarkerCreate func(childComplexity int, input SceneMarkerCreateInput) int SceneMarkerDestroy func(childComplexity int, id string) int SceneMarkerUpdate func(childComplexity int, input SceneMarkerUpdateInput) int @@ -283,6 +284,7 @@ type GalleryResolver interface { } type MutationResolver interface { SceneUpdate(ctx context.Context, input SceneUpdateInput) (*Scene, error) + SceneDestroy(ctx context.Context, input SceneDestroyInput) (bool, error) SceneMarkerCreate(ctx context.Context, input SceneMarkerCreateInput) (*SceneMarker, error) SceneMarkerUpdate(ctx context.Context, input SceneMarkerUpdateInput) (*SceneMarker, error) SceneMarkerDestroy(ctx context.Context, id string) (bool, error) @@ -610,6 +612,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.PerformerUpdate(childComplexity, args["input"].(PerformerUpdateInput)), true + case "Mutation.sceneDestroy": + if e.complexity.Mutation.SceneDestroy == nil { + break + } + + args, err := ec.field_Mutation_sceneDestroy_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.SceneDestroy(childComplexity, args["input"].(SceneDestroyInput)), true + case "Mutation.sceneMarkerCreate": if e.complexity.Mutation.SceneMarkerCreate == nil { break @@ -1833,6 +1847,7 @@ type Query { type Mutation { sceneUpdate(input: SceneUpdateInput!): Scene + sceneDestroy(input: SceneDestroyInput!): Boolean! sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker @@ -2158,6 +2173,11 @@ input SceneUpdateInput { tag_ids: [ID!] } +input SceneDestroyInput { + id: ID! + delete_file: Boolean +} + type FindScenesResultType { count: Int! scenes: [Scene!]! @@ -2284,6 +2304,20 @@ func (ec *executionContext) field_Mutation_performerUpdate_args(ctx context.Cont return args, nil } +func (ec *executionContext) field_Mutation_sceneDestroy_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 SceneDestroyInput + if tmp, ok := rawArgs["input"]; ok { + arg0, err = ec.unmarshalNSceneDestroyInput2githubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐSceneDestroyInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_sceneMarkerCreate_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3467,6 +3501,40 @@ func (ec *executionContext) _Mutation_sceneUpdate(ctx context.Context, field gra return ec.marshalOScene2ᚖgithubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐScene(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_sceneDestroy(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + } + ctx = graphql.WithResolverContext(ctx, rctx) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_sceneDestroy_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + rctx.Args = args + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().SceneDestroy(rctx, args["input"].(SceneDestroyInput)) + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_sceneMarkerCreate(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() @@ -8269,6 +8337,30 @@ func (ec *executionContext) unmarshalInputPerformerUpdateInput(ctx context.Conte return it, nil } +func (ec *executionContext) unmarshalInputSceneDestroyInput(ctx context.Context, v interface{}) (SceneDestroyInput, error) { + var it SceneDestroyInput + var asMap = v.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "id": + var err error + it.ID, err = ec.unmarshalNID2string(ctx, v) + if err != nil { + return it, err + } + case "delete_file": + var err error + it.DeleteFile, err = ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputSceneFilterType(ctx context.Context, v interface{}) (SceneFilterType, error) { var it SceneFilterType var asMap = v.(map[string]interface{}) @@ -9032,6 +9124,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = graphql.MarshalString("Mutation") case "sceneUpdate": out.Values[i] = ec._Mutation_sceneUpdate(ctx, field) + case "sceneDestroy": + out.Values[i] = ec._Mutation_sceneDestroy(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "sceneMarkerCreate": out.Values[i] = ec._Mutation_sceneMarkerCreate(ctx, field) case "sceneMarkerUpdate": @@ -11065,6 +11162,10 @@ func (ec *executionContext) marshalNScene2ᚖgithubᚗcomᚋstashappᚋstashᚋp return ec._Scene(ctx, sel, v) } +func (ec *executionContext) unmarshalNSceneDestroyInput2githubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐSceneDestroyInput(ctx context.Context, v interface{}) (SceneDestroyInput, error) { + return ec.unmarshalInputSceneDestroyInput(ctx, v) +} + func (ec *executionContext) marshalNSceneFileType2githubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐSceneFileType(ctx context.Context, sel ast.SelectionSet, v SceneFileType) graphql.Marshaler { return ec._SceneFileType(ctx, sel, &v) } diff --git a/pkg/models/generated_models.go b/pkg/models/generated_models.go index 4c1a780a8..16be350f3 100644 --- a/pkg/models/generated_models.go +++ b/pkg/models/generated_models.go @@ -136,6 +136,11 @@ type PerformerUpdateInput struct { Image *string `json:"image"` } +type SceneDestroyInput struct { + ID string `json:"id"` + DeleteFile *bool `json:"delete_file"` +} + type SceneFileType struct { Size *string `json:"size"` Duration *float64 `json:"duration"` diff --git a/pkg/models/querybuilder_joins.go b/pkg/models/querybuilder_joins.go index efcf435f2..a16961ca7 100644 --- a/pkg/models/querybuilder_joins.go +++ b/pkg/models/querybuilder_joins.go @@ -33,6 +33,14 @@ func (qb *JoinsQueryBuilder) UpdatePerformersScenes(sceneID int, updatedJoins [] return qb.CreatePerformersScenes(updatedJoins, tx) } +func (qb *JoinsQueryBuilder) DestroyPerformersScenes(sceneID int, tx *sqlx.Tx) error { + ensureTx(tx) + + // Delete the existing joins + _, err := tx.Exec("DELETE FROM performers_scenes WHERE scene_id = ?", sceneID) + return err +} + func (qb *JoinsQueryBuilder) CreateScenesTags(newJoins []ScenesTags, tx *sqlx.Tx) error { ensureTx(tx) for _, join := range newJoins { @@ -58,6 +66,15 @@ func (qb *JoinsQueryBuilder) UpdateScenesTags(sceneID int, updatedJoins []Scenes return qb.CreateScenesTags(updatedJoins, tx) } +func (qb *JoinsQueryBuilder) DestroyScenesTags(sceneID int, tx *sqlx.Tx) error { + ensureTx(tx) + + // Delete the existing joins + _, err := tx.Exec("DELETE FROM scenes_tags WHERE scene_id = ?", sceneID) + + return err +} + func (qb *JoinsQueryBuilder) CreateSceneMarkersTags(newJoins []SceneMarkersTags, tx *sqlx.Tx) error { ensureTx(tx) for _, join := range newJoins { @@ -82,3 +99,32 @@ func (qb *JoinsQueryBuilder) UpdateSceneMarkersTags(sceneMarkerID int, updatedJo } return qb.CreateSceneMarkersTags(updatedJoins, tx) } + +func (qb *JoinsQueryBuilder) DestroySceneMarkersTags(sceneMarkerID int, updatedJoins []SceneMarkersTags, tx *sqlx.Tx) error { + ensureTx(tx) + + // Delete the existing joins + _, err := tx.Exec("DELETE FROM scene_markers_tags WHERE scene_marker_id = ?", sceneMarkerID) + return err +} + +func (qb *JoinsQueryBuilder) DestroyScenesGalleries(sceneID int, tx *sqlx.Tx) error { + ensureTx(tx) + + // Unset the existing scene id from galleries + _, err := tx.Exec("UPDATE galleries SET scene_id = null WHERE scene_id = ?", sceneID) + + return err +} + +func (qb *JoinsQueryBuilder) DestroyScenesMarkers(sceneID int, tx *sqlx.Tx) error { + ensureTx(tx) + + // Delete the scene marker tags + _, err := tx.Exec("DELETE t FROM scene_markers_tags t join scene_markers m on t.scene_marker_id = m.id WHERE m.scene_id = ?", sceneID) + + // Delete the existing joins + _, err = tx.Exec("DELETE FROM scene_markers WHERE scene_id = ?", sceneID) + + return err +} diff --git a/pkg/models/querybuilder_scene.go b/pkg/models/querybuilder_scene.go index ee6090e95..312d37364 100644 --- a/pkg/models/querybuilder_scene.go +++ b/pkg/models/querybuilder_scene.go @@ -2,10 +2,11 @@ package models import ( "database/sql" - "github.com/jmoiron/sqlx" - "github.com/stashapp/stash/pkg/database" "strconv" "strings" + + "github.com/jmoiron/sqlx" + "github.com/stashapp/stash/pkg/database" ) const scenesForPerformerQuery = ` @@ -76,6 +77,9 @@ func (qb *SceneQueryBuilder) Update(updatedScene Scene, tx *sqlx.Tx) (*Scene, er return &updatedScene, nil } +func (qb *SceneQueryBuilder) Destroy(id string, tx *sqlx.Tx) error { + return executeDeleteQuery("scenes", id, tx) +} func (qb *SceneQueryBuilder) Find(id int) (*Scene, error) { query := "SELECT * FROM scenes WHERE id = ? LIMIT 1" args := []interface{}{id} diff --git a/ui/v2/src/components/scenes/SceneDetails/Scene.tsx b/ui/v2/src/components/scenes/SceneDetails/Scene.tsx index c8ff0a051..b2ba64b10 100644 --- a/ui/v2/src/components/scenes/SceneDetails/Scene.tsx +++ b/ui/v2/src/components/scenes/SceneDetails/Scene.tsx @@ -82,7 +82,12 @@ export const Scene: FunctionComponent = (props: ISceneProps) => { setScene(newScene)} />} + panel={ + setScene(newScene)} + onDelete={() => props.history.push("/scenes")} + />} /> diff --git a/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx index d8f40542a..76e2bbcdb 100644 --- a/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx @@ -1,4 +1,5 @@ import { + Alert, Button, FormGroup, HTMLSelect, @@ -19,6 +20,7 @@ import { ValidGalleriesSelect } from "../../select/ValidGalleriesSelect"; interface IProps { scene: GQL.SceneDataFragment; onUpdate: (scene: GQL.SceneDataFragment) => void; + onDelete: () => void; } export const SceneEditPanel: FunctionComponent = (props: IProps) => { @@ -33,10 +35,14 @@ export const SceneEditPanel: FunctionComponent = (props: IProps) => { const [performerIds, setPerformerIds] = useState(undefined); const [tagIds, setTagIds] = useState(undefined); + const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); + const [deleteFile, setDeleteFile] = useState(false); + // Network state const [isLoading, setIsLoading] = useState(false); const updateScene = StashService.useSceneUpdate(getSceneInput()); + const deleteScene = StashService.useSceneDestroy(getSceneDeleteInput()); function updateSceneEditState(state: Partial) { const perfIds = !!state.performers ? state.performers.map((performer) => performer.id) : undefined; @@ -89,6 +95,27 @@ export const SceneEditPanel: FunctionComponent = (props: IProps) => { setIsLoading(false); } + function getSceneDeleteInput(): GQL.SceneDestroyInput { + return { + id: props.scene.id, + delete_file: deleteFile + }; + } + + async function onDelete() { + setIsDeleteAlertOpen(false); + setIsLoading(true); + try { + await deleteScene(); + ToastUtils.success("Deleted scene"); + } catch (e) { + ErrorUtils.handle(e); + } + setIsLoading(false); + + props.onDelete(); + } + function renderMultiSelect(type: "performers" | "tags", initialIds: string[] | undefined) { return ( = (props: IProps) => { ); } + function renderDeleteAlert() { + return ( + setIsDeleteAlertOpen(false)} + onConfirm={() => onDelete()} + > +

+ Are you sure you want to delete this scene? Unless the file is also deleted, this scene will be re-added when scan is performed. +

+
+ ); + } + return ( <> + {renderDeleteAlert()} {isLoading ? : undefined}
@@ -171,6 +217,7 @@ export const SceneEditPanel: FunctionComponent = (props: IProps) => {
+ + + + + ); } diff --git a/ui/v2/src/core/generated-graphql.tsx b/ui/v2/src/core/generated-graphql.tsx index 3bb636e50..979497be7 100644 --- a/ui/v2/src/core/generated-graphql.tsx +++ b/ui/v2/src/core/generated-graphql.tsx @@ -1,6 +1,6 @@ /* tslint:disable */ /* eslint-disable */ -// Generated in 2019-08-15T13:55:54+10:00 +// Generated in 2019-08-15T18:05:18+10:00 export type Maybe = T | undefined; export interface SceneFilterType { @@ -92,6 +92,8 @@ export interface SceneDestroyInput { id: string; delete_file?: Maybe; + + delete_generated?: Maybe; } export interface SceneMarkerCreateInput { @@ -399,6 +401,7 @@ export type SceneUpdateSceneUpdate = SceneDataFragment; export type SceneDestroyVariables = { id: string; delete_file?: Maybe; + delete_generated?: Maybe; }; export type SceneDestroyMutation = { @@ -1792,8 +1795,18 @@ export function useSceneUpdate( >(SceneUpdateDocument, baseOptions); } export const SceneDestroyDocument = gql` - mutation SceneDestroy($id: ID!, $delete_file: Boolean) { - sceneDestroy(input: { id: $id, delete_file: $delete_file }) + mutation SceneDestroy( + $id: ID! + $delete_file: Boolean + $delete_generated: Boolean + ) { + sceneDestroy( + input: { + id: $id + delete_file: $delete_file + delete_generated: $delete_generated + } + ) } `; export function useSceneDestroy( From d7d164ee992df5e3bb60f956ddebd7c7a94423c6 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 16 Aug 2019 07:52:05 +1000 Subject: [PATCH 3/5] Add margin between edit and delete button --- .../src/components/scenes/SceneDetails/SceneEditPanel.tsx | 4 ++-- ui/v2/src/index.scss | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx index 6bba97acf..bdb99179f 100644 --- a/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx @@ -232,8 +232,8 @@ export const SceneEditPanel: FunctionComponent = (props: IProps) => { {renderMultiSelect("tags", tagIds)} -