diff --git a/graphql/schema/types/gallery-chapter.graphql b/graphql/schema/types/gallery-chapter.graphql index 0db36f91d..139e46be8 100644 --- a/graphql/schema/types/gallery-chapter.graphql +++ b/graphql/schema/types/gallery-chapter.graphql @@ -15,9 +15,9 @@ input GalleryChapterCreateInput { input GalleryChapterUpdateInput { id: ID! - gallery_id: ID! - title: String! - image_index: Int! + gallery_id: ID + title: String + image_index: Int } type FindGalleryChaptersResultType { diff --git a/graphql/schema/types/scene-marker.graphql b/graphql/schema/types/scene-marker.graphql index 8e3e54c81..870af3b77 100644 --- a/graphql/schema/types/scene-marker.graphql +++ b/graphql/schema/types/scene-marker.graphql @@ -26,10 +26,10 @@ input SceneMarkerCreateInput { input SceneMarkerUpdateInput { id: ID! - title: String! - seconds: Float! - scene_id: ID! - primary_tag_id: ID! + title: String + seconds: Float + scene_id: ID + primary_tag_id: ID tag_ids: [ID!] } diff --git a/internal/api/resolver_mutation_gallery.go b/internal/api/resolver_mutation_gallery.go index 9ea04e8bd..368808d2c 100644 --- a/internal/api/resolver_mutation_gallery.go +++ b/internal/api/resolver_mutation_gallery.go @@ -498,23 +498,11 @@ func (r *mutationResolver) getGalleryChapter(ctx context.Context, id int) (ret * func (r *mutationResolver) GalleryChapterCreate(ctx context.Context, input GalleryChapterCreateInput) (*models.GalleryChapter, error) { galleryID, err := strconv.Atoi(input.GalleryID) if err != nil { - return nil, err - } - - var imageCount int - if err := r.withTxn(ctx, func(ctx context.Context) error { - imageCount, err = r.repository.Image.CountByGalleryID(ctx, galleryID) - return err - }); err != nil { - return nil, err - } - // Sanity Check of Index - if input.ImageIndex > imageCount || input.ImageIndex < 1 { - return nil, errors.New("Image # must greater than zero and in range of the gallery images") + return nil, fmt.Errorf("converting gallery id: %w", err) } currentTime := time.Now() - newGalleryChapter := models.GalleryChapter{ + newChapter := models.GalleryChapter{ Title: input.Title, ImageIndex: input.ImageIndex, GalleryID: galleryID, @@ -522,21 +510,29 @@ func (r *mutationResolver) GalleryChapterCreate(ctx context.Context, input Galle UpdatedAt: currentTime, } - if err != nil { + // Start the transaction and save the gallery chapter + if err := r.withTxn(ctx, func(ctx context.Context) error { + imageCount, err := r.repository.Image.CountByGalleryID(ctx, galleryID) + if err != nil { + return err + } + + // Sanity Check of Index + if newChapter.ImageIndex > imageCount || newChapter.ImageIndex < 1 { + return errors.New("Image # must greater than zero and in range of the gallery images") + } + + return r.repository.GalleryChapter.Create(ctx, &newChapter) + }); err != nil { return nil, err } - err = r.changeChapter(ctx, create, &newGalleryChapter) - if err != nil { - return nil, err - } - - r.hookExecutor.ExecutePostHooks(ctx, newGalleryChapter.ID, plugin.GalleryChapterCreatePost, input, nil) - return r.getGalleryChapter(ctx, newGalleryChapter.ID) + r.hookExecutor.ExecutePostHooks(ctx, newChapter.ID, plugin.GalleryChapterCreatePost, input, nil) + return r.getGalleryChapter(ctx, newChapter.ID) } func (r *mutationResolver) GalleryChapterUpdate(ctx context.Context, input GalleryChapterUpdateInput) (*models.GalleryChapter, error) { - galleryChapterID, err := strconv.Atoi(input.ID) + chapterID, err := strconv.Atoi(input.ID) if err != nil { return nil, err } @@ -545,39 +541,60 @@ func (r *mutationResolver) GalleryChapterUpdate(ctx context.Context, input Galle inputMap: getUpdateInputMap(ctx), } - galleryID, err := strconv.Atoi(input.GalleryID) + // Populate gallery chapter from the input + updatedChapter := models.NewGalleryChapterPartial() + + updatedChapter.Title = translator.optionalString(input.Title, "title") + updatedChapter.ImageIndex = translator.optionalInt(input.ImageIndex, "image_index") + updatedChapter.GalleryID, err = translator.optionalIntFromString(input.GalleryID, "gallery_id") if err != nil { - return nil, err + return nil, fmt.Errorf("converting gallery id: %w", err) } - var imageCount int + // Start the transaction and save the gallery chapter if err := r.withTxn(ctx, func(ctx context.Context) error { - imageCount, err = r.repository.Image.CountByGalleryID(ctx, galleryID) - return err + qb := r.repository.GalleryChapter + + existingChapter, err := qb.Find(ctx, chapterID) + if err != nil { + return err + } + if existingChapter == nil { + return fmt.Errorf("gallery chapter with id %d not found", chapterID) + } + + galleryID := existingChapter.GalleryID + imageIndex := existingChapter.ImageIndex + + if updatedChapter.GalleryID.Set { + galleryID = updatedChapter.GalleryID.Value + } + if updatedChapter.ImageIndex.Set { + imageIndex = updatedChapter.ImageIndex.Value + } + + imageCount, err := r.repository.Image.CountByGalleryID(ctx, galleryID) + if err != nil { + return err + } + + // Sanity Check of Index + if imageIndex > imageCount || imageIndex < 1 { + return errors.New("Image # must greater than zero and in range of the gallery images") + } + + _, err = qb.UpdatePartial(ctx, chapterID, updatedChapter) + if err != nil { + return err + } + + return nil }); err != nil { return nil, err } - // Sanity Check of Index - if input.ImageIndex > imageCount || input.ImageIndex < 1 { - return nil, errors.New("Image # must greater than zero and in range of the gallery images") - } - // Populate gallery chapter from the input - updatedGalleryChapter := models.GalleryChapter{ - ID: galleryChapterID, - Title: input.Title, - ImageIndex: input.ImageIndex, - GalleryID: galleryID, - UpdatedAt: time.Now(), - } - - err = r.changeChapter(ctx, update, &updatedGalleryChapter) - if err != nil { - return nil, err - } - - r.hookExecutor.ExecutePostHooks(ctx, updatedGalleryChapter.ID, plugin.GalleryChapterUpdatePost, input, translator.getFields()) - return r.getGalleryChapter(ctx, updatedGalleryChapter.ID) + r.hookExecutor.ExecutePostHooks(ctx, chapterID, plugin.GalleryChapterUpdatePost, input, translator.getFields()) + return r.getGalleryChapter(ctx, chapterID) } func (r *mutationResolver) GalleryChapterDestroy(ctx context.Context, id string) (bool, error) { @@ -608,24 +625,3 @@ func (r *mutationResolver) GalleryChapterDestroy(ctx context.Context, id string) return true, nil } - -func (r *mutationResolver) changeChapter(ctx context.Context, changeType int, changedChapter *models.GalleryChapter) error { - // Start the transaction and save the gallery chapter - var err = r.withTxn(ctx, func(ctx context.Context) error { - qb := r.repository.GalleryChapter - var err error - - switch changeType { - case create: - err = qb.Create(ctx, changedChapter) - case update: - err = qb.Update(ctx, changedChapter) - if err != nil { - return err - } - } - return err - }) - - return err -} diff --git a/internal/api/resolver_mutation_movie.go b/internal/api/resolver_mutation_movie.go index a27e8f98c..b06d84a7f 100644 --- a/internal/api/resolver_mutation_movie.go +++ b/internal/api/resolver_mutation_movie.go @@ -28,8 +28,6 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp inputMap: getUpdateInputMap(ctx), } - // generate checksum from movie name rather than image - // Populate a new movie from the input currentTime := time.Now() newMovie := models.Movie{ diff --git a/internal/api/resolver_mutation_scene.go b/internal/api/resolver_mutation_scene.go index ef1c603ab..1846d554d 100644 --- a/internal/api/resolver_mutation_scene.go +++ b/internal/api/resolver_mutation_scene.go @@ -655,18 +655,18 @@ func (r *mutationResolver) getSceneMarker(ctx context.Context, id int) (ret *mod } func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMarkerCreateInput) (*models.SceneMarker, error) { - primaryTagID, err := strconv.Atoi(input.PrimaryTagID) - if err != nil { - return nil, err - } - sceneID, err := strconv.Atoi(input.SceneID) if err != nil { - return nil, err + return nil, fmt.Errorf("converting scene id: %w", err) + } + + primaryTagID, err := strconv.Atoi(input.PrimaryTagID) + if err != nil { + return nil, fmt.Errorf("converting primary tag id: %w", err) } currentTime := time.Now() - newSceneMarker := models.SceneMarker{ + newMarker := models.SceneMarker{ Title: input.Title, Seconds: input.Seconds, PrimaryTagID: primaryTagID, @@ -677,50 +677,31 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds) if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) + } + + if err := r.withTxn(ctx, func(ctx context.Context) error { + qb := r.repository.SceneMarker + + err := qb.Create(ctx, &newMarker) + if err != nil { + return err + } + + // Save the marker tags + // If this tag is the primary tag, then let's not add it. + tagIDs = intslice.IntExclude(tagIDs, []int{newMarker.PrimaryTagID}) + return qb.UpdateTags(ctx, newMarker.ID, tagIDs) + }); err != nil { return nil, err } - err = r.changeMarker(ctx, create, &newSceneMarker, tagIDs) - if err != nil { - return nil, err - } - - r.hookExecutor.ExecutePostHooks(ctx, newSceneMarker.ID, plugin.SceneMarkerCreatePost, input, nil) - return r.getSceneMarker(ctx, newSceneMarker.ID) + r.hookExecutor.ExecutePostHooks(ctx, newMarker.ID, plugin.SceneMarkerCreatePost, input, nil) + return r.getSceneMarker(ctx, newMarker.ID) } func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMarkerUpdateInput) (*models.SceneMarker, error) { - // Populate scene marker from the input - sceneMarkerID, err := strconv.Atoi(input.ID) - if err != nil { - return nil, err - } - - primaryTagID, err := strconv.Atoi(input.PrimaryTagID) - if err != nil { - return nil, err - } - - sceneID, err := strconv.Atoi(input.SceneID) - if err != nil { - return nil, err - } - - updatedSceneMarker := models.SceneMarker{ - ID: sceneMarkerID, - Title: input.Title, - Seconds: input.Seconds, - SceneID: sceneID, - PrimaryTagID: primaryTagID, - UpdatedAt: time.Now(), - } - - tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds) - if err != nil { - return nil, err - } - - err = r.changeMarker(ctx, update, &updatedSceneMarker, tagIDs) + markerID, err := strconv.Atoi(input.ID) if err != nil { return nil, err } @@ -728,8 +709,93 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar translator := changesetTranslator{ inputMap: getUpdateInputMap(ctx), } - r.hookExecutor.ExecutePostHooks(ctx, updatedSceneMarker.ID, plugin.SceneMarkerUpdatePost, input, translator.getFields()) - return r.getSceneMarker(ctx, updatedSceneMarker.ID) + + // Populate scene marker from the input + updatedMarker := models.NewSceneMarkerPartial() + + updatedMarker.Title = translator.optionalString(input.Title, "title") + updatedMarker.Seconds = translator.optionalFloat64(input.Seconds, "seconds") + updatedMarker.SceneID, err = translator.optionalIntFromString(input.SceneID, "scene_id") + if err != nil { + return nil, fmt.Errorf("converting scene id: %w", err) + } + updatedMarker.PrimaryTagID, err = translator.optionalIntFromString(input.PrimaryTagID, "primary_tag_id") + if err != nil { + return nil, fmt.Errorf("converting primary tag id: %w", err) + } + + var tagIDs []int + tagIdsIncluded := translator.hasField("tag_ids") + if input.TagIds != nil { + tagIDs, err = stringslice.StringSliceToIntSlice(input.TagIds) + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) + } + } + + mgr := manager.GetInstance() + + fileDeleter := &scene.FileDeleter{ + Deleter: file.NewDeleter(), + FileNamingAlgo: mgr.Config.GetVideoFileNamingAlgorithm(), + Paths: mgr.Paths, + } + + // Start the transaction and save the scene marker + if err := r.withTxn(ctx, func(ctx context.Context) error { + qb := r.repository.SceneMarker + sqb := r.repository.Scene + + // check to see if timestamp was changed + existingMarker, err := qb.Find(ctx, markerID) + if err != nil { + return err + } + if existingMarker == nil { + return fmt.Errorf("scene marker with id %d not found", markerID) + } + + newMarker, err := qb.UpdatePartial(ctx, markerID, updatedMarker) + if err != nil { + return err + } + + existingScene, err := sqb.Find(ctx, existingMarker.SceneID) + if err != nil { + return err + } + if existingScene == nil { + return fmt.Errorf("scene with id %d not found", existingMarker.SceneID) + } + + // remove the marker preview if the scene changed or if the timestamp was changed + if existingMarker.SceneID != newMarker.SceneID || existingMarker.Seconds != newMarker.Seconds { + seconds := int(existingMarker.Seconds) + if err := fileDeleter.MarkMarkerFiles(existingScene, seconds); err != nil { + return err + } + } + + if tagIdsIncluded { + // Save the marker tags + // If this tag is the primary tag, then let's not add it. + tagIDs = intslice.IntExclude(tagIDs, []int{newMarker.PrimaryTagID}) + if err := qb.UpdateTags(ctx, markerID, tagIDs); err != nil { + return err + } + } + + return nil + }); err != nil { + fileDeleter.Rollback() + return nil, err + } + + // perform the post-commit actions + fileDeleter.Commit() + + r.hookExecutor.ExecutePostHooks(ctx, markerID, plugin.SceneMarkerUpdatePost, input, translator.getFields()) + return r.getSceneMarker(ctx, markerID) } func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (bool, error) { @@ -783,72 +849,6 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b return true, nil } -func (r *mutationResolver) changeMarker(ctx context.Context, changeType int, changedMarker *models.SceneMarker, tagIDs []int) error { - fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm() - - fileDeleter := &scene.FileDeleter{ - Deleter: file.NewDeleter(), - FileNamingAlgo: fileNamingAlgo, - Paths: manager.GetInstance().Paths, - } - - // Start the transaction and save the scene marker - if err := r.withTxn(ctx, func(ctx context.Context) error { - qb := r.repository.SceneMarker - sqb := r.repository.Scene - - switch changeType { - case create: - err := qb.Create(ctx, changedMarker) - if err != nil { - return err - } - case update: - // check to see if timestamp was changed - existingMarker, err := qb.Find(ctx, changedMarker.ID) - if err != nil { - return err - } - if existingMarker == nil { - return fmt.Errorf("scene marker with id %d not found", changedMarker.ID) - } - - err = qb.Update(ctx, changedMarker) - if err != nil { - return err - } - - s, err := sqb.Find(ctx, existingMarker.SceneID) - if err != nil { - return err - } - if s == nil { - return fmt.Errorf("scene with id %d not found", existingMarker.ID) - } - - // remove the marker preview if the timestamp was changed - if existingMarker.Seconds != changedMarker.Seconds { - seconds := int(existingMarker.Seconds) - if err := fileDeleter.MarkMarkerFiles(s, seconds); err != nil { - return err - } - } - } - - // Save the marker tags - // If this tag is the primary tag, then let's not add it. - tagIDs = intslice.IntExclude(tagIDs, []int{changedMarker.PrimaryTagID}) - return qb.UpdateTags(ctx, changedMarker.ID, tagIDs) - }); err != nil { - fileDeleter.Rollback() - return err - } - - // perform the post-commit actions - fileDeleter.Commit() - return nil -} - func (r *mutationResolver) SceneSaveActivity(ctx context.Context, id string, resumeTime *float64, playDuration *float64) (ret bool, err error) { sceneID, err := strconv.Atoi(id) if err != nil { diff --git a/internal/api/types.go b/internal/api/types.go index fb65420e3..13d86f975 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -8,12 +8,6 @@ import ( "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) -// An enum https://golang.org/ref/spec#Iota -const ( - create = iota // 0 - update = iota // 1 -) - // #1572 - Inf and NaN values cause the JSON marshaller to fail // Return nil for these values func handleFloat64(v float64) *float64 { diff --git a/pkg/models/gallery_chapter.go b/pkg/models/gallery_chapter.go index 12e8bcf70..005780982 100644 --- a/pkg/models/gallery_chapter.go +++ b/pkg/models/gallery_chapter.go @@ -11,6 +11,7 @@ type GalleryChapterReader interface { type GalleryChapterWriter interface { Create(ctx context.Context, newGalleryChapter *GalleryChapter) error Update(ctx context.Context, updatedGalleryChapter *GalleryChapter) error + UpdatePartial(ctx context.Context, id int, updatedGalleryChapter GalleryChapterPartial) (*GalleryChapter, error) Destroy(ctx context.Context, id int) error } diff --git a/pkg/models/mocks/GalleryChapterReaderWriter.go b/pkg/models/mocks/GalleryChapterReaderWriter.go index ab22a7b03..3adc980ac 100644 --- a/pkg/models/mocks/GalleryChapterReaderWriter.go +++ b/pkg/models/mocks/GalleryChapterReaderWriter.go @@ -124,3 +124,26 @@ func (_m *GalleryChapterReaderWriter) Update(ctx context.Context, updatedGallery return r0 } + +// UpdatePartial provides a mock function with given fields: ctx, id, updatedGalleryChapter +func (_m *GalleryChapterReaderWriter) UpdatePartial(ctx context.Context, id int, updatedGalleryChapter models.GalleryChapterPartial) (*models.GalleryChapter, error) { + ret := _m.Called(ctx, id, updatedGalleryChapter) + + var r0 *models.GalleryChapter + if rf, ok := ret.Get(0).(func(context.Context, int, models.GalleryChapterPartial) *models.GalleryChapter); ok { + r0 = rf(ctx, id, updatedGalleryChapter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.GalleryChapter) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int, models.GalleryChapterPartial) error); ok { + r1 = rf(ctx, id, updatedGalleryChapter) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/models/mocks/SceneMarkerReaderWriter.go b/pkg/models/mocks/SceneMarkerReaderWriter.go index 535b48029..2be3b1184 100644 --- a/pkg/models/mocks/SceneMarkerReaderWriter.go +++ b/pkg/models/mocks/SceneMarkerReaderWriter.go @@ -287,6 +287,29 @@ func (_m *SceneMarkerReaderWriter) Update(ctx context.Context, updatedSceneMarke return r0 } +// UpdatePartial provides a mock function with given fields: ctx, id, updatedSceneMarker +func (_m *SceneMarkerReaderWriter) UpdatePartial(ctx context.Context, id int, updatedSceneMarker models.SceneMarkerPartial) (*models.SceneMarker, error) { + ret := _m.Called(ctx, id, updatedSceneMarker) + + var r0 *models.SceneMarker + if rf, ok := ret.Get(0).(func(context.Context, int, models.SceneMarkerPartial) *models.SceneMarker); ok { + r0 = rf(ctx, id, updatedSceneMarker) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.SceneMarker) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int, models.SceneMarkerPartial) error); ok { + r1 = rf(ctx, id, updatedSceneMarker) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // UpdateTags provides a mock function with given fields: ctx, markerID, tagIDs func (_m *SceneMarkerReaderWriter) UpdateTags(ctx context.Context, markerID int, tagIDs []int) error { ret := _m.Called(ctx, markerID, tagIDs) diff --git a/pkg/models/model_gallery_chapter.go b/pkg/models/model_gallery_chapter.go index 6c43c44cb..5c9fc05b2 100644 --- a/pkg/models/model_gallery_chapter.go +++ b/pkg/models/model_gallery_chapter.go @@ -13,12 +13,19 @@ type GalleryChapter struct { UpdatedAt time.Time `json:"updated_at"` } -type GalleryChapters []*GalleryChapter - -func (m *GalleryChapters) Append(o interface{}) { - *m = append(*m, o.(*GalleryChapter)) +// GalleryChapterPartial represents part of a GalleryChapter object. +// It is used to update the database entry. +type GalleryChapterPartial struct { + Title OptionalString + ImageIndex OptionalInt + GalleryID OptionalInt + CreatedAt OptionalTime + UpdatedAt OptionalTime } -func (m *GalleryChapters) New() interface{} { - return &GalleryChapter{} +func NewGalleryChapterPartial() GalleryChapterPartial { + updatedTime := time.Now() + return GalleryChapterPartial{ + UpdatedAt: NewOptionalTime(updatedTime), + } } diff --git a/pkg/models/model_scene_marker.go b/pkg/models/model_scene_marker.go index a84e5f740..434c0c2d9 100644 --- a/pkg/models/model_scene_marker.go +++ b/pkg/models/model_scene_marker.go @@ -14,12 +14,20 @@ type SceneMarker struct { UpdatedAt time.Time `json:"updated_at"` } -type SceneMarkers []*SceneMarker - -func (m *SceneMarkers) Append(o interface{}) { - *m = append(*m, o.(*SceneMarker)) +// SceneMarkerPartial represents part of a SceneMarker object. It is used to update +// the database entry. +type SceneMarkerPartial struct { + Title OptionalString + Seconds OptionalFloat64 + PrimaryTagID OptionalInt + SceneID OptionalInt + CreatedAt OptionalTime + UpdatedAt OptionalTime } -func (m *SceneMarkers) New() interface{} { - return &SceneMarker{} +func NewSceneMarkerPartial() SceneMarkerPartial { + updatedTime := time.Now() + return SceneMarkerPartial{ + UpdatedAt: NewOptionalTime(updatedTime), + } } diff --git a/pkg/models/scene_marker.go b/pkg/models/scene_marker.go index deaf1ac16..673a547e9 100644 --- a/pkg/models/scene_marker.go +++ b/pkg/models/scene_marker.go @@ -46,6 +46,7 @@ type SceneMarkerReader interface { type SceneMarkerWriter interface { Create(ctx context.Context, newSceneMarker *SceneMarker) error Update(ctx context.Context, updatedSceneMarker *SceneMarker) error + UpdatePartial(ctx context.Context, id int, updatedSceneMarker SceneMarkerPartial) (*SceneMarker, error) Destroy(ctx context.Context, id int) error UpdateTags(ctx context.Context, markerID int, tagIDs []int) error } diff --git a/pkg/sqlite/gallery_chapter.go b/pkg/sqlite/gallery_chapter.go index 024c7aa1d..82b997358 100644 --- a/pkg/sqlite/gallery_chapter.go +++ b/pkg/sqlite/gallery_chapter.go @@ -49,6 +49,18 @@ func (r *galleryChapterRow) resolve() *models.GalleryChapter { return ret } +type galleryChapterRowRecord struct { + updateRecord +} + +func (r *galleryChapterRowRecord) fromPartial(o models.GalleryChapterPartial) { + r.setString("title", o.Title) + r.setInt("image_index", o.ImageIndex) + r.setInt("gallery_id", o.GalleryID) + r.setTimestamp("created_at", o.CreatedAt) + r.setTimestamp("updated_at", o.UpdatedAt) +} + type GalleryChapterStore struct { repository @@ -103,6 +115,24 @@ func (qb *GalleryChapterStore) Update(ctx context.Context, updatedObject *models return nil } +func (qb *GalleryChapterStore) UpdatePartial(ctx context.Context, id int, partial models.GalleryChapterPartial) (*models.GalleryChapter, error) { + r := galleryChapterRowRecord{ + updateRecord{ + Record: make(exp.Record), + }, + } + + r.fromPartial(partial) + + if len(r.Record) > 0 { + if err := qb.tableMgr.updateByID(ctx, id, r.Record); err != nil { + return nil, err + } + } + + return qb.find(ctx, id) +} + func (qb *GalleryChapterStore) Destroy(ctx context.Context, id int) error { return qb.destroyExisting(ctx, []int{id}) } diff --git a/pkg/sqlite/scene_marker.go b/pkg/sqlite/scene_marker.go index 23ce50cfa..317a90995 100644 --- a/pkg/sqlite/scene_marker.go +++ b/pkg/sqlite/scene_marker.go @@ -57,6 +57,19 @@ func (r *sceneMarkerRow) resolve() *models.SceneMarker { return ret } +type sceneMarkerRowRecord struct { + updateRecord +} + +func (r *sceneMarkerRowRecord) fromPartial(o models.SceneMarkerPartial) { + r.setNullString("title", o.Title) + r.setFloat64("seconds", o.Seconds) + r.setInt("primary_tag_id", o.PrimaryTagID) + r.setInt("scene_id", o.SceneID) + r.setTimestamp("created_at", o.CreatedAt) + r.setTimestamp("updated_at", o.UpdatedAt) +} + type SceneMarkerStore struct { repository @@ -100,6 +113,24 @@ func (qb *SceneMarkerStore) Create(ctx context.Context, newObject *models.SceneM return nil } +func (qb *SceneMarkerStore) UpdatePartial(ctx context.Context, id int, partial models.SceneMarkerPartial) (*models.SceneMarker, error) { + r := sceneMarkerRowRecord{ + updateRecord{ + Record: make(exp.Record), + }, + } + + r.fromPartial(partial) + + if len(r.Record) > 0 { + if err := qb.tableMgr.updateByID(ctx, id, r.Record); err != nil { + return nil, err + } + } + + return qb.find(ctx, id) +} + func (qb *SceneMarkerStore) Update(ctx context.Context, updatedObject *models.SceneMarker) error { var r sceneMarkerRow r.fromSceneMarker(*updatedObject) diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryChapterForm.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryChapterForm.tsx index a8c7133d7..0b8b711b0 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryChapterForm.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryChapterForm.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Button, Form } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; -import { Form as FormikForm, Formik } from "formik"; +import { useFormik } from "formik"; import * as yup from "yup"; import * as GQL from "src/core/generated-graphql"; import { @@ -12,11 +12,6 @@ import { import { useToast } from "src/hooks/Toast"; import isEqual from "lodash-es/isEqual"; -interface IFormFields { - title: string; - imageIndex: number; -} - interface IGalleryChapterForm { galleryID: string; editingChapter?: GQL.GalleryChapterDataFragment; @@ -37,122 +32,130 @@ export const GalleryChapterForm: React.FC = ({ const schema = yup.object({ title: yup.string().ensure(), - imageIndex: yup + image_index: yup .number() + .integer() .required() - .label(intl.formatMessage({ id: "image_index" })) - .moreThan(0), + .moreThan(0) + .label(intl.formatMessage({ id: "image_index" })), }); - const onSubmit = (values: IFormFields) => { - const variables: - | GQL.GalleryChapterUpdateInput - | GQL.GalleryChapterCreateInput = { - title: values.title, - image_index: values.imageIndex, - gallery_id: galleryID, - }; - - if (!editingChapter) { - galleryChapterCreate({ variables }) - .then(onClose) - .catch((err) => Toast.error(err)); - } else { - const updateVariables = variables as GQL.GalleryChapterUpdateInput; - updateVariables.id = editingChapter!.id; - galleryChapterUpdate({ variables: updateVariables }) - .then(onClose) - .catch((err) => Toast.error(err)); - } + const initialValues = { + title: editingChapter?.title ?? "", + image_index: editingChapter?.image_index ?? 1, }; - const onDelete = () => { + type InputValues = yup.InferType; + + const formik = useFormik({ + initialValues, + validationSchema: schema, + enableReinitialize: true, + onSubmit: (values) => onSave(values), + }); + + async function onSave(input: InputValues) { + try { + if (!editingChapter) { + await galleryChapterCreate({ + variables: { + gallery_id: galleryID, + ...input, + }, + }); + } else { + await galleryChapterUpdate({ + variables: { + id: editingChapter.id, + gallery_id: galleryID, + ...input, + }, + }); + } + } catch (e) { + Toast.error(e); + } finally { + onClose(); + } + } + + async function onDelete() { if (!editingChapter) return; - galleryChapterDestroy({ variables: { id: editingChapter.id } }) - .then(onClose) - .catch((err) => Toast.error(err)); - }; - - const values: IFormFields = { - title: editingChapter?.title ?? "", - imageIndex: editingChapter?.image_index ?? 1, - }; + try { + await galleryChapterDestroy({ variables: { id: editingChapter.id } }); + } catch (e) { + Toast.error(e); + } finally { + onClose(); + } + } return ( - - {(formik) => ( - -
- - - - +
+
+ + + + - - - {formik.getFieldMeta("title").error} - - + + + {formik.errors.title} + + - - - - + + + + - - - {formik.getFieldMeta("imageIndex").error} - - -
-
-
- - - {editingChapter && ( - - )} -
-
- - )} - + + + {formik.errors.image_index} + + +
+
+
+ + + {editingChapter && ( + + )} +
+
+ ); }; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx index 67f04ef36..e73ce7cc9 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx @@ -1,7 +1,8 @@ import React from "react"; import { Button, Form } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; -import { Field, FieldProps, Form as FormikForm, Formik } from "formik"; +import { useFormik } from "formik"; +import * as yup from "yup"; import * as GQL from "src/core/generated-graphql"; import { useSceneMarkerCreate, @@ -12,13 +13,7 @@ import { DurationInput } from "src/components/Shared/DurationInput"; import { TagSelect, MarkerTitleSuggest } from "src/components/Shared/Select"; import { getPlayerPosition } from "src/components/ScenePlayer/util"; import { useToast } from "src/hooks/Toast"; - -interface IFormFields { - title: string; - seconds: string; - primaryTagId: string; - tagIds: string[]; -} +import isEqual from "lodash-es/isEqual"; interface ISceneMarkerForm { sceneID: string; @@ -36,168 +31,170 @@ export const SceneMarkerForm: React.FC = ({ const [sceneMarkerDestroy] = useSceneMarkerDestroy(); const Toast = useToast(); - const onSubmit = (values: IFormFields) => { - const variables: GQL.SceneMarkerUpdateInput | GQL.SceneMarkerCreateInput = { - title: values.title, - seconds: parseFloat(values.seconds), - scene_id: sceneID, - primary_tag_id: values.primaryTagId, - tag_ids: values.tagIds, - }; - if (!editingMarker) { - sceneMarkerCreate({ variables }) - .then(onClose) - .catch((err) => Toast.error(err)); - } else { - const updateVariables = variables as GQL.SceneMarkerUpdateInput; - updateVariables.id = editingMarker!.id; - sceneMarkerUpdate({ variables: updateVariables }) - .then(onClose) - .catch((err) => Toast.error(err)); - } + const schema = yup.object({ + title: yup.string().ensure(), + seconds: yup.number().required().integer(), + primary_tag_id: yup.string().required(), + tag_ids: yup.array(yup.string().required()).defined(), + }); + + const initialValues = { + title: editingMarker?.title ?? "", + seconds: editingMarker?.seconds ?? Math.round(getPlayerPosition() ?? 0), + primary_tag_id: editingMarker?.primary_tag.id ?? "", + tag_ids: editingMarker?.tags.map((tag) => tag.id) ?? [], }; - const onDelete = () => { + type InputValues = yup.InferType; + + const formik = useFormik({ + initialValues, + validationSchema: schema, + enableReinitialize: true, + onSubmit: (values) => onSave(values), + }); + + async function onSave(input: InputValues) { + try { + if (!editingMarker) { + await sceneMarkerCreate({ + variables: { + scene_id: sceneID, + ...input, + }, + }); + } else { + await sceneMarkerUpdate({ + variables: { + id: editingMarker.id, + scene_id: sceneID, + ...input, + }, + }); + } + } catch (e) { + Toast.error(e); + } finally { + onClose(); + } + } + + async function onDelete() { if (!editingMarker) return; - sceneMarkerDestroy({ variables: { id: editingMarker.id } }) - .then(onClose) - .catch((err) => Toast.error(err)); - }; - const renderTitleField = (fieldProps: FieldProps) => ( - - fieldProps.form.setFieldValue("title", query) - } - /> - ); + try { + await sceneMarkerDestroy({ variables: { id: editingMarker.id } }); + } catch (e) { + Toast.error(e); + } finally { + onClose(); + } + } - const renderSecondsField = (fieldProps: FieldProps) => ( - fieldProps.form.setFieldValue("seconds", s)} - onReset={() => - fieldProps.form.setFieldValue( - "seconds", - Math.round(getPlayerPosition() ?? 0) - ) - } - numericValue={Number.parseInt(fieldProps.field.value ?? "0", 10)} - mandatory - /> - ); - - const renderPrimaryTagField = (fieldProps: FieldProps) => ( - - fieldProps.form.setFieldValue("primaryTagId", tags[0]?.id) - } - ids={fieldProps.field.value ? [fieldProps.field.value] : []} - noSelectionString="Select/create tag..." - hoverPlacement="right" - /> - ); - - const renderTagsField = (fieldProps: FieldProps) => ( - - fieldProps.form.setFieldValue( - "tagIds", - tags.map((tag) => tag.id) - ) - } - ids={fieldProps.field.value} - noSelectionString="Select/create tags..." - hoverPlacement="right" - /> - ); - - const values: IFormFields = { - title: editingMarker?.title ?? "", - seconds: ( - editingMarker?.seconds ?? Math.round(getPlayerPosition() ?? 0) - ).toString(), - primaryTagId: editingMarker?.primary_tag.id ?? "", - tagIds: editingMarker?.tags.map((tag) => tag.id) ?? [], - }; + const primaryTagId = formik.values.primary_tag_id; return ( - - -
- - - Marker Title - -
- {renderTitleField} -
-
- - - Primary Tag - -
- {renderPrimaryTagField} -
-
-
- - Time - -
- {renderSecondsField} -
+
+
+ + + Marker Title + +
+ formik.setFieldValue("title", query)} + /> +
+
+ + + Primary Tag + +
+ + formik.setFieldValue("primary_tag_id", tags[0]?.id) + } + ids={primaryTagId ? [primaryTagId] : []} + noSelectionString="Select/create tag..." + hoverPlacement="right" + /> + + {formik.errors.primary_tag_id} + +
+
+
+ + Time + +
+ formik.setFieldValue("seconds", s)} + onReset={() => + formik.setFieldValue( + "seconds", + Math.round(getPlayerPosition() ?? 0) + ) + } + numericValue={formik.values.seconds} + mandatory + />
- - - - Tags - -
- {renderTagsField} -
-
-
-
-
- - - {editingMarker && ( - - )}
+ + + + Tags + +
+ + formik.setFieldValue( + "tag_ids", + tags.map((tag) => tag.id) + ) + } + ids={formik.values.tag_ids} + noSelectionString="Select/create tags..." + hoverPlacement="right" + /> +
+
+
+
+
+ + + {editingMarker && ( + + )}
- - +
+ ); }; diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index 2aa3a0c65..d7d753519 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -977,6 +977,14 @@ dl.details-list { vertical-align: middle; } +.invalid-feedback { + display: block; + + &:empty { + display: none; + } +} + // Fix Safari styling on dropdowns select { -webkit-appearance: none;