diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-23 23:40:27 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-23 23:40:27 -0400 |
| commit | 8869a083c3cc28c20efa63e8932347329b2865f4 (patch) | |
| tree | 8959775eb2cae28a3418542ab8e2fc235fe465f2 /pearl.el | |
| parent | 92ae9d8b56a9fabadf6ea8beb2163655c7a8ffb8 (diff) | |
| download | pearl-8869a083c3cc28c20efa63e8932347329b2865f4.tar.gz pearl-8869a083c3cc28c20efa63e8932347329b2865f4.zip | |
feat(sources): single-issue source (favorites + open-issue-by-id)
I wired the issue kind into the same pipeline every other source uses, so a favorited issue renders in the buffer as its own subtree instead of the v1 browser punt. The new pearl-open-issue-by-id command does the same for an issue you type by identifier or id. It's handy when someone hands you ENG-123 and you'd rather read it in Pearl than a browser.
Linear's issue(id:) accepts both the UUID and the human identifier, so there's no resolution step. I fetch by whatever was given and build the :type issue source from the returned node, so the stored id and identifier stay authoritative. Refresh re-fetches the one issue and merges by LINEAR-ID like the filter and view sources.
Tests cover the source constructor, the header round-trip, and the fetch/render and command paths across Normal, Boundary, and Error.
Diffstat (limited to 'pearl.el')
| -rw-r--r-- | pearl.el | 74 |
1 files changed, 71 insertions, 3 deletions
@@ -1404,11 +1404,21 @@ both normalize to `:kind label' so the dispatch collapses them." (cdr (assoc 'identifier (cdr (assoc 'issue node)))))) base))) +(defun pearl--issue-source (id identifier) + "Build a `:type issue' source plist for the single issue ID/IDENTIFIER. +IDENTIFIER is the human key (ENG-1) and becomes the source name so the buffer +title and `#+LINEAR-SOURCE' read naturally; it falls back to ID when absent, so +`:name' is never nil. Mirrors the other source shapes (`:type view', +`:type filter') so the issue flows through the same fetch/render/refresh path." + (list :type 'issue :id id :identifier identifier + :name (or identifier id))) + (defun pearl--favorite->source (fav) "Resolve a normalized FAV into a runnable source plist for the existing query/render path, or a browser action `(:browser t :url U :title T :kind K)' -when the favorite is not a list source in v1 (issue / team / document / -dashboard / other)." +when the favorite is not a list source in v1 (team / document / dashboard / +other). An issue favorite resolves to a `:type issue' source so a starred +issue renders in the buffer like every other source." (let ((kind (plist-get fav :kind)) (title (plist-get fav :title)) (url (plist-get fav :url)) @@ -1423,6 +1433,7 @@ dashboard / other)." :filter (list :label-id id :open t))) ('user (list :type 'filter :name title :filter (list :assignee-id id :open t))) + ('issue (pearl--issue-source id (plist-get fav :identifier))) (_ (list :browser t :url url :title title :kind kind))))) (defconst pearl--favorites-query @@ -4920,6 +4931,9 @@ missing setup." (pearl--open-favorite-url source)) ((eq (plist-get source :type) 'view) (pearl--run-view-source source)) + ((eq (plist-get source :type) 'issue) + (pearl--progress "Opening %s..." (pearl--source-name source)) + (pearl--fetch-and-render-issue (plist-get source :id))) (t (let ((filter (plist-get source :filter))) (pearl--validate-issue-filter filter) @@ -4929,6 +4943,44 @@ missing setup." (lambda (result) (pearl--render-query-result result source)) (pearl--sort->order-by (plist-get source :sort)))))))))))) +(defun pearl--fetch-and-render-issue (ref) + "Fetch the issue named by REF and render it as the active source. +REF is a Linear issue id (UUID) or its human identifier (ENG-1); Linear's +`issue(id:)' accepts either. On success the single node is rendered through +the shared `pearl--render-query-result' boundary -- the same full-replace path +list-issues and views use -- tagged with a `:type issue' source built from the +node's own id and identifier, so refresh and the `#+LINEAR-SOURCE' header work +exactly as they do for every other source. A missing or errored fetch reports +and leaves the buffer untouched." + (pearl--fetch-issue-async + ref + (lambda (result) + (pcase result + (:error + (message "Linear returned an error fetching %s" ref)) + (:missing + (message "Issue %s not found on Linear (deleted, or no access)" ref)) + (raw + (pearl--render-query-result + (pearl--make-query-result 'ok :issues (list raw)) + (pearl--issue-source (cdr (assoc 'id raw)) + (cdr (assoc 'identifier raw))))))))) + +;;;###autoload +(defun pearl-open-issue-by-id (ref) + "Open a single Linear issue by its identifier or id and render it in Pearl. +Prompts for REF -- a human identifier like ENG-123 (or a raw issue id) -- then +fetches that one issue and renders it in the active file as its own source, the +same shape every other source produces. Useful when someone hands you an issue +id and you want it in Pearl rather than a browser." + (interactive (list (read-string "Issue identifier (e.g. ENG-123): "))) + (pearl--require-account-context) + (let ((trimmed (string-trim ref))) + (when (string-empty-p trimmed) + (user-error "No issue identifier given")) + (pearl--progress "Opening %s..." trimmed) + (pearl--fetch-and-render-issue trimmed))) + ;;; Copy down: save a Linear view as a local view (see the reverse-compile above) (defun pearl--customview-filterdata-async (view-id callback) @@ -7176,6 +7228,20 @@ commands. Errors if no source is recorded." (plist-get source :sort) (pearl--view-completed-filter (plist-get source :show-completed)))) + ('issue + (pearl--progress "Refreshing %s..." (pearl--source-name source)) + (pearl--fetch-issue-async + (plist-get source :id) + (lambda (result) + (pcase result + (:error + (message "Linear returned an error refreshing %s" + (pearl--source-name source))) + (:missing + (message "Issue %s is no longer on Linear (deleted, or no access)" + (pearl--source-name source))) + (raw + (funcall merge (pearl--make-query-result 'ok :issues (list raw)))))))) (_ (user-error "Unknown Linear source type: %s" (plist-get source :type))))))) ;;; Interactive sort/order of the active view @@ -8366,7 +8432,8 @@ body stay." ("l" "open default view" pearl-open-default-view) ("m" "my open issues" pearl-list-issues) ("p" "by project" pearl-list-issues-by-project) - ("f" "build a filter" pearl-list-issues-filtered)] + ("f" "build a filter" pearl-list-issues-filtered) + ("i" "open issue by id" pearl-open-issue-by-id)] ["Views" ("V" "create local view" pearl-create-local-view) ("e" "edit local view" pearl-edit-local-view) @@ -8423,6 +8490,7 @@ body stay." (define-key map "o" (cons "my open issues" #'pearl-list-issues)) (define-key map "p" (cons "by project" #'pearl-list-issues-by-project)) (define-key map "f" (cons "build a filter" #'pearl-list-issues-filtered)) + (define-key map "i" (cons "open issue by id" #'pearl-open-issue-by-id)) (define-key map "c" (cons "all comments for issue" #'pearl-fetch-all-comments)) map) "Pearl issue-fetch commands; a sub-keymap of `pearl-prefix-map'. |
