mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44: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 {
|
||||
id: ID!
|
||||
gallery_id: ID!
|
||||
title: String!
|
||||
image_index: Int!
|
||||
gallery_id: ID
|
||||
title: String
|
||||
image_index: Int
|
||||
}
|
||||
|
||||
type FindGalleryChaptersResultType {
|
||||
|
||||
@@ -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!]
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<IGalleryChapterForm> = ({
|
||||
|
||||
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<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;
|
||||
|
||||
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
|
||||
initialValues={values}
|
||||
onSubmit={onSubmit}
|
||||
validationSchema={schema}
|
||||
>
|
||||
{(formik) => (
|
||||
<FormikForm>
|
||||
<div>
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="title" />
|
||||
</Form.Label>
|
||||
<Form noValidate onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="title" />
|
||||
</Form.Label>
|
||||
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
placeholder={intl.formatMessage({ id: "title" })}
|
||||
{...formik.getFieldProps("title")}
|
||||
isInvalid={!!formik.getFieldMeta("title").error}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formik.getFieldMeta("title").error}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
placeholder={intl.formatMessage({ id: "title" })}
|
||||
isInvalid={!!formik.errors.title}
|
||||
{...formik.getFieldProps("title")}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formik.errors.title}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="image_index" />
|
||||
</Form.Label>
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="image_index" />
|
||||
</Form.Label>
|
||||
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
placeholder={intl.formatMessage({ id: "image_index" })}
|
||||
{...formik.getFieldProps("imageIndex")}
|
||||
isInvalid={!!formik.getFieldMeta("imageIndex").error}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formik.getFieldMeta("imageIndex").error}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
</div>
|
||||
<div className="buttons-container row">
|
||||
<div className="col d-flex">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={
|
||||
(editingChapter && !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>
|
||||
{editingChapter && (
|
||||
<Button
|
||||
variant="danger"
|
||||
className="ml-auto"
|
||||
onClick={() => onDelete()}
|
||||
>
|
||||
<FormattedMessage id="actions.delete" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</FormikForm>
|
||||
)}
|
||||
</Formik>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
placeholder={intl.formatMessage({ id: "image_index" })}
|
||||
isInvalid={!!formik.errors.image_index}
|
||||
{...formik.getFieldProps("image_index")}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formik.errors.image_index}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
</div>
|
||||
<div className="buttons-container row">
|
||||
<div className="col d-flex">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={
|
||||
(editingChapter && !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>
|
||||
{editingChapter && (
|
||||
<Button
|
||||
variant="danger"
|
||||
className="ml-auto"
|
||||
onClick={() => onDelete()}
|
||||
>
|
||||
<FormattedMessage id="actions.delete" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<ISceneMarkerForm> = ({
|
||||
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<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;
|
||||
|
||||
sceneMarkerDestroy({ variables: { id: editingMarker.id } })
|
||||
.then(onClose)
|
||||
.catch((err) => Toast.error(err));
|
||||
};
|
||||
const renderTitleField = (fieldProps: FieldProps<string>) => (
|
||||
<MarkerTitleSuggest
|
||||
initialMarkerTitle={fieldProps.field.value}
|
||||
onChange={(query: string) =>
|
||||
fieldProps.form.setFieldValue("title", query)
|
||||
}
|
||||
/>
|
||||
);
|
||||
try {
|
||||
await sceneMarkerDestroy({ variables: { id: editingMarker.id } });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
const renderSecondsField = (fieldProps: FieldProps<string>) => (
|
||||
<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) ?? [],
|
||||
};
|
||||
const primaryTagId = formik.values.primary_tag_id;
|
||||
|
||||
return (
|
||||
<Formik initialValues={values} onSubmit={onSubmit}>
|
||||
<FormikForm>
|
||||
<div>
|
||||
<Form.Group className="row">
|
||||
<Form.Label
|
||||
htmlFor="title"
|
||||
className="col-sm-3 col-md-2 col-xl-12 col-form-label"
|
||||
>
|
||||
Marker Title
|
||||
</Form.Label>
|
||||
<div className="col-sm-9 col-md-10 col-xl-12">
|
||||
<Field name="title">{renderTitleField}</Field>
|
||||
</div>
|
||||
</Form.Group>
|
||||
<Form.Group className="row">
|
||||
<Form.Label
|
||||
htmlFor="primaryTagId"
|
||||
className="col-sm-3 col-md-2 col-xl-12 col-form-label"
|
||||
>
|
||||
Primary Tag
|
||||
</Form.Label>
|
||||
<div className="col-sm-4 col-md-6 col-xl-12 mb-3 mb-sm-0 mb-xl-3">
|
||||
<Field name="primaryTagId">{renderPrimaryTagField}</Field>
|
||||
</div>
|
||||
<div className="col-sm-5 col-md-4 col-xl-12">
|
||||
<div className="row">
|
||||
<Form.Label
|
||||
htmlFor="seconds"
|
||||
className="col-sm-4 col-md-4 col-xl-12 col-form-label text-sm-right text-xl-left"
|
||||
>
|
||||
Time
|
||||
</Form.Label>
|
||||
<div className="col-sm-8 col-xl-12">
|
||||
<Field name="seconds">{renderSecondsField}</Field>
|
||||
</div>
|
||||
<Form noValidate onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<Form.Group className="row">
|
||||
<Form.Label className="col-sm-3 col-md-2 col-xl-12 col-form-label">
|
||||
Marker Title
|
||||
</Form.Label>
|
||||
<div className="col-sm-9 col-md-10 col-xl-12">
|
||||
<MarkerTitleSuggest
|
||||
initialMarkerTitle={formik.values.title}
|
||||
onChange={(query: string) => formik.setFieldValue("title", query)}
|
||||
/>
|
||||
</div>
|
||||
</Form.Group>
|
||||
<Form.Group className="row">
|
||||
<Form.Label className="col-sm-3 col-md-2 col-xl-12 col-form-label">
|
||||
Primary Tag
|
||||
</Form.Label>
|
||||
<div className="col-sm-4 col-md-6 col-xl-12 mb-3 mb-sm-0 mb-xl-3">
|
||||
<TagSelect
|
||||
onSelect={(tags) =>
|
||||
formik.setFieldValue("primary_tag_id", tags[0]?.id)
|
||||
}
|
||||
ids={primaryTagId ? [primaryTagId] : []}
|
||||
noSelectionString="Select/create tag..."
|
||||
hoverPlacement="right"
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formik.errors.primary_tag_id}
|
||||
</Form.Control.Feedback>
|
||||
</div>
|
||||
<div className="col-sm-5 col-md-4 col-xl-12">
|
||||
<div className="row">
|
||||
<Form.Label className="col-sm-4 col-md-4 col-xl-12 col-form-label text-sm-right text-xl-left">
|
||||
Time
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</FormikForm>
|
||||
</Formik>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user