aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el137
1 files changed, 105 insertions, 32 deletions
diff --git a/pearl.el b/pearl.el
index cb65ab3..724664b 100644
--- a/pearl.el
+++ b/pearl.el
@@ -1158,7 +1158,7 @@ query takes. CALLBACK is invoked with a plist
(let* ((query "mutation CustomViewCreate($input: CustomViewCreateInput!) {
customViewCreate(input: $input) {
success
- customView { id name shared team { id key name } }
+ customView { id name shared url team { id key name } }
}
}")
(input (append (list (cons "name" name)
@@ -1215,7 +1215,7 @@ plist `(:success BOOL :view ALIST-OR-NIL :error STRING-OR-NIL)'."
(let* ((query "mutation CustomViewUpdate($id: String!, $input: CustomViewUpdateInput!) {
customViewUpdate(id: $id, input: $input) {
success
- customView { id name shared team { id key name } }
+ customView { id name shared url team { id key name } }
}
}")
(variables `(("id" . ,view-id)
@@ -3405,13 +3405,50 @@ active file with the query recorded as the source."
;;; pearl-pick-source -- the unified source picker (favorites + saved queries)
-(defun pearl--pick-source-candidates (favorites saved-queries)
+(defun pearl--saved-query-scope-label (spec teams)
+ "Return the scope display label for SPEC (a saved-query plist).
+Returns nil for a local-only entry (no `:linear-view-id'), \"Personal\"
+for a synced entry with no team scope, the team name when SPEC's
+`:linear-view-team-id' resolves against TEAMS, or \"?\" when the team
+id is set but missing from TEAMS (which surfaces a stale link to the
+user rather than silently mis-labelling)."
+ (let ((view-id (plist-get spec :linear-view-id))
+ (team-id (plist-get spec :linear-view-team-id)))
+ (when view-id
+ (cond ((null team-id) "Personal")
+ ((null teams) "?")
+ (t (or (cdr (assoc 'name
+ (seq-find
+ (lambda (tm)
+ (equal team-id (cdr (assoc 'id tm))))
+ teams)))
+ "?"))))))
+
+(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-saved-queries' alist. Favorites come first in ascending `:sort-order';
-saved queries follow alphabetically. Each DISPLAY is `[KIND] TITLE' and is
-made unique by appending ` (#N)' on a true collision -- dispatch reads
-SOURCE-OR-ACTION, not the display string."
+`pearl-saved-queries' 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'; saved queries follow alphabetically.
+
+Each DISPLAY is one of:
+ `[KIND] TITLE' for favorites
+ `[saved] NAME' for a local-only saved query
+ `[saved \\u2192 SCOPE] NAME' for a synced saved query
+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)
@@ -3438,11 +3475,23 @@ SOURCE-OR-ACTION, not the display string."
(dolist (sq saved-sorted)
(let* ((name (car sq))
(spec (cdr sq))
- (display (format "[saved] %s" name))
- (source (list :type 'filter :name name
- :filter (plist-get spec :filter)
- :sort (plist-get spec :sort)
- :order (plist-get spec :order))))
+ (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)))))
(push-unique display source))))
(nreverse alist)))
@@ -3473,7 +3522,14 @@ missing setup."
(let ((saved pearl-saved-queries))
(pearl--favorites-async
(lambda (favorites)
- (let ((cands (pearl--pick-source-candidates favorites saved)))
+ ;; Only fetch teams when at least one saved query is synced -- scope
+ ;; resolution is the only thing teams data feeds, and an all-local
+ ;; configuration has no scopes to resolve.
+ (let* ((teams (and (cl-some (lambda (sq)
+ (plist-get (cdr sq) :linear-view-id))
+ saved)
+ (pearl--all-teams)))
+ (cands (pearl--pick-source-candidates favorites saved teams)))
(unless cands
(user-error
"No favorites or saved queries -- star views in Linear, or set `pearl-saved-queries'"))
@@ -3747,14 +3803,20 @@ Returns the matching view alist or nil."
(t (or (eq view-shared :json-false) (null view-shared)))))))
views)))
-(defun pearl--save-query-mark-synced (name team-id shared view-id)
+(defun pearl--save-query-mark-synced (name team-id shared view-id &optional view-url)
"Persist the sync metadata for saved query NAME.
TEAM-ID is the team UUID or nil (personal scope). SHARED is t or nil.
-VIEW-ID is the Linear custom view UUID returned by the API. Adds or
-overwrites `:linear-view-id', `:linear-view-team-id', `:linear-view-shared',
-`:linear-view-synced-at' on the entry, then persists `pearl-saved-queries'
-via Customize. Signals `user-error' when NAME isn't in
-`pearl-saved-queries'."
+VIEW-ID is the Linear custom view UUID returned by the API. VIEW-URL,
+when provided, is the view's Linear web URL so
+`pearl-open-current-view-in-linear' can open the rendered buffer's
+source view in the browser; omit on a re-sync where the URL is
+unavailable to leave the prior `:linear-view-url' untouched.
+
+Adds or overwrites `:linear-view-id', `:linear-view-team-id',
+`:linear-view-shared', `:linear-view-synced-at', and (when VIEW-URL is
+non-nil) `:linear-view-url' on the entry, then persists
+`pearl-saved-queries' via Customize. Signals `user-error' when NAME
+isn't in `pearl-saved-queries'."
(let ((entry (assoc name pearl-saved-queries)))
(unless entry
(user-error "No saved query named %s" name))
@@ -3762,6 +3824,8 @@ via Customize. Signals `user-error' when NAME isn't in
(filter (plist-get spec :filter))
(sort (plist-get spec :sort))
(order (plist-get spec :order))
+ (existing-url (plist-get spec :linear-view-url))
+ (effective-url (or view-url existing-url))
(synced-at (format-time-string "%Y-%m-%dT%H:%M:%SZ" nil t))
(new-spec (append (list :filter filter)
(when sort (list :sort sort))
@@ -3769,26 +3833,32 @@ via Customize. Signals `user-error' when NAME isn't in
(list :linear-view-id view-id
:linear-view-team-id team-id
:linear-view-shared shared
- :linear-view-synced-at synced-at))))
+ :linear-view-synced-at synced-at)
+ (when effective-url
+ (list :linear-view-url effective-url)))))
(setq pearl-saved-queries
(cons (cons name new-spec)
(assoc-delete-all name (copy-sequence pearl-saved-queries))))
(customize-save-variable 'pearl-saved-queries pearl-saved-queries))))
(defun pearl--sync-record-or-orphan-error (name team-id shared view-id verb
- &optional scope-label)
+ &optional scope-label view-url)
"Persist sync metadata for NAME, or report an orphan view on persist failure.
TEAM-ID, SHARED, VIEW-ID match the just-completed sync (see
`pearl--save-query-mark-synced'). VERB is the past-tense action word
shown in the success message (\"Synced\" for create, \"Updated\" for
-update); SCOPE-LABEL is the optional scope-and-visibility display string
-appended in parens. Returns t on success, nil on failure. On failure
-the message names VIEW-ID explicitly so the user can find the orphan in
-Linear or re-sync with Replace to reconcile -- the API call already
-succeeded; only the local link couldn't be saved."
+update). SCOPE-LABEL is the optional scope-and-visibility display
+string appended in brackets. VIEW-URL, when provided, is the view's
+Linear web URL stored alongside the rest of the sync metadata so
+`pearl-open-current-view-in-linear' can dispatch to it.
+
+Returns t on success, nil on failure. On failure the message names
+VIEW-ID explicitly so the user can find the orphan in Linear or re-sync
+with Replace to reconcile -- the API call already succeeded; only the
+local link couldn't be saved."
(condition-case err
(progn
- (pearl--save-query-mark-synced name team-id shared view-id)
+ (pearl--save-query-mark-synced name team-id shared view-id view-url)
(message "%s %s to Linear (view id %s)%s"
verb name view-id
(if scope-label (format " [%s]" scope-label) ""))
@@ -3955,12 +4025,13 @@ scope-and-visibility display string for the success message."
#'pearl--customview-create-async
target-name team-id shared filter-data))
(success (plist-get result :success))
- (view-id (and success
- (cdr (assoc 'id (plist-get result :view))))))
+ (view (and success (plist-get result :view)))
+ (view-id (and view (cdr (assoc 'id view))))
+ (view-url (and view (cdr (assoc 'url view)))))
(cond
((and success view-id)
(pearl--sync-record-or-orphan-error
- name team-id shared view-id "Synced" scope-label))
+ name team-id shared view-id "Synced" scope-label view-url))
((plist-get result :timeout)
(message
"Sync of %s timed out after %ds; the create may still complete on Linear -- re-run and pick Replace to reconcile if a duplicate appears."
@@ -3982,11 +4053,13 @@ the scope-and-visibility display string for the success message."
(result (pearl--sync-saved-query-await
#'pearl--customview-update-async
view-id update-input))
- (success (plist-get result :success)))
+ (success (plist-get result :success))
+ (view (and success (plist-get result :view)))
+ (view-url (and view (cdr (assoc 'url view)))))
(cond
(success
(pearl--sync-record-or-orphan-error
- name team-id shared view-id "Updated" scope-label))
+ name team-id shared view-id "Updated" scope-label view-url))
((plist-get result :timeout)
(message
"Update of Linear view for %s timed out after %ds; the update may still apply -- refresh in Linear to confirm."