Add child studio support (child_ids) to Studio API and UI

This commit is contained in:
Slick Daddy 2026-05-08 22:10:23 +03:00 committed by GitHub
commit 314994f129
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 135 additions and 0 deletions

View file

@ -36,6 +36,7 @@ input StudioCreateInput {
url: String @deprecated(reason: "Use urls")
urls: [String!]
parent_id: ID
child_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
image: String
stash_ids: [StashIDInput!]
@ -58,6 +59,7 @@ input StudioUpdateInput {
url: String @deprecated(reason: "Use urls")
urls: [String!]
parent_id: ID
child_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
image: String
stash_ids: [StashIDInput!]

View file

@ -14,6 +14,63 @@ import (
)
// used to refetch studio after hooks run
func clearRemovedChildStudios(ctx context.Context, qb models.StudioReaderWriter, parentStudioID int, childStudioIDs []int) error {
currentChildren, err := qb.FindChildren(ctx, parentStudioID)
if err != nil {
return err
}
newChildStudioIDs := make(map[int]struct{}, len(childStudioIDs))
for _, childStudioID := range childStudioIDs {
newChildStudioIDs[childStudioID] = struct{}{}
}
for _, currentChild := range currentChildren {
if _, keep := newChildStudioIDs[currentChild.ID]; keep {
continue
}
clearParentPartial := models.NewStudioPartial()
clearParentPartial.ID = currentChild.ID
clearParentPartial.ParentID = models.NewOptionalIntPtr(nil)
if _, err := qb.UpdatePartial(ctx, clearParentPartial); err != nil {
return err
}
}
return nil
}
func setChildStudios(ctx context.Context, qb models.StudioReaderWriter, parentStudioID int, childStudioIDs []int) error {
if err := clearRemovedChildStudios(ctx, qb, parentStudioID, childStudioIDs); err != nil {
return err
}
newChildStudioIDs := make(map[int]struct{}, len(childStudioIDs))
for _, childStudioID := range childStudioIDs {
if _, found := newChildStudioIDs[childStudioID]; found {
continue
}
newChildStudioIDs[childStudioID] = struct{}{}
childPartial := models.NewStudioPartial()
childPartial.ID = childStudioID
childPartial.ParentID = models.NewOptionalInt(parentStudioID)
if err := studio.ValidateModify(ctx, childPartial, qb); err != nil {
return err
}
if _, err := qb.UpdatePartial(ctx, childPartial); err != nil {
return err
}
}
return nil
}
func (r *mutationResolver) getStudio(ctx context.Context, id int) (ret *models.Studio, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.Find(ctx, id)
@ -62,6 +119,11 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
childStudioIDs, err := stringslice.StringSliceToIntSlice(input.ChildIds)
if err != nil {
return nil, fmt.Errorf("converting child ids: %w", err)
}
newStudio.CustomFields = convertMapJSONNumbers(input.CustomFields)
// Process the base 64 encoded image string
@ -93,6 +155,12 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
}
}
if input.ChildIds != nil {
if err := setChildStudios(ctx, qb, newStudio.ID, childStudioIDs); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
@ -135,6 +203,11 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
return nil, fmt.Errorf("converting tag ids: %w", err)
}
childStudioIDs, err := stringslice.StringSliceToIntSlice(input.ChildIds)
if err != nil {
return nil, fmt.Errorf("converting child ids: %w", err)
}
if translator.hasField("urls") {
// ensure url not included in the input
if err := validateNoLegacyURLs(translator); err != nil {
@ -197,6 +270,12 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
}
}
if translator.hasField("child_ids") {
if err := clearRemovedChildStudios(ctx, qb, studioID, childStudioIDs); err != nil {
return err
}
}
if err := studio.ValidateModify(ctx, updatedStudio, qb); err != nil {
return err
}
@ -212,6 +291,12 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
}
}
if translator.hasField("child_ids") {
if err := setChildStudios(ctx, qb, studioID, childStudioIDs); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err

View file

@ -62,6 +62,7 @@ type StudioCreateInput struct {
URL *string `json:"url"` // deprecated
Urls []string `json:"urls"`
ParentID *string `json:"parent_id"`
ChildIds []string `json:"child_ids"`
// This should be a URL or a base64 encoded data URL
Image *string `json:"image"`
StashIds []StashIDInput `json:"stash_ids"`
@ -82,6 +83,7 @@ type StudioUpdateInput struct {
URL *string `json:"url"` // deprecated
Urls []string `json:"urls"`
ParentID *string `json:"parent_id"`
ChildIds []string `json:"child_ids"`
// This should be a URL or a base64 encoded data URL
Image *string `json:"image"`
StashIds []StashIDInput `json:"stash_ids"`

View file

@ -57,12 +57,14 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
const [isLoading, setIsLoading] = useState(false);
const [parentStudio, setParentStudio] = useState<Studio | null>(null);
const [childStudios, setChildStudios] = useState<Studio[]>([]);
const schema = yup.object({
name: yup.string().required(),
urls: yup.array(yup.string().required()).defined(),
details: yup.string().ensure(),
parent_id: yup.string().required().nullable(),
child_ids: yup.array(yup.string().required()).defined(),
aliases: yupRequiredStringArray(intl).defined(),
tag_ids: yup.array(yup.string().required()).defined(),
ignore_auto_tag: yup.boolean().defined(),
@ -77,6 +79,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
urls: studio.urls ?? [],
details: studio.details ?? "",
parent_id: studio.parent_studio?.id ?? null,
child_ids: (studio.child_studios ?? []).map((child) => child.id),
aliases: studio.aliases ?? [],
tag_ids: (studio.tags ?? []).map((t) => t.id),
ignore_auto_tag: studio.ignore_auto_tag ?? false,
@ -112,6 +115,14 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
formik.setFieldValue("parent_id", item ? item.id : null);
}
function onSetChildStudios(items: Studio[]) {
setChildStudios(items);
formik.setFieldValue(
"child_ids",
items.map((item) => item.id)
);
}
const encodingImage = ImageUtils.usePasteImage((imageData) =>
formik.setFieldValue("image", imageData)
);
@ -128,6 +139,17 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
);
}, [studio.parent_studio]);
useEffect(() => {
setChildStudios(
(studio.child_studios ?? []).map((childStudio) => ({
id: childStudio.id,
name: childStudio.name,
aliases: [],
image_path: childStudio.image_path,
}))
);
}, [studio.child_studios]);
useEffect(() => {
setImage(formik.values.image);
}, [formik.values.image, setImage]);
@ -199,12 +221,35 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
onSetParentStudio(items.length > 0 ? items[0] : null)
}
values={parentStudio ? [parentStudio] : []}
excludeIds={[
...(studio?.id ? [studio.id] : []),
...formik.values.child_ids,
]}
/>
);
return renderField("parent_id", title, control);
}
function renderSubStudiosField() {
const title = intl.formatMessage({ id: "subsidiary_studios" });
const control = (
<StudioSelect
isMulti
onSelect={onSetChildStudios}
values={childStudios.filter((childStudio) =>
formik.values.child_ids.includes(childStudio.id)
)}
excludeIds={[
...(studio?.id ? [studio.id] : []),
...(formik.values.parent_id ? [formik.values.parent_id] : []),
]}
/>
);
return renderField("child_ids", title, control);
}
function renderTagsField() {
const title = intl.formatMessage({ id: "tags" });
return renderField("tag_ids", title, tagsControl());
@ -246,6 +291,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
{renderStringListField("urls")}
{renderInputField("details", "textarea")}
{renderParentStudioField()}
{renderSubStudiosField()}
{renderTagsField()}
{renderStashIDsField(
"stash_ids",