aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-03 07:52:30 -0500
committerCraig Jennings <c@cjennings.net>2026-06-03 07:52:30 -0500
commit580309bb65a5e529c7aab5e58d3983a37c471023 (patch)
tree10ebe78cbd8909a39ba126c77a560bc96174817a /pearl.el
parent6ef196e306320804b80a502dab29dbafd12285bc (diff)
downloadpearl-580309bb65a5e529c7aab5e58d3983a37c471023.tar.gz
pearl-580309bb65a5e529c7aab5e58d3983a37c471023.zip
fix(views): honor a Linear view's completed-issues display preference
A Linear view that hides completed issues still showed done and cancelled issues when run in pearl. The open-only restriction isn't in the view's filterData. It's a separate display preference, viewPreferencesValues.showCompletedIssues, which pearl fetched for sort but never applied. So the customView.issues connection returned every state type, and a "by status" or "open issues" view didn't look like itself. I now read showCompletedIssues alongside the sort fields and translate it to an IssueFilter passed to the connection, AND-combined with the view's own filter. "all" adds nothing, "none" excludes the closed state types (completed, canceled, duplicate), and a recency window (day/week/month/quarter/year) keeps open issues plus those completed or canceled within the window. I stamp the raw preference on the source so a refresh rebuilds the window relative to the refresh time rather than first-run time. Both entry points route through a shared pearl--view-node-prefs resolver, so pearl-run-linear-view and a favorited view picked in pearl-pick-source behave the same. That also closes a latent gap where the pick-source path ignored the view's configured sort. Verified live against the workspace: a "none" view returns only open issues, and a "week" view returns open issues plus those closed within seven days, with none older leaking through.
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el131
1 files changed, 114 insertions, 17 deletions
diff --git a/pearl.el b/pearl.el
index a5fa205..1b51577 100644
--- a/pearl.el
+++ b/pearl.el
@@ -1608,14 +1608,70 @@ direction is set."
(let ((order (if (equal direction "asc") "Ascending" "Descending")))
(vector (list (cons ordering (list (cons "order" order))))))))
+(defconst pearl--completed-state-types '("completed" "canceled" "duplicate")
+ "WorkflowState `type' values Linear treats as closed for a view's
+`showCompletedIssues' preference. Linear's UI groups Done under `completed'
+and both Canceled and Duplicate states under `canceled'/`duplicate', and the
+\"completed issues\" toggle hides all three. Excluding by closed type (rather
+than including by open type) fails toward showing a stray closed issue rather
+than hiding genuine open work if Linear ever adds a type.")
+
+(defconst pearl--view-completed-windows
+ '(("day" . 1) ("week" . 7) ("month" . 30) ("quarter" . 90) ("year" . 365))
+ "Linear `showCompletedIssues' recency keywords mapped to a day count.
+A view set to one of these shows open issues plus issues completed or canceled
+within the window. The month/quarter/year counts are fixed-day approximations
+of Linear's calendar windows -- close enough for a list that refreshes often,
+and erring toward showing slightly more rather than hiding recent work.")
+
+(defun pearl--view-open-issues-clause ()
+ "IssueFilter clause matching only open issues -- excludes the closed state
+types in `pearl--completed-state-types'."
+ (list (cons "state"
+ (list (cons "type"
+ (list (cons "nin"
+ (vconcat pearl--completed-state-types))))))))
+
+(defun pearl--view-completed-filter (show-completed &optional now)
+ "Translate a Custom View's SHOW-COMPLETED preference into an `IssueFilter'.
+SHOW-COMPLETED is Linear's `viewPreferencesValues.showCompletedIssues' string.
+NOW is the reference time for a recency window (defaults to `current-time');
+recomputing it on each run/refresh keeps a window relative to fetch time rather
+than to when the view was first opened.
+
+Returns an alist suitable as the `filter' GraphQL variable, AND-combined with
+the view's own `filterData' by the `customView.issues' connection, or nil when
+no extra filtering is needed:
+- \"all\", nil, or any value pearl doesn't recognize -> nil (show everything,
+ today's behavior; degrading an unknown value to \"all\" never hides open work).
+- \"none\" -> only open issues (see `pearl--view-open-issues-clause').
+- a recency window (\"day\"/\"week\"/\"month\"/\"quarter\"/\"year\") -> open issues
+ OR issues whose `completedAt' or `canceledAt' falls within the window."
+ (cond
+ ((member show-completed '(nil "all")) nil)
+ ((equal show-completed "none") (pearl--view-open-issues-clause))
+ ((assoc show-completed pearl--view-completed-windows)
+ (let* ((days (cdr (assoc show-completed pearl--view-completed-windows)))
+ (cutoff (time-subtract (or now (current-time)) (days-to-time days)))
+ (iso (format-time-string "%Y-%m-%dT%H:%M:%S.000Z" cutoff t)))
+ (list (cons "or"
+ (vector (pearl--view-open-issues-clause)
+ (list (cons "completedAt" (list (cons "gte" iso))))
+ (list (cons "canceledAt" (list (cons "gte" iso)))))))))
+ (t nil)))
+
(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. 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!]) {
+`pearl--view-sort-arg' and `pearl--default-issue-sort'. The `filter' variable
+is AND-combined with the view's stored `filterData' -- pearl uses it to honor
+the view's `showCompletedIssues' preference, which lives outside `filterData'
+and so isn't applied by the connection alone (see
+`pearl--view-completed-filter')."
+ (format "query ViewIssues($id: String!, $first: Int!, $after: String, $sort: [IssueSortInput!], $filter: IssueFilter) {
customView(id: $id) {
- issues(first: $first, after: $after, sort: $sort) {
+ issues(first: $first, after: $after, sort: $sort, filter: $filter) {
nodes {
id identifier title description priority url updatedAt
state { id name type }
@@ -1631,13 +1687,16 @@ view-populated list renders them. The `sort' variable orders the issues; see
}
}" (pearl--list-comments-fragment)))
-(defun pearl--query-view-async (view-id callback &optional sort)
+(defun pearl--query-view-async (view-id callback &optional sort filter)
"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.
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."
+than Linear's createdAt-desc API default.
+FILTER is an optional `IssueFilter' AND-combined with the view's own filter (see
+`pearl--view-completed-filter'); when nil no `filter' variable is sent, so the
+view's `filterData' applies unchanged."
(let* ((sort (or sort pearl--default-issue-sort))
(page-fn
(lambda (after page-cb)
@@ -1646,6 +1705,7 @@ than Linear's createdAt-desc API default."
`(("id" . ,view-id)
("first" . 100)
("sort" . ,sort)
+ ,@(when filter (list (cons "filter" filter)))
,@(when after (list (cons "after" after))))
(lambda (response)
(if (or (null response)
@@ -1681,7 +1741,7 @@ Selects no `url' -- Linear's `CustomView' type has none, and requesting it
"query CustomViews($first: Int!, $after: String) {
customViews(first: $first, after: $after) {
nodes { id name description shared team { id }
- viewPreferencesValues { viewOrdering viewOrderingDirection } }
+ viewPreferencesValues { viewOrdering viewOrderingDirection showCompletedIssues } }
pageInfo { hasNextPage endCursor }
}
}")
@@ -1689,7 +1749,9 @@ Selects no `url' -- Linear's `CustomView' type has none, and requesting it
(defun pearl--custom-views (&optional force)
"Return the workspace's Custom Views, fetching once and caching.
Each node carries id/name/description/shared/team plus viewPreferencesValues
-(the configured ordering, read by `pearl--view-sort-arg'). A non-nil FORCE
+(the configured ordering, read by `pearl--view-sort-arg', and the
+showCompletedIssues preference, read by `pearl--view-completed-filter'). A
+non-nil FORCE
refetches."
(or (and (not force) pearl--cache-views)
(let* ((response (pearl--graphql-request (pearl--custom-views-query)
@@ -4256,10 +4318,7 @@ missing setup."
((plist-get source :browser)
(pearl--open-favorite-url source))
((eq (plist-get source :type) 'view)
- (pearl--progress "Running view %s..." (plist-get source :name))
- (pearl--query-view-async
- (plist-get source :id)
- (lambda (result) (pearl--render-query-result result source))))
+ (pearl--run-view-source source))
(t
(let ((filter (plist-get source :filter)))
(pearl--validate-issue-filter filter)
@@ -6283,9 +6342,45 @@ commands. Errors if no source is recorded."
('view
(pearl--progress "Refreshing %s..." (pearl--source-name source))
(pearl--query-view-async (plist-get source :id) merge
- (plist-get source :sort)))
+ (plist-get source :sort)
+ (pearl--view-completed-filter
+ (plist-get source :show-completed))))
(_ (user-error "Unknown Linear source type: %s" (plist-get source :type)))))))
+(defun pearl--view-node-prefs (view)
+ "Extract the runnable view preferences from a Custom View node VIEW.
+Returns `(:sort SORT :show-completed SC)': SORT is the `IssueSortInput' for the
+view's configured ordering (see `pearl--view-sort-arg', nil when pearl can't
+reproduce it), SC is the raw `showCompletedIssues' string (see
+`pearl--view-completed-filter'). A nil VIEW (e.g. a favorite referencing a view
+the list query didn't return) yields both nil, so the caller falls back to the
+house sort and shows all issues."
+ (let ((prefs (cdr (assoc 'viewPreferencesValues view))))
+ (list :sort (pearl--view-sort-arg (cdr (assoc 'viewOrdering prefs))
+ (cdr (assoc 'viewOrderingDirection prefs)))
+ :show-completed (cdr (assoc 'showCompletedIssues prefs)))))
+
+(defun pearl--run-view-source (source)
+ "Run a view SOURCE (`:type view', at least `:id'/`:name') and render it.
+Resolves the view's configured sort and `showCompletedIssues' from the cached
+Custom Views by id, stamps them onto SOURCE so a later refresh reuses them, and
+forwards both to the query. Used by `pearl-pick-source' for a favorited Linear
+view, whose favorite node carries no view preferences -- without this it would
+ignore the view's sort and show its completed/canceled issues."
+ (let* ((prefs (pearl--view-node-prefs
+ (seq-find (lambda (v) (equal (cdr (assoc 'id v))
+ (plist-get source :id)))
+ (pearl--custom-views))))
+ (sort (plist-get prefs :sort))
+ (show-completed (plist-get prefs :show-completed))
+ (source (append source (list :sort sort :show-completed show-completed))))
+ (pearl--progress "Running view %s..." (plist-get source :name))
+ (pearl--query-view-async
+ (plist-get source :id)
+ (lambda (result) (pearl--render-query-result result source))
+ sort
+ (pearl--view-completed-filter show-completed))))
+
;;;###autoload
(defun pearl-run-linear-view (view-name)
"Run a Linear Custom View by VIEW-NAME and render it into the active file.
@@ -6302,17 +6397,19 @@ 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* ((prefs (cdr (assoc 'viewPreferencesValues view)))
- (sort (pearl--view-sort-arg (cdr (assoc 'viewOrdering prefs))
- (cdr (assoc 'viewOrderingDirection prefs))))
+ (let* ((prefs (pearl--view-node-prefs view))
+ (sort (plist-get prefs :sort))
+ (show-completed (plist-get prefs :show-completed))
(source (list :type 'view :name view-name :id view-id
:url (pearl--view-url view-id)
- :sort sort)))
+ :sort sort
+ :show-completed show-completed)))
(pearl--progress "Running view %s..." view-name)
(pearl--query-view-async
view-id
(lambda (result) (pearl--render-query-result result source))
- sort))))
+ sort
+ (pearl--view-completed-filter show-completed)))))
;;;###autoload
(defun pearl-open-current-view-in-linear ()