aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-28 10:24:21 -0500
committerCraig Jennings <c@cjennings.net>2026-05-28 10:24:21 -0500
commite5a07739c5b58b041bcb7192eae35a2c4d98bce3 (patch)
treea424567f5c6acef8ce7d0b2494c3cb250abf8e08 /pearl.el
parent7939a598bc1f2e045dd4d4fe9e5dc4b7437147d5 (diff)
downloadpearl-e5a07739c5b58b041bcb7192eae35a2c4d98bce3.tar.gz
pearl-e5a07739c5b58b041bcb7192eae35a2c4d98bce3.zip
refactor(view-sync): pick-scope returns (display . plist), drops rassoc lookup
pearl-sync-saved-query-to-linear used to rebuild the entire scope-candidates list a second time (full sort + 2N row allocations) just to rassoc the chosen scope plist back to its display string for the success message. The picker had thrown away the display string the user picked, so the caller had no choice but to re-derive it. I changed pearl--sync-saved-query-pick-scope to return (DISPLAY . PLIST) instead of just PLIST. The caller now reads scope-label from (car scope-pair) and scope from (cdr scope-pair). The rassoc is gone, and so is the second pearl--sync-scope-candidates call. To keep the re-sync path label-consistent with the picker (so a "Synced X (Team: Engineering, visible to the team)" message matches what the picker would have shown for the equivalent first-time sync), I extracted pearl--sync-scope-label as the single source of truth for the label format. The picker, the re-sync branch, and any future caller that needs to describe a scope all go through the helper. Personal scope always renders "[ Personal, only I see it ]" regardless of the shared flag (a Personal + shared combination is meaningless on Linear). Team scopes render "[ Team: NAME, visible to the team ]" or "[ Team: NAME, only I see it ]" per the flag. I added five tests covering the new behavior: scope-label for personal (ignores shared) and team (both shared and private), pick-scope returns the team pair, pick-scope returns the personal pair, pick-scope returns nil on cancel. All 668 ert tests pass. make compile and make lint are clean.
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el145
1 files changed, 83 insertions, 62 deletions
diff --git a/pearl.el b/pearl.el
index ad88d4d..a8192b9 100644
--- a/pearl.el
+++ b/pearl.el
@@ -3738,6 +3738,18 @@ stored when given."
;;; Saved-query -> Linear view sync (see docs/saved-query-sync-spec.org)
+(defun pearl--sync-scope-label (team-name shared)
+ "Return the display label for the scope identified by TEAM-NAME and SHARED.
+TEAM-NAME is either \"Personal\" or a real team name. SHARED is t
+\(visible to the team) or nil (only the viewer). The single source of
+truth so the picker, the re-sync path, and the success message all
+agree byte-for-byte."
+ (if (equal team-name "Personal")
+ "[ Personal, only I see it ]"
+ (format "[ Team: %s, %s ]"
+ team-name
+ (if shared "visible to the team" "only I see it"))))
+
(defun pearl--sync-scope-candidates (teams filter-team-key)
"Build the (DISPLAY . PLIST) candidate list for the scope-and-visibility prompt.
TEAMS is the team alist list returned by `pearl--all-teams' (each carries
@@ -3760,7 +3772,7 @@ The meaningless `Personal + shared' combination is absent."
(equal filter-team-key (cdr (assoc 'key tm))))
sorted-teams)))
(personal-row
- (cons "[ Personal, only I see it ]"
+ (cons (pearl--sync-scope-label "Personal" nil)
(list :team-id nil :team-name "Personal" :shared nil)))
(team-rows
(cl-mapcan
@@ -3768,16 +3780,15 @@ The meaningless `Personal + shared' combination is absent."
(let ((name (cdr (assoc 'name tm)))
(id (cdr (assoc 'id tm))))
(list
- (cons (format "[ Team: %s, visible to the team ]" name)
+ (cons (pearl--sync-scope-label name t)
(list :team-id id :team-name name :shared t))
- (cons (format "[ Team: %s, only I see it ]" name)
+ (cons (pearl--sync-scope-label name nil)
(list :team-id id :team-name name :shared nil)))))
sorted-teams))
(all-rows (cons personal-row team-rows))
(default-display
(if filter-team
- (format "[ Team: %s, visible to the team ]"
- (cdr (assoc 'name filter-team)))
+ (pearl--sync-scope-label (cdr (assoc 'name filter-team)) t)
(car personal-row)))
(default-row (cl-find default-display all-rows
:key #'car :test #'string=)))
@@ -3873,9 +3884,11 @@ local link couldn't be saved."
(defun pearl--sync-saved-query-pick-scope (filter-plist)
"Prompt the user for the destination scope of a sync-up.
FILTER-PLIST is the saved query's authoring filter (used to derive the
-default candidate). Returns the chosen scope plist
-`(:team-id ID-OR-NIL :team-name NAME :shared BOOL)', or nil when the
-user cancels via the cancel sentinel."
+default candidate). Returns `(DISPLAY . PLIST)', or nil when the user
+cancels via the cancel sentinel. DISPLAY is the candidate string the
+user picked; PLIST is `(:team-id ID-OR-NIL :team-name NAME :shared
+BOOL)'. Returning both halves saves the caller from rebuilding the
+candidate list to look the display string back up via rassoc."
(let* ((teams (pearl--all-teams))
(cands (pearl--sync-scope-candidates teams (plist-get filter-plist :team)))
(displays (mapcar #'car cands))
@@ -3885,7 +3898,7 @@ user cancels via the cancel sentinel."
(pearl--with-sentinel pearl--filter-cancel displays))
nil t)))
(unless (pearl--filter-sentinel-value-p choice)
- (cdr (assoc choice cands)))))
+ (assoc choice cands))))
(defun pearl--sync-saved-query-collision-action (name)
"Prompt the user when a view named NAME already exists in the chosen scope.
@@ -3963,64 +3976,72 @@ the user can re-sync with Replace to reconcile."
(user-error
"Saved query %s has no filter constraints; Linear views need at least one constraint -- add one and re-sync"
name))
- (let ((scope (if existing-view-id
- ;; Re-sync uses the stored scope.
- (list :team-id existing-team-id
- :team-name (or (and existing-team-id
- (cdr (assoc 'name
- (seq-find
- (lambda (tm)
- (equal existing-team-id
- (cdr (assoc 'id tm))))
- (pearl--all-teams)))))
- "Personal")
- :shared existing-shared)
- (pearl--sync-saved-query-pick-scope filter-plist))))
- (unless scope
+ (let ((scope-pair
+ (if existing-view-id
+ ;; Re-sync: build the scope pair from stored metadata,
+ ;; resolving the team name from id (defaults to "Personal"
+ ;; when team-id is nil OR the team is no longer in the
+ ;; cache). The label comes from the same helper the
+ ;; picker uses so the success message matches what the
+ ;; user would have seen on first sync.
+ (let* ((team-name (or (and existing-team-id
+ (cdr (assoc 'name
+ (seq-find
+ (lambda (tm)
+ (equal existing-team-id
+ (cdr (assoc 'id tm))))
+ (pearl--all-teams)))))
+ "Personal")))
+ (cons (pearl--sync-scope-label team-name existing-shared)
+ (list :team-id existing-team-id
+ :team-name team-name
+ :shared existing-shared)))
+ ;; First-time sync: pick-scope returns (display . plist) or
+ ;; nil (cancel).
+ (pearl--sync-saved-query-pick-scope filter-plist))))
+ (unless scope-pair
(user-error "Cancelled; no saved query synced"))
;; Validate the filter at the boundary so a malformed entry surfaces
;; before the API call.
(pearl--validate-issue-filter filter-plist)
- (let* ((team-id (plist-get scope :team-id))
- (shared (plist-get scope :shared))
- (scope-label (car (rassoc scope
- (pearl--sync-scope-candidates
- (pearl--all-teams)
- (plist-get filter-plist :team))))))
+ (let* ((scope-label (car scope-pair))
+ (scope (cdr scope-pair))
+ (team-id (plist-get scope :team-id))
+ (shared (plist-get scope :shared)))
(cond
- ;; First-time sync: check for a same-name collision in the chosen
- ;; scope and either update-by-id (Replace), re-prompt (Rename), or
- ;; bail (Cancel).
- ((null existing-view-id)
- (let* ((collision (pearl--find-view-by-name-in-scope
- (pearl--custom-views t)
- target-name team-id shared))
- (action (and collision
- (pearl--sync-saved-query-collision-action
- target-name))))
- (pcase action
- ('cancel
- (message "Cancelled; %s left unsynced" target-name))
- ('rename
- (setq target-name
- (read-string (format "New view name (was %s): " name)))
- (when (or (null target-name) (string-empty-p target-name))
- (user-error "Cancelled; no name entered"))
- (pearl--sync-saved-query-do-create
- name target-name team-id shared filter-data scope-label))
- ('replace
- (pearl--sync-saved-query-do-update
- name (cdr (assoc 'id collision))
- team-id shared filter-data scope-label))
- (_
- ;; No collision -- straight create.
- (pearl--sync-saved-query-do-create
- name target-name team-id shared filter-data scope-label)))))
- ;; Re-sync: update by stored id; no collision check (the id targets
- ;; the existing view directly).
- (t
- (pearl--sync-saved-query-do-update
- name existing-view-id team-id shared filter-data scope-label))))))))
+ ;; First-time sync: check for a same-name collision in the chosen
+ ;; scope and either update-by-id (Replace), re-prompt (Rename), or
+ ;; bail (Cancel).
+ ((null existing-view-id)
+ (let* ((collision (pearl--find-view-by-name-in-scope
+ (pearl--custom-views t)
+ target-name team-id shared))
+ (action (and collision
+ (pearl--sync-saved-query-collision-action
+ target-name))))
+ (pcase action
+ ('cancel
+ (message "Cancelled; %s left unsynced" target-name))
+ ('rename
+ (setq target-name
+ (read-string (format "New view name (was %s): " name)))
+ (when (or (null target-name) (string-empty-p target-name))
+ (user-error "Cancelled; no name entered"))
+ (pearl--sync-saved-query-do-create
+ name target-name team-id shared filter-data scope-label))
+ ('replace
+ (pearl--sync-saved-query-do-update
+ name (cdr (assoc 'id collision))
+ team-id shared filter-data scope-label))
+ (_
+ ;; No collision -- straight create.
+ (pearl--sync-saved-query-do-create
+ name target-name team-id shared filter-data scope-label)))))
+ ;; Re-sync: update by stored id; no collision check (the id targets
+ ;; the existing view directly).
+ (t
+ (pearl--sync-saved-query-do-update
+ name existing-view-id team-id shared filter-data scope-label))))))))
(defun pearl--sync-saved-query-do-create (name target-name team-id shared
filter-data scope-label)