mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Feature: Tag StashID support (#6255)
This commit is contained in:
parent
a08d2e258a
commit
c99825a453
30 changed files with 387 additions and 34 deletions
|
|
@ -71,6 +71,8 @@ type ScrapedTag {
|
||||||
"Set if tag matched"
|
"Set if tag matched"
|
||||||
stored_id: ID
|
stored_id: ID
|
||||||
name: String!
|
name: String!
|
||||||
|
"Remote site ID, if applicable"
|
||||||
|
remote_site_id: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrapedScene {
|
type ScrapedScene {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ type Tag {
|
||||||
created_at: Time!
|
created_at: Time!
|
||||||
updated_at: Time!
|
updated_at: Time!
|
||||||
favorite: Boolean!
|
favorite: Boolean!
|
||||||
|
stash_ids: [StashID!]!
|
||||||
image_path: String # Resolver
|
image_path: String # Resolver
|
||||||
scene_count(depth: Int): Int! # Resolver
|
scene_count(depth: Int): Int! # Resolver
|
||||||
scene_marker_count(depth: Int): Int! # Resolver
|
scene_marker_count(depth: Int): Int! # Resolver
|
||||||
|
|
@ -35,6 +36,7 @@ input TagCreateInput {
|
||||||
favorite: Boolean
|
favorite: Boolean
|
||||||
"This should be a URL or a base64 encoded data URL"
|
"This should be a URL or a base64 encoded data URL"
|
||||||
image: String
|
image: String
|
||||||
|
stash_ids: [StashIDInput!]
|
||||||
|
|
||||||
parent_ids: [ID!]
|
parent_ids: [ID!]
|
||||||
child_ids: [ID!]
|
child_ids: [ID!]
|
||||||
|
|
@ -51,6 +53,7 @@ input TagUpdateInput {
|
||||||
favorite: Boolean
|
favorite: Boolean
|
||||||
"This should be a URL or a base64 encoded data URL"
|
"This should be a URL or a base64 encoded data URL"
|
||||||
image: String
|
image: String
|
||||||
|
stash_ids: [StashIDInput!]
|
||||||
|
|
||||||
parent_ids: [ID!]
|
parent_ids: [ID!]
|
||||||
child_ids: [ID!]
|
child_ids: [ID!]
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,16 @@ func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) (ret []strin
|
||||||
return obj.Aliases.List(), nil
|
return obj.Aliases.List(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *tagResolver) StashIds(ctx context.Context, obj *models.Tag) ([]*models.StashID, error) {
|
||||||
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
|
return obj.LoadStashIDs(ctx, r.repository.Tag)
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
|
func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = scene.CountByTagID(ctx, r.repository.Scene, obj.ID, depth)
|
ret, err = scene.CountByTagID(ctx, r.repository.Scene, obj.ID, depth)
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,14 @@ func (r *mutationResolver) makeSceneDraft(ctx context.Context, s *models.Scene,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load StashIDs for tags
|
||||||
|
tqb := r.repository.Tag
|
||||||
|
for _, t := range draft.Tags {
|
||||||
|
if err := t.LoadStashIDs(ctx, tqb); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
draft.Cover = cover
|
draft.Cover = cover
|
||||||
|
|
||||||
return draft, nil
|
return draft, nil
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,14 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
|
||||||
newTag.Description = translator.string(input.Description)
|
newTag.Description = translator.string(input.Description)
|
||||||
newTag.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
newTag.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
||||||
|
|
||||||
|
var stashIDInputs models.StashIDInputs
|
||||||
|
for _, sid := range input.StashIds {
|
||||||
|
if sid != nil {
|
||||||
|
stashIDInputs = append(stashIDInputs, *sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newTag.StashIDs = models.NewRelatedStashIDs(stashIDInputs.ToStashIDs())
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
newTag.ParentIDs, err = translator.relatedIds(input.ParentIds)
|
newTag.ParentIDs, err = translator.relatedIds(input.ParentIds)
|
||||||
|
|
@ -110,6 +118,14 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
|
||||||
|
|
||||||
updatedTag.Aliases = translator.updateStrings(input.Aliases, "aliases")
|
updatedTag.Aliases = translator.updateStrings(input.Aliases, "aliases")
|
||||||
|
|
||||||
|
var updateStashIDInputs models.StashIDInputs
|
||||||
|
for _, sid := range input.StashIds {
|
||||||
|
if sid != nil {
|
||||||
|
updateStashIDInputs = append(updateStashIDInputs, *sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatedTag.StashIDs = translator.updateStashIDs(updateStashIDInputs, "stash_ids")
|
||||||
|
|
||||||
updatedTag.ParentIDs, err = translator.updateIds(input.ParentIds, "parent_ids")
|
updatedTag.ParentIDs, err = translator.updateIds(input.ParentIds, "parent_ids")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("converting parent tag ids: %w", err)
|
return nil, fmt.Errorf("converting parent tag ids: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,8 @@ func (g sceneRelationships) tags(ctx context.Context) ([]int, error) {
|
||||||
tagIDs = originalTagIDs
|
tagIDs = originalTagIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
endpoint := g.result.source.RemoteSite
|
||||||
|
|
||||||
for _, t := range scraped {
|
for _, t := range scraped {
|
||||||
if t.StoredID != nil {
|
if t.StoredID != nil {
|
||||||
// existing tag, just add it
|
// existing tag, just add it
|
||||||
|
|
@ -163,10 +165,9 @@ func (g sceneRelationships) tags(ctx context.Context) ([]int, error) {
|
||||||
|
|
||||||
tagIDs = sliceutil.AppendUnique(tagIDs, int(tagID))
|
tagIDs = sliceutil.AppendUnique(tagIDs, int(tagID))
|
||||||
} else if createMissing {
|
} else if createMissing {
|
||||||
newTag := models.NewTag()
|
newTag := t.ToTag(endpoint, nil)
|
||||||
newTag.Name = t.Name
|
|
||||||
|
|
||||||
err := g.tagCreator.Create(ctx, &newTag)
|
err := g.tagCreator.Create(ctx, newTag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating tag: %w", err)
|
return nil, fmt.Errorf("error creating tag: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ func (r SceneRelationships) MatchRelationships(ctx context.Context, s *models.Sc
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, t := range s.Tags {
|
for _, t := range s.Tags {
|
||||||
err := ScrapedTag(ctx, r.TagFinder, t)
|
err := ScrapedTag(ctx, r.TagFinder, t, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -190,11 +190,29 @@ func ScrapedGroup(ctx context.Context, qb GroupNamesFinder, storedID *string, na
|
||||||
|
|
||||||
// ScrapedTag matches the provided tag with the tags
|
// ScrapedTag matches the provided tag with the tags
|
||||||
// in the database and sets the ID field if one is found.
|
// in the database and sets the ID field if one is found.
|
||||||
func ScrapedTag(ctx context.Context, qb models.TagQueryer, s *models.ScrapedTag) error {
|
func ScrapedTag(ctx context.Context, qb models.TagQueryer, s *models.ScrapedTag, stashBoxEndpoint string) error {
|
||||||
if s.StoredID != nil {
|
if s.StoredID != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a tag with the StashID already exists
|
||||||
|
if stashBoxEndpoint != "" && s.RemoteSiteID != nil {
|
||||||
|
if finder, ok := qb.(models.TagFinder); ok {
|
||||||
|
tags, err := finder.FindByStashID(ctx, models.StashID{
|
||||||
|
StashID: *s.RemoteSiteID,
|
||||||
|
Endpoint: stashBoxEndpoint,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(tags) > 0 {
|
||||||
|
id := strconv.Itoa(tags[0].ID)
|
||||||
|
s.StoredID = &id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
t, err := tag.ByName(ctx, qb, s.Name)
|
t, err := tag.ByName(ctx, qb, s.Name)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/json"
|
"github.com/stashapp/stash/pkg/models/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -18,6 +19,7 @@ type Tag struct {
|
||||||
Image string `json:"image,omitempty"`
|
Image string `json:"image,omitempty"`
|
||||||
Parents []string `json:"parents,omitempty"`
|
Parents []string `json:"parents,omitempty"`
|
||||||
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
|
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
|
||||||
|
StashIDs []models.StashID `json:"stash_ids,omitempty"`
|
||||||
CreatedAt json.JSONTime `json:"created_at,omitempty"`
|
CreatedAt json.JSONTime `json:"created_at,omitempty"`
|
||||||
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
|
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -427,6 +427,29 @@ func (_m *TagReaderWriter) FindBySceneMarkerID(ctx context.Context, sceneMarkerI
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindByStashID provides a mock function with given fields: ctx, stashID
|
||||||
|
func (_m *TagReaderWriter) FindByStashID(ctx context.Context, stashID models.StashID) ([]*models.Tag, error) {
|
||||||
|
ret := _m.Called(ctx, stashID)
|
||||||
|
|
||||||
|
var r0 []*models.Tag
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, models.StashID) []*models.Tag); ok {
|
||||||
|
r0 = rf(ctx, stashID)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*models.Tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, models.StashID) error); ok {
|
||||||
|
r1 = rf(ctx, stashID)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// FindByStudioID provides a mock function with given fields: ctx, studioID
|
// FindByStudioID provides a mock function with given fields: ctx, studioID
|
||||||
func (_m *TagReaderWriter) FindByStudioID(ctx context.Context, studioID int) ([]*models.Tag, error) {
|
func (_m *TagReaderWriter) FindByStudioID(ctx context.Context, studioID int) ([]*models.Tag, error) {
|
||||||
ret := _m.Called(ctx, studioID)
|
ret := _m.Called(ctx, studioID)
|
||||||
|
|
@ -565,6 +588,29 @@ func (_m *TagReaderWriter) GetParentIDs(ctx context.Context, relatedID int) ([]i
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetStashIDs provides a mock function with given fields: ctx, relatedID
|
||||||
|
func (_m *TagReaderWriter) GetStashIDs(ctx context.Context, relatedID int) ([]models.StashID, error) {
|
||||||
|
ret := _m.Called(ctx, relatedID)
|
||||||
|
|
||||||
|
var r0 []models.StashID
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int) []models.StashID); ok {
|
||||||
|
r0 = rf(ctx, relatedID)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]models.StashID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
||||||
|
r1 = rf(ctx, relatedID)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// HasImage provides a mock function with given fields: ctx, tagID
|
// HasImage provides a mock function with given fields: ctx, tagID
|
||||||
func (_m *TagReaderWriter) HasImage(ctx context.Context, tagID int) (bool, error) {
|
func (_m *TagReaderWriter) HasImage(ctx context.Context, tagID int) (bool, error) {
|
||||||
ret := _m.Called(ctx, tagID)
|
ret := _m.Called(ctx, tagID)
|
||||||
|
|
|
||||||
|
|
@ -449,10 +449,29 @@ type ScrapedTag struct {
|
||||||
// Set if tag matched
|
// Set if tag matched
|
||||||
StoredID *string `json:"stored_id"`
|
StoredID *string `json:"stored_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
RemoteSiteID *string `json:"remote_site_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ScrapedTag) IsScrapedContent() {}
|
func (ScrapedTag) IsScrapedContent() {}
|
||||||
|
|
||||||
|
func (t *ScrapedTag) ToTag(endpoint string, excluded map[string]bool) *Tag {
|
||||||
|
currentTime := time.Now()
|
||||||
|
ret := NewTag()
|
||||||
|
ret.Name = t.Name
|
||||||
|
|
||||||
|
if t.RemoteSiteID != nil && endpoint != "" {
|
||||||
|
ret.StashIDs = NewRelatedStashIDs([]StashID{
|
||||||
|
{
|
||||||
|
Endpoint: endpoint,
|
||||||
|
StashID: *t.RemoteSiteID,
|
||||||
|
UpdatedAt: currentTime,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ret
|
||||||
|
}
|
||||||
|
|
||||||
func ScrapedTagSortFunction(a, b *ScrapedTag) int {
|
func ScrapedTagSortFunction(a, b *ScrapedTag) int {
|
||||||
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
|
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ type Tag struct {
|
||||||
Aliases RelatedStrings `json:"aliases"`
|
Aliases RelatedStrings `json:"aliases"`
|
||||||
ParentIDs RelatedIDs `json:"parent_ids"`
|
ParentIDs RelatedIDs `json:"parent_ids"`
|
||||||
ChildIDs RelatedIDs `json:"tag_ids"`
|
ChildIDs RelatedIDs `json:"tag_ids"`
|
||||||
|
StashIDs RelatedStashIDs `json:"stash_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTag() Tag {
|
func NewTag() Tag {
|
||||||
|
|
@ -46,6 +47,12 @@ func (s *Tag) LoadChildIDs(ctx context.Context, l TagRelationLoader) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Tag) LoadStashIDs(ctx context.Context, l StashIDLoader) error {
|
||||||
|
return s.StashIDs.load(func() ([]StashID, error) {
|
||||||
|
return l.GetStashIDs(ctx, s.ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type TagPartial struct {
|
type TagPartial struct {
|
||||||
Name OptionalString
|
Name OptionalString
|
||||||
SortName OptionalString
|
SortName OptionalString
|
||||||
|
|
@ -58,6 +65,7 @@ type TagPartial struct {
|
||||||
Aliases *UpdateStrings
|
Aliases *UpdateStrings
|
||||||
ParentIDs *UpdateIDs
|
ParentIDs *UpdateIDs
|
||||||
ChildIDs *UpdateIDs
|
ChildIDs *UpdateIDs
|
||||||
|
StashIDs *UpdateStashIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTagPartial() TagPartial {
|
func NewTagPartial() TagPartial {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ type TagFinder interface {
|
||||||
FindByStudioID(ctx context.Context, studioID int) ([]*Tag, error)
|
FindByStudioID(ctx context.Context, studioID int) ([]*Tag, error)
|
||||||
FindByName(ctx context.Context, name string, nocase bool) (*Tag, error)
|
FindByName(ctx context.Context, name string, nocase bool) (*Tag, error)
|
||||||
FindByNames(ctx context.Context, names []string, nocase bool) ([]*Tag, error)
|
FindByNames(ctx context.Context, names []string, nocase bool) ([]*Tag, error)
|
||||||
|
FindByStashID(ctx context.Context, stashID StashID) ([]*Tag, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagQueryer provides methods to query tags.
|
// TagQueryer provides methods to query tags.
|
||||||
|
|
@ -87,6 +88,7 @@ type TagReader interface {
|
||||||
|
|
||||||
AliasLoader
|
AliasLoader
|
||||||
TagRelationLoader
|
TagRelationLoader
|
||||||
|
StashIDLoader
|
||||||
|
|
||||||
All(ctx context.Context) ([]*Tag, error)
|
All(ctx context.Context) ([]*Tag, error)
|
||||||
GetImage(ctx context.Context, tagID int) ([]byte, error)
|
GetImage(ctx context.Context, tagID int) ([]byte, error)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ func postProcessTags(ctx context.Context, tqb models.TagQueryer, scrapedTags []*
|
||||||
ret = make([]*models.ScrapedTag, 0, len(scrapedTags))
|
ret = make([]*models.ScrapedTag, 0, len(scrapedTags))
|
||||||
|
|
||||||
for _, t := range scrapedTags {
|
for _, t := range scrapedTags {
|
||||||
err := match.ScrapedTag(ctx, tqb, t)
|
// Pass empty string for endpoint since this is used by general scrapers, not just stash-box
|
||||||
|
err := match.ScrapedTag(ctx, tqb, t, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ func (db *Anonymiser) deleteStashIDs() error {
|
||||||
func() error { return db.truncateTable("scene_stash_ids") },
|
func() error { return db.truncateTable("scene_stash_ids") },
|
||||||
func() error { return db.truncateTable("studio_stash_ids") },
|
func() error { return db.truncateTable("studio_stash_ids") },
|
||||||
func() error { return db.truncateTable("performer_stash_ids") },
|
func() error { return db.truncateTable("performer_stash_ids") },
|
||||||
|
func() error { return db.truncateTable("tag_stash_ids") },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ const (
|
||||||
cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE"
|
cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE"
|
||||||
)
|
)
|
||||||
|
|
||||||
var appSchemaVersion uint = 73
|
var appSchemaVersion uint = 74
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*.sql
|
||||||
var migrationsBox embed.FS
|
var migrationsBox embed.FS
|
||||||
|
|
|
||||||
7
pkg/sqlite/migrations/74_tag_stash_ids.up.sql
Normal file
7
pkg/sqlite/migrations/74_tag_stash_ids.up.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE `tag_stash_ids` (
|
||||||
|
`tag_id` integer,
|
||||||
|
`endpoint` varchar(255),
|
||||||
|
`stash_id` varchar(36),
|
||||||
|
`updated_at` datetime not null default '1970-01-01T00:00:00Z',
|
||||||
|
foreign key(`tag_id`) references `tags`(`id`) on delete CASCADE
|
||||||
|
);
|
||||||
|
|
@ -29,6 +29,7 @@ func testStashIDReaderWriter(ctx context.Context, t *testing.T, r stashIDReaderW
|
||||||
stashID := models.StashID{
|
stashID := models.StashID{
|
||||||
StashID: stashIDStr,
|
StashID: stashIDStr,
|
||||||
Endpoint: endpoint,
|
Endpoint: endpoint,
|
||||||
|
UpdatedAt: epochTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
// update stash ids and ensure was updated
|
// update stash ids and ensure was updated
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ var (
|
||||||
|
|
||||||
tagsAliasesJoinTable = goqu.T(tagAliasesTable)
|
tagsAliasesJoinTable = goqu.T(tagAliasesTable)
|
||||||
tagRelationsJoinTable = goqu.T(tagRelationsTable)
|
tagRelationsJoinTable = goqu.T(tagRelationsTable)
|
||||||
|
tagsStashIDsJoinTable = goqu.T("tag_stash_ids")
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -375,6 +376,13 @@ var (
|
||||||
}
|
}
|
||||||
|
|
||||||
tagsChildTagsTableMgr = *tagsParentTagsTableMgr.invert()
|
tagsChildTagsTableMgr = *tagsParentTagsTableMgr.invert()
|
||||||
|
|
||||||
|
tagsStashIDsTableMgr = &stashIDTable{
|
||||||
|
table: table{
|
||||||
|
table: tagsStashIDsJoinTable,
|
||||||
|
idColumn: tagsStashIDsJoinTable.Col(tagIDColumn),
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ type tagRepositoryType struct {
|
||||||
repository
|
repository
|
||||||
|
|
||||||
aliases stringRepository
|
aliases stringRepository
|
||||||
|
stashIDs stashIDRepository
|
||||||
|
|
||||||
scenes joinRepository
|
scenes joinRepository
|
||||||
images joinRepository
|
images joinRepository
|
||||||
|
|
@ -121,6 +122,12 @@ var (
|
||||||
},
|
},
|
||||||
stringColumn: tagAliasColumn,
|
stringColumn: tagAliasColumn,
|
||||||
},
|
},
|
||||||
|
stashIDs: stashIDRepository{
|
||||||
|
repository{
|
||||||
|
tableName: "tag_stash_ids",
|
||||||
|
idColumn: tagIDColumn,
|
||||||
|
},
|
||||||
|
},
|
||||||
scenes: joinRepository{
|
scenes: joinRepository{
|
||||||
repository: repository{
|
repository: repository{
|
||||||
tableName: scenesTagsTable,
|
tableName: scenesTagsTable,
|
||||||
|
|
@ -199,6 +206,12 @@ func (qb *TagStore) Create(ctx context.Context, newObject *models.Tag) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if newObject.StashIDs.Loaded() {
|
||||||
|
if err := tagsStashIDsTableMgr.insertJoins(ctx, id, newObject.StashIDs.List()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updated, err := qb.find(ctx, id)
|
updated, err := qb.find(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("finding after create: %w", err)
|
return fmt.Errorf("finding after create: %w", err)
|
||||||
|
|
@ -242,6 +255,12 @@ func (qb *TagStore) UpdatePartial(ctx context.Context, id int, partial models.Ta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if partial.StashIDs != nil {
|
||||||
|
if err := tagsStashIDsTableMgr.modifyJoins(ctx, id, partial.StashIDs.StashIDs, partial.StashIDs.Mode); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return qb.find(ctx, id)
|
return qb.find(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,6 +290,12 @@ func (qb *TagStore) Update(ctx context.Context, updatedObject *models.Tag) error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if updatedObject.StashIDs.Loaded() {
|
||||||
|
if err := tagsStashIDsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.StashIDs.List()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -509,6 +534,24 @@ func (qb *TagStore) FindByNames(ctx context.Context, names []string, nocase bool
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *TagStore) FindByStashID(ctx context.Context, stashID models.StashID) ([]*models.Tag, error) {
|
||||||
|
sq := dialect.From(tagsStashIDsJoinTable).Select(tagsStashIDsJoinTable.Col(tagIDColumn)).Where(
|
||||||
|
tagsStashIDsJoinTable.Col("stash_id").Eq(stashID.StashID),
|
||||||
|
tagsStashIDsJoinTable.Col("endpoint").Eq(stashID.Endpoint),
|
||||||
|
)
|
||||||
|
|
||||||
|
idsQuery := qb.selectDataset().Where(
|
||||||
|
qb.table().Col(idColumn).In(sq),
|
||||||
|
)
|
||||||
|
|
||||||
|
ret, err := qb.getMany(ctx, idsQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting tags for stash ID %s: %w", stashID.StashID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *TagStore) GetParentIDs(ctx context.Context, relatedID int) ([]int, error) {
|
func (qb *TagStore) GetParentIDs(ctx context.Context, relatedID int) ([]int, error) {
|
||||||
return tagsParentTagsTableMgr.get(ctx, relatedID)
|
return tagsParentTagsTableMgr.get(ctx, relatedID)
|
||||||
}
|
}
|
||||||
|
|
@ -779,6 +822,14 @@ func (qb *TagStore) UpdateAliases(ctx context.Context, tagID int, aliases []stri
|
||||||
return tagRepository.aliases.replace(ctx, tagID, aliases)
|
return tagRepository.aliases.replace(ctx, tagID, aliases)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *TagStore) GetStashIDs(ctx context.Context, tagID int) ([]models.StashID, error) {
|
||||||
|
return tagsStashIDsTableMgr.get(ctx, tagID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *TagStore) UpdateStashIDs(ctx context.Context, tagID int, stashIDs []models.StashID) error {
|
||||||
|
return tagsStashIDsTableMgr.replaceJoins(ctx, tagID, stashIDs)
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *TagStore) Merge(ctx context.Context, source []int, destination int) error {
|
func (qb *TagStore) Merge(ctx context.Context, source []int, destination int) error {
|
||||||
if len(source) == 0 {
|
if len(source) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -840,6 +891,19 @@ AND NOT EXISTS(SELECT 1 FROM `+table+` o WHERE o.`+idColumn+` = `+table+`.`+idCo
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge StashIDs - move all source StashIDs to destination (ignoring duplicates)
|
||||||
|
_, err = dbWrapper.Exec(ctx, `UPDATE OR IGNORE `+"tag_stash_ids"+`
|
||||||
|
SET tag_id = ?
|
||||||
|
WHERE tag_id IN `+inBinding, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete remaining source StashIDs that couldn't be moved (duplicates)
|
||||||
|
if _, err := dbWrapper.Exec(ctx, `DELETE FROM tag_stash_ids WHERE tag_id IN `+inBinding, srcArgs...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for _, id := range source {
|
for _, id := range source {
|
||||||
err = qb.Destroy(ctx, id)
|
err = qb.Destroy(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -900,6 +900,66 @@ func TestTagUpdateAlias(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTagStashIDs(t *testing.T) {
|
||||||
|
if err := withTxn(func(ctx context.Context) error {
|
||||||
|
qb := db.Tag
|
||||||
|
|
||||||
|
// create tag to test against
|
||||||
|
const name = "TestTagStashIDs"
|
||||||
|
tag := models.Tag{
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
err := qb.Create(ctx, &tag)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating tag: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
testStashIDReaderWriter(ctx, t, qb, tag.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagFindByStashID(t *testing.T) {
|
||||||
|
withTxn(func(ctx context.Context) error {
|
||||||
|
qb := db.Tag
|
||||||
|
|
||||||
|
// create tag to test against
|
||||||
|
const name = "TestTagFindByStashID"
|
||||||
|
const stashID = "stashid"
|
||||||
|
const endpoint = "endpoint"
|
||||||
|
tag := models.Tag{
|
||||||
|
Name: name,
|
||||||
|
StashIDs: models.NewRelatedStashIDs([]models.StashID{{StashID: stashID, Endpoint: endpoint}}),
|
||||||
|
}
|
||||||
|
err := qb.Create(ctx, &tag)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating tag: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// find by stash ID
|
||||||
|
tags, err := qb.FindByStashID(ctx, models.StashID{StashID: stashID, Endpoint: endpoint})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error finding by stash ID: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, tags, 1)
|
||||||
|
assert.Equal(t, tag.ID, tags[0].ID)
|
||||||
|
|
||||||
|
// find by non-existent stash ID
|
||||||
|
tags, err = qb.FindByStashID(ctx, models.StashID{StashID: "nonexistent", Endpoint: endpoint})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error finding by stash ID: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, tags, 0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestTagMerge(t *testing.T) {
|
func TestTagMerge(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@ func (c Client) sceneFragmentToScrapedScene(ctx context.Context, s *graphql.Scen
|
||||||
for _, t := range s.Tags {
|
for _, t := range s.Tags {
|
||||||
st := &models.ScrapedTag{
|
st := &models.ScrapedTag{
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
|
RemoteSiteID: &t.ID,
|
||||||
}
|
}
|
||||||
ss.Tags = append(ss.Tags, st)
|
ss.Tags = append(ss.Tags, st)
|
||||||
}
|
}
|
||||||
|
|
@ -242,6 +243,7 @@ type SceneDraft struct {
|
||||||
Performers []*models.Performer
|
Performers []*models.Performer
|
||||||
// StashIDs must be loaded
|
// StashIDs must be loaded
|
||||||
Studio *models.Studio
|
Studio *models.Studio
|
||||||
|
// StashIDs must be loaded
|
||||||
Tags []*models.Tag
|
Tags []*models.Tag
|
||||||
Cover []byte
|
Cover []byte
|
||||||
}
|
}
|
||||||
|
|
@ -347,7 +349,17 @@ func newSceneDraftInput(d SceneDraft, endpoint string) graphql.SceneDraftInput {
|
||||||
var tags []*graphql.DraftEntityInput
|
var tags []*graphql.DraftEntityInput
|
||||||
sceneTags := d.Tags
|
sceneTags := d.Tags
|
||||||
for _, tag := range sceneTags {
|
for _, tag := range sceneTags {
|
||||||
tags = append(tags, &graphql.DraftEntityInput{Name: tag.Name})
|
tagDraft := graphql.DraftEntityInput{Name: tag.Name}
|
||||||
|
|
||||||
|
stashIDs := tag.StashIDs.List()
|
||||||
|
for _, stashID := range stashIDs {
|
||||||
|
if stashID.Endpoint == endpoint {
|
||||||
|
tagDraft.ID = &stashID.StashID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = append(tags, &tagDraft)
|
||||||
}
|
}
|
||||||
draft.Tags = tags
|
draft.Tags = tags
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ type FinderAliasImageGetter interface {
|
||||||
GetAliases(ctx context.Context, studioID int) ([]string, error)
|
GetAliases(ctx context.Context, studioID int) ([]string, error)
|
||||||
GetImage(ctx context.Context, tagID int) ([]byte, error)
|
GetImage(ctx context.Context, tagID int) ([]byte, error)
|
||||||
FindByChildTagID(ctx context.Context, childID int) ([]*models.Tag, error)
|
FindByChildTagID(ctx context.Context, childID int) ([]*models.Tag, error)
|
||||||
|
models.StashIDLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToJSON converts a Tag object into its JSON equivalent.
|
// ToJSON converts a Tag object into its JSON equivalent.
|
||||||
|
|
@ -37,6 +38,15 @@ func ToJSON(ctx context.Context, reader FinderAliasImageGetter, tag *models.Tag)
|
||||||
|
|
||||||
newTagJSON.Aliases = aliases
|
newTagJSON.Aliases = aliases
|
||||||
|
|
||||||
|
if err := tag.LoadStashIDs(ctx, reader); err != nil {
|
||||||
|
return nil, fmt.Errorf("loading tag stash ids: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stashIDs := tag.StashIDs.List()
|
||||||
|
if len(stashIDs) > 0 {
|
||||||
|
newTagJSON.StashIDs = stashIDs
|
||||||
|
}
|
||||||
|
|
||||||
image, err := reader.GetImage(ctx, tag.ID)
|
image, err := reader.GetImage(ctx, tag.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Error getting tag image: %v", err)
|
logger.Errorf("Error getting tag image: %v", err)
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,13 @@ func TestToJSON(t *testing.T) {
|
||||||
db.Tag.On("GetAliases", testCtx, withParentsID).Return(nil, nil).Once()
|
db.Tag.On("GetAliases", testCtx, withParentsID).Return(nil, nil).Once()
|
||||||
db.Tag.On("GetAliases", testCtx, errParentsID).Return(nil, nil).Once()
|
db.Tag.On("GetAliases", testCtx, errParentsID).Return(nil, nil).Once()
|
||||||
|
|
||||||
|
db.Tag.On("GetStashIDs", testCtx, tagID).Return(nil, nil).Once()
|
||||||
|
db.Tag.On("GetStashIDs", testCtx, noImageID).Return(nil, nil).Once()
|
||||||
|
db.Tag.On("GetStashIDs", testCtx, errImageID).Return(nil, nil).Once()
|
||||||
|
// errAliasID test fails before GetStashIDs is called, so no mock needed
|
||||||
|
db.Tag.On("GetStashIDs", testCtx, withParentsID).Return(nil, nil).Once()
|
||||||
|
db.Tag.On("GetStashIDs", testCtx, errParentsID).Return(nil, nil).Once()
|
||||||
|
|
||||||
db.Tag.On("GetImage", testCtx, tagID).Return(imageBytes, nil).Once()
|
db.Tag.On("GetImage", testCtx, tagID).Return(imageBytes, nil).Once()
|
||||||
db.Tag.On("GetImage", testCtx, noImageID).Return(nil, nil).Once()
|
db.Tag.On("GetImage", testCtx, noImageID).Return(nil, nil).Once()
|
||||||
db.Tag.On("GetImage", testCtx, errImageID).Return(nil, imageErr).Once()
|
db.Tag.On("GetImage", testCtx, errImageID).Return(nil, imageErr).Once()
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ func (i *Importer) PreImport(ctx context.Context) error {
|
||||||
Description: i.Input.Description,
|
Description: i.Input.Description,
|
||||||
Favorite: i.Input.Favorite,
|
Favorite: i.Input.Favorite,
|
||||||
IgnoreAutoTag: i.Input.IgnoreAutoTag,
|
IgnoreAutoTag: i.Input.IgnoreAutoTag,
|
||||||
|
StashIDs: models.NewRelatedStashIDs(i.Input.StashIDs),
|
||||||
CreatedAt: i.Input.CreatedAt.GetTime(),
|
CreatedAt: i.Input.CreatedAt.GetTime(),
|
||||||
UpdatedAt: i.Input.UpdatedAt.GetTime(),
|
UpdatedAt: i.Input.UpdatedAt.GetTime(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@ fragment ScrapedSceneStudioData on ScrapedStudio {
|
||||||
fragment ScrapedSceneTagData on ScrapedTag {
|
fragment ScrapedSceneTagData on ScrapedTag {
|
||||||
stored_id
|
stored_id
|
||||||
name
|
name
|
||||||
|
remote_site_id
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment ScrapedSceneData on ScrapedScene {
|
fragment ScrapedSceneData on ScrapedScene {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@ fragment TagData on Tag {
|
||||||
aliases
|
aliases
|
||||||
ignore_auto_tag
|
ignore_auto_tag
|
||||||
favorite
|
favorite
|
||||||
|
stash_ids {
|
||||||
|
endpoint
|
||||||
|
stash_id
|
||||||
|
updated_at
|
||||||
|
}
|
||||||
image_path
|
image_path
|
||||||
scene_count
|
scene_count
|
||||||
scene_count_all: scene_count(depth: -1)
|
scene_count_all: scene_count(depth: -1)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { getStashboxBase } from "src/utils/stashbox";
|
import { getStashboxBase } from "src/utils/stashbox";
|
||||||
import { ExternalLink } from "./ExternalLink";
|
import { ExternalLink } from "./ExternalLink";
|
||||||
|
|
||||||
export type LinkType = "performers" | "scenes" | "studios";
|
export type LinkType = "performers" | "scenes" | "studios" | "tags";
|
||||||
|
|
||||||
export const StashIDPill: React.FC<{
|
export const StashIDPill: React.FC<{
|
||||||
stashID: Pick<StashId, "endpoint" | "stash_id">;
|
stashID: Pick<StashId, "endpoint" | "stash_id">;
|
||||||
|
|
|
||||||
|
|
@ -715,6 +715,18 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
||||||
|
|
||||||
async function onCreateTag(t: GQL.ScrapedTag) {
|
async function onCreateTag(t: GQL.ScrapedTag) {
|
||||||
const toCreate: GQL.TagCreateInput = { name: t.name };
|
const toCreate: GQL.TagCreateInput = { name: t.name };
|
||||||
|
|
||||||
|
// If the tag has a remote_site_id and we have an endpoint, include the stash_id
|
||||||
|
const endpoint = currentSource?.sourceInput.stash_box_endpoint;
|
||||||
|
if (t.remote_site_id && endpoint) {
|
||||||
|
toCreate.stash_ids = [
|
||||||
|
{
|
||||||
|
endpoint: endpoint,
|
||||||
|
stash_id: t.remote_site_id,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const newTagID = await createNewTag(t, toCreate);
|
const newTagID = await createNewTag(t, toCreate);
|
||||||
if (newTagID !== undefined) {
|
if (newTagID !== undefined) {
|
||||||
setTagIDs([...tagIDs, newTagID]);
|
setTagIDs([...tagIDs, newTagID]);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { TagLink } from "src/components/Shared/TagLink";
|
import { TagLink } from "src/components/Shared/TagLink";
|
||||||
import { DetailItem } from "src/components/Shared/DetailItem";
|
import { DetailItem } from "src/components/Shared/DetailItem";
|
||||||
|
import { StashIDPill } from "src/components/Shared/StashID";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
|
||||||
interface ITagDetails {
|
interface ITagDetails {
|
||||||
|
|
@ -51,6 +52,22 @@ export const TagDetailsPanel: React.FC<ITagDetails> = ({ tag, fullWidth }) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderStashIDs() {
|
||||||
|
if (!tag.stash_ids?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="pl-0">
|
||||||
|
{tag.stash_ids.map((stashID) => (
|
||||||
|
<li key={stashID.stash_id} className="row no-gutters">
|
||||||
|
<StashIDPill stashID={stashID} linkType="tags" />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="detail-group">
|
<div className="detail-group">
|
||||||
<DetailItem
|
<DetailItem
|
||||||
|
|
@ -68,6 +85,11 @@ export const TagDetailsPanel: React.FC<ITagDetails> = ({ tag, fullWidth }) => {
|
||||||
value={renderChildrenField()}
|
value={renderChildrenField()}
|
||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
/>
|
/>
|
||||||
|
<DetailItem
|
||||||
|
id="stash_ids"
|
||||||
|
value={renderStashIDs()}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { useToast } from "src/hooks/Toast";
|
||||||
import { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
import { formikUtils } from "src/utils/form";
|
import { formikUtils } from "src/utils/form";
|
||||||
import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup";
|
import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup";
|
||||||
|
import { getStashIDs } from "src/utils/stashIds";
|
||||||
import { Tag, TagSelect } from "../TagSelect";
|
import { Tag, TagSelect } from "../TagSelect";
|
||||||
|
|
||||||
interface ITagEditPanel {
|
interface ITagEditPanel {
|
||||||
|
|
@ -52,6 +53,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
parent_ids: yup.array(yup.string().required()).defined(),
|
parent_ids: yup.array(yup.string().required()).defined(),
|
||||||
child_ids: yup.array(yup.string().required()).defined(),
|
child_ids: yup.array(yup.string().required()).defined(),
|
||||||
ignore_auto_tag: yup.boolean().defined(),
|
ignore_auto_tag: yup.boolean().defined(),
|
||||||
|
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
|
||||||
image: yup.string().nullable().optional(),
|
image: yup.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -63,6 +65,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
parent_ids: (tag?.parents ?? []).map((t) => t.id),
|
parent_ids: (tag?.parents ?? []).map((t) => t.id),
|
||||||
child_ids: (tag?.children ?? []).map((t) => t.id),
|
child_ids: (tag?.children ?? []).map((t) => t.id),
|
||||||
ignore_auto_tag: tag?.ignore_auto_tag ?? false,
|
ignore_auto_tag: tag?.ignore_auto_tag ?? false,
|
||||||
|
stash_ids: getStashIDs(tag?.stash_ids),
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputValues = yup.InferType<typeof schema>;
|
type InputValues = yup.InferType<typeof schema>;
|
||||||
|
|
@ -140,10 +143,12 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
ImageUtils.onImageChange(event, onImageLoad);
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { renderField, renderInputField, renderStringListField } = formikUtils(
|
const {
|
||||||
intl,
|
renderField,
|
||||||
formik
|
renderInputField,
|
||||||
);
|
renderStringListField,
|
||||||
|
renderStashIDsField,
|
||||||
|
} = formikUtils(intl, formik);
|
||||||
|
|
||||||
function renderParentTagsField() {
|
function renderParentTagsField() {
|
||||||
const title = intl.formatMessage({ id: "parent_tags" });
|
const title = intl.formatMessage({ id: "parent_tags" });
|
||||||
|
|
@ -210,6 +215,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
{renderInputField("description", "textarea")}
|
{renderInputField("description", "textarea")}
|
||||||
{renderParentTagsField()}
|
{renderParentTagsField()}
|
||||||
{renderSubTagsField()}
|
{renderSubTagsField()}
|
||||||
|
{renderStashIDsField("stash_ids", "tags")}
|
||||||
<hr />
|
<hr />
|
||||||
{renderInputField("ignore_auto_tag", "checkbox")}
|
{renderInputField("ignore_auto_tag", "checkbox")}
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue