diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-28 10:15:54 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-28 10:15:54 -0500 |
| commit | 7939a598bc1f2e045dd4d4fe9e5dc4b7437147d5 (patch) | |
| tree | bcbee40446999e8bf9f403196b401139217e7173 /pearl.el | |
| parent | fc57dcac9a7c972fa59dbd0aded06ae0725000ca (diff) | |
| download | pearl-7939a598bc1f2e045dd4d4fe9e5dc4b7437147d5.tar.gz pearl-7939a598bc1f2e045dd4d4fe9e5dc4b7437147d5.zip | |
fix(view-sync): refuse sync of a saved query whose filter is empty
A saved query with no filter constraints compiled to nil via pearl--build-issue-filter. The sync command then passed (cons "filterData" nil) to customViewCreate, which JSON-encoded as filterData=null. Linear's CustomViewCreateInput.filterData expects a JSON object, so the API rejected with an opaque error message after the user had already clicked through the scope picker.
I added an early refusal in pearl-sync-saved-query-to-linear: when filter-data is nil, signal a user-error naming what's wrong before the scope prompt fires. The message tells the user to add a constraint and re-sync rather than leaving them to puzzle out an opaque API rejection.
I restructured the let* nesting to put the empty-filter check between the bindings (where filter-data is computed) and the scope prompt (where the user is asked to pick a destination). The scope binding moved out of the outer let* into an inner let so it doesn't evaluate until after the filter-data guard runs.
Two tests cover the guard. The first-time-sync test (no :linear-view-id on the entry) asserts neither the scope prompt nor customViewCreate fires. The re-sync test (entry has :linear-view-id) asserts customViewUpdate doesn't fire either. The re-sync test guards against a future refactor accidentally hoisting the check into the create-only branch. All 663 ert tests pass. make compile and make lint are clean.
Diffstat (limited to 'pearl.el')
| -rw-r--r-- | pearl.el | 63 |
1 files changed, 36 insertions, 27 deletions
@@ -3953,32 +3953,41 @@ the user can re-sync with Replace to reconcile." ;; silently flip a personal view to team-shared. (existing-shared (eq (plist-get spec :linear-view-shared) t)) (filter-data (pearl--build-issue-filter filter-plist)) - (target-name name) - (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 - (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)))))) - (cond + (target-name name)) + ;; Linear's CustomViewCreateInput.filterData expects a JSON object. An + ;; empty filter plist compiles to nil, which JSON-encodes as `null' and + ;; the API rejects with an opaque error. Refuse early -- before we + ;; prompt the user for scope -- so they see what's wrong instead of + ;; clicking through the scope picker only to hit "Linear refused". + (unless filter-data + (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 + (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)))))) + (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). @@ -4011,7 +4020,7 @@ the user can re-sync with Replace to reconcile." ;; the existing view directly). (t (pearl--sync-saved-query-do-update - name existing-view-id team-id shared filter-data scope-label))))))) + 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) |
