mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Fix scene marker issues (#3955)
* Fix scene marker NOT NULL constraint error * similar changes to gallery chapters * Fix NULL conversion error if names are NULL in DB * Fix scene marker form resetting
This commit is contained in:
@@ -14,8 +14,8 @@ type SceneMarker struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// SceneMarkerPartial represents part of a SceneMarker object. It is used to update
|
||||
// the database entry.
|
||||
// SceneMarkerPartial represents part of a SceneMarker object.
|
||||
// It is used to update the database entry.
|
||||
type SceneMarkerPartial struct {
|
||||
Title OptionalString
|
||||
Seconds OptionalFloat64
|
||||
|
||||
@@ -20,7 +20,7 @@ const (
|
||||
|
||||
type galleryChapterRow struct {
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Title string `db:"title"`
|
||||
Title string `db:"title"` // TODO: make db schema (and gql schema) nullable
|
||||
ImageIndex int `db:"image_index"`
|
||||
GalleryID int `db:"gallery_id"`
|
||||
CreatedAt Timestamp `db:"created_at"`
|
||||
@@ -54,7 +54,12 @@ type galleryChapterRowRecord struct {
|
||||
}
|
||||
|
||||
func (r *galleryChapterRowRecord) fromPartial(o models.GalleryChapterPartial) {
|
||||
r.setString("title", o.Title)
|
||||
// TODO: replace with setNullString after schema is made nullable
|
||||
// r.setNullString("title", o.Title)
|
||||
// saves a null input as the empty string
|
||||
if o.Title.Set {
|
||||
r.set("title", o.Title.Value)
|
||||
}
|
||||
r.setInt("image_index", o.ImageIndex)
|
||||
r.setInt("gallery_id", o.GalleryID)
|
||||
r.setTimestamp("created_at", o.CreatedAt)
|
||||
|
||||
@@ -30,7 +30,7 @@ const (
|
||||
|
||||
type performerRow struct {
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Name string `db:"name"`
|
||||
Name null.String `db:"name"` // TODO: make schema non-nullable
|
||||
Disambigation zero.String `db:"disambiguation"`
|
||||
Gender zero.String `db:"gender"`
|
||||
URL zero.String `db:"url"`
|
||||
@@ -65,7 +65,7 @@ type performerRow struct {
|
||||
|
||||
func (r *performerRow) fromPerformer(o models.Performer) {
|
||||
r.ID = o.ID
|
||||
r.Name = o.Name
|
||||
r.Name = null.StringFrom(o.Name)
|
||||
r.Disambigation = zero.StringFrom(o.Disambiguation)
|
||||
if o.Gender != nil && o.Gender.IsValid() {
|
||||
r.Gender = zero.StringFrom(o.Gender.String())
|
||||
@@ -101,7 +101,7 @@ func (r *performerRow) fromPerformer(o models.Performer) {
|
||||
func (r *performerRow) resolve() *models.Performer {
|
||||
ret := &models.Performer{
|
||||
ID: r.ID,
|
||||
Name: r.Name,
|
||||
Name: r.Name.String,
|
||||
Disambiguation: r.Disambigation.String,
|
||||
URL: r.URL.String,
|
||||
Twitter: r.Twitter.String,
|
||||
|
||||
@@ -25,7 +25,7 @@ GROUP BY scene_markers.id
|
||||
|
||||
type sceneMarkerRow struct {
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Title string `db:"title"`
|
||||
Title string `db:"title"` // TODO: make db schema (and gql schema) nullable
|
||||
Seconds float64 `db:"seconds"`
|
||||
PrimaryTagID int `db:"primary_tag_id"`
|
||||
SceneID int `db:"scene_id"`
|
||||
@@ -62,7 +62,12 @@ type sceneMarkerRowRecord struct {
|
||||
}
|
||||
|
||||
func (r *sceneMarkerRowRecord) fromPartial(o models.SceneMarkerPartial) {
|
||||
r.setNullString("title", o.Title)
|
||||
// TODO: replace with setNullString after schema is made nullable
|
||||
// r.setNullString("title", o.Title)
|
||||
// saves a null input as the empty string
|
||||
if o.Title.Set {
|
||||
r.set("title", o.Title.Value)
|
||||
}
|
||||
r.setFloat64("seconds", o.Seconds)
|
||||
r.setInt("primary_tag_id", o.PrimaryTagID)
|
||||
r.setInt("scene_id", o.SceneID)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/doug-martin/goqu/v9/exp"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gopkg.in/guregu/null.v4"
|
||||
"gopkg.in/guregu/null.v4/zero"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -27,7 +28,7 @@ const (
|
||||
|
||||
type tagRow struct {
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Name string `db:"name"`
|
||||
Name null.String `db:"name"` // TODO: make schema non-nullable
|
||||
Description zero.String `db:"description"`
|
||||
IgnoreAutoTag bool `db:"ignore_auto_tag"`
|
||||
CreatedAt Timestamp `db:"created_at"`
|
||||
@@ -39,7 +40,7 @@ type tagRow struct {
|
||||
|
||||
func (r *tagRow) fromTag(o models.Tag) {
|
||||
r.ID = o.ID
|
||||
r.Name = o.Name
|
||||
r.Name = null.StringFrom(o.Name)
|
||||
r.Description = zero.StringFrom(o.Description)
|
||||
r.IgnoreAutoTag = o.IgnoreAutoTag
|
||||
r.CreatedAt = Timestamp{Timestamp: o.CreatedAt}
|
||||
@@ -49,7 +50,7 @@ func (r *tagRow) fromTag(o models.Tag) {
|
||||
func (r *tagRow) resolve() *models.Tag {
|
||||
ret := &models.Tag{
|
||||
ID: r.ID,
|
||||
Name: r.Name,
|
||||
Name: r.Name.String,
|
||||
Description: r.Description.String,
|
||||
IgnoreAutoTag: r.IgnoreAutoTag,
|
||||
CreatedAt: r.CreatedAt.Timestamp,
|
||||
|
||||
@@ -14,13 +14,13 @@ import isEqual from "lodash-es/isEqual";
|
||||
|
||||
interface IGalleryChapterForm {
|
||||
galleryID: string;
|
||||
editingChapter?: GQL.GalleryChapterDataFragment;
|
||||
chapter?: GQL.GalleryChapterDataFragment;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
|
||||
galleryID,
|
||||
editingChapter,
|
||||
chapter,
|
||||
onClose,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
@@ -30,6 +30,8 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
|
||||
const [galleryChapterDestroy] = useGalleryChapterDestroy();
|
||||
const Toast = useToast();
|
||||
|
||||
const isNew = chapter === undefined;
|
||||
|
||||
const schema = yup.object({
|
||||
title: yup.string().ensure(),
|
||||
image_index: yup
|
||||
@@ -41,8 +43,8 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
title: editingChapter?.title ?? "",
|
||||
image_index: editingChapter?.image_index ?? 1,
|
||||
title: chapter?.title ?? "",
|
||||
image_index: chapter?.image_index ?? 1,
|
||||
};
|
||||
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
@@ -56,7 +58,7 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
|
||||
|
||||
async function onSave(input: InputValues) {
|
||||
try {
|
||||
if (!editingChapter) {
|
||||
if (isNew) {
|
||||
await galleryChapterCreate({
|
||||
variables: {
|
||||
gallery_id: galleryID,
|
||||
@@ -66,7 +68,7 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
|
||||
} else {
|
||||
await galleryChapterUpdate({
|
||||
variables: {
|
||||
id: editingChapter.id,
|
||||
id: chapter.id,
|
||||
gallery_id: galleryID,
|
||||
...input,
|
||||
},
|
||||
@@ -80,10 +82,10 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
|
||||
}
|
||||
|
||||
async function onDelete() {
|
||||
if (!editingChapter) return;
|
||||
if (isNew) return;
|
||||
|
||||
try {
|
||||
await galleryChapterDestroy({ variables: { id: editingChapter.id } });
|
||||
await galleryChapterDestroy({ variables: { id: chapter.id } });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
@@ -130,9 +132,7 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
|
||||
<div className="col d-flex">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={
|
||||
(editingChapter && !formik.dirty) || !isEqual(formik.errors, {})
|
||||
}
|
||||
disabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
<FormattedMessage id="actions.save" />
|
||||
@@ -145,7 +145,7 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
|
||||
>
|
||||
<FormattedMessage id="actions.cancel" />
|
||||
</Button>
|
||||
{editingChapter && (
|
||||
{!isNew && (
|
||||
<Button
|
||||
variant="danger"
|
||||
className="ml-auto"
|
||||
|
||||
@@ -12,22 +12,24 @@ interface IGalleryChapterPanelProps {
|
||||
onClickChapter: (index: number) => void;
|
||||
}
|
||||
|
||||
export const GalleryChapterPanel: React.FC<IGalleryChapterPanelProps> = (
|
||||
props: IGalleryChapterPanelProps
|
||||
) => {
|
||||
export const GalleryChapterPanel: React.FC<IGalleryChapterPanelProps> = ({
|
||||
gallery,
|
||||
isVisible,
|
||||
onClickChapter,
|
||||
}) => {
|
||||
const [isEditorOpen, setIsEditorOpen] = useState<boolean>(false);
|
||||
const [editingChapter, setEditingChapter] =
|
||||
useState<GQL.GalleryChapterDataFragment>();
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
if (props.isVisible) {
|
||||
Mousetrap.bind("n", () => onOpenEditor());
|
||||
if (!isVisible) return;
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("n");
|
||||
};
|
||||
}
|
||||
Mousetrap.bind("n", () => onOpenEditor());
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("n");
|
||||
};
|
||||
});
|
||||
|
||||
function onOpenEditor(chapter?: GQL.GalleryChapterDataFragment) {
|
||||
@@ -35,10 +37,6 @@ export const GalleryChapterPanel: React.FC<IGalleryChapterPanelProps> = (
|
||||
setEditingChapter(chapter ?? undefined);
|
||||
}
|
||||
|
||||
function onClickChapter(image_index: number) {
|
||||
props.onClickChapter(image_index);
|
||||
}
|
||||
|
||||
const closeEditor = () => {
|
||||
setEditingChapter(undefined);
|
||||
setIsEditorOpen(false);
|
||||
@@ -47,8 +45,8 @@ export const GalleryChapterPanel: React.FC<IGalleryChapterPanelProps> = (
|
||||
if (isEditorOpen)
|
||||
return (
|
||||
<GalleryChapterForm
|
||||
galleryID={props.gallery.id}
|
||||
editingChapter={editingChapter}
|
||||
galleryID={gallery.id}
|
||||
chapter={editingChapter}
|
||||
onClose={closeEditor}
|
||||
/>
|
||||
);
|
||||
@@ -60,7 +58,7 @@ export const GalleryChapterPanel: React.FC<IGalleryChapterPanelProps> = (
|
||||
</Button>
|
||||
<div className="container">
|
||||
<ChapterEntries
|
||||
galleryChapters={props.gallery.chapters}
|
||||
galleryChapters={gallery.chapters}
|
||||
onClickChapter={onClickChapter}
|
||||
onEdit={onOpenEditor}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useFormik } from "formik";
|
||||
@@ -17,13 +17,13 @@ import isEqual from "lodash-es/isEqual";
|
||||
|
||||
interface ISceneMarkerForm {
|
||||
sceneID: string;
|
||||
editingMarker?: GQL.SceneMarkerDataFragment;
|
||||
marker?: GQL.SceneMarkerDataFragment;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
||||
sceneID,
|
||||
editingMarker,
|
||||
marker,
|
||||
onClose,
|
||||
}) => {
|
||||
const [sceneMarkerCreate] = useSceneMarkerCreate();
|
||||
@@ -31,6 +31,8 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
||||
const [sceneMarkerDestroy] = useSceneMarkerDestroy();
|
||||
const Toast = useToast();
|
||||
|
||||
const isNew = marker === undefined;
|
||||
|
||||
const schema = yup.object({
|
||||
title: yup.string().ensure(),
|
||||
seconds: yup.number().required().integer(),
|
||||
@@ -38,12 +40,16 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
||||
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) ?? [],
|
||||
};
|
||||
// useMemo to only run getPlayerPosition when the input marker actually changes
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
title: marker?.title ?? "",
|
||||
seconds: marker?.seconds ?? Math.round(getPlayerPosition() ?? 0),
|
||||
primary_tag_id: marker?.primary_tag.id ?? "",
|
||||
tag_ids: marker?.tags.map((tag) => tag.id) ?? [],
|
||||
}),
|
||||
[marker]
|
||||
);
|
||||
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
@@ -56,7 +62,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
||||
|
||||
async function onSave(input: InputValues) {
|
||||
try {
|
||||
if (!editingMarker) {
|
||||
if (isNew) {
|
||||
await sceneMarkerCreate({
|
||||
variables: {
|
||||
scene_id: sceneID,
|
||||
@@ -66,7 +72,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
||||
} else {
|
||||
await sceneMarkerUpdate({
|
||||
variables: {
|
||||
id: editingMarker.id,
|
||||
id: marker.id,
|
||||
scene_id: sceneID,
|
||||
...input,
|
||||
},
|
||||
@@ -80,10 +86,10 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
||||
}
|
||||
|
||||
async function onDelete() {
|
||||
if (!editingMarker) return;
|
||||
if (isNew) return;
|
||||
|
||||
try {
|
||||
await sceneMarkerDestroy({ variables: { id: editingMarker.id } });
|
||||
await sceneMarkerDestroy({ variables: { id: marker.id } });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
@@ -169,9 +175,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
||||
<div className="col d-flex">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={
|
||||
(editingMarker && !formik.dirty) || !isEqual(formik.errors, {})
|
||||
}
|
||||
disabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
<FormattedMessage id="actions.save" />
|
||||
@@ -184,7 +188,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
||||
>
|
||||
<FormattedMessage id="actions.cancel" />
|
||||
</Button>
|
||||
{editingMarker && (
|
||||
{!isNew && (
|
||||
<Button
|
||||
variant="danger"
|
||||
className="ml-auto"
|
||||
|
||||
@@ -13,13 +13,13 @@ interface ISceneMarkersPanelProps {
|
||||
onClickMarker: (marker: GQL.SceneMarkerDataFragment) => void;
|
||||
}
|
||||
|
||||
export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
|
||||
props: ISceneMarkersPanelProps
|
||||
) => {
|
||||
export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = ({
|
||||
sceneId,
|
||||
isVisible,
|
||||
onClickMarker,
|
||||
}) => {
|
||||
const { data, loading } = GQL.useFindSceneMarkerTagsQuery({
|
||||
variables: {
|
||||
id: props.sceneId,
|
||||
},
|
||||
variables: { id: sceneId },
|
||||
});
|
||||
const [isEditorOpen, setIsEditorOpen] = useState<boolean>(false);
|
||||
const [editingMarker, setEditingMarker] =
|
||||
@@ -27,13 +27,13 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
if (props.isVisible) {
|
||||
Mousetrap.bind("n", () => onOpenEditor());
|
||||
if (!isVisible) return;
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("n");
|
||||
};
|
||||
}
|
||||
Mousetrap.bind("n", () => onOpenEditor());
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("n");
|
||||
};
|
||||
});
|
||||
|
||||
if (loading) return null;
|
||||
@@ -43,10 +43,6 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
|
||||
setEditingMarker(marker ?? undefined);
|
||||
}
|
||||
|
||||
function onClickMarker(marker: GQL.SceneMarkerDataFragment) {
|
||||
props.onClickMarker(marker);
|
||||
}
|
||||
|
||||
const closeEditor = () => {
|
||||
setEditingMarker(undefined);
|
||||
setIsEditorOpen(false);
|
||||
@@ -55,8 +51,8 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
|
||||
if (isEditorOpen)
|
||||
return (
|
||||
<SceneMarkerForm
|
||||
sceneID={props.sceneId}
|
||||
editingMarker={editingMarker}
|
||||
sceneID={sceneId}
|
||||
marker={editingMarker}
|
||||
onClose={closeEditor}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user