aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pearl.el82
-rw-r--r--tests/test-pearl-favorites.el17
-rw-r--r--tests/test-pearl-reverse-compile.el78
3 files changed, 139 insertions, 38 deletions
diff --git a/pearl.el b/pearl.el
index f66490c..41abab6 100644
--- a/pearl.el
+++ b/pearl.el
@@ -1365,9 +1365,18 @@ a list source). For an issue favorite, `:identifier' carries the human key
like ENG-1; everything else carries only `:id'. =label= and =projectLabel=
both normalize to `:kind label' so the dispatch collapses them."
(let* ((type (cdr (assoc 'type node)))
- (title (or (cdr (assoc 'title node)) ""))
+ (title-raw (cdr (assoc 'title node)))
(url (cdr (assoc 'url node)))
(sort-order (cdr (assoc 'sortOrder node)))
+ ;; Linear populates `Favorite.title' only for favorite folders; an
+ ;; entity favorite (a starred custom view) arrives with a null/empty
+ ;; title, so fall back to the entity's own name. Today the favorites
+ ;; query selects only `customView.name', the case Pearl dispatches on.
+ (title (cond ((and (stringp title-raw) (not (string-empty-p title-raw)))
+ title-raw)
+ ((string= type "customView")
+ (or (cdr (assoc 'name (cdr (assoc 'customView node)))) ""))
+ (t "")))
(kind+id
(pcase type
("customView" (cons 'view (pearl--favorite-entity-id node 'customView)))
@@ -1413,7 +1422,7 @@ dashboard / other)."
"query Favorites($after: String) {
favorites(first: 100, after: $after) {
nodes { id type title url sortOrder
- customView { id } project { id } cycle { id }
+ customView { id name } project { id } cycle { id }
label { id } user { id } issue { id identifier }
team { id } projectLabel { id } document { id } dashboard { id } }
pageInfo { hasNextPage endCursor } } }"
@@ -4928,16 +4937,6 @@ GraphQL error or a view with no readable filter."
(funcall callback fd)))
(lambda (&rest _) (funcall callback nil))))
-(defun pearl--linear-view-favorites (favorites)
- "Return `((TITLE . VIEW-ID) ...)' for the view-kind entries in FAVORITES.
-These are the copy-down candidates -- only a favorited Linear view carries a
-Custom View id and a readable filter."
- (delq nil
- (mapcar (lambda (fav)
- (when (eq (plist-get fav :kind) 'view)
- (cons (plist-get fav :title) (plist-get fav :id))))
- favorites)))
-
(defun pearl--save-linear-view-locally-finish (title view-id filter-data)
"Reverse-compile FILTER-DATA for Linear view TITLE/VIEW-ID and save a fork.
On a refusal, signal a `user-error' naming the construct and pointing at
@@ -4965,13 +4964,27 @@ link, `:copied-from-view-id' provenance only) and report the independence."
(ignore-errors
(customize-save-variable 'pearl-local-views pearl-local-views))))
(message
- "Saved Linear view %s as independent local view %s; edits won't update the original until you publish it back"
- title name)))))
+ "Saved '%s' as an independent local view (forked from '%s'); run it with pearl-run-local-view or set it as your default with pearl-set-default-view"
+ name title)))))
+
+(defun pearl--save-linear-view-locally-fetch (title view-id)
+ "Fetch VIEW-ID's filter and hand it to the copy-down finisher for TITLE.
+Shared by both copy-down entry points -- saving the current buffer's view and
+picking one from the custom-view list -- so they reverse-compile and save
+identically."
+ (pearl--progress "Fetching filter for %s..." title)
+ (pearl--customview-filterdata-async
+ view-id
+ (lambda (filter-data)
+ (pearl--save-linear-view-locally-finish title view-id filter-data))))
;;;###autoload
(defun pearl-save-linear-view-locally ()
- "Copy a favorited Linear view down into a new, editable local view.
-Lists your favorited Linear views, fetches the chosen view's filter, reverse-
+ "Copy a Linear Custom View down into a new, editable local view.
+When the current buffer is showing a Linear view, offers to save that view
+directly. Otherwise -- or when you decline -- prompts over *all* the
+workspace's Custom Views (not only favorited ones, matching what
+`pearl-run-linear-view' lists). Fetches the chosen view's filter, reverse-
compiles it into a Pearl authoring filter, and saves a forked local view -- no
tracking link, so editing or renaming it never touches the Linear view. The new
view is stamped with the active account. When the view's filter is outside
@@ -4980,21 +4993,28 @@ refuses and names the construct; run such a view directly with
`pearl-run-linear-view'."
(interactive)
(pearl--require-account-context t)
- (pearl--progress "Fetching favorited Linear views...")
- (pearl--favorites-async
- (lambda (favorites)
- (let ((views (pearl--linear-view-favorites favorites)))
- (unless views
- (user-error
- "No favorited Linear views; favorite a view in Linear first, then copy it down"))
- (let* ((title (completing-read "Save Linear view locally: "
- (mapcar #'car views) nil t))
- (view-id (cdr (assoc title views))))
- (pearl--progress "Fetching filter for %s..." title)
- (pearl--customview-filterdata-async
- view-id
- (lambda (filter-data)
- (pearl--save-linear-view-locally-finish title view-id filter-data))))))))
+ (let* ((source (pearl--read-active-source))
+ (current (and (eq (plist-get source :type) 'view)
+ (plist-get source :id)
+ source)))
+ (if (and current
+ (pearl--read-yes-no
+ (format "Save current view '%s' locally? "
+ (plist-get current :name))))
+ (pearl--save-linear-view-locally-fetch
+ (plist-get current :name) (plist-get current :id))
+ (let ((views (pearl--custom-views)))
+ (unless views
+ (user-error "No Custom Views in this workspace to copy down"))
+ (let* ((title (completing-read "Save Linear view locally: "
+ (mapcar (lambda (v) (cdr (assoc 'name v)))
+ views)
+ nil t))
+ (view (seq-find (lambda (v) (string= (cdr (assoc 'name v)) title))
+ views))
+ (view-id (and view (cdr (assoc 'id view)))))
+ (unless view-id (user-error "No Custom View named %s" title))
+ (pearl--save-linear-view-locally-fetch title view-id))))))
(defun pearl--assemble-filter (team open state project labels assignee)
"Assemble a filter authoring plist from the chosen dimensions.
diff --git a/tests/test-pearl-favorites.el b/tests/test-pearl-favorites.el
index 55808b3..3f1452f 100644
--- a/tests/test-pearl-favorites.el
+++ b/tests/test-pearl-favorites.el
@@ -56,6 +56,23 @@ For an issue node, IDENTIFIER fills the issue's user-facing key."
(should (string= "Active bugs" (plist-get p :title)))
(should (string= "view-1" (plist-get p :id)))))
+(ert-deftest test-pearl-normalize-favorite-custom-view-blank-title-uses-name ()
+ "A favorited custom view with a blank own title takes the customView's name.
+Linear populates Favorite.title only for favorite folders, so an entity favorite
+arrives with a null/empty title and the entity's own name is the only display
+string -- the favorites query selects customView.name for exactly this."
+ (let* ((node (list (cons 'id "fav-customView")
+ (cons 'type "customView")
+ (cons 'title nil)
+ (cons 'url "https://linear.app/x/view/v1")
+ (cons 'sortOrder 0.1)
+ (cons 'customView (list (cons 'id "view-1")
+ (cons 'name "Pearl Open Issues")))))
+ (p (pearl--normalize-favorite node)))
+ (should (eq 'view (plist-get p :kind)))
+ (should (string= "Pearl Open Issues" (plist-get p :title)))
+ (should (string= "view-1" (plist-get p :id)))))
+
(ert-deftest test-pearl-normalize-favorite-project ()
(let ((p (pearl--normalize-favorite
(test-pearl-fav--node "project" "Platform" "https://linear.app/x/project/p1" 0.2
diff --git a/tests/test-pearl-reverse-compile.el b/tests/test-pearl-reverse-compile.el
index 382c72a..209d9a6 100644
--- a/tests/test-pearl-reverse-compile.el
+++ b/tests/test-pearl-reverse-compile.el
@@ -195,13 +195,77 @@
;;; copy-down command-side: candidate builder + finish
-(ert-deftest test-pearl-linear-view-favorites-extracts-views ()
- "Only view-kind favorites become copy-down candidates, as (title . id)."
- (let ((favs (list (list :kind 'view :title "V1" :id "vid-1")
- (list :kind 'project :title "P" :id "pid")
- (list :kind 'view :title "V2" :id "vid-2"))))
- (should (equal (pearl--linear-view-favorites favs)
- '(("V1" . "vid-1") ("V2" . "vid-2"))))))
+(ert-deftest test-pearl-save-linear-view-locally-saves-current-view ()
+ "When the buffer shows a Linear view, the command offers to save that view
+directly -- without touching favorites or fetching the custom-view list -- and
+hands its name + id to the finisher."
+ (let (finished favorites-called custom-views-called)
+ (cl-letf (((symbol-function 'pearl--require-account-context) (lambda (&rest _) nil))
+ ((symbol-function 'pearl--read-active-source)
+ (lambda () '(:type view :name "Pearl Open Issues" :id "view-1")))
+ ((symbol-function 'pearl--read-yes-no) (lambda (&rest _) t))
+ ((symbol-function 'pearl--progress) (lambda (&rest _) nil))
+ ((symbol-function 'pearl--favorites-async)
+ (lambda (&rest _) (setq favorites-called t)))
+ ((symbol-function 'pearl--custom-views)
+ (lambda (&rest _) (setq custom-views-called t) nil))
+ ((symbol-function 'pearl--customview-filterdata-async)
+ (lambda (view-id cb) (funcall cb (list view-id))))
+ ((symbol-function 'pearl--save-linear-view-locally-finish)
+ (lambda (title view-id _fd) (setq finished (cons title view-id)))))
+ (pearl-save-linear-view-locally)
+ (should (equal finished '("Pearl Open Issues" . "view-1")))
+ (should-not favorites-called)
+ (should-not custom-views-called))))
+
+(ert-deftest test-pearl-save-linear-view-locally-picks-from-all-views ()
+ "With no view in the buffer, the picker lists ALL custom views (not just
+favorites) and hands the chosen view's name + id to the finisher."
+ (let (finished favorites-called)
+ (cl-letf (((symbol-function 'pearl--require-account-context) (lambda (&rest _) nil))
+ ((symbol-function 'pearl--read-active-source) (lambda () nil))
+ ((symbol-function 'pearl--progress) (lambda (&rest _) nil))
+ ((symbol-function 'pearl--favorites-async)
+ (lambda (&rest _) (setq favorites-called t)))
+ ((symbol-function 'pearl--custom-views)
+ (lambda (&rest _)
+ '(((id . "v1") (name . "Pearl Open Issues"))
+ ((id . "v2") (name . "My bugs")))))
+ ((symbol-function 'completing-read) (lambda (&rest _) "My bugs"))
+ ((symbol-function 'pearl--customview-filterdata-async)
+ (lambda (view-id cb) (funcall cb (list view-id))))
+ ((symbol-function 'pearl--save-linear-view-locally-finish)
+ (lambda (title view-id _fd) (setq finished (cons title view-id)))))
+ (pearl-save-linear-view-locally)
+ (should (equal finished '("My bugs" . "v2")))
+ (should-not favorites-called))))
+
+(ert-deftest test-pearl-save-linear-view-locally-declined-current-falls-to-picker ()
+ "Declining the current-view offer falls through to the all-views picker."
+ (let (finished)
+ (cl-letf (((symbol-function 'pearl--require-account-context) (lambda (&rest _) nil))
+ ((symbol-function 'pearl--read-active-source)
+ (lambda () '(:type view :name "Current" :id "cur")))
+ ((symbol-function 'pearl--read-yes-no) (lambda (&rest _) nil))
+ ((symbol-function 'pearl--progress) (lambda (&rest _) nil))
+ ((symbol-function 'pearl--custom-views)
+ (lambda (&rest _) '(((id . "v9") (name . "Picked")))))
+ ((symbol-function 'completing-read) (lambda (&rest _) "Picked"))
+ ((symbol-function 'pearl--customview-filterdata-async)
+ (lambda (view-id cb) (funcall cb (list view-id))))
+ ((symbol-function 'pearl--save-linear-view-locally-finish)
+ (lambda (title view-id _fd) (setq finished (cons title view-id)))))
+ (pearl-save-linear-view-locally)
+ (should (equal finished '("Picked" . "v9"))))))
+
+(ert-deftest test-pearl-save-linear-view-locally-no-views-errors ()
+ "With no current view and no custom views, the command errors rather than
+silently saving nothing."
+ (cl-letf (((symbol-function 'pearl--require-account-context) (lambda (&rest _) nil))
+ ((symbol-function 'pearl--read-active-source) (lambda () nil))
+ ((symbol-function 'pearl--progress) (lambda (&rest _) nil))
+ ((symbol-function 'pearl--custom-views) (lambda (&rest _) nil)))
+ (should-error (pearl-save-linear-view-locally) :type 'user-error)))
(ert-deftest test-pearl-save-linear-view-locally-finish-nil-filterdata-errors ()
"A nil filterData (fetch failure / no filter) errors rather than saving."