aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el105
1 files changed, 89 insertions, 16 deletions
diff --git a/pearl.el b/pearl.el
index 236c95d..22c5ebe 100644
--- a/pearl.el
+++ b/pearl.el
@@ -1288,7 +1288,8 @@ display name) so the assignee @-tag can use the short form."
(when raw
(list :id (cdr (assoc 'id raw))
:name (cdr (assoc 'name raw))
- :type (cdr (assoc 'type raw)))))
+ :type (cdr (assoc 'type raw))
+ :position (cdr (assoc 'position raw)))))
(defun pearl--normalize-team (raw)
"Normalize a Linear team alist RAW to a plist, or nil."
@@ -1609,7 +1610,7 @@ populated list renders them, not just the single-issue refresh."
issues(filter: $filter, first: $first, after: $after, orderBy: $orderBy) {
nodes {
id identifier title description priority url updatedAt
- state { id name type }
+ state { id name type position }
assignee { id name displayName email }
team { id key name }
project { id name }
@@ -1625,7 +1626,7 @@ populated list renders them, not just the single-issue refresh."
"query Issue($id: String!) {
issue(id: $id) {
id identifier title description priority url updatedAt
- state { id name type }
+ state { id name type position }
assignee { id name displayName email }
team { id key name }
project { id name }
@@ -1766,20 +1767,67 @@ view. Returns nil for a grouping pearl doesn't section (see
(t (format "Cycle %s" (plist-get cycle :number))))))
(_ nil)))
+(defconst pearl--state-type-order
+ '(("triage" . 0) ("backlog" . 1) ("unstarted" . 2)
+ ("started" . 3) ("completed" . 4) ("canceled" . 5))
+ "Rank of each Linear workflow-state type, matching the board's column order.
+Used as the primary key when ordering `workflowState' group sections; the
+state's `position' breaks ties within a type.")
+
+(defun pearl--state-type-rank (type)
+ "Return the board-order rank of a workflow-state TYPE (unknown sorts last)."
+ (or (cdr (assoc type pearl--state-type-order)) 6))
+
+(defun pearl--priority-group-rank (priority)
+ "Return the group-order rank of a Linear PRIORITY number.
+Urgent first .. Low, then None last -- matching how Linear orders a
+priority-grouped board. A nil or 0 priority is None."
+ (pcase priority (1 0) (2 1) (3 2) (4 3) (_ 4)))
+
+(defun pearl--order-groups (grouping groups)
+ "Order GROUPS, a list of (LABEL . ISSUES), by GROUPING's natural order.
+`workflowState' orders by (state type rank, state position) so the sections
+match Linear's board columns; `priority' orders Urgent..None; every other
+dimension sorts alphabetically by label. The sort is stable, so issues that
+tie on the key keep their first-appearance order."
+ (pcase grouping
+ ("workflowState"
+ (sort (copy-sequence groups)
+ (lambda (a b)
+ (let* ((sa (plist-get (cadr a) :state))
+ (sb (plist-get (cadr b) :state))
+ (ra (pearl--state-type-rank (plist-get sa :type)))
+ (rb (pearl--state-type-rank (plist-get sb :type))))
+ (if (/= ra rb)
+ (< ra rb)
+ (< (or (plist-get sa :position) 0)
+ (or (plist-get sb :position) 0)))))))
+ ("priority"
+ (sort (copy-sequence groups)
+ (lambda (a b)
+ (< (pearl--priority-group-rank (plist-get (cadr a) :priority))
+ (pearl--priority-group-rank (plist-get (cadr b) :priority))))))
+ (_
+ (sort (copy-sequence groups)
+ (lambda (a b) (string< (downcase (or (car a) "")) (downcase (or (car b) ""))))))))
+
(defun pearl--group-issues (issues grouping)
"Partition ISSUES into ordered groups by GROUPING.
-Returns a list of (LABEL . ISSUES): groups in first-appearance order, and within
-each group the issues keep their input order so the view's configured sort
-survives inside every section. Returns nil when GROUPING isn't sectioned (see
-`pearl--view-grouping-active-p'), signaling the caller to render the flat list."
+Returns a list of (LABEL . ISSUES) ordered by `pearl--order-groups' (Linear's
+board order for `workflowState'/`priority', alphabetical otherwise); within each
+group the issues keep their input order so the view's configured sort survives.
+Returns nil when GROUPING isn't sectioned (see `pearl--view-grouping-active-p'),
+signaling the caller to render the flat list."
(when (pearl--view-grouping-active-p grouping)
(let ((order '()) (table (make-hash-table :test 'equal)))
(dolist (issue issues)
(let ((label (pearl--view-group-label issue grouping)))
(unless (gethash label table) (push label order))
(push issue (gethash label table))))
- (mapcar (lambda (label) (cons label (nreverse (gethash label table))))
- (nreverse order)))))
+ (pearl--order-groups
+ grouping
+ (mapcar (lambda (label) (cons label (nreverse (gethash label table))))
+ (nreverse order))))))
(defun pearl--view-issues-query ()
"GraphQL query running a Custom View's own filter server-side, by view id.
@@ -1795,7 +1843,7 @@ and so isn't applied by the connection alone (see
issues(first: $first, after: $after, sort: $sort, filter: $filter) {
nodes {
id identifier title description priority url updatedAt
- state { id name type }
+ state { id name type position }
assignee { id name displayName email }
team { id key name }
project { id name }
@@ -2894,6 +2942,7 @@ identifier prefix -- so an unedited heading is a no-op on title sync)."
(format ":LINEAR-STATE-ID-SYNCED: %s\n" (or (plist-get state :id) ""))
(format ":LINEAR-STATE-NAME: %s\n" (or (plist-get state :name) ""))
(format ":LINEAR-STATE-TYPE: %s\n" (or (plist-get state :type) ""))
+ (format ":LINEAR-STATE-POSITION: %s\n" (or (plist-get state :position) ""))
(format ":LINEAR-ASSIGNEE-ID: %s\n" (or (plist-get assignee :id) ""))
(format ":LINEAR-ASSIGNEE-ID-SYNCED: %s\n" (or (plist-get assignee :id) ""))
(format ":LINEAR-ASSIGNEE-NAME: %s\n" (or (plist-get assignee :name) ""))
@@ -7011,6 +7060,18 @@ GROUPING outside the drawer-backed set yields nil."
(string-to-number (or (org-entry-get nil "LINEAR-PRIORITY") "0"))))
(_ nil)))
+(defun pearl--buffer-issue-group-rep ()
+ "Return a minimal issue plist for group ordering, read from the drawer at point.
+Carries just the fields `pearl--order-groups' needs -- the state type/position
+and the priority -- so a buffer regroup orders its sections the same way the
+render path does. Point must be on the issue heading."
+ (let ((pos (org-entry-get nil "LINEAR-STATE-POSITION"))
+ (pri (org-entry-get nil "LINEAR-PRIORITY")))
+ (list :state (list :type (org-entry-get nil "LINEAR-STATE-TYPE")
+ :position (and pos (not (string-empty-p pos))
+ (string-to-number pos)))
+ :priority (and pri (not (string-empty-p pri)) (string-to-number pri)))))
+
(defun pearl--subtree-contains-issue-p (beg end)
"Return non-nil when the region BEG..END holds an issue heading (a `LINEAR-ID')."
(save-excursion
@@ -7054,6 +7115,7 @@ grouped. Returns t, or `no-issues' when there are none."
;; A level-2 issue (flat buffer): already canonical level 2.
((and id (not (string-empty-p id)))
(push (list :label (pearl--buffer-issue-group-label grouping)
+ :rep (pearl--buffer-issue-group-rep)
:text (buffer-substring-no-properties beg end))
issues))
;; A level-2 group heading (contains issues): descend, capture each
@@ -7073,6 +7135,7 @@ grouped. Returns t, or `no-issues' when there are none."
(iend (save-excursion (org-end-of-subtree t t) (point)))
(lvl (org-current-level)))
(push (list :label (pearl--buffer-issue-group-label grouping)
+ :rep (pearl--buffer-issue-group-rep)
:text (pearl--shift-heading-levels
(buffer-substring-no-properties ibeg iend)
(- 2 lvl)))
@@ -7088,15 +7151,25 @@ grouped. Returns t, or `no-issues' when there are none."
(delete-region content-beg parent-end)
(goto-char content-beg)
(if grouping
- (let ((order '()) (table (make-hash-table :test 'equal)))
+ (let ((order '()) (table (make-hash-table :test 'equal))
+ (reps (make-hash-table :test 'equal)))
(dolist (it issues)
(let ((label (plist-get it :label)))
- (unless (gethash label table) (push label order))
+ (unless (gethash label table)
+ (push label order)
+ (puthash label (plist-get it :rep) reps))
(push it (gethash label table))))
- (dolist (label (nreverse order))
- (insert (format "** %s\n" label))
- (dolist (it (nreverse (gethash label table)))
- (insert (pearl--shift-heading-levels (plist-get it :text) 1)))))
+ ;; Order the sections the same way the render path does: a
+ ;; one-rep group per label fed through `pearl--order-groups'.
+ (let ((ordered (pearl--order-groups
+ grouping
+ (mapcar (lambda (l) (cons l (list (gethash l reps))))
+ (nreverse order)))))
+ (dolist (g ordered)
+ (let ((label (car g)))
+ (insert (format "** %s\n" label))
+ (dolist (it (nreverse (gethash label table)))
+ (insert (pearl--shift-heading-levels (plist-get it :text) 1)))))))
(dolist (it issues)
(insert (plist-get it :text))))
(dolist (m malformed) (insert m))