diff options
Diffstat (limited to 'pearl.el')
| -rw-r--r-- | pearl.el | 137 |
1 files changed, 105 insertions, 32 deletions
@@ -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." |
