From 483660f9477cc9504a35a3a29ee1fbca52cccdb1 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 3 Jun 2026 06:52:14 -0500 Subject: feat(views): run a Linear view in its configured sort order A Linear view's issue query passed no orderBy, so issues came back in Linear's API default, createdAt descending. That's neither the order set on the view in Linear nor the updatedAt-desc the rest of pearl uses, and the two unrelated defaults colliding is what made a view's order look random. Linear exposes a view's configured sort in viewPreferencesValues (viewOrdering plus viewOrderingDirection), and the issues connection accepts an IssueSortInput sort arg that applies it server-side. I read the view's ordering when running it, translate it to that sort arg, and pass it through, so a view arrives in the order you gave it in Linear. The sort is stamped on the recorded source so a refresh keeps the same order. An ordering pearl can't map, or a view with no preference, falls back to updatedAt-desc rather than the createdAt default, so it lines up with every other pearl fetch. This is the sort half of matching the Linear UI. Grouping (issueGrouping into org sections) is the larger remaining half and is tracked separately. --- pearl.el | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 12 deletions(-) (limited to 'pearl.el') diff --git a/pearl.el b/pearl.el index d6f14de..ca9d0b1 100644 --- a/pearl.el +++ b/pearl.el @@ -1581,13 +1581,41 @@ gone\" apart from \"the API call failed.\"" (funcall callback (or issue :missing))))) (lambda (_error _response _data) (funcall callback :error)))) +(defconst pearl--default-issue-sort + (vector '(("updatedAt" ("order" . "Descending")))) + "Fallback issue sort for a Custom View whose configured ordering pearl can't +reproduce: most-recently-updated first, matching the default `pearl-list-issues' +uses. Keeps a view from falling through to Linear's createdAt-desc API default, +which reads as random next to the rest of pearl.") + +(defconst pearl--view-sortable-orderings + '("priority" "estimate" "title" "label" "slaStatus" "createdAt" "updatedAt" + "completedAt" "dueDate" "accumulatedStateUpdatedAt" "cycle" "milestone" + "assignee" "project" "team" "manual" "workflowState") + "Linear `viewOrdering' values that also exist as `IssueSortInput' keys, so +pearl can reproduce the view's sort server-side. An ordering outside this set +falls back to `pearl--default-issue-sort'.") + +(defun pearl--view-sort-arg (ordering direction) + "Translate a Custom View's ORDERING and DIRECTION into an `IssueSortInput' list. +ORDERING is Linear's `viewOrdering' string; DIRECTION is `viewOrderingDirection' +(\"asc\", \"desc\", or nil). Returns a one-element vector usable as the `sort' +query variable, or nil when ORDERING isn't one pearl can reproduce -- the caller +then falls back to `pearl--default-issue-sort'. A nil DIRECTION defaults to +Descending, the field's natural order and what Linear's UI shows when no +direction is set." + (when (member ordering pearl--view-sortable-orderings) + (let ((order (if (equal direction "asc") "Ascending" "Descending"))) + (vector (list (cons ordering (list (cons "order" order)))))))) + (defun pearl--view-issues-query () "GraphQL query running a Custom View's own filter server-side, by view id. Pulls each issue's recent comments (see `pearl--list-comments-fragment') so a -view-populated list renders them." - (format "query ViewIssues($id: String!, $first: Int!, $after: String) { +view-populated list renders them. The `sort' variable orders the issues; see +`pearl--view-sort-arg' and `pearl--default-issue-sort'." + (format "query ViewIssues($id: String!, $first: Int!, $after: String, $sort: [IssueSortInput!]) { customView(id: $id) { - issues(first: $first, after: $after) { + issues(first: $first, after: $after, sort: $sort) { nodes { id identifier title description priority url updatedAt state { id name type } @@ -1603,16 +1631,21 @@ view-populated list renders them." } }" (pearl--list-comments-fragment))) -(defun pearl--query-view-async (view-id callback) +(defun pearl--query-view-async (view-id callback &optional sort) "Run the Custom View VIEW-ID server-side, calling CALLBACK with a query-result. The view applies its own stored filter on Linear's side; issues come back as -raw nodes (normalized at the render boundary), paged like the general fetch." - (let ((page-fn +raw nodes (normalized at the render boundary), paged like the general fetch. +SORT is an `IssueSortInput' list (the `sort' variable); when nil the view runs +under `pearl--default-issue-sort' so its order matches the rest of pearl rather +than Linear's createdAt-desc API default." + (let* ((sort (or sort pearl--default-issue-sort)) + (page-fn (lambda (after page-cb) (pearl--graphql-request-async (pearl--view-issues-query) `(("id" . ,view-id) ("first" . 100) + ("sort" . ,sort) ,@(when after (list (cons "after" after)))) (lambda (response) (if (or (null response) @@ -1647,14 +1680,17 @@ Selects no `url' -- Linear's `CustomView' type has none, and requesting it 400s the whole query (see `pearl--view-url' for how the link is rebuilt)." "query CustomViews($first: Int!, $after: String) { customViews(first: $first, after: $after) { - nodes { id name description shared team { id } } + nodes { id name description shared team { id } + viewPreferencesValues { viewOrdering viewOrderingDirection } } pageInfo { hasNextPage endCursor } } }") (defun pearl--custom-views (&optional force) "Return the workspace's Custom Views, fetching once and caching. -Each node carries id/name/description/shared/team. A non-nil FORCE refetches." +Each node carries id/name/description/shared/team plus viewPreferencesValues +(the configured ordering, read by `pearl--view-sort-arg'). A non-nil FORCE +refetches." (or (and (not force) pearl--cache-views) (let* ((response (pearl--graphql-request (pearl--custom-views-query) '(("first" . 100)))) @@ -6246,7 +6282,8 @@ commands. Errors if no source is recorded." merge)) ('view (pearl--progress "Refreshing %s..." (pearl--source-name source)) - (pearl--query-view-async (plist-get source :id) merge)) + (pearl--query-view-async (plist-get source :id) merge + (plist-get source :sort))) (_ (user-error "Unknown Linear source type: %s" (plist-get source :type))))))) ;;;###autoload @@ -6265,12 +6302,17 @@ dirty-buffer guard) and records the view as the active source." (view-id (and view (cdr (assoc 'id view))))) (unless view-id (user-error "No Custom View named %s" view-name)) - (let ((source (list :type 'view :name view-name :id view-id - :url (pearl--view-url view-id)))) + (let* ((prefs (cdr (assoc 'viewPreferencesValues view))) + (sort (pearl--view-sort-arg (cdr (assoc 'viewOrdering prefs)) + (cdr (assoc 'viewOrderingDirection prefs)))) + (source (list :type 'view :name view-name :id view-id + :url (pearl--view-url view-id) + :sort sort))) (pearl--progress "Running view %s..." view-name) (pearl--query-view-async view-id - (lambda (result) (pearl--render-query-result result source)))))) + (lambda (result) (pearl--render-query-result result source)) + sort)))) ;;;###autoload (defun pearl-open-current-view-in-linear () -- cgit v1.2.3