M api/graph/generated.go => api/graph/generated.go +63 -0
@@ 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)
M api/graph/schema.graphqls => api/graph/schema.graphqls +1 -0
@@ 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)
M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +11 -13
@@ 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,
}
M cmd/links/main.go => cmd/links/main.go +10 -1
@@ 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 {
M cmd/test/helpers.go => cmd/test/helpers.go +3 -0
@@ 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 {
M core/routes.go => core/routes.go +120 -17
@@ 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)
}
M helpers.go => helpers.go +21 -0
@@ 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()
+}
M models/base_url.go => models/base_url.go +14 -0
@@ 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
+}
M models/org_link.go => models/org_link.go +18 -0
@@ 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 {
M models/utils.go => models/utils.go +14 -0
@@ 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
+}
M templates/link_list.html => templates/link_list.html +11 -10
@@ 173,16 173,17 @@
{{end}}
</div>
{{end}}
- {{if not $.isPopular}}
- <p class="text-grey">
- <small>On</small>
- <time datetime="{{formatDate .CreatedOn}}">
- <small>{{formatDate .CreatedOn}}</small>
- </time>
- <small>{{$.pd.Data.by}}</small>
- <small><a href="{{reverse "core:org_link_list" .OrgSlug}}" class="underline">{{.OrgSlug}}</a></small>
- </p>
- {{end}}
+ <p class="text-grey">
+ {{if not $.isPopular}}
+ <small>On</small>
+ <time datetime="{{formatDate .CreatedOn}}">
+ <small>{{formatDate .CreatedOn}}</small>
+ </time>
+ <small>{{$.pd.Data.by}}</small>
+ <small><a href="{{reverse "core:org_link_list" .OrgSlug}}" class="underline">{{.OrgSlug}}</a></small>
+ {{end}}
+ {{ if not $.isHome }}<small>(<a href="{{ getAddLinkURL $.context . }}" target="_blank">copy link</a>)</small>{{ end }}
+ </p>
</article>
</li>
{{else}}