Fix scene marker/gallery chapter update overwriting created at date (#3945)

* Add UpdatePartial to gallery chapter
* Add UpdatePartial to gallery marker
* Fix UI, use yup and useFormik
This commit is contained in:
DingDongSoLong4
2023-07-27 01:44:06 +02:00
committed by GitHub
parent b3fa3c326a
commit 2ae30028ac
17 changed files with 599 additions and 479 deletions

View File

@@ -15,9 +15,9 @@ input GalleryChapterCreateInput {
input GalleryChapterUpdateInput { input GalleryChapterUpdateInput {
id: ID! id: ID!
gallery_id: ID! gallery_id: ID
title: String! title: String
image_index: Int! image_index: Int
} }
type FindGalleryChaptersResultType { type FindGalleryChaptersResultType {

View File

@@ -26,10 +26,10 @@ input SceneMarkerCreateInput {
input SceneMarkerUpdateInput { input SceneMarkerUpdateInput {
id: ID! id: ID!
title: String! title: String
seconds: Float! seconds: Float
scene_id: ID! scene_id: ID
primary_tag_id: ID! primary_tag_id: ID
tag_ids: [ID!] tag_ids: [ID!]
} }

View File

@@ -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) { func (r *mutationResolver) GalleryChapterCreate(ctx context.Context, input GalleryChapterCreateInput) (*models.GalleryChapter, error) {
galleryID, err := strconv.Atoi(input.GalleryID) galleryID, err := strconv.Atoi(input.GalleryID)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("converting gallery id: %w", 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")
} }
currentTime := time.Now() currentTime := time.Now()
newGalleryChapter := models.GalleryChapter{ newChapter := models.GalleryChapter{
Title: input.Title, Title: input.Title,
ImageIndex: input.ImageIndex, ImageIndex: input.ImageIndex,
GalleryID: galleryID, GalleryID: galleryID,
@@ -522,21 +510,29 @@ func (r *mutationResolver) GalleryChapterCreate(ctx context.Context, input Galle
UpdatedAt: currentTime, 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 return nil, err
} }
err = r.changeChapter(ctx, create, &newGalleryChapter) r.hookExecutor.ExecutePostHooks(ctx, newChapter.ID, plugin.GalleryChapterCreatePost, input, nil)
if err != nil { return r.getGalleryChapter(ctx, newChapter.ID)
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, newGalleryChapter.ID, plugin.GalleryChapterCreatePost, input, nil)
return r.getGalleryChapter(ctx, newGalleryChapter.ID)
} }
func (r *mutationResolver) GalleryChapterUpdate(ctx context.Context, input GalleryChapterUpdateInput) (*models.GalleryChapter, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -545,39 +541,60 @@ func (r *mutationResolver) GalleryChapterUpdate(ctx context.Context, input Galle
inputMap: getUpdateInputMap(ctx), 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 { 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 { if err := r.withTxn(ctx, func(ctx context.Context) error {
imageCount, err = r.repository.Image.CountByGalleryID(ctx, galleryID) qb := r.repository.GalleryChapter
return err
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 { }); err != nil {
return nil, err 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 r.hookExecutor.ExecutePostHooks(ctx, chapterID, plugin.GalleryChapterUpdatePost, input, translator.getFields())
updatedGalleryChapter := models.GalleryChapter{ return r.getGalleryChapter(ctx, chapterID)
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)
} }
func (r *mutationResolver) GalleryChapterDestroy(ctx context.Context, id string) (bool, error) { 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 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
}

View File

@@ -28,8 +28,6 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
inputMap: getUpdateInputMap(ctx), inputMap: getUpdateInputMap(ctx),
} }
// generate checksum from movie name rather than image
// Populate a new movie from the input // Populate a new movie from the input
currentTime := time.Now() currentTime := time.Now()
newMovie := models.Movie{ newMovie := models.Movie{

View File

@@ -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) { 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) sceneID, err := strconv.Atoi(input.SceneID)
if err != nil { 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() currentTime := time.Now()
newSceneMarker := models.SceneMarker{ newMarker := models.SceneMarker{
Title: input.Title, Title: input.Title,
Seconds: input.Seconds, Seconds: input.Seconds,
PrimaryTagID: primaryTagID, PrimaryTagID: primaryTagID,
@@ -677,50 +677,31 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar
tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds) tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
if err != nil { 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 return nil, err
} }
err = r.changeMarker(ctx, create, &newSceneMarker, tagIDs) r.hookExecutor.ExecutePostHooks(ctx, newMarker.ID, plugin.SceneMarkerCreatePost, input, nil)
if err != nil { return r.getSceneMarker(ctx, newMarker.ID)
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, newSceneMarker.ID, plugin.SceneMarkerCreatePost, input, nil)
return r.getSceneMarker(ctx, newSceneMarker.ID)
} }
func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMarkerUpdateInput) (*models.SceneMarker, error) { func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMarkerUpdateInput) (*models.SceneMarker, error) {
// Populate scene marker from the input markerID, err := strconv.Atoi(input.ID)
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)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -728,8 +709,93 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
translator := changesetTranslator{ translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx), 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) { 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 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) { func (r *mutationResolver) SceneSaveActivity(ctx context.Context, id string, resumeTime *float64, playDuration *float64) (ret bool, err error) {
sceneID, err := strconv.Atoi(id) sceneID, err := strconv.Atoi(id)
if err != nil { if err != nil {

View File

@@ -8,12 +8,6 @@ import (
"github.com/stashapp/stash/pkg/sliceutil/stringslice" "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 // #1572 - Inf and NaN values cause the JSON marshaller to fail
// Return nil for these values // Return nil for these values
func handleFloat64(v float64) *float64 { func handleFloat64(v float64) *float64 {

View File

@@ -11,6 +11,7 @@ type GalleryChapterReader interface {
type GalleryChapterWriter interface { type GalleryChapterWriter interface {
Create(ctx context.Context, newGalleryChapter *GalleryChapter) error Create(ctx context.Context, newGalleryChapter *GalleryChapter) error
Update(ctx context.Context, updatedGalleryChapter *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 Destroy(ctx context.Context, id int) error
} }

View File

@@ -124,3 +124,26 @@ func (_m *GalleryChapterReaderWriter) Update(ctx context.Context, updatedGallery
return r0 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
}

View File

@@ -287,6 +287,29 @@ func (_m *SceneMarkerReaderWriter) Update(ctx context.Context, updatedSceneMarke
return r0 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 // UpdateTags provides a mock function with given fields: ctx, markerID, tagIDs
func (_m *SceneMarkerReaderWriter) UpdateTags(ctx context.Context, markerID int, tagIDs []int) error { func (_m *SceneMarkerReaderWriter) UpdateTags(ctx context.Context, markerID int, tagIDs []int) error {
ret := _m.Called(ctx, markerID, tagIDs) ret := _m.Called(ctx, markerID, tagIDs)

View File

@@ -13,12 +13,19 @@ type GalleryChapter struct {
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
type GalleryChapters []*GalleryChapter // GalleryChapterPartial represents part of a GalleryChapter object.
// It is used to update the database entry.
func (m *GalleryChapters) Append(o interface{}) { type GalleryChapterPartial struct {
*m = append(*m, o.(*GalleryChapter)) Title OptionalString
ImageIndex OptionalInt
GalleryID OptionalInt
CreatedAt OptionalTime
UpdatedAt OptionalTime
} }
func (m *GalleryChapters) New() interface{} { func NewGalleryChapterPartial() GalleryChapterPartial {
return &GalleryChapter{} updatedTime := time.Now()
return GalleryChapterPartial{
UpdatedAt: NewOptionalTime(updatedTime),
}
} }

View File

@@ -14,12 +14,20 @@ type SceneMarker struct {
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
type SceneMarkers []*SceneMarker // SceneMarkerPartial represents part of a SceneMarker object. It is used to update
// the database entry.
func (m *SceneMarkers) Append(o interface{}) { type SceneMarkerPartial struct {
*m = append(*m, o.(*SceneMarker)) Title OptionalString
Seconds OptionalFloat64
PrimaryTagID OptionalInt
SceneID OptionalInt
CreatedAt OptionalTime
UpdatedAt OptionalTime
} }
func (m *SceneMarkers) New() interface{} { func NewSceneMarkerPartial() SceneMarkerPartial {
return &SceneMarker{} updatedTime := time.Now()
return SceneMarkerPartial{
UpdatedAt: NewOptionalTime(updatedTime),
}
} }

View File

@@ -46,6 +46,7 @@ type SceneMarkerReader interface {
type SceneMarkerWriter interface { type SceneMarkerWriter interface {
Create(ctx context.Context, newSceneMarker *SceneMarker) error Create(ctx context.Context, newSceneMarker *SceneMarker) error
Update(ctx context.Context, updatedSceneMarker *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 Destroy(ctx context.Context, id int) error
UpdateTags(ctx context.Context, markerID int, tagIDs []int) error UpdateTags(ctx context.Context, markerID int, tagIDs []int) error
} }

View File

@@ -49,6 +49,18 @@ func (r *galleryChapterRow) resolve() *models.GalleryChapter {
return ret 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 { type GalleryChapterStore struct {
repository repository
@@ -103,6 +115,24 @@ func (qb *GalleryChapterStore) Update(ctx context.Context, updatedObject *models
return nil 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 { func (qb *GalleryChapterStore) Destroy(ctx context.Context, id int) error {
return qb.destroyExisting(ctx, []int{id}) return qb.destroyExisting(ctx, []int{id})
} }

View File

@@ -57,6 +57,19 @@ func (r *sceneMarkerRow) resolve() *models.SceneMarker {
return ret 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 { type SceneMarkerStore struct {
repository repository
@@ -100,6 +113,24 @@ func (qb *SceneMarkerStore) Create(ctx context.Context, newObject *models.SceneM
return nil 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 { func (qb *SceneMarkerStore) Update(ctx context.Context, updatedObject *models.SceneMarker) error {
var r sceneMarkerRow var r sceneMarkerRow
r.fromSceneMarker(*updatedObject) r.fromSceneMarker(*updatedObject)

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { Button, Form } from "react-bootstrap"; import { Button, Form } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { Form as FormikForm, Formik } from "formik"; import { useFormik } from "formik";
import * as yup from "yup"; import * as yup from "yup";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import {
@@ -12,11 +12,6 @@ import {
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import isEqual from "lodash-es/isEqual"; import isEqual from "lodash-es/isEqual";
interface IFormFields {
title: string;
imageIndex: number;
}
interface IGalleryChapterForm { interface IGalleryChapterForm {
galleryID: string; galleryID: string;
editingChapter?: GQL.GalleryChapterDataFragment; editingChapter?: GQL.GalleryChapterDataFragment;
@@ -37,122 +32,130 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
const schema = yup.object({ const schema = yup.object({
title: yup.string().ensure(), title: yup.string().ensure(),
imageIndex: yup image_index: yup
.number() .number()
.integer()
.required() .required()
.label(intl.formatMessage({ id: "image_index" })) .moreThan(0)
.moreThan(0), .label(intl.formatMessage({ id: "image_index" })),
}); });
const onSubmit = (values: IFormFields) => { const initialValues = {
const variables: title: editingChapter?.title ?? "",
| GQL.GalleryChapterUpdateInput image_index: editingChapter?.image_index ?? 1,
| 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 onDelete = () => { type InputValues = yup.InferType<typeof schema>;
const formik = useFormik<InputValues>({
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; if (!editingChapter) return;
galleryChapterDestroy({ variables: { id: editingChapter.id } }) try {
.then(onClose) await galleryChapterDestroy({ variables: { id: editingChapter.id } });
.catch((err) => Toast.error(err)); } catch (e) {
}; Toast.error(e);
} finally {
const values: IFormFields = { onClose();
title: editingChapter?.title ?? "", }
imageIndex: editingChapter?.image_index ?? 1, }
};
return ( return (
<Formik <Form noValidate onSubmit={formik.handleSubmit}>
initialValues={values} <div>
onSubmit={onSubmit} <Form.Group>
validationSchema={schema} <Form.Label>
> <FormattedMessage id="title" />
{(formik) => ( </Form.Label>
<FormikForm>
<div>
<Form.Group>
<Form.Label>
<FormattedMessage id="title" />
</Form.Label>
<Form.Control <Form.Control
className="text-input" className="text-input"
placeholder={intl.formatMessage({ id: "title" })} placeholder={intl.formatMessage({ id: "title" })}
{...formik.getFieldProps("title")} isInvalid={!!formik.errors.title}
isInvalid={!!formik.getFieldMeta("title").error} {...formik.getFieldProps("title")}
/> />
<Form.Control.Feedback type="invalid"> <Form.Control.Feedback type="invalid">
{formik.getFieldMeta("title").error} {formik.errors.title}
</Form.Control.Feedback> </Form.Control.Feedback>
</Form.Group> </Form.Group>
<Form.Group> <Form.Group>
<Form.Label> <Form.Label>
<FormattedMessage id="image_index" /> <FormattedMessage id="image_index" />
</Form.Label> </Form.Label>
<Form.Control <Form.Control
className="text-input" className="text-input"
placeholder={intl.formatMessage({ id: "image_index" })} placeholder={intl.formatMessage({ id: "image_index" })}
{...formik.getFieldProps("imageIndex")} isInvalid={!!formik.errors.image_index}
isInvalid={!!formik.getFieldMeta("imageIndex").error} {...formik.getFieldProps("image_index")}
/> />
<Form.Control.Feedback type="invalid"> <Form.Control.Feedback type="invalid">
{formik.getFieldMeta("imageIndex").error} {formik.errors.image_index}
</Form.Control.Feedback> </Form.Control.Feedback>
</Form.Group> </Form.Group>
</div> </div>
<div className="buttons-container row"> <div className="buttons-container row">
<div className="col d-flex"> <div className="col d-flex">
<Button <Button
variant="primary" variant="primary"
disabled={ disabled={
(editingChapter && !formik.dirty) || (editingChapter && !formik.dirty) || !isEqual(formik.errors, {})
!isEqual(formik.errors, {}) }
} onClick={() => formik.submitForm()}
onClick={() => formik.submitForm()} >
> <FormattedMessage id="actions.save" />
<FormattedMessage id="actions.save" /> </Button>
</Button> <Button
<Button variant="secondary"
variant="secondary" type="button"
type="button" onClick={onClose}
onClick={onClose} className="ml-2"
className="ml-2" >
> <FormattedMessage id="actions.cancel" />
<FormattedMessage id="actions.cancel" /> </Button>
</Button> {editingChapter && (
{editingChapter && ( <Button
<Button variant="danger"
variant="danger" className="ml-auto"
className="ml-auto" onClick={() => onDelete()}
onClick={() => onDelete()} >
> <FormattedMessage id="actions.delete" />
<FormattedMessage id="actions.delete" /> </Button>
</Button> )}
)} </div>
</div> </div>
</div> </Form>
</FormikForm>
)}
</Formik>
); );
}; };

View File

@@ -1,7 +1,8 @@
import React from "react"; import React from "react";
import { Button, Form } from "react-bootstrap"; import { Button, Form } from "react-bootstrap";
import { FormattedMessage } from "react-intl"; 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 * as GQL from "src/core/generated-graphql";
import { import {
useSceneMarkerCreate, useSceneMarkerCreate,
@@ -12,13 +13,7 @@ import { DurationInput } from "src/components/Shared/DurationInput";
import { TagSelect, MarkerTitleSuggest } from "src/components/Shared/Select"; import { TagSelect, MarkerTitleSuggest } from "src/components/Shared/Select";
import { getPlayerPosition } from "src/components/ScenePlayer/util"; import { getPlayerPosition } from "src/components/ScenePlayer/util";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import isEqual from "lodash-es/isEqual";
interface IFormFields {
title: string;
seconds: string;
primaryTagId: string;
tagIds: string[];
}
interface ISceneMarkerForm { interface ISceneMarkerForm {
sceneID: string; sceneID: string;
@@ -36,168 +31,170 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
const [sceneMarkerDestroy] = useSceneMarkerDestroy(); const [sceneMarkerDestroy] = useSceneMarkerDestroy();
const Toast = useToast(); const Toast = useToast();
const onSubmit = (values: IFormFields) => { const schema = yup.object({
const variables: GQL.SceneMarkerUpdateInput | GQL.SceneMarkerCreateInput = { title: yup.string().ensure(),
title: values.title, seconds: yup.number().required().integer(),
seconds: parseFloat(values.seconds), primary_tag_id: yup.string().required(),
scene_id: sceneID, tag_ids: yup.array(yup.string().required()).defined(),
primary_tag_id: values.primaryTagId, });
tag_ids: values.tagIds,
}; const initialValues = {
if (!editingMarker) { title: editingMarker?.title ?? "",
sceneMarkerCreate({ variables }) seconds: editingMarker?.seconds ?? Math.round(getPlayerPosition() ?? 0),
.then(onClose) primary_tag_id: editingMarker?.primary_tag.id ?? "",
.catch((err) => Toast.error(err)); tag_ids: editingMarker?.tags.map((tag) => tag.id) ?? [],
} else {
const updateVariables = variables as GQL.SceneMarkerUpdateInput;
updateVariables.id = editingMarker!.id;
sceneMarkerUpdate({ variables: updateVariables })
.then(onClose)
.catch((err) => Toast.error(err));
}
}; };
const onDelete = () => { type InputValues = yup.InferType<typeof schema>;
const formik = useFormik<InputValues>({
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; if (!editingMarker) return;
sceneMarkerDestroy({ variables: { id: editingMarker.id } }) try {
.then(onClose) await sceneMarkerDestroy({ variables: { id: editingMarker.id } });
.catch((err) => Toast.error(err)); } catch (e) {
}; Toast.error(e);
const renderTitleField = (fieldProps: FieldProps<string>) => ( } finally {
<MarkerTitleSuggest onClose();
initialMarkerTitle={fieldProps.field.value} }
onChange={(query: string) => }
fieldProps.form.setFieldValue("title", query)
}
/>
);
const renderSecondsField = (fieldProps: FieldProps<string>) => ( const primaryTagId = formik.values.primary_tag_id;
<DurationInput
onValueChange={(s) => 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<string>) => (
<TagSelect
onSelect={(tags) =>
fieldProps.form.setFieldValue("primaryTagId", tags[0]?.id)
}
ids={fieldProps.field.value ? [fieldProps.field.value] : []}
noSelectionString="Select/create tag..."
hoverPlacement="right"
/>
);
const renderTagsField = (fieldProps: FieldProps<string[]>) => (
<TagSelect
isMulti
onSelect={(tags) =>
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) ?? [],
};
return ( return (
<Formik initialValues={values} onSubmit={onSubmit}> <Form noValidate onSubmit={formik.handleSubmit}>
<FormikForm> <div>
<div> <Form.Group className="row">
<Form.Group className="row"> <Form.Label className="col-sm-3 col-md-2 col-xl-12 col-form-label">
<Form.Label Marker Title
htmlFor="title" </Form.Label>
className="col-sm-3 col-md-2 col-xl-12 col-form-label" <div className="col-sm-9 col-md-10 col-xl-12">
> <MarkerTitleSuggest
Marker Title initialMarkerTitle={formik.values.title}
</Form.Label> onChange={(query: string) => formik.setFieldValue("title", query)}
<div className="col-sm-9 col-md-10 col-xl-12"> />
<Field name="title">{renderTitleField}</Field> </div>
</div> </Form.Group>
</Form.Group> <Form.Group className="row">
<Form.Group className="row"> <Form.Label className="col-sm-3 col-md-2 col-xl-12 col-form-label">
<Form.Label Primary Tag
htmlFor="primaryTagId" </Form.Label>
className="col-sm-3 col-md-2 col-xl-12 col-form-label" <div className="col-sm-4 col-md-6 col-xl-12 mb-3 mb-sm-0 mb-xl-3">
> <TagSelect
Primary Tag onSelect={(tags) =>
</Form.Label> formik.setFieldValue("primary_tag_id", tags[0]?.id)
<div className="col-sm-4 col-md-6 col-xl-12 mb-3 mb-sm-0 mb-xl-3"> }
<Field name="primaryTagId">{renderPrimaryTagField}</Field> ids={primaryTagId ? [primaryTagId] : []}
</div> noSelectionString="Select/create tag..."
<div className="col-sm-5 col-md-4 col-xl-12"> hoverPlacement="right"
<div className="row"> />
<Form.Label <Form.Control.Feedback type="invalid">
htmlFor="seconds" {formik.errors.primary_tag_id}
className="col-sm-4 col-md-4 col-xl-12 col-form-label text-sm-right text-xl-left" </Form.Control.Feedback>
> </div>
Time <div className="col-sm-5 col-md-4 col-xl-12">
</Form.Label> <div className="row">
<div className="col-sm-8 col-xl-12"> <Form.Label className="col-sm-4 col-md-4 col-xl-12 col-form-label text-sm-right text-xl-left">
<Field name="seconds">{renderSecondsField}</Field> Time
</div> </Form.Label>
<div className="col-sm-8 col-xl-12">
<DurationInput
onValueChange={(s) => formik.setFieldValue("seconds", s)}
onReset={() =>
formik.setFieldValue(
"seconds",
Math.round(getPlayerPosition() ?? 0)
)
}
numericValue={formik.values.seconds}
mandatory
/>
</div> </div>
</div> </div>
</Form.Group>
<Form.Group className="row">
<Form.Label
htmlFor="tagIds"
className="col-sm-3 col-md-2 col-xl-12 col-form-label"
>
Tags
</Form.Label>
<div className="col-sm-9 col-md-10 col-xl-12">
<Field name="tagIds">{renderTagsField}</Field>
</div>
</Form.Group>
</div>
<div className="buttons-container row">
<div className="col d-flex">
<Button variant="primary" type="submit">
Submit
</Button>
<Button
variant="secondary"
type="button"
onClick={onClose}
className="ml-2"
>
<FormattedMessage id="actions.cancel" />
</Button>
{editingMarker && (
<Button
variant="danger"
className="ml-auto"
onClick={() => onDelete()}
>
<FormattedMessage id="actions.delete" />
</Button>
)}
</div> </div>
</Form.Group>
<Form.Group className="row">
<Form.Label className="col-sm-3 col-md-2 col-xl-12 col-form-label">
Tags
</Form.Label>
<div className="col-sm-9 col-md-10 col-xl-12">
<TagSelect
isMulti
onSelect={(tags) =>
formik.setFieldValue(
"tag_ids",
tags.map((tag) => tag.id)
)
}
ids={formik.values.tag_ids}
noSelectionString="Select/create tags..."
hoverPlacement="right"
/>
</div>
</Form.Group>
</div>
<div className="buttons-container row">
<div className="col d-flex">
<Button
variant="primary"
disabled={
(editingMarker && !formik.dirty) || !isEqual(formik.errors, {})
}
onClick={() => formik.submitForm()}
>
<FormattedMessage id="actions.save" />
</Button>
<Button
variant="secondary"
type="button"
onClick={onClose}
className="ml-2"
>
<FormattedMessage id="actions.cancel" />
</Button>
{editingMarker && (
<Button
variant="danger"
className="ml-auto"
onClick={() => onDelete()}
>
<FormattedMessage id="actions.delete" />
</Button>
)}
</div> </div>
</FormikForm> </div>
</Formik> </Form>
); );
}; };

View File

@@ -977,6 +977,14 @@ dl.details-list {
vertical-align: middle; vertical-align: middle;
} }
.invalid-feedback {
display: block;
&:empty {
display: none;
}
}
// Fix Safari styling on dropdowns // Fix Safari styling on dropdowns
select { select {
-webkit-appearance: none; -webkit-appearance: none;