~netlandish/links

b7d2fdf2ce59efd72bb608ecb73ae05b8b24ec30 — Peter Sanchez 6 days ago 2e2333e
Moving updateLink to use org link hash instead of db ID
M api/api_test.go => api/api_test.go +30 -13
@@ 534,16 534,24 @@ func TestAPI(t *testing.T) {
	})

	t.Run("invalid org link update", func(t *testing.T) {
		// We know this has already been saved earlier. We need the hash to fetch it so
		// let's query the db
		ols, err := models.GetOrgLinks(dbCtx,
			&database.FilterOptions{Filter: sq.Eq{"ol.id": 3}, Limit: 1},
		)
		c.NoError(err)
		ol := ols[0]

		type GraphQLResponse struct {
			Link models.OrgLink `json:"updateLink"`
		}
		var result GraphQLResponse
		q := `mutation UpdateLink($id: Int!, $title: String!,
		q := `mutation UpdateLink($hash: String!, $title: String!,
								 $url: String!, $visibility: Int!,
								 $tags: String) {
				updateLink(input: {
					title: $title,
					id: $id,
					hash: $hash,
					url: $url,
					visibility: $visibility,
					tags: $tags,


@@ 551,17 559,17 @@ func TestAPI(t *testing.T) {
			}`
		op := gqlclient.NewOperation(q)
		op.Var("title", "New title")
		op.Var("id", "3")
		op.Var("hash", ol.Hash)
		op.Var("url", "invalid url")
		op.Var("visibility", models.OrgLinkVisibilityPublic)
		op.Var("tags", "one, two, three")
		err := links.Execute(ctx, op, &result)
		err = links.Execute(ctx, op, &result)
		c.Error(err)
		c.Equal("gqlclient: server failure: Invalid URL.", err.Error())

		op = gqlclient.NewOperation(q)
		op.Var("title", "")
		op.Var("id", "3")
		op.Var("hash", ol.Hash)
		op.Var("url", "https://netlandish.com")
		op.Var("visibility", models.OrgLinkVisibilityPublic)
		op.Var("tags", "one, two, three")


@@ 571,7 579,7 @@ func TestAPI(t *testing.T) {

		op = gqlclient.NewOperation(q)
		op.Var("title", "New Title")
		op.Var("id", "3")
		op.Var("hash", ol.Hash)
		op.Var("url", "https://netlandish.com")
		op.Var("visibility", 100)
		op.Var("tags", "one, two, three")


@@ 581,16 589,24 @@ func TestAPI(t *testing.T) {
	})

	t.Run("org link update", func(t *testing.T) {
		// We know this has already been saved earlier. We need the hash to fetch it so
		// let's query the db
		ols, err := models.GetOrgLinks(dbCtx,
			&database.FilterOptions{Filter: sq.Eq{"ol.id": 3}, Limit: 1},
		)
		c.NoError(err)
		ol := ols[0]

		type GraphQLResponse struct {
			Link models.OrgLink `json:"updateLink"`
		}
		var result GraphQLResponse
		q := `mutation UpdateLink($id: Int!, $title: String!,
		q := `mutation UpdateLink($hash: String!, $title: String!,
								 $url: String!, $visibility: Int!,
								 $tags: String) {
				updateLink(input: {
					title: $title,
					id: $id,
					hash: $hash,
					url: $url,
					visibility: $visibility,
					tags: $tags,


@@ 598,12 614,12 @@ func TestAPI(t *testing.T) {
			}`
		op := gqlclient.NewOperation(q)
		op.Var("title", "New title")
		op.Var("id", "3")
		op.Var("hash", ol.Hash)
		op.Var("url", "https://netlandish.com")
		op.Var("visibility", models.OrgLinkVisibilityPrivate)
		// We remove a tag and add a new one
		op.Var("tags", "one, three, four")
		err := links.Execute(ctx, op, &result)
		err = links.Execute(ctx, op, &result)
		c.NoError(err)
		c.Equal("New title", result.Link.Title)
		c.Equal("https://netlandish.com", result.Link.URL)


@@ 1870,16 1886,17 @@ func TestAPI(t *testing.T) {
		ctx := server.ServerContext(context.Background(), srv)
		ctx = auth.Context(ctx, test.NewTestUser(2, false, false, true, true))
		ctx = crypto.Context(ctx, entropy)

		type GraphQLResponse struct {
			Link models.OrgLink `json:"updateLink"`
		}
		var result GraphQLResponse
		q := `mutation UpdateLink($id: Int!, $title: String!,
		q := `mutation UpdateLink($hash: String!, $title: String!,
								 $url: String!, $visibility: Int!,
								 $tags: String) {
				updateLink(input: {
					title: $title,
					id: $id,
					hash: $hash,
					url: $url,
					visibility: $visibility,
					tags: $tags,


@@ 1887,7 1904,7 @@ func TestAPI(t *testing.T) {
			}`
		op := gqlclient.NewOperation(q)
		op.Var("title", "New title")
		op.Var("id", "3")
		op.Var("hash", "not valid hash")
		op.Var("url", "https://netlandish.com")
		op.Var("visibility", models.OrgLinkVisibilityPrivate)
		err := links.Execute(ctx, op, &result)

M api/graph/generated.go => api/graph/generated.go +5 -5
@@ 23114,20 23114,20 @@ func (ec *executionContext) unmarshalInputUpdateLinkInput(ctx context.Context, o
		asMap[k] = v
	}

	fieldsInOrder := [...]string{"id", "title", "description", "url", "visibility", "unread", "starred", "tags"}
	fieldsInOrder := [...]string{"hash", "title", "description", "url", "visibility", "unread", "starred", "tags"}
	for _, k := range fieldsInOrder {
		v, ok := asMap[k]
		if !ok {
			continue
		}
		switch k {
		case "id":
			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
			data, err := ec.unmarshalNInt2int(ctx, v)
		case "hash":
			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("hash"))
			data, err := ec.unmarshalNString2string(ctx, v)
			if err != nil {
				return it, err
			}
			it.ID = data
			it.Hash = data
		case "title":
			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("title"))
			data, err := ec.unmarshalOString2áš–string(ctx, v)

M api/graph/model/models_gen.go => api/graph/model/models_gen.go +1 -1
@@ 387,7 387,7 @@ type UpdateAdminDomainInput struct {
}

type UpdateLinkInput struct {
	ID          int     `json:"id"`
	Hash        string  `json:"hash"`
	Title       *string `json:"title,omitempty"`
	Description *string `json:"description,omitempty"`
	URL         *string `json:"url,omitempty"`

M api/graph/schema.graphqls => api/graph/schema.graphqls +1 -1
@@ 521,7 521,7 @@ input GetListingDetailInput {
}

input UpdateLinkInput{
    id: Int!
    hash: String!
    title: String
    description: String
    url: String

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +3 -6
@@ 328,7 328,7 @@ func (r *mutationResolver) UpdateLink(ctx context.Context, input *model.UpdateLi
	lt := localizer.GetLocalizer(lang)

	validator := valid.New(ctx)
	validator.Expect(input.ID != 0, lt.Translate("Id required")).
	validator.Expect(input.Hash != "", lt.Translate("Link hash required")).
		WithField("id").
		WithCode(valid.ErrValidationCode)



@@ 371,11 371,8 @@ func (r *mutationResolver) UpdateLink(ctx context.Context, input *model.UpdateLi
	}

	opts := &database.FilterOptions{
		Filter: sq.And{
			sq.Eq{"ol.id": input.ID},
			sq.Eq{"ol.user_id": user.ID},
		},
		Limit: 1,
		Filter: sq.Eq{"ol.hash": input.Hash},
		Limit:  1,
	}

	orgLinks, err := models.GetOrgLinks(ctx, opts)

M core/routes.go => core/routes.go +35 -35
@@ 87,19 87,19 @@ func (s *Service) RegisterRoutes() {
	s.Group.GET("/home", s.OrgLinksList).Name = s.RouteName("home_link_list")
	s.Group.GET("/add", s.OrgLinksCreate).Name = s.RouteName("link_create")
	s.Group.POST("/add", s.OrgLinksCreate).Name = s.RouteName("link_create_post")
	s.Group.GET("/read/:id", s.OrgLinkAsReadToggle).Name = s.RouteName("link_mark_as_read")
	s.Group.GET("/star/:id", s.OrgLinkStarToggle).Name = s.RouteName("link_star_toggle")
	s.Group.GET("/read/:hash", s.OrgLinkAsReadToggle).Name = s.RouteName("link_mark_as_read")
	s.Group.GET("/star/:hash", s.OrgLinkStarToggle).Name = s.RouteName("link_star_toggle")
	s.Group.GET("/link/:hash/delete", s.OrgLinkDelete).Name = s.RouteName("link_delete")
	s.Group.POST("/link/:hash/delete", s.OrgLinkDelete).Name = s.RouteName("link_delete")
	s.Group.GET("/link/:id/edit", s.OrgLinkUpdate).Name = s.RouteName("link_edit")
	s.Group.POST("/link/:id/edit", s.OrgLinkUpdate).Name = s.RouteName("link_edit_post")
	s.Group.GET("/link/:hash/edit", s.OrgLinkUpdate).Name = s.RouteName("link_edit")
	s.Group.POST("/link/:hash/edit", s.OrgLinkUpdate).Name = s.RouteName("link_edit_post")
	s.Group.GET("/qr/:hash/detail", s.QRManageDetail).Name = s.RouteName("qr_manage_detail")
	s.Group.GET("/qr/:id/delete", s.QRManageDelete).Name = s.RouteName("qr_manage_delete")
	s.Group.POST("/qr/:id/delete", s.QRManageDelete).Name = s.RouteName("qr_manage_delete_post")
	s.Group.GET("/note/add", s.NoteCreate).Name = s.RouteName("note_create")
	s.Group.POST("/note/add", s.NoteCreate).Name = s.RouteName("note_create_post")
	s.Group.GET("/note/:id/edit", s.NoteUpdate).Name = s.RouteName("note_edit")
	s.Group.POST("/note/:id/edit", s.NoteUpdate).Name = s.RouteName("note_edit_post")
	s.Group.GET("/note/:hash/edit", s.NoteUpdate).Name = s.RouteName("note_edit")
	s.Group.POST("/note/:hash/edit", s.NoteUpdate).Name = s.RouteName("note_edit_post")
}

func (s *Service) InvalidDomain(c echo.Context) error {


@@ 2409,8 2409,8 @@ func (s *Service) OrgLinkDelete(c echo.Context) error {
// OrgLinkUpdate ...
func (s *Service) OrgLinkUpdate(c echo.Context) error {
	gctx := c.(*server.Context)
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
	hash := c.Param("hash")
	if hash == "" {
		return echo.NotFoundHandler(c)
	}



@@ 2418,7 2418,7 @@ func (s *Service) OrgLinkUpdate(c echo.Context) error {
	opts := &database.FilterOptions{
		Filter: sq.And{
			sq.Eq{"ol.user_id": user.ID},
			sq.Eq{"ol.id": id},
			sq.Eq{"ol.hash": hash},
		},
		Limit: 1,
	}


@@ 2461,7 2461,7 @@ func (s *Service) OrgLinkUpdate(c echo.Context) error {
		"pd":                 pd,
		"visibilityOpt":      visibilityOpt,
		"orgs":               orgs,
		"orgLinkID":          orgLink.ID,
		"orgLinkHash":        orgLink.Hash,
		"navFlag":            "addLink",
		"useTagAutocomplete": true,
		"autoCompleteOrgID":  orgLink.OrgID,


@@ 2486,25 2486,25 @@ func (s *Service) OrgLinkUpdate(c echo.Context) error {

		var result GraphQLResponse
		op := gqlclient.NewOperation(
			`mutation UpdateLink($id: Int!, $title: String!, $description: String,
			`mutation UpdateLink($hash: String!, $title: String!, $description: String,
								 $url: String!, $visibility: Int!, $unread: Boolean,
								 $starred: Boolean, $tags: String) {
					updateLink(input: {
						title: $title,
						description: $description,
						id: $id,
						hash: $hash,
						url: $url,
						visibility: $visibility,
						unread: $unread,
						starred: $starred,
						tags: $tags,
					}) {
						id
						hash
					}
				}`)
		op.Var("title", form.Title)
		op.Var("description", form.Description)
		op.Var("id", orgLink.ID)
		op.Var("hash", orgLink.Hash)
		op.Var("url", form.URL)
		op.Var("tags", form.Tags)
		op.Var("unread", form.Unread)


@@ 3034,14 3034,14 @@ func (s *Service) ImportData(c echo.Context) error {

func (s *Service) OrgLinkStarToggle(c echo.Context) error {
	gctx := c.(*server.Context)
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return err
	hash := c.Param("hash")
	if hash == "" {
		return echo.NotFoundHandler(c)
	}

	opts := &database.FilterOptions{
		Filter: sq.And{
			sq.Eq{"ol.id": id},
			sq.Eq{"ol.hash": hash},
			sq.Eq{"ol.user_id": gctx.User.GetID()},
		},
		Limit: 1,


@@ 3058,15 3058,15 @@ func (s *Service) OrgLinkStarToggle(c echo.Context) error {

	var result GraphQLResponse
	op := gqlclient.NewOperation(
		`mutation UpdateLink($id: Int!, $starred: Boolean) {
		`mutation UpdateLink($hash: String!, $starred: Boolean) {
				updateLink(input: {
					id: $id,
					hash: $hash,
					starred: $starred,
				}) {
					id
				}
			}`)
	op.Var("id", link.ID)
	op.Var("hash", link.Hash)
	op.Var("starred", !link.Starred)
	err = links.Execute(c.Request().Context(), op, &result)
	if err != nil {


@@ 3082,14 3082,14 @@ func (s *Service) OrgLinkStarToggle(c echo.Context) error {

func (s *Service) OrgLinkAsReadToggle(c echo.Context) error {
	gctx := c.(*server.Context)
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return err
	hash := c.Param("hash")
	if hash == "" {
		return echo.NotFoundHandler(c)
	}

	opts := &database.FilterOptions{
		Filter: sq.And{
			sq.Eq{"ol.id": id},
			sq.Eq{"ol.hash": hash},
			sq.Eq{"ol.user_id": gctx.User.GetID()},
		},
		Limit: 1,


@@ 3106,15 3106,15 @@ func (s *Service) OrgLinkAsReadToggle(c echo.Context) error {

	var result GraphQLResponse
	op := gqlclient.NewOperation(
		`mutation UpdateLink($id: Int!, $unread: Boolean) {
		`mutation UpdateLink($hash: String!, $unread: Boolean) {
				updateLink(input: {
					id: $id,
					hash: $hash,
					unread: $unread,
				}) {
					id
				}
			}`)
	op.Var("id", link.ID)
	op.Var("hash", link.Hash)
	op.Var("unread", !link.Unread)
	err = links.Execute(c.Request().Context(), op, &result)
	if err != nil {


@@ 3231,8 3231,8 @@ func (s *Service) FollowToggle(c echo.Context) error {

func (s *Service) NoteUpdate(c echo.Context) error {
	gctx := c.(*server.Context)
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
	hash := c.Param("hash")
	if hash == "" {
		return echo.NotFoundHandler(c)
	}



@@ 3240,7 3240,7 @@ func (s *Service) NoteUpdate(c echo.Context) error {
	opts := &database.FilterOptions{
		Filter: sq.And{
			sq.Eq{"ol.user_id": user.ID},
			sq.Eq{"ol.id": id},
			sq.Eq{"ol.hash": hash},
		},
		Limit: 1,
	}


@@ 3281,7 3281,7 @@ func (s *Service) NoteUpdate(c echo.Context) error {
		"pd":                 pd,
		"visibilityOpt":      visibilityOpt,
		"orgs":               orgs,
		"orgLinkID":          orgLink.ID,
		"orgLinkHash":        orgLink.Hash,
		"navFlag":            "addNote",
		"useTagAutocomplete": true,
		"autoCompleteOrgID":  orgLink.OrgID,


@@ 3306,10 3306,10 @@ func (s *Service) NoteUpdate(c echo.Context) error {

		var result GraphQLResponse
		op := gqlclient.NewOperation(
			`mutation UpdateLink($id: Int!, $title: String!, $description: String,
			`mutation UpdateLink($hash: String!, $title: String!, $description: String,
								 $visibility: Int!, $starred: Boolean, $tags: String) {
					updateLink(input: {
						id: $id,
						hash: $hash,
						title: $title,
						description: $description,
						visibility: $visibility,


@@ 3322,7 3322,7 @@ func (s *Service) NoteUpdate(c echo.Context) error {
				}`)
		op.Var("title", form.Title)
		op.Var("description", form.Description)
		op.Var("id", orgLink.ID)
		op.Var("hash", orgLink.Hash)
		op.Var("tags", form.Tags)
		op.Var("starred", form.Starred)
		op.Var("visibility", form.Visibility)

M core/routes_test.go => core/routes_test.go +6 -7
@@ 11,7 11,6 @@ import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"strings"
	"testing"



@@ 244,9 243,9 @@ func TestHandlers(t *testing.T) {
			Context: e.NewContext(request, recorder),
			User:    loggedInUser,
		}
		ctx.SetPath("/link/:id/edit")
		ctx.SetParamNames("id")
		ctx.SetParamValues(strconv.Itoa(orgLink.ID))
		ctx.SetPath("/link/:hash/edit")
		ctx.SetParamNames("hash")
		ctx.SetParamValues(orgLink.Hash)
		// We get the error from the API call
		err = test.MakeRequestWithDomain(srv, coreService.OrgLinkUpdate, ctx, domains[0])
		c.NoError(err)


@@ 279,9 278,9 @@ func TestHandlers(t *testing.T) {
			Context: e.NewContext(request, recorder),
			User:    loggedInUser,
		}
		ctx.SetPath("/link/:id/edit")
		ctx.SetParamNames("id")
		ctx.SetParamValues(strconv.Itoa(orgLink.ID))
		ctx.SetPath("/link/:hash/edit")
		ctx.SetParamNames("hash")
		ctx.SetParamValues(orgLink.Hash)
		// We get the error from the API call
		err = test.MakeRequestWithDomain(srv, coreService.OrgLinkUpdate, ctx, domains[0])
		c.NoError(err)

M migrations/test_migration.up.sql => migrations/test_migration.up.sql +2 -2
@@ 10,10 10,10 @@ INSERT INTO organizations (owner_id, name, slug) VALUES (2, 'api test org', 'api
INSERT INTO base_urls (url, hash) VALUES ('http://base.com', 'abcdefg');

INSERT INTO org_links (title, url, base_url_id, user_id, org_id, visibility, hash) VALUES
    ('Public Business url', 'http://base.com?vis=public', 1, 1, 2, 0, 'hijklmn');
    ('Public Business url', 'http://base.com?vis=public', 1, 1, 2, 0, 'hash1');

INSERT INTO org_links (title, url, base_url_id, user_id, org_id, visibility, hash) VALUES
    ('Private Business url', 'http://base.com?vis=private', 1, 1, 2, 1, 'opqrstu');
    ('Private Business url', 'http://base.com?vis=private', 1, 1, 2, 1, 'hash2');

INSERT INTO domains (name, lookup_name, org_id, service, status) VALUES ('short domain', 'short.domain.org', 1, 1, 1);
INSERT INTO domains (name, lookup_name, org_id, service, status, level) VALUES ('listing domain', 'list.domain.org', 1, 2, 1, 1);

M templates/link_create.html => templates/link_create.html +1 -1
@@ 6,7 6,7 @@
</section>

<section class="card shadow-card">
  <form id="link-form" action="{{if .orgLinkID }}{{reverse "core:link_edit_post" .orgLinkID}}{{else}}{{reverse "core:link_create_post" }}{{end}}" method="POST">
  <form id="link-form" action="{{if .orgLinkHash }}{{reverse "core:link_edit_post" .orgLinkHash}}{{else}}{{reverse "core:link_create_post" }}{{end}}" method="POST">
    <input type="hidden" name="csrf" value="{{ .CSRF }}">
    {{if .errors._global_ }}
    {{range .errors._global_}}

M templates/link_detail.html => templates/link_detail.html +2 -2
@@ 43,9 43,9 @@
  {{if eq .link.UserID .currentUserID}}
  <footer class="is-right">
    {{if eq .link.Type 0}}
        <a class="button dark" href="{{reverse "core:link_edit" .link.ID}}">{{.pd.Data.edit}}</a>
        <a class="button dark" href="{{reverse "core:link_edit" .link.Hash}}">{{.pd.Data.edit}}</a>
    {{else}}
        <a class="button dark" href="{{reverse "core:note_edit" .link.ID}}">{{.pd.Data.edit}}</a>
        <a class="button dark" href="{{reverse "core:note_edit" .link.Hash}}">{{.pd.Data.edit}}</a>
    {{end}}
  </footer>
  {{end}}

M templates/link_list.html => templates/link_list.html +5 -5
@@ 100,13 100,13 @@
            {{if eq .UserID $.currentUserID}}
                <!-- Read/Unread -->
                {{if .Unread}}
                <a class="icon-link tooltip-link" href="{{reverse "core:link_mark_as_read" .ID}}" data-tooltip="{{$.pd.Data.mark_as_read}}">
                <a class="icon-link tooltip-link" href="{{reverse "core:link_mark_as_read" .Hash}}" data-tooltip="{{$.pd.Data.mark_as_read}}">
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:20px;">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
                    </svg>
                </a>
                {{else}}
                <a class="icon-link tooltip-link" href="{{reverse "core:link_mark_as_read" .ID}}" data-tooltip="{{$.pd.Data.mark_as_unread}}">
                <a class="icon-link tooltip-link" href="{{reverse "core:link_mark_as_read" .Hash}}" data-tooltip="{{$.pd.Data.mark_as_unread}}">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" style="width:20px;">
                        <path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clip-rule="evenodd" />
                    </svg>


@@ 115,13 115,13 @@

                <!-- Star/Unstar -->
                {{if .Starred}}
                <a class="icon-link tooltip-lin tooltip-link" href="{{reverse "core:link_star_toggle" .ID}}" data-tooltip="{{$.pd.Data.mark_as_non_starred}}">
                <a class="icon-link tooltip-lin tooltip-link" href="{{reverse "core:link_star_toggle" .Hash}}" data-tooltip="{{$.pd.Data.mark_as_non_starred}}">
                    <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:20px;">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
                    </svg>
                </a>
                {{else}}
                <a class="icon-link non-starred tooltip-link" href="{{reverse "core:link_star_toggle" .ID}}" data-tooltip="{{$.pd.Data.mark_as_starred}}">
                <a class="icon-link non-starred tooltip-link" href="{{reverse "core:link_star_toggle" .Hash}}" data-tooltip="{{$.pd.Data.mark_as_starred}}">
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:20px;">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
                    </svg>


@@ 130,7 130,7 @@

                <!-- Edit -->
                <a class="icon-link tooltip-link"
                    href="{{if eq .Type 0}}{{reverse "core:link_edit" .ID}}{{else if eq .Type 1}}{{reverse "core:note_edit" .ID}}{{end}}" data-tooltip="{{$.pd.Data.edit}}">
                    href="{{if eq .Type 0}}{{reverse "core:link_edit" .Hash}}{{else if eq .Type 1}}{{reverse "core:note_edit" .Hash}}{{end}}" data-tooltip="{{$.pd.Data.edit}}">
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" style="width:20px;">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
                    </svg>

M templates/note_create.html => templates/note_create.html +1 -1
@@ 6,7 6,7 @@
</section>

<section class="card shadow-card">
  <form id="link-form" action="{{if .orgLinkID }}{{reverse "core:note_edit_post" .orgLinkID}}{{else}}{{reverse "core:note_create_post" }}{{end}}" method="POST">
  <form id="link-form" action="{{if .orgLinkHash }}{{reverse "core:note_edit_post" .orgLinkHash}}{{else}}{{reverse "core:note_create_post" }}{{end}}" method="POST">
    <input type="hidden" name="csrf" value="{{ .CSRF }}">
    {{if .errors._global_ }}
    {{range .errors._global_}}

M templates/org_link_as_read.html => templates/org_link_as_read.html +1 -1
@@ 1,6 1,6 @@
{{template "base" .}}
<section class="card shadow-card">
    <form action="{{reverse "core:link_mark_as_read_post" .link.ID}}" method="POST">
    <form action="{{reverse "core:link_mark_as_read_post" .link.Hash}}" method="POST">
        <input type="hidden" name="csrf" value="{{ .CSRF }}">
        <p class="text-center">{{.pd.Data.message}}?</p>
        <p class="text-center">