From 59358ca8513674b2c31a7a36121fe6178b8dc76f Mon Sep 17 00:00:00 2001 From: Peter Sanchez Date: Mon, 25 Mar 2024 15:08:04 -0600 Subject: [PATCH] Add simple copy link to user account functionality. Implements: https://todo.code.netlandish.com/~netlandish/links/68 --- api/graph/generated.go | 63 ++++++++++++++++ api/graph/schema.graphqls | 1 + api/graph/schema.resolvers.go | 24 +++--- cmd/links/main.go | 11 ++- cmd/test/helpers.go | 3 + core/routes.go | 137 +++++++++++++++++++++++++++++----- helpers.go | 21 ++++++ models/base_url.go | 14 ++++ models/org_link.go | 18 +++++ models/utils.go | 14 ++++ templates/link_list.html | 21 +++--- 11 files changed, 286 insertions(+), 41 deletions(-) diff --git a/api/graph/generated.go b/api/graph/generated.go index 690dc6c..f87f26d 100644 --- a/api/graph/generated.go +++ b/api/graph/generated.go @@ -260,6 +260,7 @@ type ComplexityRoot struct { CreatedOn func(childComplexity int) int Description func(childComplexity int) int ID func(childComplexity int) int + NoteHash func(childComplexity int) int OrgID func(childComplexity int) int OrgSlug func(childComplexity int) int Starred func(childComplexity int) int @@ -1622,6 +1623,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.OrgLink.ID(childComplexity), true + case "OrgLink.noteHash": + if e.complexity.OrgLink.NoteHash == nil { + break + } + + return e.complexity.OrgLink.NoteHash(childComplexity), true + case "OrgLink.orgId": if e.complexity.OrgLink.OrgID == nil { break @@ -8754,6 +8762,8 @@ func (ec *executionContext) fieldContext_Mutation_addLink(ctx context.Context, f return ec.fieldContext_OrgLink_archiveUrl(ctx, field) case "type": return ec.fieldContext_OrgLink_type(ctx, field) + case "noteHash": + return ec.fieldContext_OrgLink_noteHash(ctx, field) case "tags": return ec.fieldContext_OrgLink_tags(ctx, field) case "author": @@ -8875,6 +8885,8 @@ func (ec *executionContext) fieldContext_Mutation_addNote(ctx context.Context, f return ec.fieldContext_OrgLink_archiveUrl(ctx, field) case "type": return ec.fieldContext_OrgLink_type(ctx, field) + case "noteHash": + return ec.fieldContext_OrgLink_noteHash(ctx, field) case "tags": return ec.fieldContext_OrgLink_tags(ctx, field) case "author": @@ -9194,6 +9206,8 @@ func (ec *executionContext) fieldContext_Mutation_updateLink(ctx context.Context return ec.fieldContext_OrgLink_archiveUrl(ctx, field) case "type": return ec.fieldContext_OrgLink_type(ctx, field) + case "noteHash": + return ec.fieldContext_OrgLink_noteHash(ctx, field) case "tags": return ec.fieldContext_OrgLink_tags(ctx, field) case "author": @@ -12559,6 +12573,47 @@ func (ec *executionContext) fieldContext_OrgLink_type(ctx context.Context, field return fc, nil } +func (ec *executionContext) _OrgLink_noteHash(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_OrgLink_noteHash(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.NoteHash, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalOString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_OrgLink_noteHash(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "OrgLink", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _OrgLink_tags(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) { fc, err := ec.fieldContext_OrgLink_tags(ctx, field) if err != nil { @@ -12928,6 +12983,8 @@ func (ec *executionContext) fieldContext_OrgLinkCursor_result(ctx context.Contex return ec.fieldContext_OrgLink_archiveUrl(ctx, field) case "type": return ec.fieldContext_OrgLink_type(ctx, field) + case "noteHash": + return ec.fieldContext_OrgLink_noteHash(ctx, field) case "tags": return ec.fieldContext_OrgLink_tags(ctx, field) case "author": @@ -16550,6 +16607,8 @@ func (ec *executionContext) fieldContext_Query_getOrgLink(ctx context.Context, f return ec.fieldContext_OrgLink_archiveUrl(ctx, field) case "type": return ec.fieldContext_OrgLink_type(ctx, field) + case "noteHash": + return ec.fieldContext_OrgLink_noteHash(ctx, field) case "tags": return ec.fieldContext_OrgLink_tags(ctx, field) case "author": @@ -24541,6 +24600,10 @@ func (ec *executionContext) _OrgLink(ctx context.Context, sel ast.SelectionSet, if out.Values[i] == graphql.Null { atomic.AddUint32(&invalids, 1) } + case "noteHash": + + out.Values[i] = ec._OrgLink_noteHash(ctx, field, obj) + case "tags": out.Values[i] = ec._OrgLink_tags(ctx, field, obj) diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index d556c82..0cf5c0c 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -152,6 +152,7 @@ type OrgLink { starred: Boolean! @access(scope: LINKS, kind: RO) archiveUrl: String! type: Int! + noteHash: String tags: [Tag]! author: String! orgSlug: String! @access(scope: LINKS, kind: RO) diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 05b64a5..2dca7e9 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -4986,21 +4986,19 @@ func (r *queryResolver) GetOrgLink(ctx context.Context, id *int, hash *string) ( } opts := &database.FilterOptions{ - Filter: sq.And{ - sq.Or{ - sq.Eq{"o.owner_id": user.ID}, // If the user is the org owner - // if the user is a member of the org is able to see private and public links - sq.And{ - sq.Eq{"ou.user_id": user.ID}, - sq.Eq{"ou.is_active": true}, - sq.Eq{"ol.visibility": []int{ - models.OrgLinkVisibilityPublic, - models.OrgLinkVisibilityPrivate}, - }, + Filter: sq.Or{ + sq.Eq{"o.owner_id": user.ID}, // If the user is the org owner + // if the user is a member of the org is able to see private and public links + sq.And{ + sq.Eq{"ou.user_id": user.ID}, + sq.Eq{"ou.is_active": true}, + sq.Eq{"ol.visibility": []int{ + models.OrgLinkVisibilityPublic, + models.OrgLinkVisibilityPrivate}, }, - // Otherwise the user is only able to see public links - sq.Eq{"ol.visibility": models.OrgLinkVisibilityPublic}, }, + // Otherwise the user is only able to see public links + sq.Eq{"ol.visibility": models.OrgLinkVisibilityPublic}, }, Limit: 1, } diff --git a/cmd/links/main.go b/cmd/links/main.go index 6981efa..51de240 100644 --- a/cmd/links/main.go +++ b/cmd/links/main.go @@ -5,6 +5,7 @@ import ( "net/http" "net/url" "os" + "slices" "strings" "text/template" "time" @@ -263,9 +264,17 @@ func run() error { return links.FromPenniesToFloat(amt) }, "isTagUsedInFilter": func(tag string, activeTags string) bool { - return strings.Contains(activeTags, tag) + tags := make([]string, 0) + for _, t := range strings.Split(activeTags, ",") { + tags = append(tags, strings.TrimSpace(t)) + } + if len(tags) == 0 { + return false + } + return slices.Contains(tags, strings.TrimSpace(tag)) }, "addQueryElement": links.AddQueryElement, + "getAddLinkURL": links.GetAddLinkURL, }) err = srv.LoadTemplatesFS(links.TemplateFS, "templates/*.html", "templates/*.txt") if err != nil { diff --git a/cmd/test/helpers.go b/cmd/test/helpers.go index 17f02e5..d096afc 100644 --- a/cmd/test/helpers.go +++ b/cmd/test/helpers.go @@ -33,6 +33,7 @@ import ( gaccts "netlandish.com/x/gobwebs/accounts" "netlandish.com/x/gobwebs/auth" "netlandish.com/x/gobwebs/config" + gcore "netlandish.com/x/gobwebs/core" "netlandish.com/x/gobwebs/crypto" "netlandish.com/x/gobwebs/database" "netlandish.com/x/gobwebs/email" @@ -81,6 +82,7 @@ func NewWebTestServer(t *testing.T) (*server.Server, *echo.Echo) { } srv, e, _ := newTestServer(t, "test-web-server", config) srv = srv.WithDefaultMiddleware() + srv.AddStaticFunc(gcore.AddContext) srv.AddFuncs(template.FuncMap{ "trans": core.TmplTranslate, "mediaURL": func(path string) string { @@ -128,6 +130,7 @@ func NewWebTestServer(t *testing.T) (*server.Server, *echo.Echo) { return strings.Contains(activeTags, tag) }, "addQueryElement": links.AddQueryElement, + "getAddLinkURL": links.GetAddLinkURL, }) err = srv.LoadTemplatesFS(links.TemplateFS, "templates/*.html", "templates/*.txt") if err != nil { diff --git a/core/routes.go b/core/routes.go index d419e13..f3375db 100644 --- a/core/routes.go +++ b/core/routes.go @@ -1216,6 +1216,7 @@ func (s *Service) OrgLinksCreate(c echo.Context) error { "autoCompleteOrgID": autoCompleteOrgID, } req := gctx.Request() + if req.Method == http.MethodPost { form := &LinkForm{} if err := form.Validate(c); err != nil { @@ -1228,10 +1229,10 @@ func (s *Service) OrgLinksCreate(c echo.Context) error { return err } } + type GraphQLResponse struct { Link models.OrgLink `json:"addLink"` } - var result GraphQLResponse op := gqlclient.NewOperation( `mutation AddLink($title: String!, $url: String!, $description: String, @@ -1297,10 +1298,81 @@ func (s *Service) OrgLinksCreate(c echo.Context) error { if c.QueryParam("url") != "" { form.URL = c.QueryParam("url") } + if c.QueryParam("tags") != "" { + form.Tags = c.QueryParam("tags") + } if c.QueryParam("next") == "same" { form.Redirect = req.Referer() } + // BaseURL + id, err := strconv.Atoi(c.QueryParam("burlid")) + if err == nil { + opts := &database.FilterOptions{ + Filter: sq.And{ + sq.Eq{"b.id": id}, + sq.Eq{"b.public_ready": true}, + }, + } + burls, err := models.GetBaseURLs(c.Request().Context(), opts) + if err != nil { + return err + } + if len(burls) == 0 { + messages.Error( + c, + lt.Translate("Error fetching referenced url: Not found"), + ) + } else { + b := burls[0] + form.Title = b.Title + form.Description = b.Data.Meta.Description + form.URL = b.URL + form.Tags = b.TagsToString() + } + } else { + // OrgLink + id, err = strconv.Atoi(c.QueryParam("linkid")) + if err == nil { + type GraphQLResponse struct { + Link models.OrgLink `json:"getOrgLink"` + } + var result GraphQLResponse + + op := gqlclient.NewOperation( + `query getOrgLink($id: Int) { + getOrgLink(id: $id) { + title + description + url + tags { + id + name + slug + } + } + }`) + op.Var("id", id) + err = links.Execute(c.Request().Context(), op, &result) + if err != nil { + if graphError, ok := err.(*gqlclient.Error); ok { + messages.Error( + c, + lt.Translate("Error fetching referenced link: %v", graphError.Message), + ) + } else { + return err + } + } else { + // Populate form with referenced link data + link := result.Link + form.Title = link.Title + form.Description = link.Description + form.URL = link.URL + form.Tags = link.TagsToString() + } + } + } gmap["form"] = form return links.Render(c, http.StatusOK, "link_create.html", gmap) } @@ -1642,6 +1714,7 @@ func (s *Service) OrgLinksList(c echo.Context) error { starred archiveUrl type + noteHash tags { id name @@ -1665,8 +1738,8 @@ func (s *Service) OrgLinksList(c echo.Context) error { }`) var ( - isOrgLink, advancedSearch, isRSSAuth bool - currURL, navFlag, rssURL, followAction string + isOrgLink, isHome, advancedSearch, isRSSAuth bool + currURL, navFlag, rssURL, followAction string ) org := &models.Organization{} if c.Path() != c.Echo().Reverse(s.RouteName("recent_link_list")) && @@ -1755,6 +1828,7 @@ func (s *Service) OrgLinksList(c echo.Context) error { } else { currURL = c.Echo().Reverse(s.RouteName("home_link_list")) + isHome = true } rssURL, err = links.GetRSSURL(c, org, user) if err != nil { @@ -1912,6 +1986,7 @@ func (s *Service) OrgLinksList(c echo.Context) error { "links": orgLinks, "org": org, "isOrgLink": isOrgLink, + "isHome": isHome, "canRead": canRead, "tagFilter": strings.Replace(tag, ",", ", ", -1), "excludeTagFilter": strings.Replace(excludeTag, ",", ", ", -1), @@ -2264,13 +2339,7 @@ func (s *Service) OrgLinkUpdate(c echo.Context) error { OrgSlug: orgLink.OrgSlug, Unread: orgLink.Unread, Starred: orgLink.Starred, - } - if len(orgLink.Tags) > 0 { - tagNameList := make([]string, 0) - for _, tag := range orgLink.Tags { - tagNameList = append(tagNameList, tag.Name) - } - form.Tags = strings.Join(tagNameList, ", ") + Tags: orgLink.TagsToString(), } gmap["form"] = form return links.Render(c, http.StatusOK, "link_create.html", gmap) @@ -3064,13 +3133,7 @@ func (s *Service) NoteUpdate(c echo.Context) error { Visibility: orgLink.Visibility, OrgSlug: orgLink.OrgSlug, Starred: orgLink.Starred, - } - if len(orgLink.Tags) > 0 { - tagNameList := make([]string, 0) - for _, tag := range orgLink.Tags { - tagNameList = append(tagNameList, tag.Name) - } - form.Tags = strings.Join(tagNameList, ", ") + Tags: orgLink.TagsToString(), } gmap["form"] = form return links.Render(c, http.StatusOK, "note_create.html", gmap) @@ -3243,6 +3306,46 @@ func (s *Service) NoteCreate(c echo.Context) error { Visibility: models.OrgLinkVisibilityPublic, OrgSlug: formSlug, } + + id := c.QueryParam("linkid") + if id != "" { + type GraphQLResponse struct { + Link models.OrgLink `json:"getOrgLink"` + } + var result GraphQLResponse + + op := gqlclient.NewOperation( + `query getOrgLink($hash: String) { + getOrgLink(hash: $hash) { + title + description + tags { + id + name + slug + } + } + }`) + op.Var("hash", id) + err = links.Execute(c.Request().Context(), op, &result) + if err != nil { + if graphError, ok := err.(*gqlclient.Error); ok { + messages.Error( + c, + lt.Translate("Error fetching referenced note: %v", graphError.Message), + ) + } else { + return err + } + } else { + // Populate form with referenced link data + link := result.Link + form.Title = link.Title + form.Description = link.Description + form.Tags = link.TagsToString() + } + } + gmap["form"] = form return links.Render(c, http.StatusOK, "note_create.html", gmap) } diff --git a/helpers.go b/helpers.go index ed20b2d..0e21629 100644 --- a/helpers.go +++ b/helpers.go @@ -947,3 +947,24 @@ func DecodeRSSUserHash(c echo.Context, hash string) (int, error) { } return userID, nil } + +type addLinker interface { + QueryParams() url.Values +} + +func GetAddLinkURL(c echo.Context, gURL addLinker) string { + gctx := c.(*server.Context) + nextURL := &url.URL{ + Scheme: gctx.Server.Config.Scheme, + Host: GetLinksDomain(c), + Path: c.Echo().Reverse("core:link_create"), + } + if ol, ok := gURL.(*models.OrgLink); ok { + if ol.Type == models.NoteType { + nextURL.Path = c.Echo().Reverse("core:note_create") + } + } + qs := gURL.QueryParams() + nextURL.RawQuery = qs.Encode() + return nextURL.String() +} diff --git a/models/base_url.go b/models/base_url.go index 700af93..8105b60 100644 --- a/models/base_url.go +++ b/models/base_url.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "net/url" "regexp" "time" @@ -217,3 +218,16 @@ func (b *BaseURL) UpdateCounter(ctx context.Context, add bool) error { }) return err } + +// TagsToString will convert linked tags to a comma separated string (used in forms) +func (b *BaseURL) TagsToString() string { + return TagsToString(b.Tags) +} + +// QueryParams will return a URL that can auto add information to easily save this BaseURL +// to a users bookmarks +func (b *BaseURL) QueryParams() url.Values { + qs := url.Values{} + qs.Set("burlid", fmt.Sprint(b.ID)) + return qs +} diff --git a/models/org_link.go b/models/org_link.go index 5e0366e..8fb64c7 100644 --- a/models/org_link.go +++ b/models/org_link.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "fmt" + "net/url" "regexp" "time" @@ -198,6 +199,23 @@ func (o *OrgLink) IsPrivate() bool { return o.Visibility == OrgLinkVisibilityPrivate } +// TagsToString will convert linked tags to a comma separated string (used in forms) +func (o *OrgLink) TagsToString() string { + return TagsToString(o.Tags) +} + +// QueryParams will return a URL that can auto add information to easily save this link +// to a users bookmarks +func (o *OrgLink) QueryParams() url.Values { + qs := url.Values{} + if o.Type == NoteType { + qs.Set("linkid", o.NoteHash) + } else { + qs.Set("linkid", fmt.Sprint(o.ID)) + } + return qs +} + // Returns a list of links and total clicks func GetOrgLinksAnalytics(ctx context.Context, opts *database.FilterOptions) ([]*OrgLink, error) { if opts == nil { diff --git a/models/utils.go b/models/utils.go index d2a83be..eed05ba 100644 --- a/models/utils.go +++ b/models/utils.go @@ -8,6 +8,7 @@ import ( "encoding/hex" "fmt" mrand "math/rand" + "strings" "time" sq "github.com/Masterminds/squirrel" @@ -69,3 +70,16 @@ func checkCode(ctx context.Context, tx *sql.Tx, code []byte, table, field string } return true, nil } + +// TagsToString will convert linked tags to a comma separated string (used in forms) +func TagsToString(t []Tag) string { + var tags string + if len(t) > 0 { + tagNameList := make([]string, 0) + for _, tag := range t { + tagNameList = append(tagNameList, tag.Name) + } + tags = strings.Join(tagNameList, ", ") + } + return tags +} diff --git a/templates/link_list.html b/templates/link_list.html index a474a5b..d20d74b 100644 --- a/templates/link_list.html +++ b/templates/link_list.html @@ -173,16 +173,17 @@ {{end}} {{end}} - {{if not $.isPopular}} -

- On - - {{$.pd.Data.by}} - {{.OrgSlug}} -

- {{end}} +

+ {{if not $.isPopular}} + On + + {{$.pd.Data.by}} + {{.OrgSlug}} + {{end}} + {{ if not $.isHome }}(copy link){{ end }} +

{{else}} -- 2.43.0