From 8bae955521cdf3e2f8d5ff015b3ae36b5b8a75e8 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 1 Jun 2026 23:40:11 -0500 Subject: feat(filter): :state accepts a list of state names I made :state polymorphic, mirroring :state-type. A string still compiles to state.name.eq. A list now compiles to state.name.in, so (:state ("Todo" "In Review")) matches issues in either state. The reverse-compile inverts state.name.in back to a scalar (one name) or a list (several), so copy-down no longer refuses a Linear view filtering on a set of named states. The validator accepts a string or a list of non-empty names, the same shape it already enforces for :labels, and a list-valued :state keeps its precedence over :state-type and :open. Phase 1 of docs/multi-state-filter-spec.org. Suite, compile, and lint green. --- pearl.el | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) (limited to 'pearl.el') diff --git a/pearl.el b/pearl.el index 0ecc03a..9aebcef 100644 --- a/pearl.el +++ b/pearl.el @@ -849,13 +849,15 @@ PRIORITY is an integer 0-4 or a symbol in `pearl--priority-symbols'." (defun pearl--compile-state-filter (plist) "Return the state sub-filter alist for PLIST, or nil when no state key is set. -An explicit `:state' (name) or `:state-type' (one type or a list) takes -precedence over `:open', the broad not-closed predicate." +An explicit `:state' (a name or a list of names) or `:state-type' (one type or +a list) takes precedence over `:open', the broad not-closed predicate." (let ((state (plist-get plist :state)) (state-type (plist-get plist :state-type)) (open (plist-get plist :open))) (cond - (state (list (cons "name" (pearl--eq state)))) + (state (list (cons "name" (if (listp state) + (pearl--in state) + (pearl--eq state))))) (state-type (list (cons "type" (pearl--in (if (listp state-type) state-type @@ -984,8 +986,18 @@ multi-value filter would change which issues match." ((or (assq 'and node) (assq 'or node)) (pearl--rc-refuse "state uses OR logic Pearl can't represent")) ((assq 'name node) - (let ((r (pearl--rc-scalar (cdr (assq 'name node)) "state.name"))) - (if (eq (car r) 'ok) (cons 'ok (list :state (cdr r))) r))) + ;; :state is plural-capable (a name or a list), like :state-type: + ;; name.eq / name.in[one] -> scalar, name.in[many] -> list. + (let ((op (cdr (assq 'name node)))) + (cond + ((assq 'eq op) (cons 'ok (list :state (cdr (assq 'eq op))))) + ((assq 'in op) + (let ((vals (append (cdr (assq 'in op)) nil))) + (cond + ((null vals) (pearl--rc-refuse "state.name has an empty `in'")) + ((= 1 (length vals)) (cons 'ok (list :state (car vals)))) + (t (cons 'ok (list :state vals)))))) + (t (pearl--rc-refuse "state.name uses an operator Pearl can't represent"))))) ((assq 'type node) (let ((op (cdr (assq 'type node)))) (cond @@ -1126,7 +1138,7 @@ needs team context) is resolved upstream, not here." (let ((order (plist-get plist :order))) (when (and order (not (memq order '(asc desc)))) (user-error ":order must be `asc' or `desc', got %S" order))) - (dolist (key '(:state :project :team :cycle :assignee-id :label-id)) + (dolist (key '(:project :team :cycle :assignee-id :label-id)) (let ((val (plist-get plist key))) (when (and (stringp val) (string-empty-p val)) (user-error "%s must not be an empty string" key)))) @@ -1136,6 +1148,18 @@ needs team context) is resolved upstream, not here." (cl-some (lambda (x) (not (and (stringp x) (not (string-empty-p x))))) labels))) (user-error ":labels must be a list of non-empty strings"))) + ;; :state is a single name OR a list of non-empty names (mirrors :labels). + (let ((state (plist-get plist :state))) + (cond + ((null state)) + ((stringp state) + (when (string-empty-p state) + (user-error ":state must not be an empty string"))) + ((listp state) + (when (cl-some (lambda (x) (not (and (stringp x) (not (string-empty-p x))))) + state) + (user-error ":state must be a name or a list of non-empty names"))) + (t (user-error ":state must be a name or a list of names, got %S" state)))) t) ;;; Issue Model Normalization -- cgit v1.2.3