~netlandish/links-dev

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
1

[PATCH links] Allow removal of all tags from a bookmark, note, listing or short url.

Details
Message ID
<20251127172620.3173-1-peter@netlandish.com>
Sender timestamp
1764242777
DKIM signature
missing
Download raw message
Patch: +204 -81
Fixes: https://todo.code.netlandish.com/~netlandish/links/117
Changelog-fixed: Issue blocking the removal of ALL tags from an object.
---
 api/api_test.go               |  81 +++++++++++++-
 api/graph/schema.resolvers.go | 204 +++++++++++++++++++++-------------
 2 files changed, 204 insertions(+), 81 deletions(-)

diff --git a/api/api_test.go b/api/api_test.go
index d098661..b0125bd 100644
--- a/api/api_test.go
+++ b/api/api_test.go
@@ -3346,7 +3346,7 @@ func TestAPI(t *testing.T) {
		opA.Var("title", "Link A isolation")
		opA.Var("url", "https://example.com/link-a-isolation")
		opA.Var("description", "")
		opA.Var("visibility", models.OrgLinkVisibilityPublic)
		opA.Var("visibility", models.OrgLinkVisibilityPrivate)
		opA.Var("tags", "isolation-common, isolation-unique-a")
		opA.Var("slug", "personal-org")
		opA.Var("unread", false)
@@ -3366,7 +3366,7 @@ func TestAPI(t *testing.T) {
		opB.Var("title", "Link B isolation")
		opB.Var("url", "https://example.com/link-b-isolation")
		opB.Var("description", "")
		opB.Var("visibility", models.OrgLinkVisibilityPublic)
		opB.Var("visibility", models.OrgLinkVisibilityPrivate)
		opB.Var("tags", "isolation-common, isolation-unique-b")
		opB.Var("slug", "personal-org")
		opB.Var("unread", false)
@@ -3398,7 +3398,7 @@ func TestAPI(t *testing.T) {
		opUpdate.Var("hash", linkA.Link.Hash)
		opUpdate.Var("title", "Link A isolation")
		opUpdate.Var("url", "https://example.com/link-a-isolation")
		opUpdate.Var("visibility", models.OrgLinkVisibilityPublic)
		opUpdate.Var("visibility", models.OrgLinkVisibilityPrivate)
		opUpdate.Var("tags", "isolation-unique-a")
		err = links.Execute(ctx, opUpdate, &updatedA)
		c.NoError(err)
@@ -3426,4 +3426,79 @@ func TestAPI(t *testing.T) {
		c.True(tagSlugs["isolation-common"], "Link B should still have 'isolation-common' tag")
		c.True(tagSlugs["isolation-unique-b"], "Link B should still have 'isolation-unique-b' tag")
	})

	t.Run("remove all tags from link", func(t *testing.T) {
		c := require.New(t)
		type AddLinkResponse struct {
			Link models.OrgLink `json:"addLink"`
		}
		type UpdateLinkResponse struct {
			Link models.OrgLink `json:"updateLink"`
		}

		addLinkQuery := `mutation AddLink($title: String!, $url: String!, $description: String,
						  $visibility: LinkVisibility!, $unread: Boolean!, $starred: Boolean!,
						  $archive: Boolean! $slug: String!, $tags: String) {
					addLink(input: {
								title: $title,
								url: $url,
								description: $description,
								visibility: $visibility,
								unread: $unread,
								starred: $starred,
								archive: $archive,
								orgSlug: $slug,
								tags: $tags}) {
						id
						hash
					}
				}`

		var linkA AddLinkResponse
		opA := gqlclient.NewOperation(addLinkQuery)
		opA.Var("title", "Link with tags to remove")
		opA.Var("url", "https://test-tags.example.org/remove-all-tags")
		opA.Var("description", "")
		opA.Var("visibility", models.OrgLinkVisibilityPrivate)
		opA.Var("tags", "tag-to-remove-1, tag-to-remove-2")
		opA.Var("slug", "personal-org")
		opA.Var("unread", false)
		opA.Var("starred", false)
		opA.Var("archive", false)
		err := links.Execute(ctx, opA, &linkA)
		c.NoError(err)
		c.NotZero(linkA.Link.ID)

		tagLinksA, err := models.GetTagLinks(dbCtx,
			&database.FilterOptions{Filter: sq.Expr("org_link_id = ?", linkA.Link.ID)})
		c.NoError(err)
		c.Equal(2, len(tagLinksA), "Link should have 2 tags after creation")

		updateLinkQuery := `mutation UpdateLink($hash: String!, $title: String!,
								 $url: String!, $visibility: LinkVisibility!,
								 $tags: String) {
				updateLink(input: {
					title: $title,
					hash: $hash,
					url: $url,
					visibility: $visibility,
					tags: $tags,
				}) {id, hash}
			}`

		var updatedA UpdateLinkResponse
		opUpdate := gqlclient.NewOperation(updateLinkQuery)
		opUpdate.Var("hash", linkA.Link.Hash)
		opUpdate.Var("title", "Link with tags to remove")
		opUpdate.Var("url", "https://test-tags.example.org/remove-all-tags")
		opUpdate.Var("visibility", models.OrgLinkVisibilityPrivate)
		opUpdate.Var("tags", "")
		err = links.Execute(ctx, opUpdate, &updatedA)
		c.NoError(err)

		tagLinksAAfter, err := models.GetTagLinks(dbCtx,
			&database.FilterOptions{Filter: sq.Expr("org_link_id = ?", linkA.Link.ID)})
		c.NoError(err)
		c.Equal(0, len(tagLinksAAfter), "Link should have 0 tags after update with empty tags")
	})
}
diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go
index 74e3a84..b04fe6e 100644
--- a/api/graph/schema.resolvers.go
+++ b/api/graph/schema.resolvers.go
@@ -655,7 +655,8 @@ func (r *mutationResolver) UpdateLink(ctx context.Context, input *model.UpdateLi
			WithCode(valid.ErrValidationCode)
	}
	tags := make([]string, 0)
	if input.Tags != nil && *input.Tags != "" {
	tagsProvided := input.Tags != nil
	if tagsProvided && *input.Tags != "" {
		tags = strings.Split(strings.TrimSpace(*input.Tags), ",")
		validator.Expect(len(tags) <= 10, "%s", lt.Translate("Tags may not exceed 10")).
			WithField("tags").
@@ -761,35 +762,50 @@ func (r *mutationResolver) UpdateLink(ctx context.Context, input *model.UpdateLi
		return nil, err
	}

	if len(tags) > 0 {
		// Calculate the difference between original tags and new ones
		tagIDs, err := links.ProcessTags(ctx, tags)
		if err != nil {
			return nil, err
		}
		originalTags := orgLink.Tags
		tagIDsToRemove := make([]int, 0)
		m := make(map[int]bool)
		for _, id := range tagIDs {
			m[id] = true
		}

		for _, originalTag := range originalTags {
			if !m[originalTag.ID] {
				tagIDsToRemove = append(tagIDsToRemove, originalTag.ID)
			}
		}

		if len(tagIDsToRemove) > 0 {
			err = models.RemoveTagsFromLink(ctx, orgLink.ID, tagIDsToRemove)
	if tagsProvided {
		if len(tags) > 0 {
			// Calculate the difference between original tags and new ones
			tagIDs, err := links.ProcessTags(ctx, tags)
			if err != nil {
				return nil, err
			}
		}
			originalTags := orgLink.Tags
			tagIDsToRemove := make([]int, 0)
			m := make(map[int]bool)
			for _, id := range tagIDs {
				m[id] = true
			}

		err = models.CreateBatchTagLinks(ctx, orgLink.ID, tagIDs)
		if err != nil {
			return nil, err
			for _, originalTag := range originalTags {
				if !m[originalTag.ID] {
					tagIDsToRemove = append(tagIDsToRemove, originalTag.ID)
				}
			}

			if len(tagIDsToRemove) > 0 {
				err = models.RemoveTagsFromLink(ctx, orgLink.ID, tagIDsToRemove)
				if err != nil {
					return nil, err
				}
			}

			err = models.CreateBatchTagLinks(ctx, orgLink.ID, tagIDs)
			if err != nil {
				return nil, err
			}
		} else {
			// Tags field provided but empty - remove all existing tags
			originalTags := orgLink.Tags
			if len(originalTags) > 0 {
				tagIDsToRemove := make([]int, len(originalTags))
				for i, t := range originalTags {
					tagIDsToRemove[i] = t.ID
				}
				err = models.RemoveTagsFromLink(ctx, orgLink.ID, tagIDsToRemove)
				if err != nil {
					return nil, err
				}
			}
		}
	}

@@ -2457,7 +2473,8 @@ func (r *mutationResolver) UpdateLinkShort(ctx context.Context, input *model.Upd
	}

	tags := make([]string, 0)
	if input.Tags != nil && *input.Tags != "" {
	tagsProvided := input.Tags != nil
	if tagsProvided && *input.Tags != "" {
		tags = strings.Split(strings.TrimSpace(*input.Tags), ",")
		validator.Expect(len(tags) <= 10, "%s", lt.Translate("Tags may not exceed 10")).
			WithField("tags").
@@ -2558,35 +2575,50 @@ func (r *mutationResolver) UpdateLinkShort(ctx context.Context, input *model.Upd
		return nil, err
	}

	if len(tags) > 0 {
		tagIDs, err := links.ProcessTags(ctx, tags)
		if err != nil {
			return nil, err
		}
		// Calculate the difference between original tags and new ones
		originalTags := linkShort.Tags
		tagIDsToRemove := make([]int, 0)
		m := make(map[int]bool)
		for _, id := range tagIDs {
			m[id] = true
		}

		for _, originalTag := range originalTags {
			if !m[originalTag.ID] {
				tagIDsToRemove = append(tagIDsToRemove, originalTag.ID)
			}
		}

		if len(tagIDsToRemove) > 0 {
			err = models.RemoveTagsFromLinkShort(ctx, linkShort.ID, tagIDsToRemove)
	if tagsProvided {
		if len(tags) > 0 {
			tagIDs, err := links.ProcessTags(ctx, tags)
			if err != nil {
				return nil, err
			}
		}
			// Calculate the difference between original tags and new ones
			originalTags := linkShort.Tags
			tagIDsToRemove := make([]int, 0)
			m := make(map[int]bool)
			for _, id := range tagIDs {
				m[id] = true
			}

		err = models.CreateBatchTagLinkShorts(ctx, linkShort.ID, tagIDs)
		if err != nil {
			return nil, err
			for _, originalTag := range originalTags {
				if !m[originalTag.ID] {
					tagIDsToRemove = append(tagIDsToRemove, originalTag.ID)
				}
			}

			if len(tagIDsToRemove) > 0 {
				err = models.RemoveTagsFromLinkShort(ctx, linkShort.ID, tagIDsToRemove)
				if err != nil {
					return nil, err
				}
			}

			err = models.CreateBatchTagLinkShorts(ctx, linkShort.ID, tagIDs)
			if err != nil {
				return nil, err
			}
		} else {
			// Tags field provided but empty - remove all existing tags
			originalTags := linkShort.Tags
			if len(originalTags) > 0 {
				tagIDsToRemove := make([]int, len(originalTags))
				for i, t := range originalTags {
					tagIDsToRemove[i] = t.ID
				}
				err = models.RemoveTagsFromLinkShort(ctx, linkShort.ID, tagIDsToRemove)
				if err != nil {
					return nil, err
				}
			}
		}
	}

@@ -3177,7 +3209,8 @@ func (r *mutationResolver) UpdateListing(ctx context.Context, input *model.Updat
	}

	tags := make([]string, 0)
	if input.Tags != nil && *input.Tags != "" {
	tagsProvided := input.Tags != nil
	if tagsProvided && *input.Tags != "" {
		tags = strings.Split(strings.TrimSpace(*input.Tags), ",")
		validator.Expect(len(tags) <= 10, "%s", lt.Translate("Tags may not exceed 10")).
			WithField("tags").
@@ -3393,35 +3426,50 @@ func (r *mutationResolver) UpdateListing(ctx context.Context, input *model.Updat
		return nil, err
	}

	if len(tags) > 0 {
		tagIDs, err := links.ProcessTags(ctx, tags)
		if err != nil {
			return nil, err
		}
		// Calculate the difference between original tags and new ones
		originalTags := listing.Tags
		tagIDsToRemove := make([]int, 0)
		m := make(map[int]bool)
		for _, id := range tagIDs {
			m[id] = true
		}

		for _, originalTag := range originalTags {
			if !m[originalTag.ID] {
				tagIDsToRemove = append(tagIDsToRemove, originalTag.ID)
			}
		}

		if len(tagIDsToRemove) > 0 {
			err = models.RemoveTagsFromListing(ctx, listing.ID, tagIDsToRemove)
	if tagsProvided {
		if len(tags) > 0 {
			tagIDs, err := links.ProcessTags(ctx, tags)
			if err != nil {
				return nil, err
			}
		}
			// Calculate the difference between original tags and new ones
			originalTags := listing.Tags
			tagIDsToRemove := make([]int, 0)
			m := make(map[int]bool)
			for _, id := range tagIDs {
				m[id] = true
			}

		err = models.CreateBatchTagListings(ctx, listing.ID, tagIDs)
		if err != nil {
			return nil, err
			for _, originalTag := range originalTags {
				if !m[originalTag.ID] {
					tagIDsToRemove = append(tagIDsToRemove, originalTag.ID)
				}
			}

			if len(tagIDsToRemove) > 0 {
				err = models.RemoveTagsFromListing(ctx, listing.ID, tagIDsToRemove)
				if err != nil {
					return nil, err
				}
			}

			err = models.CreateBatchTagListings(ctx, listing.ID, tagIDs)
			if err != nil {
				return nil, err
			}
		} else {
			// Tags field provided but empty - remove all existing tags
			originalTags := listing.Tags
			if len(originalTags) > 0 {
				tagIDsToRemove := make([]int, len(originalTags))
				for i, t := range originalTags {
					tagIDsToRemove[i] = t.ID
				}
				err = models.RemoveTagsFromListing(ctx, listing.ID, tagIDsToRemove)
				if err != nil {
					return nil, err
				}
			}
		}
	}

-- 
2.49.1
Details
Message ID
<DEJNMM18D62J.1AZ42T3EYXGHX@netlandish.com>
In-Reply-To
<20251127172620.3173-1-peter@netlandish.com> (view parent)
Sender timestamp
1764243064
DKIM signature
missing
Download raw message
Applied.

To git@git.code.netlandish.com:~netlandish/links
   babd2b9..a484451  master -> master
Reply to thread Export thread (mbox)