aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-25 22:30:49 -0500
committerCraig Jennings <c@cjennings.net>2026-05-25 22:30:49 -0500
commit504133e600a197b50f05d3916264433ee9e4cb12 (patch)
treeaf78b8d1eae61ba82b71aec53269c7fa93fb0c3b /pearl.el
parent768c9c0122130d44e9243f22f7681677623b713a (diff)
downloadpearl-504133e600a197b50f05d3916264433ee9e4cb12.tar.gz
pearl-504133e600a197b50f05d3916264433ee9e4cb12.zip
feat(render): derive the #+TODO line and keywords from team workflow states
The renderer wrote a fixed #+TODO line and mapped state names through a static defcustom, so a workspace's real states (Dev Review, PM Acceptance, ...) never showed up. Now the displayed issues' teams drive both. pearl--gather-header-states collects each team's full state set (cached via pearl--team-states, teams in first-seen order, states by position) plus every displayed issue's own state, so a failed team fetch still leaves its issues' states declared. pearl--build-org-content takes that list and derives the #+TODO via pearl--derive-todo-line. pearl--format-issue-as-org-entry renders each keyword with pearl--state-name-to-keyword instead of the static map -- slugify reproduces the old defaults, so standard states are unchanged -- and writes a :LINEAR-STATE-TYPE: drawer so a later merge scan can classify a retained heading. The team-states query gains type and position. The integration test stubs pearl--team-states so the render stays network-free. The merge-refresh header rebuild is the next commit. Until then a same-source refresh keeps the previous header.
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el62
1 files changed, 52 insertions, 10 deletions
diff --git a/pearl.el b/pearl.el
index b6e1e8e..19166db 100644
--- a/pearl.el
+++ b/pearl.el
@@ -1213,6 +1213,8 @@ trip on every one. Clear the cache to force a refresh."
nodes {
id
name
+ type
+ position
}
}
}
@@ -1820,7 +1822,7 @@ identifier prefix -- so an unedited heading is a no-op on title sync)."
(team (plist-get issue :team))
(project (plist-get issue :project))
(assignee (plist-get issue :assignee))
- (todo (pearl--map-linear-state-to-org (plist-get state :name)))
+ (todo (pearl--state-name-to-keyword (plist-get state :name)))
(priority (pearl--map-linear-priority-to-org (plist-get issue :priority)))
(display-title (pearl--heading-title issue))
(heading-title (pearl--heading-with-identifier
@@ -1848,6 +1850,7 @@ identifier prefix -- so an unedited heading is a no-op on title sync)."
(format ":LINEAR-STATE-ID: %s\n" (or (plist-get state :id) ""))
(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-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) ""))
@@ -2800,11 +2803,53 @@ writes."
(when (re-search-forward "^#\\+LINEAR-SOURCE: \\(.*\\)$" nil t)
(ignore-errors (car (read-from-string (match-string 1)))))))
-(defun pearl--build-org-content (issues &optional source truncated)
+(defun pearl--gather-header-states (issues)
+ "Collect the ordered workflow states for the ISSUES' teams, for the header.
+Teams in first-seen order across ISSUES; within a team, states by Linear
+`position'. Each team's full state set is fetched (cached) via
+`pearl--team-states'. A team whose fetch fails is skipped, but every displayed
+issue's own state is appended afterward so it is still declared (the coverage
+guarantee). Returns a list of (:name :type :position) plists;
+`pearl--derive-todo-line' de-duplicates by slug, so the appended issue states
+only add the ones a failed team fetch left out."
+ (let ((team-ids '()) (seen (make-hash-table :test 'equal)))
+ (dolist (issue issues)
+ (let ((tid (plist-get (plist-get issue :team) :id)))
+ (when (and tid (not (gethash tid seen)))
+ (puthash tid t seen)
+ (push tid team-ids))))
+ (let ((team-states
+ (apply #'append
+ (mapcar
+ (lambda (tid)
+ (mapcar
+ (lambda (node)
+ (list :name (cdr (assoc 'name node))
+ :type (cdr (assoc 'type node))
+ :position (cdr (assoc 'position node))))
+ (sort (copy-sequence (or (ignore-errors (pearl--team-states tid)) '()))
+ (lambda (a b)
+ (< (or (cdr (assoc 'position a)) most-positive-fixnum)
+ (or (cdr (assoc 'position b)) most-positive-fixnum))))))
+ (nreverse team-ids))))
+ (issue-states
+ (delq nil
+ (mapcar (lambda (issue)
+ (let ((st (plist-get issue :state)))
+ (when (plist-get st :name)
+ (list :name (plist-get st :name)
+ :type (plist-get st :type)
+ :position (plist-get st :position)))))
+ issues))))
+ (append team-states issue-states))))
+
+(defun pearl--build-org-content (issues &optional source truncated states)
"Build the Org content string for the linear org file from ISSUES.
SOURCE is the active-source descriptor recorded in the header so a later
-`pearl-refresh-current-view' can re-run it; TRUNCATED marks that the
-page cap was hit. Pure function, no side effects."
+`pearl-refresh-current-view' can re-run it; TRUNCATED marks that the page cap
+was hit. STATES is the ordered workflow-state list (see
+`pearl--gather-header-states') the `#+TODO:' line is derived from; a nil STATES
+yields the hardcoded default. Pure function, no side effects."
(let* ((src (or source '(:type filter :name "Linear issues" :filter nil)))
(name (pearl--source-name src))
(filter (plist-get src :filter)))
@@ -2812,11 +2857,7 @@ page cap was hit. Pure function, no side effects."
(insert (format "#+title: Linear — %s\n"
(if pearl-title-case-headings (pearl--title-case name) name)))
(insert "#+STARTUP: show3levels\n")
- (insert (format "#+TODO: %s\n"
- (if (bound-and-true-p org-todo-keywords)
- (let ((seq (car org-todo-keywords)))
- (mapconcat #'identity (cdr seq) " "))
- "TODO IN-PROGRESS IN-REVIEW BACKLOG BLOCKED | DONE")))
+ (insert (format "#+TODO: %s\n" (pearl--derive-todo-line states)))
;; Source-tracking metadata: the serialized source drives refresh; the
;; rest is human-readable provenance.
(insert (format "#+LINEAR-SOURCE: %s\n" (prin1-to-string src)))
@@ -2860,7 +2901,8 @@ In every case the resulting buffer is surfaced (see `pearl--surface-buffer'),
so a fetch run from a menu or dashboard actually shows the issues instead of
just writing the file and leaving it off-screen."
(let* ((org-file-path pearl-org-file-path)
- (new-content (pearl--build-org-content issues source truncated))
+ (states (pearl--gather-header-states issues))
+ (new-content (pearl--build-org-content issues source truncated states))
(existing-buf (find-buffer-visiting org-file-path)))
(cond
;; Branch A: no buffer visits the file -- atomic file write, then visit.