M api/graph/generated.go => api/graph/generated.go +24 -1
@@ 25613,7 25613,7 @@ func (ec *executionContext) unmarshalInputGetLinkInput(ctx context.Context, obj
asMap[k] = v
}
- fieldsInOrder := [...]string{"orgSlug", "limit", "after", "before", "tag", "excludeTag", "search", "filter", "tagCloudType", "tagCloudOrder"}
+ fieldsInOrder := [...]string{"orgSlug", "limit", "after", "before", "tag", "excludeTag", "search", "filter", "order", "tagCloudType", "tagCloudOrder"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
@@ 25676,6 25676,13 @@ func (ec *executionContext) unmarshalInputGetLinkInput(ctx context.Context, obj
return it, err
}
it.Filter = data
+ case "order":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("order"))
+ data, err := ec.unmarshalOOrderType2ᚖlinksᚋapiᚋgraphᚋmodelᚐOrderType(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.Order = data
case "tagCloudType":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("tagCloudType"))
data, err := ec.unmarshalOCloudType2ᚖlinksᚋapiᚋgraphᚋmodelᚐCloudType(ctx, v)
@@ 33038,6 33045,22 @@ func (ec *executionContext) unmarshalONoteInput2ᚖlinksᚋapiᚋgraphᚋmodel
return &res, graphql.ErrorOnPath(ctx, err)
}
+func (ec *executionContext) unmarshalOOrderType2ᚖlinksᚋapiᚋgraphᚋmodelᚐOrderType(ctx context.Context, v interface{}) (*model.OrderType, error) {
+ if v == nil {
+ return nil, nil
+ }
+ var res = new(model.OrderType)
+ err := res.UnmarshalGQL(v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalOOrderType2ᚖlinksᚋapiᚋgraphᚋmodelᚐOrderType(ctx context.Context, sel ast.SelectionSet, v *model.OrderType) graphql.Marshaler {
+ if v == nil {
+ return graphql.Null
+ }
+ return v
+}
+
func (ec *executionContext) marshalOOrgLink2ᚖlinksᚋmodelsᚐOrgLink(ctx context.Context, sel ast.SelectionSet, v *models.OrgLink) graphql.Marshaler {
if v == nil {
return graphql.Null
M api/graph/model/models_gen.go => api/graph/model/models_gen.go +42 -0
@@ 199,6 199,7 @@ type GetLinkInput struct {
ExcludeTag *string `json:"excludeTag,omitempty"`
Search *string `json:"search,omitempty"`
Filter *string `json:"filter,omitempty"`
+ Order *OrderType `json:"order,omitempty"`
TagCloudType *CloudType `json:"tagCloudType,omitempty"`
TagCloudOrder *CloudOrderType `json:"tagCloudOrder,omitempty"`
}
@@ 940,6 941,47 @@ func (e MemberPermission) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
+type OrderType string
+
+const (
+ OrderTypeDesc OrderType = "DESC"
+ OrderTypeAsc OrderType = "ASC"
+)
+
+var AllOrderType = []OrderType{
+ OrderTypeDesc,
+ OrderTypeAsc,
+}
+
+func (e OrderType) IsValid() bool {
+ switch e {
+ case OrderTypeDesc, OrderTypeAsc:
+ return true
+ }
+ return false
+}
+
+func (e OrderType) String() string {
+ return string(e)
+}
+
+func (e *OrderType) UnmarshalGQL(v interface{}) error {
+ str, ok := v.(string)
+ if !ok {
+ return fmt.Errorf("enums must be strings")
+ }
+
+ *e = OrderType(str)
+ if !e.IsValid() {
+ return fmt.Errorf("%s is not a valid OrderType", str)
+ }
+ return nil
+}
+
+func (e OrderType) MarshalGQL(w io.Writer) {
+ fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
type OrgBillingStatus string
const (
M => +27 -4
@@ 37,10 37,28 @@ func PaginateResults[T any](items []T, limit int, before, after *model.Cursor,
return items, &pageInfo
}
func cursorField(before, after *model.Cursor, idField, orderDir string) sq.Sqlizer {
if after != nil {
if orderDir == "DESC" {
return sq.Lt{idField: after.After}
} else if orderDir == "ASC" {
return sq.Gt{idField: after.After}
}
} else if before != nil {
if orderDir == "DESC" {
return sq.Gt{idField: before.Before}
} else if orderDir == "ASC" {
return sq.Lt{idField: before.Before}
}
}
// Should never be reached.
return nil
}
func QueryModel[T any](
ctx context.Context,
opts *database.FilterOptions,
idField string,
idField, orderDir string,
limit *int,
before, after *model.Cursor,
getModels func(context.Context, *database.FilterOptions) ([]T, error),
@@ 50,15 68,20 @@ func QueryModel[T any](
if after != nil {
opts.Filter = sq.And{
opts.Filter,
sq.Lt{idField: after.After},
cursorField(before, after, idField, orderDir),
}
numElements = after.Limit
} else if before != nil {
opts.Filter = sq.And{
opts.Filter,
sq.Gt{idField: before.Before},
cursorField(before, after, idField, orderDir),
}
if orderDir == "DESC" {
opts.OrderBy = idField + " ASC"
} else {
opts.OrderBy = idField + " DESC"
}
opts.OrderBy = idField + " ASC"
numElements = before.Limit
}
M api/graph/schema.graphqls => api/graph/schema.graphqls +6 -0
@@ 128,6 128,11 @@ enum CloudOrderType {
NAME_DESC
}
+enum OrderType {
+ DESC
+ ASC
+}
+
# Considering removing these Null* fields:
# https://todo.code.netlandish.com/~netlandish/links/75
@@ 601,6 606,7 @@ input GetLinkInput {
excludeTag: String
search: String
filter: String
+ order: OrderType
tagCloudType: CloudType
tagCloudOrder: CloudOrderType
}
M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +22 -2
@@ 4777,7 4777,7 @@ func (r *qRCodeResolver) CodeType(ctx context.Context, obj *models.QRCode) (mode
func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) {
return &model.Version{
Major: 0,
- Minor: 3,
+ Minor: 4,
Patch: 1,
DeprecationDate: nil,
}, nil
@@ 4984,6 4984,7 @@ func (r *queryResolver) GetPaymentHistory(ctx context.Context, input *model.GetP
ctx,
opts,
"i.id",
+ "DESC",
input.Limit,
input.Before,
input.After,
@@ 5236,6 5237,7 @@ func (r *queryResolver) GetOrgLinks(ctx context.Context, input *model.GetLinkInp
return nil, nil
}
+ order := model.OrderTypeDesc
cloudType := model.CloudTypeLinks
cloudOrder := model.CloudOrderTypeNameAsc
if input.TagCloudType != nil && *input.TagCloudType != "" {
@@ 5245,9 5247,18 @@ func (r *queryResolver) GetOrgLinks(ctx context.Context, input *model.GetLinkInp
cloudOrder = *input.TagCloudOrder
}
+ if input.Order != nil {
+ order = *input.Order
+ }
+
+ orderDir := "DESC"
+ if order == model.OrderTypeAsc {
+ orderDir = "ASC"
+ }
+
linkOpts := &database.FilterOptions{
Filter: sq.And{},
- OrderBy: "ol.id DESC",
+ OrderBy: "ol.id " + orderDir,
}
var org *models.Organization
@@ 5348,6 5359,7 @@ func (r *queryResolver) GetOrgLinks(ctx context.Context, input *model.GetLinkInp
ctx,
linkOpts,
"ol.id",
+ orderDir,
input.Limit,
input.Before,
input.After,
@@ 5621,6 5633,7 @@ func (r *queryResolver) GetLinkShorts(ctx context.Context, input *model.GetLinkS
ctx,
linkOpts,
"l.id",
+ "DESC",
input.Limit,
input.Before,
input.After,
@@ 5744,6 5757,7 @@ func (r *queryResolver) GetListings(ctx context.Context, input *model.GetListing
ctx,
listingOpts,
"l.id",
+ "DESC",
input.Limit,
input.Before,
input.After,
@@ 5828,6 5842,7 @@ func (r *queryResolver) GetListing(ctx context.Context, input *model.GetListingD
ctx,
linkOpts,
"ll.link_order",
+ "DESC",
input.Limit,
input.Before,
input.After,
@@ 6412,6 6427,7 @@ func (r *queryResolver) GetFeed(ctx context.Context, input *model.GetFeedInput)
ctx,
linkOpts,
"ol.id",
+ "DESC",
input.Limit,
input.Before,
input.After,
@@ 6596,6 6612,7 @@ func (r *queryResolver) GetAuditLogs(ctx context.Context, input *model.AuditLogI
ctx,
opts,
"al.id",
+ "DESC",
input.Limit,
input.Before,
input.After,
@@ 6659,6 6676,7 @@ func (r *queryResolver) GetUsers(ctx context.Context, input *model.GetUserInput)
ctx,
opts,
"u.id",
+ "DESC",
input.Limit,
input.Before,
input.After,
@@ 6750,6 6768,7 @@ func (r *queryResolver) GetAdminOrganizations(ctx context.Context, input *model.
ctx,
opts,
"o.id",
+ "DESC",
input.Limit,
input.Before,
input.After,
@@ 7000,6 7019,7 @@ func (r *queryResolver) GetAdminDomains(ctx context.Context, input *model.GetAdm
ctx,
opts,
"d.id",
+ "DESC",
input.Limit,
input.Before,
input.After,
M cmd/links/main.go => cmd/links/main.go +8 -2
@@ 3,6 3,7 @@ package main
import (
"context"
"fmt"
+ htemplate "html/template"
"net/http"
"net/url"
"os"
@@ 293,8 294,13 @@ func run() error {
}
return slices.Contains(tags, strings.TrimSpace(tag))
},
- "addQueryElement": links.AddQueryElement,
- "getAddLinkURL": links.GetAddLinkURL,
+ "addQueryElement": func(q htemplate.URL, param, val string) htemplate.URL {
+ return links.AddQueryElement(q, param, val, false)
+ },
+ "setQueryElement": func(q htemplate.URL, param, val string) htemplate.URL {
+ return links.AddQueryElement(q, param, val, true)
+ },
+ "getAddLinkURL": links.GetAddLinkURL,
"newlinebr": func(blob string) string {
return strings.ReplaceAll(blob, "\n", "<br />\n")
},
M cmd/test/helpers.go => cmd/test/helpers.go +8 -2
@@ 6,6 6,7 @@ import (
"database/sql"
"encoding/hex"
"fmt"
+ htemplate "html/template"
"io"
"links"
"links/accounts"
@@ 130,8 131,13 @@ func NewWebTestServer(t *testing.T) (*server.Server, *echo.Echo) {
"isTagUsedInFilter": func(tag string, activeTags string) bool {
return strings.Contains(activeTags, tag)
},
- "addQueryElement": links.AddQueryElement,
- "getAddLinkURL": links.GetAddLinkURL,
+ "addQueryElement": func(q htemplate.URL, param, val string) htemplate.URL {
+ return links.AddQueryElement(q, param, val, false)
+ },
+ "setQueryElement": func(q htemplate.URL, param, val string) htemplate.URL {
+ return links.AddQueryElement(q, param, val, true)
+ },
+ "getAddLinkURL": links.GetAddLinkURL,
"newlinebr": func(blob string) string {
return strings.ReplaceAll(blob, "\n", "<br />\n")
},
M core/routes.go => core/routes.go +13 -1
@@ 2065,7 2065,8 @@ func (s *Service) OrgLinksList(c echo.Context) error {
op := gqlclient.NewOperation(
`query GetOrgLinks($slug: String, $after: Cursor, $before: Cursor,
$tag: String, $excludeTag: String, $search: String,
- $filter: String, $cloudType: CloudType, $cloudOrder: CloudOrderType) {
+ $filter: String, $order: OrderType, $cloudType: CloudType,
+ $cloudOrder: CloudOrderType) {
getOrgLinks(input: {
orgSlug: $slug,
after: $after,
@@ 2074,6 2075,7 @@ func (s *Service) OrgLinksList(c echo.Context) error {
excludeTag: $excludeTag,
search: $search,
filter: $filter,
+ order: $order,
tagCloudType: $cloudType,
tagCloudOrder: $cloudOrder
}) {
@@ 2274,6 2276,13 @@ func (s *Service) OrgLinksList(c echo.Context) error {
queries.Add("q", search)
}
+ orderDir := c.QueryParam("order")
+ if orderDir != "" && (orderDir == "DESC" || orderDir == "ASC") {
+ op.Var("order", orderDir)
+ } else {
+ orderDir = "DESC" // default
+ }
+
err = links.Execute(links.LangContext(c), op, &result)
if err != nil {
if graphError, ok := err.(*gqlclient.Error); ok {
@@ 2319,6 2328,8 @@ func (s *Service) OrgLinksList(c echo.Context) error {
pd.Data["follow"] = lt.Translate("Follow")
pd.Data["unfollow"] = lt.Translate("Unfollow")
pd.Data["tags"] = lt.Translate("Tags")
+ pd.Data["newest"] = lt.Translate("newest")
+ pd.Data["oldest"] = lt.Translate("oldest")
orgLinks := result.OrgLinks.Result
if links.IsRSS(c.Path()) {
domain := fmt.Sprintf("%s://%s", gctx.Server.Config.Scheme, gctx.Server.Config.Domain)
@@ 2409,6 2420,7 @@ func (s *Service) OrgLinksList(c echo.Context) error {
"rssURL": rssURL,
"followAction": followAction,
"tagCloud": result.OrgLinks.TagCloud,
+ "orderDir": orderDir,
}
if search != "" {
M helpers.go => helpers.go +11 -6
@@ 879,18 879,23 @@ func ParseSearch(s string) string {
return s
}
-func AddQueryElement(q template.URL, param, val string) template.URL {
+func AddQueryElement(q template.URL, param, val string, replace bool) template.URL {
query, err := url.ParseQuery(string(q))
if err != nil {
return ""
}
- curVal := query.Get(param)
- if curVal == "" {
- curVal = val
+
+ if !replace {
+ curVal := query.Get(param)
+ if curVal == "" {
+ curVal = val
+ } else {
+ curVal += fmt.Sprintf(",%s", val)
+ }
+ query.Set(param, curVal)
} else {
- curVal += fmt.Sprintf(",%s", val)
+ query.Set(param, val)
}
- query.Set(param, curVal)
return template.URL(query.Encode())
}
M templates/link_list.html => templates/link_list.html +16 -4
@@ 226,9 226,21 @@
</footer>
{{end}}
</div> {{- /* col-9 */ -}}
- <div class="col-3">
+ <div class="col-3"> {{- /* right side bar */ -}}
+ <div class="row">
+ <div class="col-2">
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="menu-item__icon"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7.5 7.5 3m0 0L12 7.5M7.5 3v13.5m13.5 0L16.5 21m0 0L12 16.5m4.5 4.5V7.5" /></svg>
+ </div>
+ <div class="col is-vertical-align">
+ {{ if eq .orderDir "DESC" }}
+ <small class="link-tag__side link-tag__item--simple">{{ .pd.Data.newest }}</small> • <a href="{{if $.queries}}?{{setQueryElement $.queries "order" "ASC"}}{{else}}?order=ASC{{end}}" class="link-tag__side link-tag__item--simple">{{ .pd.Data.oldest }}</a>
+ {{ else }}
+ <a href="{{if $.queries}}?{{setQueryElement $.queries "order" "DESC"}}{{else}}?order=DESC{{end}}" class="link-tag__side link-tag__item--simple">{{ .pd.Data.newest }}</a> • <small class="link-tag__side link-tag__item--simple">{{ .pd.Data.oldest }}</small>
+ {{ end }}
+ </div>
+ </div>
<p>{{ .pd.Data.tags }}</p>
- <hr></hr>
+ <hr>
<p>
{{ range .tagCloud }}
{{if isTagUsedInFilter .Slug $.tagFilter}}
@@ 238,7 250,7 @@
{{end}}
{{ end }}
</p>
- </div>
- </div> {{- /* row */ -}}
+ </div> {{- /* end right side bar */ -}}
+ </div> {{- /* end row */ -}}
</section>
{{template "base_footer" .}}