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

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

View File

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

View File

@@ -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!]
}

View File

@@ -498,23 +498,11 @@ func (r *mutationResolver) getGalleryChapter(ctx context.Context, id int) (ret *
func (r *mutationResolver) GalleryChapterCreate(ctx context.Context, input GalleryChapterCreateInput) (*models.GalleryChapter, error) {
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,
}
// 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)
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
}

View File

@@ -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{

View File

@@ -655,18 +655,18 @@ func (r *mutationResolver) getSceneMarker(ctx context.Context, id int) (ret *mod
}
func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMarkerCreateInput) (*models.SceneMarker, error) {
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, err
return nil, fmt.Errorf("converting tag ids: %w", err)
}
err = r.changeMarker(ctx, create, &newSceneMarker, tagIDs)
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
}
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 {

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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),
}
}

View File

@@ -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),
}
}

View File

@@ -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
}

View File

@@ -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})
}

View File

@@ -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)

View File

@@ -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,56 +32,67 @@ 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,
const initialValues = {
title: editingChapter?.title ?? "",
image_index: editingChapter?.image_index ?? 1,
};
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) {
galleryChapterCreate({ variables })
.then(onClose)
.catch((err) => Toast.error(err));
await galleryChapterCreate({
variables: {
gallery_id: galleryID,
...input,
},
});
} else {
const updateVariables = variables as GQL.GalleryChapterUpdateInput;
updateVariables.id = editingChapter!.id;
galleryChapterUpdate({ variables: updateVariables })
.then(onClose)
.catch((err) => Toast.error(err));
await galleryChapterUpdate({
variables: {
id: editingChapter.id,
gallery_id: galleryID,
...input,
},
});
}
} catch (e) {
Toast.error(e);
} finally {
onClose();
}
}
};
const onDelete = () => {
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>
<Form noValidate onSubmit={formik.handleSubmit}>
<div>
<Form.Group>
<Form.Label>
@@ -96,11 +102,11 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
<Form.Control
className="text-input"
placeholder={intl.formatMessage({ id: "title" })}
isInvalid={!!formik.errors.title}
{...formik.getFieldProps("title")}
isInvalid={!!formik.getFieldMeta("title").error}
/>
<Form.Control.Feedback type="invalid">
{formik.getFieldMeta("title").error}
{formik.errors.title}
</Form.Control.Feedback>
</Form.Group>
@@ -112,11 +118,11 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
<Form.Control
className="text-input"
placeholder={intl.formatMessage({ id: "image_index" })}
{...formik.getFieldProps("imageIndex")}
isInvalid={!!formik.getFieldMeta("imageIndex").error}
isInvalid={!!formik.errors.image_index}
{...formik.getFieldProps("image_index")}
/>
<Form.Control.Feedback type="invalid">
{formik.getFieldMeta("imageIndex").error}
{formik.errors.image_index}
</Form.Control.Feedback>
</Form.Group>
</div>
@@ -125,8 +131,7 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
<Button
variant="primary"
disabled={
(editingChapter && !formik.dirty) ||
!isEqual(formik.errors, {})
(editingChapter && !formik.dirty) || !isEqual(formik.errors, {})
}
onClick={() => formik.submitForm()}
>
@@ -151,8 +156,6 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
)}
</div>
</div>
</FormikForm>
)}
</Formik>
</Form>
);
};

View File

@@ -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,147 +31,150 @@ 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>) => (
const primaryTagId = formik.values.primary_tag_id;
return (
<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) => fieldProps.form.setFieldValue("seconds", s)}
onValueChange={(s) => formik.setFieldValue("seconds", s)}
onReset={() =>
fieldProps.form.setFieldValue(
formik.setFieldValue(
"seconds",
Math.round(getPlayerPosition() ?? 0)
)
}
numericValue={Number.parseInt(fieldProps.field.value ?? "0", 10)}
numericValue={formik.values.seconds}
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 (
<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>
</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"
>
<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">
<Field name="tagIds">{renderTagsField}</Field>
<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" type="submit">
Submit
<Button
variant="primary"
disabled={
(editingMarker && !formik.dirty) || !isEqual(formik.errors, {})
}
onClick={() => formik.submitForm()}
>
<FormattedMessage id="actions.save" />
</Button>
<Button
variant="secondary"
@@ -197,7 +195,6 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
)}
</div>
</div>
</FormikForm>
</Formik>
</Form>
);
};

View File

@@ -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;