aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el101
1 files changed, 56 insertions, 45 deletions
diff --git a/pearl.el b/pearl.el
index 8d2e491..d984876 100644
--- a/pearl.el
+++ b/pearl.el
@@ -3702,36 +3702,42 @@ user rather than silently mis-labelling)."
(defun pearl--pick-source-candidates (favorites saved-queries &optional teams)
"Build the picker's candidate alist `((DISPLAY . SOURCE-OR-ACTION) ...)'.
FAVORITES is a list of normalized favorite plists; SAVED-QUERIES is the
-`pearl-local-views' alist; TEAMS is the team alist list (see
-`pearl--all-teams') used to resolve the scope name of any saved-query
-entry carrying `:linear-view-id'. Favorites come first in ascending
-`:sort-order'; local views follow alphabetically.
+`pearl-local-views' alist; TEAMS resolves the scope name of a tracked local
+view. Favorites come first in ascending `:sort-order'; local views follow
+alphabetically.
Each DISPLAY is one of:
- `[KIND] TITLE' for favorites
- `[saved] NAME' for a local-only local view
- `[saved \\u2192 SCOPE] NAME' for a synced local view
-where SCOPE is the team name resolved from `:linear-view-team-id'
-against TEAMS, or \"Personal\" when there's no team scope, or \"?\" when
-the team id is stale (no longer in TEAMS) or TEAMS is nil.
-
-A synced entry's source plist is `(:type view :name NAME :id LINEAR-VIEW-ID)'
-so dispatch flows through the existing view branch and runs
-`pearl--query-view-async' against the synced view -- the synced query
-lives on Linear now and any filter drift there is honored at refresh.
-Local-only entries keep the `:type filter' shape and dispatch through
-`pearl--query-issues-async' against the stored authoring filter.
-
-Display strings are made unique by appending ` (#N)' on a true collision
--- dispatch reads SOURCE-OR-ACTION, not the display string."
- (let ((fav-sorted (sort (copy-sequence favorites)
- (lambda (a b)
- (< (or (plist-get a :sort-order) 0.0)
- (or (plist-get b :sort-order) 0.0)))))
- (saved-sorted (sort (copy-sequence saved-queries)
- (lambda (a b) (string< (car a) (car b)))))
- (alist nil)
- (seen (make-hash-table :test 'equal)))
+ `[KIND] TITLE' for a non-view favorite (opens in browser)
+ `[linear] TITLE' for a Linear view favorite
+ `[local] NAME' for a local-only local view
+ `[local \\u2192 Linear:SCOPE] NAME' for a published (tracked) local view
+where SCOPE is the team name resolved from `:linear-view-team-id' against
+TEAMS, \"Personal\" when there's no team scope, or \"?\" when the team id is
+stale or TEAMS is nil.
+
+Dispatch follows the source-of-truth rule (decision 16): EVERY local view --
+tracked or not -- dispatches as `:type filter' and runs its LOCAL authoring
+filter via `pearl--query-issues-async', never the Linear mirror. A tracked
+entry carries its `:linear-view-url' as `:url' so the rendered buffer can
+still open the mirror. A native Linear view favorite dispatches as
+`:type view'.
+
+Cross-store dedup: a Linear view favorite whose `customView' id matches a
+tracked local view's `:linear-view-id' is dropped, so the editable local
+entry is the one shown. Local and Linear views that merely share a NAME stay
+distinct (their labels differ). Display strings are made unique by appending
+` (#N)' on a true collision."
+ (let* ((tracked-ids
+ (delq nil (mapcar (lambda (sq) (plist-get (cdr sq) :linear-view-id))
+ saved-queries)))
+ (fav-sorted (sort (copy-sequence favorites)
+ (lambda (a b)
+ (< (or (plist-get a :sort-order) 0.0)
+ (or (plist-get b :sort-order) 0.0)))))
+ (saved-sorted (sort (copy-sequence saved-queries)
+ (lambda (a b) (string< (car a) (car b)))))
+ (alist nil)
+ (seen (make-hash-table :test 'equal)))
(cl-labels
((push-unique
(display source)
@@ -3744,29 +3750,34 @@ Display strings are made unique by appending ` (#N)' on a true collision
(dolist (fav fav-sorted)
(let* ((kind (plist-get fav :kind))
(title (plist-get fav :title))
- (display (format "[%s] %s" (symbol-name kind) title))
- (source (pearl--favorite->source fav)))
- (push-unique display source)))
+ (source (pearl--favorite->source fav))
+ (fav-view-id (and (eq (plist-get source :type) 'view)
+ (plist-get source :id))))
+ ;; Cross-store dedup: drop a Linear view favorite already mirrored by
+ ;; a tracked local view, so the editable local entry is what's shown.
+ (unless (and fav-view-id (member fav-view-id tracked-ids))
+ (push-unique
+ (if (eq kind 'view)
+ (format "[linear] %s" title)
+ (format "[%s] %s" (symbol-name kind) title))
+ source))))
(dolist (sq saved-sorted)
(let* ((name (car sq))
(spec (cdr sq))
(view-id (plist-get spec :linear-view-id))
(scope-label (pearl--saved-query-scope-label spec teams))
(display (if view-id
- (format "[saved → %s] %s" scope-label name)
- (format "[saved] %s" name)))
- (source (if view-id
- ;; Include :url when stored so `pearl-open-current-
- ;; view-in-linear' works on synced sources. Entries
- ;; synced before :linear-view-url tracking landed
- ;; carry no URL; a re-sync repopulates it.
- (append (list :type 'view :name name :id view-id)
- (let ((u (plist-get spec :linear-view-url)))
- (when u (list :url u))))
- (list :type 'filter :name name
- :filter (plist-get spec :filter)
- :sort (plist-get spec :sort)
- :order (plist-get spec :order)))))
+ (format "[local → Linear:%s] %s" scope-label name)
+ (format "[local] %s" name)))
+ ;; Decision 16: every local view runs its LOCAL filter, tracked
+ ;; or not. :url carries the mirror so open-in-linear still works.
+ (source (append
+ (list :type 'filter :name name
+ :filter (plist-get spec :filter)
+ :sort (plist-get spec :sort)
+ :order (plist-get spec :order))
+ (let ((u (plist-get spec :linear-view-url)))
+ (when u (list :url u))))))
(push-unique display source))))
(nreverse alist)))