diff --git a/pkg/api/resolver_mutation_scene.go b/pkg/api/resolver_mutation_scene.go index 3cf379d41..8cbfa1e86 100644 --- a/pkg/api/resolver_mutation_scene.go +++ b/pkg/api/resolver_mutation_scene.go @@ -3,10 +3,11 @@ package api import ( "context" "database/sql" - "github.com/stashapp/stash/pkg/database" - "github.com/stashapp/stash/pkg/models" "strconv" "time" + + "github.com/stashapp/stash/pkg/database" + "github.com/stashapp/stash/pkg/models" ) func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (*models.Scene, error) { @@ -29,12 +30,20 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp if input.Date != nil { updatedScene.Date = models.SQLiteDate{String: *input.Date, Valid: true} } + if input.Rating != nil { updatedScene.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true} + } else { + // rating must be nullable + updatedScene.Rating = sql.NullInt64{Valid: false} } + if input.StudioID != nil { studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64) updatedScene.StudioID = sql.NullInt64{Int64: studioID, Valid: true} + } else { + // studio must be nullable + updatedScene.StudioID = sql.NullInt64{Valid: false} } // Start the transaction and save the scene marker @@ -47,6 +56,14 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp return nil, err } + // Clear the existing gallery value + gqb := models.NewGalleryQueryBuilder() + err = gqb.ClearGalleryId(sceneID, tx) + if err != nil { + _ = tx.Rollback() + return nil, err + } + if input.GalleryID != nil { // Save the gallery galleryID, _ := strconv.Atoi(*input.GalleryID) diff --git a/pkg/models/querybuilder_gallery.go b/pkg/models/querybuilder_gallery.go index 95c1f54a5..f77fe0de2 100644 --- a/pkg/models/querybuilder_gallery.go +++ b/pkg/models/querybuilder_gallery.go @@ -2,9 +2,10 @@ package models import ( "database/sql" + "path/filepath" + "github.com/jmoiron/sqlx" "github.com/stashapp/stash/pkg/database" - "path/filepath" ) type GalleryQueryBuilder struct{} @@ -50,6 +51,24 @@ func (qb *GalleryQueryBuilder) Update(updatedGallery Gallery, tx *sqlx.Tx) (*Gal return &updatedGallery, nil } +type GalleryNullSceneID struct { + SceneID sql.NullInt64 +} + +func (qb *GalleryQueryBuilder) ClearGalleryId(sceneID int, tx *sqlx.Tx) error { + ensureTx(tx) + _, err := tx.NamedExec( + `UPDATE galleries SET scene_id = null WHERE scene_id = :sceneid`, + GalleryNullSceneID{ + SceneID: sql.NullInt64{ + Int64: int64(sceneID), + Valid: true, + }, + }, + ) + return err +} + func (qb *GalleryQueryBuilder) Find(id int) (*Gallery, error) { query := "SELECT * FROM galleries WHERE id = ? LIMIT 1" args := []interface{}{id} diff --git a/pkg/models/querybuilder_sql.go b/pkg/models/querybuilder_sql.go index 35de14d29..3f5021cb3 100644 --- a/pkg/models/querybuilder_sql.go +++ b/pkg/models/querybuilder_sql.go @@ -3,13 +3,14 @@ package models import ( "database/sql" "fmt" - "github.com/jmoiron/sqlx" - "github.com/stashapp/stash/pkg/database" - "github.com/stashapp/stash/pkg/logger" "math/rand" "reflect" "strconv" "strings" + + "github.com/jmoiron/sqlx" + "github.com/stashapp/stash/pkg/database" + "github.com/stashapp/stash/pkg/logger" ) var randomSortFloat = rand.Float64() @@ -266,21 +267,17 @@ func SQLGenKeys(i interface{}) string { query = append(query, fmt.Sprintf("%s=:%s", key, key)) } case sql.NullString: - if t.Valid { - query = append(query, fmt.Sprintf("%s=:%s", key, key)) - } + // should be nullable + query = append(query, fmt.Sprintf("%s=:%s", key, key)) case sql.NullBool: - if t.Valid { - query = append(query, fmt.Sprintf("%s=:%s", key, key)) - } + // should be nullable + query = append(query, fmt.Sprintf("%s=:%s", key, key)) case sql.NullInt64: - if t.Valid { - query = append(query, fmt.Sprintf("%s=:%s", key, key)) - } + // should be nullable + query = append(query, fmt.Sprintf("%s=:%s", key, key)) case sql.NullFloat64: - if t.Valid { - query = append(query, fmt.Sprintf("%s=:%s", key, key)) - } + // should be nullable + query = append(query, fmt.Sprintf("%s=:%s", key, key)) default: reflectValue := reflect.ValueOf(t) kind := reflectValue.Kind() diff --git a/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx index d8f40542a..27cc6a8f6 100644 --- a/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx @@ -46,7 +46,7 @@ export const SceneEditPanel: FunctionComponent = (props: IProps) => { setDetails(state.details); setUrl(state.url); setDate(state.date); - setRating(state.rating); + setRating(state.rating == null ? NaN : state.rating); setGalleryId(state.gallery ? state.gallery.id : undefined); setStudioId(state.studio ? state.studio.id : undefined); setPerformerIds(perfIds); @@ -150,14 +150,14 @@ export const SceneEditPanel: FunctionComponent = (props: IProps) => { setGalleryId(item.id)} + onSelectItem={(item) => setGalleryId(item ? item.id : undefined)} /> setStudioId(item.id)} + onSelectItem={(item) => setStudioId(item ? item.id : undefined)} initialId={studioId} /> diff --git a/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx b/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx index 63f4e71b4..cdd1089df 100644 --- a/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx +++ b/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx @@ -163,7 +163,7 @@ export const SceneMarkersPanel: FunctionComponent = (pr return ( fieldProps.form.setFieldValue("primaryTagId", tag.id)} + onSelectItem={(tag) => fieldProps.form.setFieldValue("primaryTagId", tag ? tag.id : undefined)} initialId={!!editingMarker ? editingMarker.primary_tag.id : undefined} /> ); diff --git a/ui/v2/src/components/select/FilterSelect.tsx b/ui/v2/src/components/select/FilterSelect.tsx index 8e5ebd992..9b91e603c 100644 --- a/ui/v2/src/components/select/FilterSelect.tsx +++ b/ui/v2/src/components/select/FilterSelect.tsx @@ -18,7 +18,12 @@ type ValidTypes = interface IProps extends HTMLInputProps { type: "performers" | "studios" | "tags"; initialId?: string; - onSelectItem: (item: ValidTypes) => void; + onSelectItem: (item: ValidTypes | undefined) => void; +} + +function addNoneOption(items: ValidTypes[]) { + // Add a none option to clear the gallery + if (!items.find((item) => item.id === "0")) { items.unshift({id: "0", name: "None"}); } } export const FilterSelect: React.FunctionComponent = (props: IProps) => { @@ -28,12 +33,14 @@ export const FilterSelect: React.FunctionComponent = (props: IProps) => case "performers": { const { data } = StashService.useAllPerformersForFilter(); items = !!data && !!data.allPerformers ? data.allPerformers : []; + addNoneOption(items); InternalSelect = InternalPerformerSelect; break; } case "studios": { const { data } = StashService.useAllStudiosForFilter(); items = !!data && !!data.allStudios ? data.allStudios : []; + addNoneOption(items); InternalSelect = InternalStudioSelect; break; } @@ -50,7 +57,7 @@ export const FilterSelect: React.FunctionComponent = (props: IProps) => } /* eslint-disable react-hooks/rules-of-hooks */ - const [selectedItem, setSelectedItem] = React.useState(null); + const [selectedItem, setSelectedItem] = React.useState(undefined); const [isInitialized, setIsInitialized] = React.useState(false); /* eslint-enable */ @@ -80,7 +87,11 @@ export const FilterSelect: React.FunctionComponent = (props: IProps) => return item.name!.toLowerCase().indexOf(query.toLowerCase()) >= 0; }; - function onItemSelect(item: ValidTypes) { + function onItemSelect(item: ValidTypes | undefined) { + if (item && item.id == "0") { + item = undefined; + } + props.onSelectItem(item); setSelectedItem(item); } diff --git a/ui/v2/src/components/select/ValidGalleriesSelect.tsx b/ui/v2/src/components/select/ValidGalleriesSelect.tsx index 312cedcec..1c3c140df 100644 --- a/ui/v2/src/components/select/ValidGalleriesSelect.tsx +++ b/ui/v2/src/components/select/ValidGalleriesSelect.tsx @@ -11,7 +11,7 @@ const InternalSelect = Select.ofType void; + onSelectItem: (item: GQL.ValidGalleriesForSceneValidGalleriesForScene | undefined) => void; } export const ValidGalleriesSelect: React.FunctionComponent = (props: IProps) => { @@ -20,7 +20,7 @@ export const ValidGalleriesSelect: React.FunctionComponent = (props: IPr // Add a none option to clear the gallery if (!items.find((item) => item.id === "0")) { items.unshift({id: "0", path: "None"}); } - const [selectedItem, setSelectedItem] = React.useState(null); + const [selectedItem, setSelectedItem] = React.useState(undefined); const [isInitialized, setIsInitialized] = React.useState(false); if (!!props.initialId && !selectedItem && !isInitialized) { @@ -49,7 +49,11 @@ export const ValidGalleriesSelect: React.FunctionComponent = (props: IPr return item.path!.toLowerCase().indexOf(query.toLowerCase()) >= 0; }; - function onItemSelect(item: GQL.ValidGalleriesForSceneValidGalleriesForScene) { + function onItemSelect(item: GQL.ValidGalleriesForSceneValidGalleriesForScene | undefined) { + if (item && item.id == "0") { + item = undefined; + } + props.onSelectItem(item); setSelectedItem(item); } diff --git a/ui/v2/src/utils/table.tsx b/ui/v2/src/utils/table.tsx index 581a5c1a9..8aaf2df63 100644 --- a/ui/v2/src/utils/table.tsx +++ b/ui/v2/src/utils/table.tsx @@ -124,7 +124,7 @@ export class TableUtils { title: string, type: "performers" | "studios" | "tags", initialId: string | undefined, - onChange: ((id: string) => void), + onChange: ((id: string | undefined) => void), }) { return ( @@ -132,7 +132,7 @@ export class TableUtils { options.onChange(item.id)} + onSelectItem={(item) => options.onChange(item ? item.id : undefined)} initialId={options.initialId} />