mirror of
https://github.com/stashapp/stash.git
synced 2026-05-09 05:05:29 +02:00
Add child studio support (child_ids) to Studio API and UI
This commit is contained in:
commit
314994f129
4 changed files with 135 additions and 0 deletions
|
|
@ -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!]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue