mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 21:04:37 +03:00
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:
@@ -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 {
|
||||||
|
|||||||
@@ -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!]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user