diff --git a/graphql/schema/types/studio.graphql b/graphql/schema/types/studio.graphql index 51a87bf4f..be11a1d84 100644 --- a/graphql/schema/types/studio.graphql +++ b/graphql/schema/types/studio.graphql @@ -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!] diff --git a/internal/api/resolver_mutation_studio.go b/internal/api/resolver_mutation_studio.go index c7af918a1..c809e3e0f 100644 --- a/internal/api/resolver_mutation_studio.go +++ b/internal/api/resolver_mutation_studio.go @@ -14,6 +14,50 @@ import ( ) // used to refetch studio after hooks run + +func setChildStudios(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 { + 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 + } + } + + 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 (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 +106,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 +142,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 +190,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 { @@ -212,6 +272,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 diff --git a/pkg/models/studio.go b/pkg/models/studio.go index 7ad8719ac..06c4b2cfd 100644 --- a/pkg/models/studio.go +++ b/pkg/models/studio.go @@ -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"` diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx index b1de160b1..837d21a87 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx @@ -57,12 +57,14 @@ export const StudioEditPanel: React.FC = ({ const [isLoading, setIsLoading] = useState(false); const [parentStudio, setParentStudio] = useState(null); + const [childStudios, setChildStudios] = useState([]); 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 = ({ 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 = ({ 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,10 @@ export const StudioEditPanel: React.FC = ({ ); }, [studio.parent_studio]); + useEffect(() => { + setChildStudios(studio.child_studios ?? []); + }, [studio.child_studios]); + useEffect(() => { setImage(formik.values.image); }, [formik.values.image, setImage]); @@ -205,6 +220,20 @@ export const StudioEditPanel: React.FC = ({ return renderField("parent_id", title, control); } + function renderSubStudiosField() { + const title = intl.formatMessage({ id: "subsidiary_studios" }); + const control = ( + + ); + + return renderField("child_ids", title, control); + } + function renderTagsField() { const title = intl.formatMessage({ id: "tags" }); return renderField("tag_ids", title, tagsControl()); @@ -246,6 +275,7 @@ export const StudioEditPanel: React.FC = ({ {renderStringListField("urls")} {renderInputField("details", "textarea")} {renderParentStudioField()} + {renderSubStudiosField()} {renderTagsField()} {renderStashIDsField( "stash_ids",