diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-28 09:12:41 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-28 09:12:41 -0500 |
| commit | c8b9ad1b72c60f87c15e942f17f7ff1066a41cb2 (patch) | |
| tree | 714d360d2f0dc9e565f04ac67a7bd784650fd2bc /pearl.el | |
| parent | 60a026be803e4fb2e56520dd9e2cc72ba6bfc0e3 (diff) | |
| download | pearl-c8b9ad1b72c60f87c15e942f17f7ff1066a41cb2.tar.gz pearl-c8b9ad1b72c60f87c15e942f17f7ff1066a41cb2.zip | |
feat(view-sync): extend pearl-delete-saved-query with delete-on-Linear prompt
Phase 3 of docs/saved-query-sync-spec.org. pearl-delete-saved-query on a synced entry (one carrying :linear-view-id) now asks a second question after the local-delete confirmation: also delete the linked Linear view? Yes calls customViewDelete, no unlinks only and leaves the Linear view in place. Local-only entries take the unchanged single-prompt path.
If customViewDelete fails on the API side, a fallback yes-or-no prompt asks whether to drop the local entry anyway. Accepting orphans the Linear view, and the success message names the view id explicitly so the user can clean it up by hand. The same view-id-in-message pattern fires on the timeout branch, since a timeout doesn't tell us whether the delete actually completed server-side.
I added pearl--customview-delete-async (the mutation, parallel to the create and update helpers from Phase 2) and pearl--delete-saved-query-local (factors the cl-remove + customize-save-variable that both the local-only and the synced-unlink branches need). The new pearl--delete-saved-query-do-linear-delete carries the API path so the top-level command stays readable.
I added 7 tests in test-pearl-saved-query-sync.el covering the customViewDelete success and failure parses, the synced yes/yes path (API call fires + local removed), the synced yes/no path (unlink only, no API call), the API-failure-then-delete-anyway path (orphan message names the view id), the API-failure-then-keep path (asserts the API call fired so a refactor can't silently route through the unlink branch), and the local-only entry unchanged path. The existing pearl-delete-saved-query tests in test-pearl-adhoc.el stay green. Their fixtures don't carry :linear-view-id, so they take the local-only branch. 638 tests total green. make compile and make lint clean.
Diffstat (limited to 'pearl.el')
| -rw-r--r-- | pearl.el | 106 |
1 files changed, 93 insertions, 13 deletions
@@ -1182,6 +1182,31 @@ query takes. CALLBACK is invoked with a plist :error (format "%s" error)))))) (pearl--graphql-request-async query variables success-fn error-fn))) +(defun pearl--customview-delete-async (view-id callback) + "Delete a Custom View on Linear by VIEW-ID. +CALLBACK is invoked with a plist +`(:success BOOL :error STRING-OR-NIL)'. Linear's `customViewDelete' is +a soft delete that drops the view into the workspace trash (recoverable +within Linear's standard window)." + (let* ((query "mutation CustomViewDelete($id: String!) { + customViewDelete(id: $id) { + success + } +}") + (variables `(("id" . ,view-id))) + (success-fn + (lambda (response) + (let* ((cvd (cdr (assoc 'customViewDelete + (cdr (assoc 'data response))))) + (success (eq t (cdr (assoc 'success cvd))))) + (funcall callback (list :success success))))) + (error-fn + (lambda (error _response _data) + (funcall callback + (list :success nil + :error (format "%s" error)))))) + (pearl--graphql-request-async query variables success-fn error-fn))) + (defun pearl--customview-update-async (view-id update-input callback) "Update a Custom View on Linear by VIEW-ID. UPDATE-INPUT is an alist of `CustomViewUpdateInput' fields -- any subset @@ -3560,13 +3585,27 @@ an id), or `pearl--filter-any' (any assignee, no scoping)." :test #'string=)))) open state project labels assignee))) +(defun pearl--delete-saved-query-local (name) + "Remove NAME from `pearl-saved-queries' and persist via Customize. +The unconditional persistence path shared by the local-only delete and +the synced-entry unlink branch." + (setq pearl-saved-queries + (cl-remove name pearl-saved-queries :key #'car :test #'string=)) + (customize-save-variable 'pearl-saved-queries pearl-saved-queries)) + ;;;###autoload (defun pearl-delete-saved-query (name) "Delete the saved query NAME from `pearl-saved-queries' after confirmation. Interactively, completes over the configured query names with -`pearl--filter-cancel' at the top. Local-only: removes the entry from -the customized list and persists via `customize-save-variable'. Linear -is untouched (saved queries are local; syncing them up is a separate spec)." +`pearl--filter-cancel' at the top. Local-only entries are removed from +the customized list and persisted via `customize-save-variable'; entries +that carry a `:linear-view-id' (i.e. were synced up to Linear) trigger a +second yes-or-no prompt for whether to also delete the linked Linear +view (=customViewDelete=). Picking NO at the second prompt unlinks the +entry but leaves the Linear view in place; picking YES calls +=customViewDelete= and, on API failure, asks whether to drop the local +entry anyway (which leaves the Linear view orphaned -- the message names +the id so it can be deleted by hand)." (interactive (list (if (null pearl-saved-queries) (user-error "No saved queries to delete") @@ -3581,12 +3620,53 @@ is untouched (saved queries are local; syncing them up is a separate spec)." (message "Cancelled; no saved query deleted")) ((not (assoc name pearl-saved-queries)) (user-error "No saved query named %s" name)) - ((yes-or-no-p (format "Delete saved query %s? " name)) - (setq pearl-saved-queries - (cl-remove name pearl-saved-queries :key #'car :test #'string=)) - (customize-save-variable 'pearl-saved-queries pearl-saved-queries) - (message "Deleted saved query %s" name)) - (t (message "Kept saved query %s" name)))) + ((not (yes-or-no-p (format "Delete saved query %s? " name))) + (message "Kept saved query %s" name)) + (t + (let ((view-id (plist-get (cdr (assoc name pearl-saved-queries)) + :linear-view-id))) + (cond + ;; Local-only entry: unchanged behavior. + ((null view-id) + (pearl--delete-saved-query-local name) + (message "Deleted saved query %s" name)) + ;; Synced entry: ask whether to also drop the Linear view. + ((yes-or-no-p + (format "Also delete the linked Linear view (\"%s\")? " name)) + (pearl--delete-saved-query-do-linear-delete name view-id)) + (t + ;; Unlink only: drop the local entry, leave the Linear view alone. + (pearl--delete-saved-query-local name) + (message "Deleted saved query %s; Linear view kept (id %s)" + name view-id))))))) + +(defun pearl--delete-saved-query-do-linear-delete (name view-id) + "Call customViewDelete on VIEW-ID and remove NAME locally on success. +On API failure, ask whether to drop the local entry anyway; if accepted, +the message names the orphan view id so the user can delete it in Linear +by hand." + (pearl--progress "Deleting Linear view for %s..." name) + (let* ((result (pearl--sync-saved-query-await + #'pearl--customview-delete-async view-id)) + (success (plist-get result :success))) + (cond + (success + (pearl--delete-saved-query-local name) + (message "Deleted saved query %s and removed view from Linear" name)) + ((plist-get result :timeout) + (message + "Delete of Linear view for %s timed out after %ds; saved query left in place -- the delete may still have completed (view id %s) -- re-run after the network recovers." + name pearl-request-timeout view-id)) + ((yes-or-no-p + (format + "Linear delete failed (%s). Drop the local entry anyway? " + (or (plist-get result :error) "unknown error"))) + (pearl--delete-saved-query-local name) + (message + "Deleted saved query %s; Linear view is orphaned (id %s, delete manually)" + name view-id)) + (t + (message "Kept saved query %s; Linear view also untouched" name))))) (defun pearl--save-query (name filter-plist &optional sort order) "Save FILTER-PLIST as the saved query NAME, replacing any entry of that NAME. @@ -3753,10 +3833,10 @@ Returns one of the symbols `replace', `rename', `cancel'." (defun pearl--sync-saved-query-await (async-fn &rest args) "Run the async helper ASYNC-FN with ARGS, busy-waiting for its callback. -ASYNC-FN is one of `pearl--customview-create-async' / -`pearl--customview-update-async' -- both end by invoking a callback -appended to ARGS with a result plist. Returns that plist on completion, -or `(:success nil :timeout t)' if the wait elapsed without the callback +ASYNC-FN is one of the `pearl--customview-*-async' helpers (create, +update, or delete) -- each ends by invoking a callback appended to ARGS +with a result plist. Returns that plist on completion, or +`(:success nil :timeout t)' if the wait elapsed without the callback firing -- callers can distinguish a timeout from a Linear-side rejection so the user isn't told the API refused something that never reached it." (let ((done nil) (result nil)) |
