diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-03 07:52:30 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-03 07:52:30 -0500 |
| commit | 580309bb65a5e529c7aab5e58d3983a37c471023 (patch) | |
| tree | 10ebe78cbd8909a39ba126c77a560bc96174817a /pearl.el | |
| parent | 6ef196e306320804b80a502dab29dbafd12285bc (diff) | |
| download | pearl-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.el | 131 |
1 files changed, 114 insertions, 17 deletions
@@ -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 () |
